JSON Parsing Using PeopleTools JsonParser

By Chris Malek | Tue, Jul 17, 2018

As I wrote in JSON Parsing using PeopleCode Classes Starting in PeopleTools 8.55.11 , there is now a built in way of generating and parsing JSON in PeopleCode. This is great and removes external dependencies like the one I released: JSONtoXML Application Class - An Alternative Method to Parse JSON in Peoplecode.

The other option was to use the PeopleTools Document technology. However, I have no kind words for the document technology. It is limited, frustrating, buggy and cannot parse dynamic content. APIs often return dynamic nested JSON objects and you need to be able to programmatically inspect and respond to the data returned. In my experience the document object could not do that. I wrote about some of those in JSON Parsing Limitations in 8.53

In this article I want to show an example using the built in JsonParser class to parse a sample JSON document. As of the writing of this article the API is NOT documented because…well….I have no idea!

Animal JSON Document

In the first few examples, we will start with this JSON that I randomly pulled it from this LearnWebCode Github repository.

Some items to notice:

  • It has a “top level array”.
    • This pattern is often returned in API’s where you are searching for data and you get back an array of matches.
  • In each array element, there are nested food objects that are JSON arrays.
[
  {
    "name": "Meowsy",
    "species" : "cat",
    "foods": {
      "likes": ["tuna", "catnip"],
      "dislikes": ["ham", "zucchini"]
    }
  },
  {
    "name": "Barky",
    "species" : "dog",
    "foods": {
      "likes": ["bones", "carrots"],
      "dislikes": ["tuna"]
    }
  },
  {
    "name": "Purrpaws",
    "species" : "cat",
    "foods": {
      "likes": ["mice"],
      "dislikes": ["cookies"]
    }
  }
]

Find Favorite Foods by Name

In this example, we parse the JSON and loop through each animal and get their name and their favorite foods.

We are also using the DataDumper package I released which allows you to quickly write debug files.

We are storing our JSON to be parsed in the message catalog 22000, 1. Of course in a real world example, you would be pulling this from a web service or perhaps a file.

Here is the code that will parse out the JSON file.

  • We loop over each root array object and populate &AnimalName with the animal name.
  • We also concatenate all the favorite foods for each animal into a common separated variable called &favoriteFoods
import CHG_DEBUG:dataDumper;

Local CHG_DEBUG:dataDumper &z = create CHG_DEBUG:dataDumper("json_unit.txt");

Local JsonParser &parser = CreateJsonParser();

Local string &jsonString = MsgGetExplainText(22000, 1, "");
Local boolean &bParseResult = &parser.Parse(&jsonString);

If &bParseResult = True Then
   /* Parse Success - Valid JSON */
   &z.dump("Parse Was Successful");
   
   Local JsonObject &jObj = &parser.GetRootObject();
   
   Local JsonArray &rootArray;
   
   &rootArray = &jObj.GetJsonArray("");
   
   If &rootArray.Length() < 0 Then
      &z.dump("root array is null");
   Else
      
      Local integer &i, &j;
      Local JsonObject &rootObject;

      Local string &AnimalName, &favoriteFoods;
      
      Local JsonObject &foodObject;
      
      Local JsonArray &jsonArrayFavoriteFoods;
      
      For &i = 1 To &rootArray.Length();
         &z.indentLevel = 0;
         
         &z.dump("&i : " | &i);
         &rootObject = &rootArray.GetJsonObject(&i);
         
         &AnimalName = &rootObject.GetAsString("name");
         &z.indentLevel = 2;
         &z.dump("&AnimalName: " | &AnimalName);
         
         &foodObject = &rootObject.GetJsonObject("foods");
         
         &jsonArrayFavoriteFoods = &foodObject.GetJsonArray("likes");
         
         
         If All(&jsonArrayFavoriteFoods) Then
            For &j = 1 To &jsonArrayFavoriteFoods.Length()
               
               If &j = 1 Then
                  &favoriteFoods = &jsonArrayFavoriteFoods.GetAsString(&j);
               Else
                  &favoriteFoods = &favoriteFoods | ", " | &jsonArrayFavoriteFoods.GetAsString(&j);
                  
               End-If;
               
            End-For;
         End-If;
         &z.indentLevel = 3;
         
         &z.dump("&favoriteFoods: " | &favoriteFoods);
         
      End-For;
   End-If;
Else
   /*  Parse Failure */
   &z.dump("Parse failed");
   
   
End-If;

The debug files that is generated is:

Parse Was Successful
&i : 1
  &AnimalName: Meowsy
   &favoriteFoods: tuna, catnip
&i : 2
  &AnimalName: Barky
   &favoriteFoods: bones, carrots
&i : 3
  &AnimalName: Purrpaws
   &favoriteFoods: mice

Extracting Booleans

Let’s look at a different example of how are can extract out JSON boolean values into PeopleSoft boolean values.

We are going to modify our JSON slightly and add a boolean property called canBark.

[
  {
    "name": "Meowsy",
    "species" : "cat",
    "canBark": false,
    "foods": {
      "likes": ["tuna", "catnip"],
      "dislikes": ["ham", "zucchini"]
    }
  },
  {
    "name": "Barky",
    "species" : "dog",
    "canBark": true,
    "foods": {
      "likes": ["bones", "carrots"],
      "dislikes": ["tuna"]
    }
  },
  {
    "name": "Purrpaws",
    "species" : "cat",
    "canBark": false,
    "foods": {
      "likes": ["mice"],
      "dislikes": ["cookies"]
    }
  }
]

Now the goal of our code is to find the names of animals that “can bark”.

import CHG_DEBUG:dataDumper;

