"Unable to read Sobject" when passing LWC getRecord data to Apex

The data you get from wire service is in the form as below:

{
  "apiName": "Account",
  "childRelationships": {

  },
  "fields": {
    "Name": {
      "displayValue": null,
      "value": "University of Boston"
    },
    "POC_Datetime__c": {
      "displayValue": "8/7/2019 8:02 PM",
      "value": "2019-08-08T03:02:37.000Z"
    },
    "Phone": {
      "displayValue": null,
      "value": "1234567890"
    },
    "Type": {
      "displayValue": "Prospect Translated",
      "value": "Prospect"
    }
  },
  "id": "00128000009j45sAAA",
  "lastModifiedById": "00528000001IIBvAAO",
  "lastModifiedDate": "2019-08-08T07:44:29.000Z",
  "recordTypeInfo": null,
  "systemModstamp": "2019-08-08T07:44:29.000Z"
}

However, when we want to update an sObject in Apex by below method:

public static Account updateAccount(Account acc){
    System.debug('acc => '+JSON.serialize(acc));
    update acc;
    return acc;
}

The JSON we get is as below:

{
  "attributes": {
    "type": "Account",
    "url": "/services/data/v46.0/sobjects/Account/00128000009j45sAAA"
  },
  "Id": "00128000009j45sAAA",
  "Name": "University of Arizona",
  "Type": "Prospect",
  "Phone": "1234567890"
}

As we can clearly see the difference, the format returned by data in wired service is totally different compared to the format needed by update DML statement in Apex.

----- added based on comments------

Taking the cues from Aura attributes we can send the record data as below:

let acc = {
        sobjectType: 'Account',
        Id: '00128000009j45sAAA',
        Name: 'University of Boston',
        Phone: '1231231232'
    };

To get this format we can use below function (you can have it in utils module):

function getSObject(wiredData) {
            let sObject = {
                sobjectType: data.apiName,
                Id: data.id
            };
            Object.keys(wiredData.fields).map(fieldPath => {
                sObject[fieldPath] = wiredData.fields[fieldPath].value;
            });
            return sObject;
        }

Now getting the record data is simply let sObject = getSObject(data);

--- added based on @sfdcfox answer (with correction) -----

we can also use function as below:

function getSObject(wiredData) {
            return {
                sobjectType: data.apiName,
                Id: data.id,
                ...Object.keys(data.fields).reduce((a, f) => {
                    a[f] = data.fields[f].value;
                    return a;
                }, {})
            };
        }

Converting from LWC wire format to AuraEnabled format would probably look something like the following:

let record = {
  ...Object.keys(data.fields).reduce((a,f)=>a[f]=data.fields[f].value,{}),
  sobjectType: data.apiName
};

To fully understand this, you'll want to read about ... (the spread operator), Object.keys, and arrow functions.

We need to do this transformation because the objects are different "shapes". Why Salesforce decided to do this is anyone's guess, but at least it is fairly trivial to convert the data appropriately.


There is a small limitation with the code provided by @sfdcfox and @salesforce-sas. It works fine for non-relationship fields, however, once you start accessing a related record's values from the current object; it stops working.

const data = {
   "apiName":"Case",
   "childRelationships":{},
   "id":"500R000000AbnzLIAR",
   "lastModifiedById":"00541000008D4ZxAAK",
   "lastModifiedDate":"2020-03-07T05:53:12.000Z",
   "recordTypeId":"01241000001AT6fAAG",
   "recordTypeInfo":{
      "available":true,
      "defaultRecordTypeMapping":true,
      "master":false,
      "name":"CAG Cases",
      "recordTypeId":"01241000001AT6fAAG"
   },
   "systemModstamp":"2020-03-07T05:53:12.000Z",
   "fields":{
      "Account":{
         "displayValue":"Bob the Builder",
         "value":{
            "apiName":"Account",
            "childRelationships":{},
            "id":"001R000001QN4GRIA1",
            "lastModifiedById":"00541000008D4ZxAAK",
            "lastModifiedDate":"2020-03-07T04:25:36.000Z",
            "recordTypeId":"012410000012ujhAAA",
            "recordTypeInfo":{
               "available":true,
               "defaultRecordTypeMapping":false,
               "master":false,
               "name":"Billing",
               "recordTypeId":"012410000012ujhAAA"
            },
            "systemModstamp":"2020-03-07T04:25:36.000Z",
            "fields":{
               "Name":{
                  "displayValue":null,
                  "value":"Bob the Builder"
               }
            }
          }
      },
      "AccountId":{
         "displayValue":null,
         "value":"001R000001QN4GRIA1"
      },
      "Handle__c":{
         "displayValue":null,
         "value":null
      },
      "Origin":{
         "displayValue":"Manual",
         "value":"Manual"
      },
      "Person_Billing_Account__c":{
         "displayValue":null,
         "value":null
      },
      "Person_Billing_Account__r":{
         "displayValue":null,
         "value":null
      },
      "RecordTypeId":{
         "displayValue":null,
         "value":"01241000001AT6fAAG"
      }
   }
}

Above is some JSON.stringified real data returned to me by LWC's getRecord method. As you can see within the fields object, the assumption that every key's value is what we need to send to the back end (which is baked into the other code) is accurate- with the exception of Account. Its value is another object which contains its own fields, id, etc. I leveraged @sfdcfox's code and made an adjustment so we can handle values one relationship away. I'll leave it to whomever comes after me to support walking through > 1 relationship away.

One thing to note is I requested Person_Billing_Account__r.Name from getRecord, but since Person_Billing_Account__c was null, Person_Billing_Acccount__r's value is just null- instead of an object leading to null. To account for this, I just suppress all null values in my code since passing them to the controller doesn't help anything anyway.

Without further ado, here is my adjusted code:

const convertWiredDataToApexSobject = wiredData => {
  return {
    sobjectType: wiredData.apiName,
    Id: wiredData.id,
    ...extractValues(wiredData.fields)
  };
};

const extractValues = fields => {
  return Object.keys(fields).reduce((a, f) => {
    let { value } = fields[f];
    //check if value is object
    if (typeof value === "object" && value !== null)
      value = {
        Id: value.id,
        ...extractValues(value.fields)
      };
    //suppress nulls
    if (value === null) return a;
    a[f] = value;
    return a;
  }, {});
};

Usage:

//const data = <data from getRecord>
const ApexSobject = convertWiredDataToApexSobject(data);
console.log(JSON.stringify(ApexSobject))

Result:

{
   "sobjectType":"Case",
   "Id":"500R000000AbnzLIAR",
   "Account":{
      "Id":"001R000001QN4GRIA1",
      "Name":"Bob the Builder"
   },
   "AccountId":"001R000001QN4GRIA1",
   "Origin":"Manual",
   "RecordTypeId":"01241000001AT6fAAG"
}