0

Im deserializing a JSON (that has both reserved apex words and other uncompilable strings having 'organizer__' and '__c' ) in Apex. Ive been using a compiled class-based deserialization approach and the mogrifyJSON approach described in https://blog.deadlypenguin.com/2016/06/21/parse-json-reserved-words/ which works great when you have a known/finite number of illegal strings to mogrify/replace/handle, such as Apex reserved words, but im realizing my JSON payload ALSO has a dynamic number of other illegal strings (such as those having __c or organizer__ in them). My approach is inadequate because apex is compiled and i cannot dynamically compile inner Public strings under cls_orderby and in replacements to represent/accommodate the reality of dynamic json having endless possibilities of __c and organizer__ strings.

Objective:

I need help reaching debug statement parity without having to hardcode inner Public String member variables under cls_orderby and in replacements.

Debugging my inadequate deserialization approach (compiled class structure /typed variables structure approach) is easy, but wont scale. See it below. Assume this debug was generated after i ran my JSON through queryParamJSONDeserializer obj = queryParamJSONDeserializer.parse(jsonString); Furthermore the getType(o) method used in debugging was modeled after https://ideas.salesforce.com/s/idea/a0B8W00000GdY4zUAF/method-to-get-the-type-of-an-object-also-primitive-type-not-only-sobject

USER_DEBUG|[1125]|DEBUG|obj.where_Z.organizer_Sprint_c.eq:                      a0D3u00000AdbJIEAZ              getType: Id
USER_DEBUG|[1126]|DEBUG|obj.where_Z.organizer_ussp_Nested_Under_c:              null                            getType: 
USER_DEBUG|[1129]|DEBUG|obj.orderby[0].organizer_SortOrder_c:                   ASC                             getType: String
USER_DEBUG|[1130]|DEBUG|obj.orderby[0].organizer_sortorder_approved_c:          null                            getType: 
USER_DEBUG|[1131]|DEBUG|obj.orderby[0].organizer_sortorder_assignees_c:         null                            getType: 
USER_DEBUG|[1132]|DEBUG|obj.orderby[0].organizer_sortorder_recalled_c:          null                            getType: 
USER_DEBUG|[1133]|DEBUG|obj.orderby[0].organizer_sortorder_rejected_c:          null                            getType: 
USER_DEBUG|[1134]|DEBUG|obj.orderby[0].organizer_sortorder_submitted_c:         null                            getType: 
USER_DEBUG|[1182]|DEBUG|obj.limit_Z:                                            15                              getType: Integer

The inadequate approach code:

//Generated by AdminBooster and manually changed to incorporate handling reserved or other uncompilable strings via the mogrifyJSON approach written about in https://blog.deadlypenguin.com/2016/06/21/parse-json-reserved-words/


// USAGE
//Note1 queryParamJSONDeserializer does deserialize this, but is inadequate because its using hardcoded tactics.  
//Note2 In reality the json string can contain many more __c suffixed strings and im exploring how to pivot into a more general     design that avoids hardcoding.
/*      

String jsonInput=       '{'+
'   "where": {'+
'       "organizer__Sprint__c": {'+
'           "eq": "a0D3u00000AdbJIEAZ"'+
'       },'+
'       "organizer__ussp_Nested_Under__c": {'+
'           "eq": ""'+
'       }'+
'   },'+
'   "orderby": [{'+
'       "organizer__SortOrder__c": "ASC"'+
'   }],'+
'   "limit": 15'+
'}';
        queryParamJSONDeserializer obj = queryParamJSONDeserializer.parse(jsonString);
        System.debug('obj.where_Z.organizer_Sprint_c:' + obj.where_Z.organizer_Sprint_c);
*/

public class queryParamJSONDeserializer{
    public cls_where Where_Z;
    public cls_orderby[] orderby;
    public Integer limit_Z; //15
    public class cls_where {
        public cls_organizer_Sprint_c organizer_Sprint_c;
        public cls_organizer_ussp_Nested_Under_c organizer_ussp_Nested_Under_c;
    }
    public class cls_organizer_Sprint_c {
        public String eq;
        public String nin;
        public String ne;
        public String gte;
        public String lte;
    }
    public class cls_organizer_ussp_Nested_Under_c {
        public String eq;
        public String nin;
        public String ne;
        public String gte;
        public String lte;
    }
    public class cls_orderby {
        public String organizer_SortOrder_c;                    //works but inadequate because i cannot sustain hardcoding these public Strings
        public String organizer_sortorder_approved_c;           //works but inadequate because i cannot sustain hardcoding these public Strings
        public String organizer_sortorder_assignees_c;          //works but inadequate because i cannot sustain hardcoding these public Strings
        public String organizer_sortorder_recalled_c;           //works but inadequate because i cannot sustain hardcoding these public Strings
        public String organizer_sortorder_rejected_c;           //works but inadequate because i cannot sustain hardcoding these public Strings
        public String organizer_sortorder_submitted_c;          //works but inadequate because i cannot sustain hardcoding these public Strings
    }
    public static queryParamJSONDeserializer parse(String stringifydQueryParam){
        return (queryParamJSONDeserializer) System.JSON.deserialize(mogrifyJSON(stringifydQueryParam), queryParamJSONDeserializer.class);
    }

