תורגם בבינה מלאכותית. הקהילה עוד לא בדקה את התרגום. לקריאה באנגלית

בלוג · 13 בספטמבר 2023 · Steve Simpson · אינטגרציה

JSON Jamboree: עימות בין דה-סריאליזציה קבועה לדינמית בקריאות Apex חיצוניות

מבוא:

שלום! אני Steve Simpson, ‏Salesforce Certified Technical Architect ומדריך. במאמר הזה נבחן טכניקות שונות לפענוח JSON כשמבצעים קריאות Apex חיצוניות (callouts) ב-Salesforce. נדון בפענוח JSON קבוע, בפענוח JSON דינמי ובגישה היברידית שמציעה גמישות ונוחות שימוש.

חלק 1: פענוח JSON קבוע עם דה-סריאליזציה בשורה אחת

כשמתמודדים עם מבני JSON פשוטים שלא משתנים לעתים קרובות, אפשר ליצור מחלקת Apex שתואמת בדיוק את מבנה ה-JSON. הגישה הזו מאפשרת להשתמש בשורת קוד אחת כדי להמיר את מחרוזת ה-JSON בדה-סריאליזציה למערך. השיטה הזו חזקה ויעילה למבני JSON סטטיים.

צילום מסך: התיעוד של ה-API של OpenSky לטיסות נכנסות, עם פרמטרי הבקשה צילום מסך מ-Postman: קריאת GET ל-API של OpenSky לטיסות נכנסות שמחזירה נתוני טיסות כ-JSON

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);
}
}

עם המחלקה שלמעלה אפשר לפענח את ה-JSON בקלות בשורת קוד אחת:

//---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;
}

חלק 2: פענוח JSON דינמי

במבני JSON מורכבים יותר, כמו מערכים או אובייקטים מקוננים, ייתכן שתצטרכו פענוח JSON דינמי. הגישה הזו דורשת יותר שורות קוד וניווט זהיר בין אסימוני ה-JSON ‏(tokens). אף שהיא עשויה להיראות מסובכת יותר, פענוח JSON דינמי הכרחי כשמתמודדים עם מבני JSON מתקדמים.

דוגמה:

/****************************************************************************************
* @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);
}
}
}

חלק 3: מיפוי דינמי עם Custom Metadata

אם אתם צריכים עוד יותר גמישות, שקלו מיפוי דינמי עם Custom Metadata. הגישה הזו מאפשרת למפות שדות JSON לשדות SObject באופן דינמי בלי לשנות את הקוד. עם מיפוי דינמי אפשר להוסיף או להסיר שדות תוך כדי עבודה, מה שמציע גמישות רבה בסביבות ייצור.

דוגמה:

צרו Custom Metadata שממפה שדות JSON לשמות ה-API של שדות SObject, למיקומים ולסוגי נתונים.

טענו את ה-Custom Metadata והשתמשו בה כדי להקצות ערכים לשדות SObject באופן דינמי.

/****************************************************************************************
* @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);
}
}
}

סיכום:

בקריאות Apex חיצוניות ב-Salesforce אפשר לבחור בין טכניקות שונות לפענוח JSON, לפי הצרכים שלכם. ל-JSON פשוט וסטטי, השתמשו בפענוח קבוע עם דה-סריאליזציה בשורה אחת. למבנים מורכבים יותר, שקלו פענוח JSON דינמי. ולגמישות מרבית, השתמשו במיפוי דינמי עם Custom Metadata. לכל גישה יש מקרי שימוש משלה, אז בחרו את זו שמתאימה ביותר לדרישות הפרויקט שלכם.

קידוד מהנה!

הישארו איתנו

לתובנות ולטיפים נוספים, הישארו איתנו כאן ב-www.SteveTechArc.com ובערוץ ה-YouTube ‏@SteveTechArc (opens in a new tab). הירשמו והעמיקו את ההבנה שלכם ב-Salesforce ובאופן שבו אפשר לשלב אותה עם מערכות אחרות.

עוזרים לשנות את העולם בשיתוף מידע על אינטגרציה עם עמיתים ארכיטקטים ועם מי שנמצאים במסע שלהם לארכיטקטורה!

תמלול בסיוע בינה מלאכותית

STA 1.6