JSON Enhancements
Tags: json, xml, database, model
Published: 2021-08-10 17:25:19

InterSystems IRIS 2019.1 has been out for a while and I would like to cover some enhancements for handling JSON which might have gone unnoticed. Dealing with JSON as a serialization format is an important part of building modern applications, especially when you interact with REST endpoints. <!--break-->

## Formatting JSON

First of all, it helps if you can format JSON to make it more human-readable. This is very handy when you have to debug your code and look at JSON content of a certain size. Simple structures are easy to scan for a human, but as soon as you have multiple nested elements it can get tricky quite easily. Here is a simple example:

<pre>{"name":"Gobi","type":"desert","location":{"continent":"Asia","countries":["China","Mongolia"]},"dimensions":{"length":1500,"length_unit":"km","width":800,"width_unit":"km"}}</pre>

A more human-readable format makes it easy to explore the structure of the content. Let’s take a look at the same JSON structure, but this time with proper line breaks and indentation:

<pre>{
  "name":"Gobi",
  "type":"desert",
  "location":{
    "continent":"Asia",
    "countries":[
      "China",
      "Mongolia"
    ]
  },
  "dimensions":{
    "length":1500,
    "length_unit":"km",
    "width":800,
    "width_unit":"km"
  }
}</pre>

Even this simple example blows up the output by quite a bit, so you can see why this is not the default in many systems. But with this verbose formatting, you can easily spot substructures and get a sense if something is wrong.

InterSystems IRIS 2019.1 introduced a package by the name %JSON. You can find a couple of useful utilities here, one being a formatter, which allows you to achieve exactly what you have seen above: Format your dynamic objects and arrays and JSON strings into a more human-readable representation. <span style="font-family:Courier New,Courier,monospace;">%JSON.Formatter</span> is a class with a very simple interface. All methods are instance methods, so you always start by retrieving an instance.

<pre>USER>set formatter = ##class(%JSON.Formatter).%New()</pre>

The reason behind this choice is that you can configure your formatter to use certain characters for the indentation (e.g. whitespaces vs. tabs) and line terminators once and then use it wherever you need it.

The method <span style="font-family:Courier New,Courier,monospace;">Format()</span> takes either a dynamic object/array or a JSON string. Let’s look at a simple example using a dynamic object:

<pre>USER>do formatter.Format({"type":"string"})
{
  "type":"string"
}</pre>

And here is an example with the same JSON content, but this time represented as a JSON string:

<pre>USER>do formatter.Format("{""type"":""string""}")
{
  "type":"string"
}</pre>

The <span style="font-family:Courier New,Courier,monospace;">Format()</span> method outputs the formatted string to the current device, but you will also see the methods <span style="font-family:Courier New,Courier,monospace;">FormatToString()</span> and <span style="font-family:Courier New,Courier,monospace;">FormatToStream() </span>in case you want to direct the output to a variable.

## Shifting Gears

The above is nice, but may not be worth an article by itself. InterSystems IRIS 2019.1 also introduces a way to conveniently serialize persistent and transient objects from and to JSON. The class you want to take a look at is <span style="font-family:Courier New,Courier,monospace;">%JSON.Adaptor</span>. The concept is very similar to <span style="font-family:Courier New,Courier,monospace;">%XML</span><span style="font-family:Courier New,Courier,monospace;">.Adaptor</span>, hence the name. Any class you would like to serialize from and to JSON needs to subclass <span style="font-family:Courier New,Courier,monospace;">%JSON.Adaptor</span>. The class will inherit a couple of handy methods, most noteworthy are <span style="font-family:Courier New,Courier,monospace;">%JSONImport()</span> and <span style="font-family:Courier New,Courier,monospace;">%JSONExport()</span>. It’s best to demonstrate this with an example. Assume we have the following classes:

<pre>Class Model.Event Extends (%Persistent, %JSON.Adaptor)
{
 Property Name As %String;
 Property Location As Model.Location;
}</pre>

and

<pre>Class Model.Location Extends (%Persistent, %JSON.Adaptor)
{
 Property City As %String;
 Property Country As %String;
}</pre>

As you can see, we have a persistent event class, which links to a location. Both classes inherit from <span style="font-family:Courier New,Courier,monospace;">%JSON.Adaptor</span>. This enables us to populate an object graph and directly export it as a JSON string:

<pre>USER>set event = ##class(Model.Event).%New()
 
USER>set event.Name = "Global Summit"
 
USER>set location = ##class(Model.Location).%New()
 
USER>set location.City = "Boston"
 
USER>set location.Country = "United States of America"
 
