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!

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.