Local CHG_DEBUG:dataDumper &z = create CHG_DEBUG:dataDumper("json_unit.txt");

Local JsonParser &parser = CreateJsonParser();

Local string &jsonString = MsgGetExplainText(22000, 1, "");
Local boolean &bParseResult = &parser.Parse(&jsonString);


If &bParseResult = True Then
   /* Parse Success - Valid JSON */
   &z.dump("Parse Was Successful");
   
   Local JsonObject &jObj = &parser.GetRootObject();
   
   Local JsonArray &rootArray;
   Local boolean &bCanBark;
   &rootArray = &jObj.GetJsonArray("");
   
   If &rootArray.Length() < 0 Then
      &z.dump("root array is null");
   Else
      
      Local integer &i;
      Local JsonObject &rootObject;
      
      Local string &AnimalName;
      
      
      For &i = 1 To &rootArray.Length();
         &z.indentLevel = 0;
         
         &z.dump("&i : " | &i);
         &rootObject = &rootArray.GetJsonObject(&i);
         
         
         &bCanBark = &rootObject.GetBooleanProperty("canBark");
         
         If &bCanBark Then
            &AnimalName = &rootObject.GetAsString("name");
            
            
            &z.indentLevel = 2;
            &z.dump("&AnimalName: " | &AnimalName | " - It can bark");
            
         End-If;
      End-For;
   End-If;
Else
   /*  Parse Failure */
   &z.dump("Parse failed");
   
   
End-If;

Debug output file

Parse Was Successful
&i : 1
&i : 2
  &AnimalName: Barky - It can bark
&i : 3

Passing Invalid JSON

If your program gets some invalid JSON, it seems that the Parse method on the JsonParser class will throw an error so you really need to add a try-catch block around any parsing code.

Here is invalid JSON document that is missing a comma after the “age” property.

{
    "name": "John",
    "age": 30
    "car": "Tesla"
}

The updated PeopleCode to respond to an invalid JSON document is:

import CHG_DEBUG:dataDumper;

Local CHG_DEBUG:dataDumper &z = create CHG_DEBUG:dataDumper("json_unit.txt");



Local JsonParser &parser = CreateJsonParser();

Local string &jsonString = MsgGetExplainText(22000, 1, "");

try
   
   If &parser.Parse(&jsonString) Then;
      /* Parse Success - Valid JSON */
      &z.dump("Parse Was Successful");
      
   Else
      /*  Parse Failure */
      &z.dump("Parse failed");
      
   End-If;
   
   
catch Exception &e
   &z.dump("****There was an Error ***");
   &z.dump(&e.ToString());
end-try;

The dump file would have contained the following output which was generated in the catch.

****There was an Error ***
Invalid token [ 8 ] at position [ 45 ], Expected tokens are: [ , ] (262,2117) CMALEK_JSON.MAIN.GBL.default.1900-01-01.Step01.OnExecute  PCPC:307  Statement:5

Parsing Dynamic JSON

Let’s try to dynamically parse a JSON that we may not know what we get back.

This example is by no means complete. The methods are completely undocumented so I have been trying to figure them out. Here we will write some code to try to dynamically extract the values. It seems we can only pull out the string values. What I can’t figure out in this example is how to look at each property and determine if it is a boolean, string, etc and then parse the value into a native PeopleCode data type. What I am looking for is something like “GetPropertyType” that I can do an evaluate on and take different actions based on the JSON value type.

{
    "first": "firstValue",
    "second": "second value",
    "third": [
        "foo",
        "bar",
        "baz"
    ],
    "forth": true,
    "fifth": 6.2,
    "sixth": null,
    "seventh": {
        "foo": "bar"
    }
}
import CHG_DEBUG:dataDumper;

Local CHG_DEBUG:dataDumper &z = create CHG_DEBUG:dataDumper("json_unit.txt");



Local JsonParser &parser = CreateJsonParser();

Local string &jsonString = MsgGetExplainText(22000, 1, "");

try
   
   If &parser.Parse(&jsonString) Then;
      /* Parse Success - Valid JSON */
      &z.dump("Parse Was Successful");
      
      
      Local JsonObject &jObj = &parser.GetRootObject();
      
      Local integer &i;
      
      Local JsonObject &jObjCurrent;
      Local string &propName, &propValue;
      For &i = 1 To &jObj.ChildCount
         &z.whiteSpace(2);
         
         &z.dump("&i = " | &i);
         
         &propName = &jObj.GetPropertyNameAt(&i);
         &z.dump("&propName : " | &propName);
         
         
         
         
         If &jObj.IsJsonArray(&propName) Then
            &z.dump("This is a json array ");
         Else
            If &jObj.IsJsonObject(&propName) Then
               &z.dump("This is a json object ");
               
            Else
               &propValue = &jObj.GetAsString(&propName);
               
               &z.dump("&propValue : " | &propValue);
            End-If;
         End-If;
      End-For;
   Else
      /*  Parse Failure */
      &z.dump("Parse failed");
   End-If;
   
   
catch Exception &e
   &z.dump("****There was an Error ***");
   &z.dump(&e.ToString());
end-try;

The output file would look something like this:

Parse Was Successful


&i = 1
&propName : first
&propValue : firstValue


&i = 2
&propName : second
&propValue : second value


&i = 3
&propName : third
This is a json array 


&i = 4
&propName : forth
&propValue : true


&i = 5
&propName : fifth
&propValue : 6.200000000000000


&i = 6
&propName : sixth
&propValue : null


&i = 7
&propName : seventh
This is a json object 

Summary

As you can see we are getting better support for JSON parsing. I would like to see some more documentation on this. This document will likely grow over time with examples.