SObjectException No More. Intentional Change?

Support has agreed that this behavior is a bug and informed me there is an ETA to fix it (though they did not share that timeline). They did not share if a Known Issue was created, but it looks like this issue was reported as early as last year:

System.SObjectException is not thrown while executing piece of apex code without querying that field

Summary
System.SObjectException is not thrown while executing below apex code without querying that field after put statement.

Repro
- Issue is replicable in any org by executing the below piece of code.

Go to dev console or workbench:

  1. Execute the following anonymous apex: [WORKING AS EXPECTED]

    sobject so = [SELECT Id FROM Contact LIMIT 1]; 
    System.debug(so.get('Name')); 
    

Result: System.SObjectException: SObject row was retrieved via SOQL without querying the requested field: Contact.Name

  1. Execute this similar anonymous apex: [NOT WORKING AS EXPECTED]

    sobject so = [SELECT Id FROM Contact LIMIT 1]; 
    so.put('Description', 'This is a description'); 
    System.debug(so.get('Name')); 
    

Result: No exception is being thrown. The debug statement outputs null.

Workaround
- Please make sure to query that the required fields in your SOQL query.


I think you get the exception if you execute the following code

Contact c = [SELECT Id FROM Contact LIMIT 1];
//c.Salutation = 'Dr.'; //Commented
system.debug(c.Name);

I think that salesforce is converting the SObject to a Map of field's when you try to set a field i.e. c.Salutation = 'Dr.';.

But if you directly access the field prior to setting the value of any other field that conversion may not happen and you get an error.

This is what I think may be happening and I do not have any citation for that.


The same behavior you're seeing happens under Winter 17. I just tested on an internal engineering pod that's still on Winter 17, and saw the same results as you did where Contact.Name debugged as null.

Relevant debug log excerpt:

11:25:15.11 (14099364)|SOQL_EXECUTE_BEGIN|[1]|Aggregations:0|SELECT Id FROM Contact LIMIT 1
11:25:15.11 (49753979)|SOQL_EXECUTE_END|[1]|Rows:1
11:25:15.11 (49841982)|HEAP_ALLOCATE|[1]|Bytes:8
11:25:15.11 (49904840)|HEAP_ALLOCATE|[1]|Bytes:29
11:25:15.11 (50134484)|HEAP_ALLOCATE|[1]|Bytes:8
11:25:15.11 (50238131)|HEAP_ALLOCATE|[1]|Bytes:33
11:25:15.11 (50396815)|HEAP_ALLOCATE|[1]|Bytes:8
11:25:15.11 (51526876)|VARIABLE_ASSIGNMENT|[1]|c|{"s":1,"v":{"Id":"003D000000QelGQIAZ"}}|0x705a61cb
11:25:15.11 (51552578)|STATEMENT_EXECUTE|[2]
11:25:15.11 (51586662)|HEAP_ALLOCATE|[2]|Bytes:3
11:25:15.11 (51849551)|HEAP_ALLOCATE|[2]|Bytes:-4
11:25:15.11 (51916880)|VARIABLE_ASSIGNMENT|[2]|this.Salutation|"Dr."|0x705a61cb
11:25:15.11 (51935877)|STATEMENT_EXECUTE|[3]
11:25:15.11 (52374771)|USER_DEBUG|[3]|DEBUG|null

I happened to review the underlying core platform changes for the getPopulatedFieldsAsMap fix, and don't see how they could have impacted this - the changes were isolated to that method, and resolving an SObject field's value with this syntax doesn't call that method "under the hood".

I suspect (but can't confirm) that the behavior here is due to the special way that Contact.Name is calculated, and not a general change to apex handling of not queried fields.