USER>set event.Location = location
 
USER>do event.%JSONExport()
{"Name":"Global Summit","Location":{"City":"Boston","Country":"United States of America"}}</pre>

Of course, you can also go the other direction with %JSONImport():

<pre>USER>set jsonEvent = {"Name":"Global Summit","Location":{"City":"Boston","Country":"United States of America"}}
 
USER>set event = ##class(Model.Event).%New()
 
USER>do event.%JSONImport(jsonEvent)
 
USER>write event.Name
Global Summit
USER>write event.Location.City
Boston</pre>

The import and export methods work for arbitrarily nested structures. Similar to the <span style="font-family:Courier New,Courier,monospace;">%XML.Adaptor</span> you can specify the mapping logic for each individual property by setting corresponding parameters. Let’s change the Model.Event class to the following definition:

<pre>Class Model.Event Extends (%Persistent, %JSON.Adaptor)
{
 Property Name As %String(%JSONFIELDNAME = "eventName");
 Property Location As Model.Location(%JSONINCLUDE = "INPUTONLY");
}</pre>

Assuming we have the same object structure assigned to the variable event as in the example above, a call to <span style="font-family:Courier New,Courier,monospace;">%JSONExport()</span> would return the following result:

<pre>USER>do event.%JSONExport()
{"eventName":"Global Summit"}</pre>

The property <span style="font-family:Courier New,Courier,monospace;">Name</span> is mapped to the field name <span style="font-family:Courier New,Courier,monospace;">eventName</span> and the <span style="font-family:Courier New,Courier,monospace;">Location</span> property is excluded from the <span style="font-family:Courier New,Courier,monospace;">%JSONExport()</span> call, but will be populated when present in the JSON content during a <span style="font-family:Courier New,Courier,monospace;">%JSONImport()</span> call. There are various parameters available to allow you to tweak the mapping:

  * <span style="font-family:Courier New,Courier,monospace;">%JSONFIELDNAME</span> corresponds to the field name in the JSON content.
  * <span style="font-family:Courier New,Courier,monospace;">%JSONIGNORENULL</span> allows the developer to override the default handling of empty strings for string properties.
  * <span style="font-family:Courier New,Courier,monospace;">%JSONINCLUDE </span>controls if this property will be included in the JSON output/input.
  * If <span style="font-family:Courier New,Courier,monospace;">%JSONNULL</span> is true (=1), then unspecified properties are exported as the null value. Otherwise, the field corresponding to the property is just skipped during export.
  * <span style="font-family:Courier New,Courier,monospace;">%JSONREFERENCE</span> specifies how object references are treated. "<span style="font-family:Courier New,Courier,monospace;">OBJECT</span>" is the default and indicates that the properties of the referenced class are used to represent the referenced object. Other options are "<span style="font-family:Courier New,Courier,monospace;">ID</span>", "<span style="font-family:Courier New,Courier,monospace;">OID</span>" and "<span style="font-family:Courier New,Courier,monospace;">GUID</span>".

This provides you with a high level of control and is very handy. Gone are the days of manually mapping your objects to JSON.

## One more thing

Instead of setting the mapping parameters on the property level you can also define a JSON mapping in an XData block. The following XData block with the name <span style="font-family:Courier New,Courier,monospace;">OnlyLowercaseTopLevel</span> has the same settings as our event class above.

<pre>Class Model.Event Extends (%Persistent, %JSON.Adaptor)
{
 Property Name As %String;
 Property Location As Model.Location;
 XData OnlyLowercaseTopLevel
 {
 &amp;lt;Mapping xmlns="http://www.intersystems.com/jsonmapping">
     &amp;lt;Property Name="Name" FieldName="eventName"/>
     &amp;lt;Property Name="Location" Include="INPUTONLY"/>
 &amp;lt;/Mapping>
 }
}</pre>

There is one important difference: JSON mappings in XData blocks do not change the default behavior, but you have to reference them in the corresponding <span style="font-family:Courier New,Courier,monospace;">%JSONImport()</span> and <span style="font-family:Courier New,Courier,monospace;">%JSONExport()</span> calls as the last argument, e.g.:

<pre>USER>do event.%JSONExport("OnlyLowercaseTopLevel")
{"eventName":"Global Summit"}</pre>

If there is no XData block with the provided name, the default mapping will be used. With this approach, you can configure multiple mappings and reference the mapping you need for each call individually, granting you even more control while making your mappings more flexible and reusable.

I hope these enhancements make your life easier and I am looking forward to your feedback. Let me know what you think in the comments.