15

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?

13

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

  • 1
    Thanks, this answers my question. So would you agree that Lightning should be able to receive the attributes value automatically, even if we store them in a generic sObject List? In our case we will now go a different way, without a wrapper, and will use the Lightning Data Service and other stuff, since it is too complicated to generate all the attribute values. We already tried to use deserializeUntyped() but it got too dirty ;) – Xenia Sep 10 '18 at 12:32
  • 1
    I think lightning should be able to do alot of things :P – gNerb Sep 10 '18 at 14:22
3

You would run into the same issue even if you used a normal SObject instead of a wrapped one. The problem is that because you're sending it as a String, in order to deserialize it, it wants an attributes property with type of the record type name.

I ran into this problem recently and added the following javascript utility function:

addTypeAttribute: function(sObjects, type) {
    return sObjects.map(sObject => {
        sObject.attributes = { type };
        return sObject;
    });
},

Just pass your records and your type as a string ie. Account.

  • 1
    I actually verified passing SObject as mentioned in the code sample in my answer and it worked perfectly fine. – Jayant Das Sep 7 '18 at 1:45
  • 1
    I think thats because in your answer you aren't stringifying it but passing it as an SObject. I think the problem is with the javascript stringify and/or parse – Aequitas Sep 7 '18 at 2:30
  • 1
    That’s correct, and that’s what I recommended not to use stringify in that case but just set it directly. The issue doesn’t seem to be in stringify itself but seems how the platform parses it. The one you suggested is definitely a good approach, had not thought it that way +1! – Jayant Das Sep 7 '18 at 2:33
  • 2
    I would update your answer to show where you get the type from. The only reason I can think the OP would want to use a generic SObject is because their component doesn't necessarily know the type so hard coding the type is not ideal. – gNerb Sep 7 '18 at 14:12
  • yep, gNerb is right, we acutally store many different types in our list. – Xenia Sep 10 '18 at 12:35
3

Great question and findings! This question really helped to learn and explore quite a few new things today.

With some experimenting around this, here's some additional details and thoughts around this.

1. SObject when serialized itself always consists of additional field attributes. This field consists of values type and a url as observed in the logs (I can only find this reference in this documentation's example codes).

{"attributes":{...},"Name":"Acme","Id":"001D000000Jsm0WIAR"}}

However, when its sent back as WrappedSObject, its the content of the object which gets serialized and is returned back to the client as below:

{"record":{"Name":"Test","Id":"001...AAQ"}}

The issue here seems to be at the lightning platform level that it does not identify that the record attribute itself is an SObject and that it needs to add the necessary attributes field in there.

2. Now when you send the value back to apex, during deserialization of WrappedSObject, JSON.deserialize(..) identifies the record as an SObject and not finding the required attribtues field in there results in the failure with the error message being experienced.


While you can not prefer to use SObject altogether here, but if you want to have the data transfer using SObject only, then consider using it directly without nesting it in any custom class and without in fact the need of serializing or deserializing it. So your code will look something as below.

Apex Aura Controller

@AuraEnabled
public static SObject getMySObject() {
    return [SELECT Name FROM Account LIMIT 1];
}

@AuraEnabled
public static void saveSObject(SObject sObjectVal) {
    system.debug('SObject Type:' + sObjectJson.getSObjectType());
    system.debug('Name:' + sObjectJson.get('Name'));
}

JS Controller

action.setParams({
    sObjectVal:  cmp.get("v.mySObject")
})
  • 1
    This is great info! I think that in some cases the wrapper class is necessary though so this is only useful in cases where it is not necessary. – gNerb Sep 7 '18 at 14:10
  • That's right. This is one off case just around SObject issue here. – Jayant Das Sep 7 '18 at 14:13
  • Thanks for the effort, good to know, but as mentioned in the title we need it wrapped :( – Xenia Sep 10 '18 at 12:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.