    public static String mogrifyJSON(String data) {
        // Regex to match the start of the line and the key
        // surrounded by quotes and ending with a colon
        String regexFormat = '(?m)^\\s*"{0}"\\s*:';
    
        // Replacement format of the new key surrounded by
        // quotes and ending with a colon
        String replacementFormat = '"{0}" :';
    
        // A map of existing key to replacement key
        Map<String, String> replacements = new Map<String, String> { 
            'limit' => 'limit_Z',
            'where' => 'Where_Z',
            'organizer__SortOrder__c' => 'organizer_SortOrder_c',
            'organizer__sortorder_approved__c' => 'organizer_sortorder_approved_c',
            'organizer__sortorder_assignees__c' => 'organizer_sortorder_assignees_c',
            'organizer__sortorder_recalled__c' => 'organizer_sortorder_recalled_c',
            'organizer__sortorder_rejected__c' => 'organizer_sortorder_rejected_c',
            'organizer__sortorder_submitted__c' => 'organizer_sortorder_submitted_c',
            'organizer__ussp_Nested_Under__c' => 'organizer_ussp_Nested_Under_c',
            'organizer__Sprint__c' => 'organizer_Sprint_c'
        };
    
        // Since our JSON can come in formatted however the
        // endpoint wants, we need to format it to a standard
        // we know and can handle
        String formattedJSON = JSON.serializePretty(JSON.deserializeUntyped(data));
    
        // Iterate over all the keys we want to replace
        for (String key : replacements.keySet()) {
            // Generate our regex based on the key
            String regex = String.format(
                regexFormat,
                new List<String> {key}
            );
    
            // Generate our replacement
            String replacement = String.format(
                replacementFormat,
                new List<String> {replacements.get(key)}
            );
    
            // Find all and replace
            formattedJSON = formattedJSON.replaceAll(regex, replacement);
        }
    
        return formattedJSON;
    }
}

Questions and thinking about next steps:

How could i reach debug statement parity (similar debug statement results) without using class-based deserialization, and avoiding hardcoding fieldnames__c in cls_orderby and replacements? Im looking into deserializeUntyped instead of a compiled-class-structure because my inadequate class-based deserialization wont scale to endless fieldnames in my json.

1 Answer 1

1

JSON.deserializeUntyped seems to be the right direction for my needs.

With the untyped deserialization approach im free to avoid hardcoding member variables that represent fieldname__c strings.

In theory ill achieve dynamic deserialization of unknown quantities of fieldnames__c using something like the below, along with field/schema describes (per my dynamic requirement) careful typecasting (or recursive type detection with awareness checks to Limits.getHeapSize) to get what i need.

This could be made more elegant. It doesnt attempt to be depth agnostic. It just deserializes 2 levels:

String jsonInput=       '{'+
'   "where": {'+
'       "organizer__Sprint__c": {'+
'           "eq": "a0D3u00000AdbJIEAZ"'+
'       },'+
'       "organizer__ussp_Nested_Under__c": {'+
'           "eq": ""'+
'       }'+
'   },'+
'   "orderby": [{'+
'       "organizer__SortOrder__c": "ASC"'+
'   }],'+
'   "limit": 15'+
'}';

// This code for untyped deserialization is modeled after SFDCFOX's answer in https://salesforce.stackexchange.com/a/332870/10073
//this steps through level 1 and level 2 depth of my nested json and detects types dynamically using instanceOf.
//Using instanceOf, you can detect if the property you're examining is a Map, and if so, you can cast it to a Map<String, Object> and parse those values. 
//If you write a method for parsing these values, you can even do so recursively up to the stack limit.

 Object source = JSON.deserializeUntyped(jsonInput);
 if(source instanceOf Map<String, Object>) {
   Map<String, Object> values = (Map<String, Object>)source;
   for(String key: values.keySet()) {
     Object subValue = values.get(key);
     if(subValue instanceOf List<Object>) {
       // This is a list of something //
      System.debug('inside a list, and key is: '+key + ' has subValue: '+subValue);
     } else if(subValue instanceOf Map<String, Object>) {
       // This is another nested object //
      System.debug('inside a nested object, and key is: '+key + ' has subValue: '+subValue);
     } else {
       // String, Boolean, or number
      System.debug('String, Boolean, or number, and key is: '+key + ' has subValue: '+subValue);
     }
   }
 }

Update: To make this more elegant i could process the deserializeUntyped results recursively for dynamic depth jsons, perhaps with appropriate handling around Limits.getLimitHeapSize(). There seems to be a nice github project around this too!

You must log in to answer this question.

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