[SalesForce] unable to pass an Opportunity from a Lightning Component into an @AuraEnabled Apex controller method

I have a Lightning Component with an Opportunity attribute. The controller calls a helper method, which obtains the Opportunity from the component and attempts to pass it into Apex to be saved.

I am seeing the same "Unable to read Sobject" error message described in this post, but my component is alone in a Lightning App, rather than in Visualforce. It does have lookup fields populated, and I am setting sobjectType as that solution recommends.

I find that if Apex takes a Map then the Opportunity parameter can be passed. A simple String parameter can also be passed, but I believe the most straightforward approach for my scenario would be to pass the Opportunity record.

I'd appreciate any help or insight the community might be able to offer. Thank you! Here also is a post describing a similar issue.

The details below are updated to include the suggestions from Caspar Harmer

attribute inside component (updated)…

<aura:attribute name="oppty"
                type="Opportunity"
                default="{ 'sobjectType': 'Opportunity',
                           'Name': 'New Opportunity',
                           'StageName': 'Application Started'}"/>

(updated on 7/13 to include the following note)
…that the default value for of the attribute in the component is replaced with an Opportunity from Apex…

getOppty : function(component, selectedProgramId) {
    selectedProgramId = '001R0000012JKlG';
    var action = component.get("c.GetOpportunityFor");
    action.setParams({"selectedProgramId": selectedProgramId});
    action.setCallback(this, function(response){
        var state = response.getState();
        if (state === 'SUCCESS') {
            var oppty = response.getReturnValue();
            oppty.sobjectType = 'Opportunity';
            console.log('successful response... '+JSON.stringify(oppty));
            component.set("v.oppty", oppty);
        }
        else {
            console.log('helper.getOppty failed with a state of ' + state);
        }
    });
    $A.enqueueAction(action);
},

console output from getOppty…

successful response… {"Name":"Dev Application – Test Account – Jul-2016","StageName":"Application Started","Program_Applying_To__c":"001R0000012JKlGIAW","Contact__c":"003R00000147JmyIAE","CloseDate":"2016-07-13","OwnerId":"005360000024RsTAAU","sobjectType":"Opportunity"}

button click results in helper being called from javascript controller…

helper.saveOppty(component, function(response){
    var state = response.getState();
    if (state === 'SUCCESS') {
        console.log('saved successfully');
        component.set("v.oppty", response.getReturnValue());
        }
    }
    else if (state === "ERROR") {
        var errors = response.getError();
        if (errors) {
            if (errors[0] && errors[0].message) {
                alert("Error message: " + errors[0].message);
            }
        } 
        else {
            alert("Unknown error");
        }
    }
});

javascript helper method sets the sobjectType, but to no avail…

saveOppty : function(component, callback) {
    var oppty = component.get("v.oppty");
    oppty.sobjectType = 'Opportunity';

    console.log('saveOppty is called...' + JSON.stringify(oppty));

    var action = component.get("c.SaveOppty");

    action.setParams({"oppty": oppty});

    if (callback) {
        action.setCallback(this, callback);
    }
    $A.enqueueAction(action);
}

Apex controller method (updated)..

@AuraEnabled
public static Opportunity SaveOppty(Opportunity oppty) {
    system.debug('SaveOppty() is called... ' + oppty.Name);
    upsert oppty;
    return oppty;
}

console log output (added based on first answer from Caspar)…

saveOppty is called…{"Name":"Dev Application – Test Account – Jul-2016","StageName":"Application Started","Program_Applying_To__c":"001R0000012JKlGIAW","Contact__c":"003R00000147JmyIAE","CloseDate":"2016-07-11","OwnerId":"005360000024RsTAAU","sobjectType":"Opportunity"}

Error message: Unable to read SObject

The following updates were made after Junaid P Khader's recommendation that Sobjects should be passed from Lightning into Apex in a List.

JavaScript helper method…

saveOppty : function(component, callback) {
    var oppty = component.get("v.oppty");
    oppty.sobjectType = 'Opportunity';

    var opptyList = [ oppty ];

    console.log('saveOppty is called...' + JSON.stringify(opptyList));
    var action = component.get("c.SaveOppty");

    action.setParams({"opptyList": opptyList});

    if (callback) {
        action.setCallback(this, callback);
    }
    $A.enqueueAction(action);
}

Apex controller method…

@AuraEnabled
public static Opportunity SaveOppty(List<Opportunity> opptyList) {
    system.debug('SaveOppty() is called... ' + opptyList.size());
    upsert opptyList;
    for (Opportunity savedOppty : opptyList)
        return savedOppty;

    return null;
}

Console log output…

saveOppty is called…[{"Name":"Dev Application – Test Account – Jul-2016","StageName":"Application Started","Program_Applying_To__c":"001R0000012JKlGIAW","Contact__c":"003R00000147JmyIAE","CloseDate":"2016-07-12","OwnerId":"005360000024RsTAAU","sobjectType":"Opportunity"}]

Error message: Unable to read SObject

(Updated on 7/14 to identify the following work-around, which is not really an answer to the question, but does allow me to get past my current pain.)

I have opted to go with the JSON serialization approach that I had described in a comment on Caspar's answer a few days ago. By this approach I can pass the Opportunity back into Apex, and I do not need to include the "default" attribute on my component's Opportunity attribute.

It seems like this should be a stable approach, considering how the JSON class works in Apex, where serializing an Opportunity includes/exposes an "attributes" property of {"type":"Opportunity"}…

Opportunity oppty = new Opportunity(
    Name = 'My Opportunity',
    StageName = 'Initial Contact',
    CloseDate = Date.today()
);
system.debug(JSON.serialize(oppty));

debug output…

{"attributes":{"type":"Opportunity"},"Name":"My Opportunity","StageName":"Initial Contact","CloseDate":"2016-07-13"}

I found that I can update my Lightning Component helper's saveOppty function as follows, adding the "attributes" property to the object before calling stringify(). I tried simply adding this property and passing the Opportunity back in directly, but still saw the "unable to read" message. So I had to serialize the record first…

saveOppty : function(component, callback) {
    var oppty = component.get("v.oppty");
    console.log('saveOppty is called...' + JSON.stringify(oppty));

    oppty.attributes = {'type':'Opportunity'};
    var jsonOppty = JSON.stringify(oppty);

    var action = component.get("c.SaveJsonOppty");

    action.setParams({"jsonOppty": jsonOppty});
    console.log('params are set...' + jsonOppty);

    if (callback) {
        action.setCallback(this, callback);
    }
    $A.enqueueAction(action);
}

My Apex method just needs to deserialize before saving…

@AuraEnabled
public static Opportunity SaveJsonOppty(String jsonOppty) {
    system.debug('SaveOppty() is called... ' + jsonOppty);
    Opportunity oppty = (Opportunity)JSON.deserialize(jsonOppty, Opportunity.class);
    upsert oppty;
    return oppty;
}

Best Answer

I have found that not having default attributes can cause problems with binding.

UPDATE - this applies to ALL attributes, including the sobjectType and CloseDate .

Add all attributes (even if empty at this stage) that you want to write to the server, because if they are not at least initialised in the defaults area, they don't get sent.

Also there are perhaps some missing fields that are needed for the Opportunity to deserialize properly (in the internal deserialization routine that is called prior to passing it to your @AuraEnabled method).

So perhaps you could define your oppty attribute like this:

<aura:attribute name="opportunity" 
                type="Opportunity" 
                default="{ 'sobjectType': 'Opportunity',
                           'Name': 'New Opportunity',
                           'StageName': 'Initial Contact',
                           'CloseDate':'2016-08-01'} />

Or at least set the name and stage prior to passing it to your method.

Of course, you may be doing this already - I can't see what your console output is.

This is just a long shot, because you seem to have done everything else right.

Related Topic