JSON Jamboree: Fixed vs. Dynamic Deserialization Showdown for Apex Callouts

Introduction:

Hello! I’m Steve Simpson, a Salesforce Certified Technical Architect and instructor. In this blog, we’ll explore different techniques for parsing JSON when making Apex callouts in Salesforce. We’ll discuss fixed JSON parsing, dynamic JSON parsing, and a hybrid approach that offers flexibility and ease of use.

Section 1: Fixed JSON Parsing with Single Line Deserialization

When dealing with straightforward JSON structures that don’t change frequently, you can create an Apex class that exactly matches the JSON structure. This approach allows you to use a single line of code to deserialize the JSON string into an array. This method is powerful and efficient for static JSON structures.



public class OS_FlightStatusModel
{
    public String icao24 {get;set;}
    public Integer firstSeen {get;set;}
    public String estDepartureAirport {get;set;}
    public Integer lastSeen {get;set;}
    public String estArrivalAirport {get;set;}
    public String callsign {get;set;}
    public Integer estDepartureAirportHorizDistance {get;set;}
    public Integer estDepartureAirportVertDistance {get;set;}
    public Integer estArrivalAirportHorizDistance {get;set;}
    public Integer estArrivalAirportVertDistance {get;set;}
    public Integer departureAirportCandidatesCount {get;set;}
    public Integer arrivalAirportCandidatesCount {get;set;}
    
    //---Use the JSON Deserialize
    public static List<OS_FlightStatusModel> parse(String json) 
    {
        return (List<OS_FlightStatusModel>) System.JSON.deserialize(json, List<OS_FlightStatusModel>.class);
    }
}

With the above class, you can easily parse the JSON using a single line of code:

//---Get the Flight Request
 private static List<OS_FlightStatusModel> getFlightRequest(String requestURL)
 {
//---Return structure
List<OS_FlightStatusModel> retList = new List<OS_FlightStatusModel>();


//---Make the request
HttpResponse resp = getRequest( requestURL);


//---Check for Success
Integer respStatusCode = resp.getStatusCode();


if (respStatusCode == 200)
 {
//---Read the Payload
String payload = resp.getBody();
debug('Payload: ' + payload);


//---Parse the payload
retList = OS_FlightStatusModel.parse(payload);


if (retList != null)
 {
System.debug('Found ' + retList.size() + ' records');
for( OS_FlightStatusModel mRow : retList)
 {
debug('Row: ' + mRow);
 }
 }
 }
else
 {
//---Handle an error
debug('Response Status Code: ' + respStatusCode);
 }
return retList;
 }

Section 2: Dynamic JSON Parsing

For more complex JSON structures, like arrays or nested objects, you might need to use dynamic JSON parsing. This approach requires more lines of code and careful navigation through JSON tokens. While it might seem more complicated, dynamic JSON parsing is necessary when dealing with advanced JSON structures.

Example:

/****************************************************************************************
* @Name OS_StateModelParser
* @Author Steve Simpson - steve@stevetecharc.com
* @Date 2023
*
* @Description Parser for reading State Models from JSON
****************************************************************************************
* Version Developer Date Description
*----------------------------------------------------------------------------------------
* 1.0 Steve Simpson 2023 Initial Creation
****************************************************************************************/


public with sharing class OS_StateModelParser extends OS_BaseParser
{
private OS_StateResponseModel responseModel {get; set;}


//--Parse the payload into the OS_StateResponseModel
public OS_StateResponseModel parse(String json)
 {
//---Setup the return value
responseModel = new OS_StateResponseModel();
responseModel.States = new List<OS_StateModel>();


try
 {
parseBody(json);
 }
catch (Exception ex)
 {
debug( 'Exception: ' + ex.getMessage() + ' ' + ex.getStackTraceString());
debugJSON(json);
 }


return responseModel;
 }


//---Parse the body of the JSON
private void parseBody(String json)
 {
//---Set the default Page Size
if (PageSize == 0) PageSize = DEFAULT_PAGE_SIZE;


Boolean hasTokenTime = false;
Boolean hasTokenStates = false;


JSONParser parser = System.JSON.createParser(json);


//---Read Token by Token
while (parser.nextToken() != System.JSONToken.END_OBJECT)
 {
//---Read the current Token, check its field name
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME)
 {
String text = parser.getText();
debug( 'Json Token: ' + text);


//---If the next token has a value, then write it, this handles null values
if (parser.nextToken() != System.JSONToken.VALUE_NULL)
 {
//---Read the time
if (text == 'time')
 {
responseModel.ResponseTime = parser.getIntegerValue();
hasTokenTime = true;
 }


//---Read the states
if (text == 'states')
 {
responseModel.States = parseStateList(parser);
hasTokenStates = true;
 }
 }
 }


//---If have both tags, then break out of loop
if (hasTokenStates && hasTokenTime) break;
 }
 }


