1

I have a JSON payload which is changing a lot so I want to create a way to dynamically deserialize the payload. My starting point is that the payload will consist of max 5 nested lists 5 levels deep. I set up a custom metadata object to map the fields so that I am flexible when a field is added/removed from the (JSON)list.

I am struggling with the following. How do I loop trough the fields of the lists. As I understood it would be easier to deserialize as maps. Since my payload consists of lists how do I get the field types of the list at runtime.

Payload:

String jsonPayLoad = '{"Accts":[{"Name":"ABC","Exp":25,"Languages":[{"Name":"Apex","version": []},{"Name":"Java","versions":[{"version":1.8,"certification":true,"placeholder":{"target":"reached"}}]}]}]}';

Apex

            Map<String, Object> fieldMap = (Map<String, Object>)JSON.deserializeUntyped(jsonPayLoad);
        List<Data_Maps__mdt> mappings = [SELECT QualifiedApiName, label, Object__c , JSON_Object__c,sObjectApiNameLevel_0__c,  sObjectApiNameLevel_1__c,sObjectApiNameLevel_2__c,sObjectApiNameLevel_3__c,
                                         sObjectApiNameLevel_4__c FROM Data_Maps__mdt];
        //level-0
        if(mappings[0].sObjectApiNameLevel_0__c != null) {   
            sobject newSobjLevel_0 = (sObject)Type.forName('Schema',mappings[0].sObjectApiNameLevel_0__c).newInstance();//Account  
            List<sObject> fieldMapLevel_0 = (List<sObject>)fieldMap.get('Accts');
            GetSobjectFields(newSobjLevel_0, fieldMapLevel_0);
..


    public static sobject GetSobjectFields(sObject newSobj, List<sObject> fieldMap ){
    Map<String,String> fieldsMap = new Map<String,String>();
    Map<String, Schema.sObjectField> targetFields = newSobj.getSObjectType().getDescribe().fields.getMap();
    system.debug('[UploadDocuments]targetFields: ' + targetFields);
    for (sObject key : fieldMap) {
        Object value = key;
        value = value == ''? null: value;
        switch on targetFields.get(key)?.getDescribe().getType() {
            when DATE {
                value = Date.valueOf((String)value);
            }
            when DATETIME {
                value = DateTime.valueOf((String)value);
            }
            when PERCENT, CURRENCY {
                value = (Decimal)value;
            }
            when DOUBLE {
                value = Double.valueOf(value);
            }
            when INTEGER {
                value = (Integer)value;
            }
            when BASE64 {
                value = Blob.valueOf((String)value);
            }
        }
        newSobj.put(fieldsMap.get(key), value);
    } 
    insert newSobj; 
    system.debug('[DynamicJSON]newSobj: ' + newSobj);
    return newSobj;
}  

1 Answer 1

3

The key problem here is that your types are wrong throughout your code. It's definitely confusing, but if I can be very direct: if you don't feel completely comfortable with your understanding of the Apex type system, you're not going to be successful writing this type of code.

Here's some key identities to keep in mind:

  • A JSON list deserializes to a List<Object>. At run time, the contents of the List (the Object instances) will have a type appropriate to their value, which may be another complex type like a List or Map, or a primitive, and you can cast those elements individually.
  • A JSON object deserializes to a Map<String, Object>. At run time, the contents of the Map (the Object instances) will have a type appropriate to their value, which may be another complex type like a List or Map, or a primitive, and you can cast those elements individually.
  • The type Map<String, Object> is not related to the type sObject. The two cannot be cast to one another.
  • The type Object cannot be cast to the type sObject unless the value is actually an sObject instance, which it's not here, because untyped serialization never yields an sObject value.

You can only cast a value to a type if it's actually an instance of that type, or of a type that descends from that type in the class hierarchy.* If you attempt a cast and you're wrong about the actual type of the value, you get an exception.

* I'm deliberately excluding discussion of weird features of the Apex type system; this is a general principle in strongly-typed object-oriented programming.


As I understood it would be easier to deserialize as maps. Since my payload consists of lists how do I get the field types of the list at runtime.

This is mistaken, or mis-framed. Your payload mixes lists and maps at various levels, and you don't have a choice about which you deserialize. You have to follow the structure of the payload: the outermost level is a Map, whose Accts key contains a List, which contains Maps, and so on down the line.

List<sObject> fieldMapLevel_0 = (List<sObject>)fieldMap.get('Accts');
GetSobjectFields(newSobjLevel_0, fieldMapLevel_0);

This will fail. The value of the topmost key Accts is not a List<sObject>; it is a List<Map<String, Object>>. You know that because it's a JSON list of JSON objects. As discussed above, because we know the type of all of the List elements, we can cast to this specific type. We do not know the types of all of the Map objects (and in fact they are not all the same), so we keep the generic Map<String, Object> type.

You seem to have already written some of the code that you want, although there are a number of mistakes in logic that prevent it from working right now. If you want to iterate over the list of records in your JSON, just do so, treating every JSON list as a List<Object> and every JSON object as a Map<String, Object>. It would look something like this. This is an example: please do not copy and paste this code and expect it to work.

// I've omitted the first few lines here for clarity.
List<Object> recordList = (List<Object>)fieldMap.get('Accts');
List<sObject> objectsToInsert = new List<sObject>();
Type sobjectType = (sObject)Type.forName('Schema',mappings[0].sObjectApiNameLevel_0__c);
for (Object eachRecord : recordList) {
    objectsToInsert.add(
        GetSobjectFields(sobjectType, (Map<String, Object>)eachRecord);
    }
}
insert objectsToInsert;

Then your GetSobjectFields() method needs to change to take the appropriate types into account as well, and it definitely shouldn't be performing an insert - you need to bulkify that DML, as above.

public static sObject GetSobjectFields(Type sobjectType, Map<String, Object>  fieldMap ){

Note the different method signature. You accept a Type, the sObject type you want to construct, and the map of field names to field values. Then,

    newSObj = sobjectType.newInstance();
    for (String fieldName : fieldMap.keySet()) {
         // Do your logic here.
         // Note that you've got a lot more code to write - 
         // you haven't implemented field-name mapping,
         // or dealt with sub-lists.
         // Then
         newSObj.put(fieldName, value);
    }
    return newSObj;
}

I've skipped over a number of problems here, including the fact that fieldsMap is never populated and that the logic populating the value and key variables doesn't make sense, but I think this rough skeleton is closer to where you'd need to be.

This is very similar to the code or structure that sfdcfox provided in this question. The version you've shown here has been changed from that post in a way that makes the logic invalid, because you've swapped around Maps and Lists and then put several parts of the code into a broken state. I'm not saying that to make you feel bad, but to emphasize how important understanding types is to writing this type of code successfully.


You've asked a lot of questions about this code and I completely understand that you're struggling. I want to offer an alternative. It looks like you are trying to write an API that can accept sObject data, mapped via a Custom Metadata Type. It looks like you're possibly also trying to accept multiple levels of data, like an sObject and its own child records, although you have not yet written the code to parse that data. This objective is certainly not impossible, but it's going to be a fair amount of tricky code to do in this fully-generic way.

Why not have your remote system directly call the Salesforce REST API, specifically, the Composite API or even better the sObject Tree API? These resources are designed to accept sObject data directly, including multiple records and nested sObjects of different types. They'll likely provide better performance, you won't have to write a line of Apex, and you don't need to worry about configuring Custom Metadata because the API already accepts your schema.

Alternately, you could use a middleware platform like Mulesoft or one of its many competitors to easily transform data from some incoming service into the format needed by the Salesforce API. What you're doing right now is probably the hardest way to achieve what you seem to be aiming at.

3
  • Thanks for a good explanation I appreciate this since Im eager to learn. My purpose was to comprehend this and not to rely on the salesforce Rest API's since most of the time a middleware platform like Mulesoft is not cheap. I followed your explanation but was left wit an error System.TypeException: Invalid conversion from runtime type List<ANY> to List<Map<String,ANY>> Line: for (Map<String, Object> eachRecord : recordList) {. What did you ment here since this also relates to GetSobjectFields
    – Thomas
    Jan 28, 2021 at 7:51
  • Further illustrating the challenge of working with types, especially in untyped deserialization, I made a couple of mistakes in my answer, which I believe I've corrected above via additional casts.
    – David Reed
    Jan 28, 2021 at 14:59
  • To be clear, communicating with the Salesforce REST API is one approach, and Mulesoft or other ESB is a separate approach. You don't have to buy any other products to effectively use the Salesforce REST APIs.
    – David Reed
    Jan 28, 2021 at 17:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .