Parsing JSON with RPG is one of RPG API Express’s critical features in ensuring your web service architecture runs smoothly. After Kato Integration’s release of RPG API Express version 3.4.0, we have added numerous features and bugfixes, enhancing RPG API Express’s parsing and composition APIs. Because of these changes, we’re updating some of our most perennial content. We have also updated this blog post’s example code to the more modern Free-Format RPGLE.
With the release of RPG API Express version 3.4, Kato Integrations is bringing JSON to the IBM i. JSON is becoming increasingly popular in web service communication, and we are eager to help our customers take full advantage of this powerful, flexible data interchange format.
In this series of tutorials, we are demonstrating the JSON composition and parsing APIs that might be used to offer or communicate with a web service. In our previous installment (read it here) we used RPG API Express to compose a JSON object which represented contact information for a visitor at a trade show. In this tutorial, we are going to use the RXS JSON parsing API to read the data from an array of those contact objects.
The full source code for this program can be found below, and you can copy the text by hovering over the code box in the top-right corner and clicking “Copy”. We will refer to this code throughout the post.
/*
Ctl-Opt DftActGrp(*No) ActGrp(*Caller) BndDir('RXSBND');
/COPY QRPGLECPY,RXSCB
// This parsing subprocedure will be called by RXS_ParseJson(), and it
// will write parsed JSON data to the job log
Dcl-Pr JsonHandler Ind;
pType Int(5) Const;
pPath Const Like(RXS_Var64Kv_t);
pIndex Uns(10) Const;
pData Pointer Const;
pDataLen Uns(10) Const;
End-Pr;
// These fields are used during parsing to store address information
Dcl-S streetNbr Like(RXS_Var1Kv_t);
Dcl-S streetName Like(RXS_Var1Kv_t);
Dcl-S aptNbr Like(RXS_Var1Kv_t);
Dcl-S city Like(RXS_Var1Kv_t);
Dcl-S state Like(RXS_Var1Kv_t);
Dcl-S zip Like(RXS_Var1Kv_t);
// ParseJsonDS is used to configure the JSON parser
Dcl-Ds ParseJsonDS LikeDS(RXS_ParseJsonDS_t) Inz(*LikeDS);
Dcl-S JSON Like(RXS_Var1Kv_t);
// Reset datastructures
RXS_ResetDS( ParseJsonDS : RXS_DS_TYPE_PARSEJSON );
// This field holds a pointer to the subprocedure that will
// handle the parsed JSON data
ParseJsonDS.Handler = %Paddr( JsonHandler );
// By default, RXS_ParseJson() parses all data as though it is a string.
// This setting allows us to retrieve data as its associated JSON datatype
ParseJsonDS.ConvertDataToString = RXS_NO;
// This is the IFS file path where the JSON document is stored
ParseJsonDS.Stmf = '/tmp/parseJsonDemo.json';
// This calls the JSON parser, which will read the JSON data and
// call the JsonHandler subprocedure on each event
RXS_ParseJson( *Omit : ParseJsonDS );
// This cleans up the memory used by the JSON operations
RXS_DestroyJson( ParseJsonDS );
*INLR = *On;
return;
Dcl-Proc JsonHandler Export;
Dcl-Pi Ind;
pType Int(5) Const;
pPath Const Like(RXS_Var64Kv_t);
pIndex Uns(10) Const;
pData Pointer Const;
pDataLen Uns(10) Const;
End-Pi;
// Will hold character data retrieved from pData
Dcl-S Val Like(RXS_Var1Kv_t);
// Will retrieve an indicator from a boolean element
Dcl-S BoolVal Ind Based(pData);
// Will retrieve an integer from a numeric element
Dcl-S IntVal Int(20) Based(pData);
// This main select block lets us target different types of parsing events
select;
// Targets only array start events
when pType = RXS_JSON_ARRAY;
select;
// Start of the root array
when pPath = '[*]';
RXS_JobLog( 'Begin Parsing...' );
RXS_JobLog( '******************************' );
RXS_JobLog( 'Contact List:' );
// Start of phone array
when pPath = '[*]/phone[*]';
RXS_JobLog( 'Contact Phone(s):' );
endsl;
// Targets only end of array events
when pType = RXS_JSON_ARRAY_END;
select;
// End of the root array
when pPath = '[*]';
RXS_JobLog( 'End Parsing.' );
endsl;
// Targets only object start events
when pType = RXS_JSON_OBJECT;
select;
// Start of new contact object
when pPath = '[*]';
RXS_JobLog( '******************************' );
// Start of address child object
// We're using this node to clear the fields that hold
// address information to be printed
when pPath = '[*]/address';
clear streetNbr;
clear streetName;
clear aptNbr;
clear city;
clear state;
clear zip;
endsl;
// Targets only end of object events
when pType = RXS_JSON_OBJECT_END;
select;
// End of address object
// We'll use this event to write out the address data, which was
// previously parsed, to the job log
when pPath = '[*]/address';
RXS_JobLog( 'Contact Address:' );
RXS_JobLog( x'05' + streetNbr + ' ' + streetName );
if %Len(aptNbr) > 0;
RXS_JobLog( x'05' + 'Apt. ' + aptNbr );
endif;
RXS_JobLog( x'05' + city + ', ' + state );
RXS_JobLog( x'05' + zip );
// End of contact object
when pPath = '[*]';
RXS_JobLog( '******************************' );
endsl;
// The following criteria target the different data types that we
// expect to be returned from the JSON data
when pType = RXS_JSON_NULL;
// For any null value in our data, we want to skip any processing
// and continue parsing the remaining data
when pType = RXS_JSON_BOOLEAN;
select;
// The only element we expect to contain boolean data is the
// salesProspect element
when pPath = '[*]/salesProspect';
if BoolVal;
RXS_JobLog( 'Sales Prospect?: Yes' );
else;
RXS_JobLog( 'Sales Prospect?: No' );
endif;
endsl;
when pType = RXS_JSON_INTEGER;
select;
when pPath = '[*]/address/number';
streetNbr = %Char( IntVal );
endsl;
when pType = RXS_JSON_STRING;
// For our string elements, we will retrieve the values as
// character data using RXS_STR
select;
when pPath = '[*]/name';
clear Val;
Val = RXS_STR( pData : pDataLen );
RXS_JobLog( 'Name: ' + Val );
when pPath = '[*]/address/street';
streetName = RXS_STR( pData : pDataLen );
when pPath = '[*]/address/apartment';
aptNbr = RXS_STR( pData : pDataLen );
when pPath = '[*]/address/city';
city = RXS_STR( pData : pDataLen );
when pPath = '[*]/address/state';
state = RXS_STR( pData : pDataLen );
when pPath = '[*]/address/postCode';
zip = RXS_STR( pData : pDataLen );
// this will be called for each phone number element in the array
when pPath = '[*]/phone[*]';
clear Val;
Val = RXS_STR( pData : pDataLen );
RXS_JobLog( x'05' + Val );
when pPath = '[*]/email';
clear Val;
Val = RXS_STR( pData : pDataLen );
RXS_JobLog( 'Email: ' + Val );
endsl;
endsl;
return *On;
End-Proc;
*/
There are two main components to parsing JSON data with RPG API Express. The first is the main procedure, which will configure the JSON parsing engine, and the second is the parsing handler subprocedure, which processes retrieved JSON data.
The handler subprocedure is the most important – and the most complex – component in a JSON parsing program. This subprocedure will be called each time a JSON node is found in your document. The handler can be configured to process events or data when specific nodes are identified. Depending on your business use case, you might want to write the data to a physical file, or perform further processing on the data. An object start event, for instance, might be used to write a header section to an output document. In this example, we’re going to write the parsed data to the job log.
All JSON parsing handler subprocedures must have the following prototype:
Dcl-Pr JsonHandler Ind;
pType Int(5) Const;
pPath Const Like(RXS_Var64Kv_t);
pIndex Uns(10) Const;
pData Pointer Const;
pDataLen Uns(10) Const;
End-Pr;
In order to build the parsing handler, we need to refer to the JSON document structure to identify the JSON elements and events that we want to retrieve. Here is the document we will be parsing:
[
{
"name": "Dona Franks",
"salesProspect": true,
"address": {
"number": 20391,
"apartment": "177",
"street": "Central Avenue",
"city": "Waikele",
"state": "Minnesota",
"postCode": "60247"
},
"phone": [
"+1 (971) 596-2501",
"+1 (989) 401-2094",
"+1 (948) 493-2985"
],
"email": "donafranks@gadtron.com"
},
{
"name": "Ortega Stuart",
"salesProspect": true,
"address": {
"number": 76308,
"apartment": null,
"street": "Delmonico Place",
"city": "Fillmore",
"state": "Virginia",
"postCode": "94862"
},
"phone": [
"+1 (977) 470-3280"
],
"email": "ortegastuart@gadtron.com"
},
{
"name": "Jackie Wilcox",
"salesProspect": false,
"address": {
"number": 16574,
"apartment": "223",
"street": "Monitor Street",
"city": "Whipholt",
"state": "Washington",
"postCode": "32492"
},
"phone": [
"+1 (996) 561-3110",
"+1 (875) 534-2432"
],
"email": "jackiewilcox@gadtron.com"
}
]
The handler subprocedure is built using select blocks to control which code is executed. For each node from which we need to retrieve data, we will need to write a corresponding “when” criterion.
Note that, because our document’s root element is an array, our paths all start with “[*]“, which indicates to the parsing engine that an array will be present at that level. If our root element was an object, the paths would begin with “/” instead. As such, in order to retrieve the data from the “name” element, we would use this path:
[*]/name
The same notation is used for arrays that are nested within objects, like with the array of phone numbers in our contact object:
[*]/phone[*]
Additionally, for objects and arrays, the same path can be used to detect the start and end of the element, as well as any element data. The above path for the phone array would fire an event once for the start of the array, once for each element in the array, and once again for the end of the array. These separate events can be captured within the parsing handler subprocedure to trigger processing specific to the collection of data.
The RXSCB copybook lists constants that represent each of the JSON data types and parsing events. We can reference these constants as criteria in a select block to control which code gets triggered based on the path, the type of event, and even on the data type of the element that was parsed. Using these constants, we can build the basic structure of our parsing handler like this:
select;
when pType = RXS_JSON_ARRAY;
…
when pType = RXS_JSON_ARRAY_END;
…
when pType = RXS_JSON_OBJECT;
…
when pType = RXS_JSON_OBJECT_END;
…
When pType = RXS_JSON_BOOLEAN;
…
etc.
endsl;
Within each “when” selector, we will be writing code to handle the different paths that might trigger that event type. For example, we have two arrays in our JSON document – the root structure array, and the child array of phone numbers in each contact object. We want to do something special for the beginning of each of these arrays, so we can write another select block to address the different paths:
when pType = RXS_JSON_ARRAY;
select;
when pPath = '[*]';
RXS_JobLog( 'Begin Parsing…' );
RXS_JobLog( '******************************' );
RXS_JobLog( 'Contact List:' );
when pPath = '[*]/phone[*]';
RXS_JobLog( 'Contact Phone(s):' );
endsl;
We can also use the same pPath value of “[*]” within the RXS_JSON_ARRAY_END block to write out the end of the parsing operation:
when pType = RXS_JSON_ARRAY_END;
select;
when pPath = '[*]';
RXS_JobLog( '******************************' );
RXS_JobLog( 'End Parsing.' );
endsl;
For our address child object, we would like to concatenate the street address and the city and state before we print them to the job log. To accomplish this, we will use individual global fields to store the parsed address data, then write the entire formatted address to the job log during the object end event.
We’ll use the RXS_JSON_OBJECT event for the address child object to clear all of the global address component fields when the parser detects the beginning of a new address object:
when pType = RXS_JSON_OBJECT;
select;
when pPath = '[*]/address';
clear streetNbr;
clear streetName;
clear aptNbr;
clear city;
clear state;
clear zip;
endsl;
After the address fields have been parsed, the RXS_JSON_OBJECT_END event will trigger for the address block, and we can perform further processing on the address data before writing it to the job log:
when pType = RXS_JSON_OBJECT_END;
select;
when pPath = '[*]/address';
RXS_JobLog( 'Contact Address:' );
RXS_JobLog( x'05' + streetNbr + ' ' + streetName );
if %Len(aptNbr) > 0;
RXS_JobLog( x'05' + 'Apt. ' + aptNbr );
endif;
RXS_JobLog( x'05' + city + ', ' + state );
RXS_JobLog( x'05' + zip );
endsl;
Note that we’re using the hexadecimal notation for the TAB character to indent the address lines – this is just for display purposes and is not required. We are also checking the length of the “aptNbr” field, so that we only write the line when an apartment number is present in the parsed data.
Because we are retrieving our JSON data in its equivalent RPG data type, we need to make selectors for each of our data types and a corresponding variable within our handler subprocedure for each non-string data type. For example, in order to retrieve the parsed data as a boolean, we need to declare an RPG boolean BASED variable:
Dcl-S BoolVal Ind Based(pData);
Then we can use the following selector to capture a boolean variable event and retrieve the parsed data as an RPG indicator:
when pType = RXS_JSON_BOOLEAN;
select;
when pPath = '[*]/salesProspect';
if BoolVal;
RXS_JobLog( 'Sales Prospect?: Yes' );
else;
RXS_JobLog( 'Sales Prospect?: No' );
endif;
endsl;
We can retrieve integer values in a similar fashion, using a BASED variable like this:
Dcl-S IntVal Int(20) Based(pData);
Note that any integer values returned by the JSON parser will always be of this integer data type, regardless of the actual size of the number.
Our final pType selector will be for RXS_JSON_STRING events. We’ll use RXS_STR() to retrieve the parsed data to an RPG character data variable, and either store the data for further processing or print the data to the job log. Here is an example where we retrieve the value of the “name” element and print the parsed data to the job log:
select;
when pPath = '[*]/name';
clear Val;
Val = RXS_STR( pData : pDataLen );
RXS_JobLog( 'Name: ' + Val );
Now that our parsing handler subprocedure is complete, we will write the main subprocedure. In order to initialize the JSON parsing engine, we need to configure the RXS_ParseJson() operation using an RXS_ParseJsonDS_t data structure. First, we will specify the procedure address for our JSON handler subprocedure:
ParseJsonDS.Handler = %Paddr( JsonHandler );
By default, the JSON parser retrieves parsed data as character (string) data. Because we want to parse our data in its native JSON data type, we will use the ConvertDataToString field to override the automatic conversion:
ParseJsonDS.ConvertDataToString = RXS_NO;
RXS_NO is a constant representing the RPG boolean value *Off. This setting allows the parsing engine to return different JSON data type events, which correspond to the RXS_JSON_* types referenced in our parsing handler. If this setting is left as the default value of RXS_YES (*On), every data event will be returned as an RXS_JSON_STRING event, regardless of the actual data type.
We will be parsing data from a file in the IFS, and we will use the Stmf field in our RXS_ParseJsonDS_t data structure to specify the location of our JSON document.
ParseJsonDS.Stmf = '/tmp/parseJsonDemo.json';
RXS_ParseJson() accepts two parameters, the first of which is a variable containing JSON data to be parsed. Because we are reading data from a file, we will omit the first parameter:
RXS_ParseJson( *Omit : ParseJsonDS );
The parser will now read through the JSON document and call our JsonHandler subprocedure for each event that it detects. If the handler subprocedure finds a select option for the corresponding event and type, it will process the retrieved data. Otherwise, the handler subprocedure will return control to the JSON parser, which will move on to the next event.
Following the code we wrote earlier, our handler subprocedure writes the contact information to the job log. Here is the output from our program:
While this example demonstrated parsing a simple IFS file that we created manually, the same process can be used to parse JSON data that was received as a request to or response from a web service.
Does your business need to handle JSON data on your IBM i? Contact us here for a free 30 day trial of RPG API Express and to discuss how it can meet your JSON communication requirements!
Existing RPG API Express customers with a paid maintenance contract are eligible to the latest version at no additional cost – contact our support team here for more information!