//---Parse the States
private List<OS_StateModel> parseStateList(JSONParser parser)
 {
CurrentRecord = 0;


//---Build the return list
List<OS_StateModel> retList = new List<OS_StateModel>();


//---Go to the next token if needed
if (parser.getCurrentToken() == null) parser.nextToken();


//---Iterate over the items
while (parser.nextToken() != System.JSONToken.END_ARRAY)
 {
CurrentRecord++; //---Track the current record
String logLine = 'Rec: ' + CurrentRecord + ': ';


if (CurrentRecord > getMaxRecord())
 {
//---If past the max record, then break out of loop
debug(logLine + 'End of page');
break;
 }
else if (CurrentRecord >= getFirstRecordToProcess())
 {
//---Build a new State Row
OS_StateModel sModel = new OS_StateModel();
parseStateItem(parser, sModel);
retList.add(sModel);


debug(logLine + 'State: ' + sModel);
 }
else
 {
//---Pass in null model to trigger token advancement without reading values
parseStateItem(parser, null);
debug(logLine + 'Skipped');
 }
 }


return retList;
 }


//---Parse the States
private void parseStateItem(JSONParser parser, OS_StateModel sModel)
 {
//---Go to the next token if needed
if (parser.getCurrentToken() == null) parser.nextToken();


Integer position = 0;


while (parser.nextToken() != System.JSONToken.END_ARRAY)
 {
if (sModel != null && parser.getCurrentToken() != System.JSONToken.VALUE_NULL)
 {
if (position == 0) sModel.icao24 = parser.getText();
if (position == 1) sModel.callsign = parser.getText();
if (position == 2) sModel.origin_country = parser.getText();
if (position == 3) sModel.time_position = parser.getIntegerValue();
if (position == 4) sModel.last_contact = parser.getIntegerValue();
if (position == 5) sModel.longitude = parser.getDoubleValue();
if (position == 6) sModel.latitude = parser.getDoubleValue();
if (position == 7) sModel.baro_altitude = parser.getDoubleValue();
if (position == 8) sModel.on_ground = parser.getBooleanValue();
if (position == 9) sModel.velocity = parser.getDoubleValue();
if (position == 10) sModel.true_track = parser.getDoubleValue();
if (position == 11) sModel.vertical_rate = parser.getDoubleValue();


if (position == 12)
 {
//---Sensors, sub Array, should be null
 }


if (position == 13) sModel.geo_altitude = parser.getDoubleValue();
if (position == 14) sModel.squawk = parser.getText();
if (position == 15) sModel.spi = parser.getBooleanValue();
if (position == 16) sModel.position_source = parser.getIntegerValue();
if (position == 17) sModel.category = parser.getIntegerValue();
 }


position++;
//System.debug('Position ' + position);
 }
 }
}

Section 3: Dynamic Mapping with Custom Metadata

If you need even more flexibility, consider using dynamic mapping with custom metadata. This approach allows you to map JSON fields dynamically to SObject fields without changing the code. With dynamic mapping, you can add or remove fields on the fly, offering great flexibility in production environments.

Example:

Create custom metadata that maps JSON fields to SObject API names, positions, and data types.

Load the custom metadata and use it to dynamically assign values to SObject fields.

/****************************************************************************************
* @Name OS_StateSObjectModelParser
* @Author Steve Simpson - steve@stevetecharc.com
* @Date 2023
*
* @Description Parser for reading State Models from JSON
****************************************************************************************
* Version Developer Date Description
*----------------------------------------------------------------------------------------
* 1.0 Steve Simpson 2023 Initial Creation
****************************************************************************************/


