How to send wrapped generic SObjects from Lightning to Apex

I have a lightning component with an apex controller that wraps generic records in an inner class.

When I send the wrapped records to apex and receive them as generic SObjects, I get this error message:

System.JSONException: Nested object for polymorphic foreign key must
have an attributes field before any other fields.


MyCtrl.cls

public class MyCtrl {

    @AuraEnabled
    public static WrappedSObject getWrappedSObject() {
        return new WrappedSObject([SELECT Name FROM Account LIMIT 1]);
    }

    @AuraEnabled
    public static void saveSObjectJSON(String sobjectJson) {
        JSON.deserialize(sobjectJson, WrappedSObject.class);
    }


    // INNER
    public class WrappedSObject {
        @AuraEnabled
        public SObject record;

        public WrappedSObject(SObject record) {
            this.record = record;
        }
    }
}

my.app

<aura:application controller="myCtrl" extends="force:slds">        
    <aura:attribute name="wrappedSObject" type="Map" />

    <aura:handler name="init" action="{!c.init}" value="{!this}" />

    <lightning:button label="send JSON" onclick="{! c.sendJSON }"/>
</aura:application>

myController.js

({
    init : function(cmp, evt, hlp) {
        var action = cmp.get("c.getWrappedSObject");

        action.setCallback(this, function(response) {     
           cmp.set("v.wrappedSObject", response.getReturnValue());  
        });
        $A.enqueueAction(action);
    },

    sendJSON : function(cmp) {
        var action = cmp.get("c.saveSObjectJSON");
        action.setParams({
           sobjectJson :  JSON.stringify(cmp.get("v.wrappedSObject"))
        })
        $A.enqueueAction(action);
    },  
})

But receiving it as typed Accounts works.

public class WrappedAccount {
    @AuraEnabled
    public Account account;

    public WrappedAccount(Account account) {
        this.account = account;
    }
}

Is there a way to send my wrapper with generic SObjects from lightning to apex?

Answer

I know that when you serialize an SObject it has some extra information on it that you might not expect:

Contact c = new Contact();
system.debug(json.serialize(c));

Result:

{"attributes":{"type":"Contact"}}

I think the error message is simply telling you that you need to include this in your JSON that you send back to the controller so that it knows what type of SObject you’re attempting to represent in your JSON.

Even if you attempt to do the same with an SObject:

Contact c = new Contact();
system.debug(json.serialize((SObject)c));

The output is the same:

{"attributes":{"type":"Contact"}}

Looking at the documentation in apex you cannot construct a new SObject. According to this documentation the only way to “construct” a generic SObject is using the newSObject method. The documentation for this method is here which shows that even with the newSObject method you must call it from a specific SObject type such as account.

To me this says that you never actually work with a generic SObject. You can create a variable that is capable of storing any arbitrary SObject type but the actual instances of the SObjects all contain an attribute defining what type of SObject it is.

What to do

Add in the attributes property and set the type in your script before passing it. This will allow you to store it in a generic SObject variable.

It doesn’t matter what type of object you’re working with it should still have access to it’s own attributes property in your JS. You just need to make sure you grab it from the object and include it in your request.

Alternatively

In your controller use JSON.deserializeUntyped() to bring your JSON into a Map<String, Object>.

This will allow you to work on it as if it were a generic object, not necessarily a generic SObject.

In your case it might look like (execute anonymous copy pasta):

public class WrappedRecord {
    @AuraEnabled
    public Map<String, Object> genericRecord;

    public WrappedRecord(SObject genericRecord) {
        this.genericRecord = (Map<String, Object>) JSON.deserializeUntyped(JSON.serialize(genericRecord));
    }

    public SObject getGenericRecord() {
        return (SObject) JSON.deserialize(JSON.serialize(genericRecord), SObject.class);
    }
}

WrappedRecord test = new WrappedRecord([SELECT Id FROM Account LIMIT 1]);
system.debug(test.genericRecord.get('Id'));

Map<String, Object> attributes = (Map<String, Object>) test.genericRecord.get('attributes');
system.debug(attributes.get('type'));

update test.getGenericRecord();

You can use this answer for a little more info on working with untyped JSON.

Functionally this will be extremely similar. If you were accessing the ID of an SObject you would use SObject.get('Id'); If you were to use the untyped method, since it’s a map, accessing an Id field would look like map.get('Id');.

Extra

Contact c  = new Contact();
String s = JSON.serialize(c);
SObject so = (SObject) JSON.deserialize(s, SObject.class);
system.debug(JSON.serialize(so));

This also returns the same JSON output.

String s = '{"Id":"006C000001AKiyV"}';
SObject so = (SObject) JSON.deserialize(s, SObject.class);
system.debug(JSON.serialize(so));

This returns the exact same error you reported.

Contact c  = new Contact();
String s = '{"attributes":{"type":"SObject"}}';
SObject so = (SObject) JSON.deserialize(s, SObject.class);
system.debug(JSON.serialize(so));

This returns the following error:

Error

Attribution
Source : Link , Question Author : Xenia , Answer Author : gNerb

Leave a Comment