Why do Related Lists in SObjects serialize to JSON differently between Aura and REST APIs

apexjsonrest-apisobjectwire

I have a custom object, Shift, that has a related list of Shift Breaks. I can query this data like:

List<Shift__c> shifts = [SELECT Id, Name, (SELECT Id, Name FROM Shift_Breaks__r) FROM Shift__c];

If I then serialize this to JSON for return from a REST API (explicitly setting the REST Response body) using:

JSON.serialize(shifts, false); // Suppress null fields

I get output like:

[
  {
    "attributes": {
      "type": "Shift__c",
      "url": "/services/data/v52.0/sobjects/Shift__c/a1g1F0000019zwPQAQ"
    },
    "Id": "a1g1F0000019zwPQAQ",
    "Name": "SHFT-000417",
    "Shift_Breaks__r": {
      "totalSize": 1,
      "done": true,
      "records": [
        {
          "attributes": {
            "type": "Shift_Break__c",
            "url": "/services/data/v52.0/sobjects/Shift_Break__c/a1c1F000001DAlhQAG"
          },
          "Shift__c": "a1g1F0000019zwPQAQ",
          "Id": "a1c1F000001DAlhQAG",
          "Name": "SB-0"
        }
      ]
    }
  }
]

Ignoring the "attributes" which also do not get sent as part of the Aura Enabled response in LWC, specifically note how the related list is returned as an object with a nested array of the records in the list:

"Shift_Breaks__r": {
  "totalSize": 1,
  "done": true,
  "records": [
    ...
  ]
}

On the other hand, if the same SOQL query result is returned through an @AuraEnabled method via a wire or imperative call from an LWC, I see JSON of the form:

[
  {
    "Id": "a1g1F0000019zwPQAQ",
    "Name": "SHFT-000417",
    "Shift_Breaks__r": [
      {
        "Shift__c": "a1g1F0000019zwPQAQ",
        "Id": "a1c1F000001DAlhQAG",
        "Name": "SB-0"
      }
    ]
  }
]

Notice how in this case the related list is actually an array of the related records directly.

Why do these two APIs, which you would expect to both simply use JSON.serialize for the returned value, actually return the data differently? What's the benefit of the "totalSize" and "done" properties?

Best Answer

I think the only logical answer is that @AuraEnabled methods are not simply using JSON.serialize to return the data or else it would be the same.

You can see the following when you test with an AuraEnabled method

@AuraEnabled(cacheable=true)
public static List<sObject> getAccount() {
    List<Account> accs = [SELECT Id, (SELECT Id FROM Contacts) from Account];
    //log shows attributes as well as totalSize and done like REST API
    System.debug(JSON.serialize(accs));
    //return ends up being different
    return [select Id, (SELECT Id FROM Contacts) from Account];
}

Even if you inspect the network tab, you'll see it's returned in the following format

"returnValue":
[
    {
        "Id":"0014P000027tizaQAA",
        "Contacts":
        [
            {
                "AccountId":"0014P000027tizaQAA",
                 "Id":"0034P00002UVEuUQAX"
            },
            ...
         ]
}
...

Clearly, Aura is doing something "extra" to remove what you identified specifically (totalSize, done) as well as the attributes property.

There's examples within documentation about the "aura serialization framework" which is probably the crux of the difference I'm hinting at. Returning Data from an Apex Server-Side Controller talks about the specific behavior when returning wrappers/instances of an apex class

When an instance of an Apex class is returned from a server-side action, the instance is serialized to JSON by the framework. Only the values of public instance properties and methods annotated with @AuraEnabled are serialized and returned.

Likewise, in Summer '21 - there was a fix for filtering internal sObject fields during serialization for Lightning Components & Visualforce

When an sObject is serialized or deserialized by the Aura serialization framework when the sObject is returned by an AuraEnabled Apex method, these fields could be leaked to the client prior to Summer '21. In Summer '21, the internal fields are filtered out by the serializer or deserializer

Going off the above, I suspect your answer as to why is @AuraEnabled serialization different is that it has its own serialization framework that filters out those "other" attributes you identified.


Why might it do that?

  • totalSize contains the total number of records that match a query
  • done means there are no more results to obtain

Both are essentially related to the chunking functionality of the REST API query endpoint. If more than 2,000 results are returned, totalSize and done would be essential to understand there's more left to go through and to "get" the rest if you're interested.

In the apex context, we don't have that functionality so it's possible it's removed for that reason similar to how attributes would provide no value (url)


Another fun little difference

Even REST and SOAP seem to use diffent properties to convey a similar message in their respective query() endpoints. SOAP API uses size, while REST API uses totalSize.

Related Topic