public with sharing class OS_StateSObjectModelParser extends OS_BaseParser
{
private OS_StateSObjectResponseModel responseModel {get; set;}
private List<OS_StateMap__mdt> mapList {get;set;}


//--Parse the payload into the OS_StateResponseModel
public OS_StateSObjectResponseModel parse(String json)
 {
mapList = OS_StateMap__mdt.getAll().values();


//---Setup the return value
responseModel = new OS_StateSObjectResponseModel();
responseModel.States = new List<OS_State__c>();


try
 {
parseBody(json);
 }
catch (Exception ex)
 {
debug( 'Exception: ' + ex.getMessage() + ' ' + ex.getStackTraceString());
debugJSON(json);
 }


return responseModel;
 }


//---Parse the body of the JSON
private void parseBody(String json)
 {
//---Set the default Page Size
if (PageSize == 0) PageSize = DEFAULT_PAGE_SIZE;


Boolean hasTokenTime = false;
Boolean hasTokenStates = false;


JSONParser parser = System.JSON.createParser(json);


//---Read Token by Token
while (parser.nextToken() != System.JSONToken.END_OBJECT)
 {
//---Read the current Token, check its field name
if (parser.getCurrentToken() == System.JSONToken.FIELD_NAME)
 {
String text = parser.getText();
debug( 'Json Token: ' + text);


//---If the next token has a value, then write it, this handles null values
if (parser.nextToken() != System.JSONToken.VALUE_NULL)
 {
//---Read the time
if (text == 'time')
 {
responseModel.ResponseTime = parser.getIntegerValue();
hasTokenTime = true;
 }


//---Read the states
if (text == 'states')
 {
responseModel.States = parseStateList(parser);
hasTokenStates = true;
 }
 }
 }


//---If have both tags, then break out of loop
if (hasTokenStates && hasTokenTime) break;
 }
 }


//---Parse the States
private List<OS_State__c> parseStateList(JSONParser parser)
 {
CurrentRecord = 0;


//---Build the return list
List<OS_State__c> retList = new List<OS_State__c>();


//---Go to the next token if needed
if (parser.getCurrentToken() == null) parser.nextToken();


//---Iterate over the items
while (parser.nextToken() != System.JSONToken.END_ARRAY)
 {
CurrentRecord++; //---Track the current record
String logLine = 'Rec: ' + CurrentRecord + ': ';


if (CurrentRecord > getMaxRecord())
 {
//---If past the max record, then break out of loop
debug(logLine + 'End of page');
break;
 }
else if (CurrentRecord >= getFirstRecordToProcess())
 {
//---Build a new State Row
OS_State__c osState = new OS_State__c();
parseStateItem(parser, osState);
retList.add(osState);


debug(logLine + 'State: ' + osState);
 }
else
 {
//---Pass in null model to trigger token advancement without reading values
parseStateItem(parser, null);
debug(logLine + 'Skipped');
 }
 }


return retList;
 }


//---Parse the States
private void parseStateItem(JSONParser parser, OS_State__c osState)
 {
//---Go to the next token if needed
if (parser.getCurrentToken() == null) parser.nextToken();


Integer position = 0;


while (parser.nextToken() != System.JSONToken.END_ARRAY)
 {
if (osState != null && parser.getCurrentToken() != System.JSONToken.VALUE_NULL)
 {
OS_StateMap__mdt curMap = null;
for(OS_StateMap__mdt mapRow : mapList)
 {
if (mapRow.Position__c == position)
 {
curMap = mapRow;
break;
 }
 }


if (curMap != null)
 {
String apiName = curMap.API_Name__c;


if (curMap.Data_Type__c == 'Text')
 {
osState.put(apiName, parser.getText());
 }
if (curMap.Data_Type__c == 'Integer')
 {
osState.put(apiName, parser.getIntegerValue());
 }
if (curMap.Data_Type__c == 'Double')
 {
osState.put(apiName, parser.getDoubleValue());
 }
if (curMap.Data_Type__c == 'Boolean')
 {
osState.put(apiName, parser.getBooleanValue());
 }
 }
 }


position++;
//System.debug('Position ' + position);
 }
 }
}

Conclusion:

In Salesforce Apex callouts, you can choose from various JSON parsing techniques depending on your needs. For straightforward, static JSON, use fixed JSON parsing with single-line deserialization. For more complex structures, consider dynamic JSON parsing. And for maximum flexibility, use dynamic mapping with custom metadata. Each approach has its use cases, so choose the one that best fits your project requirements.

Happy coding!

Stay Tuned

For more insights and tips, stay tuned here on www.SteveTechArc.com and to the @SteveTechArc YouTube channel. Subscribe and enhance your understanding of Salesforce and how you can integrate it with other systems.

Helping change the world by sharing integration info with fellow Architects and those on their Architect Journey!

Transcript aided by AI

STA 1.6

Previous
Previous

Steve Data Modeling: From Picklists To Infinity And Beyond

Next
Next

Cracking the Code: “Steve's Number of Zeros”for Estimating Integration Volumes!