Native JSON Types
Jacques Marais
Description
Helium provides two native JSON types, namely json
 and jsonarray.
 These types allow developers to represent data in a manner that can, under certain circumstances, be more flexible than only making use of persistent or non-persistent objects. This document details how values for these types can be set and manipulated as well as typical use cases where they might be useful.
Creating and Manipulating JSON
Creating json and jsonarray values from strings
The simplest way to populate a json
 or jsonarray
 variable is to make use of implicit casting from string
 values to the json
 and jsonarray
 types. With the multiline string declaration this can be achieved as follows:
json jsonPerson = /% { "name": "Jack", "surname": "Marques", "phoneNumber": "555-6162" } %/;
jsonarray jsonPeople = /% [ { "name": "Jack", "surname": "Marques", "phoneNumber": "555-6162" }, { "name": "Bevin", "surname": "Sharp", "phoneNumber": "555-0123" } ] %/;
Note, that no compile time validation is done to validate that the values being converted represents correctly formatted JSON. This means that if any such conversion issues were to arise, it will only be at runtime. Any conversion errors will result in an error message being logged to the Helium Logging Service:
< {"id":"ca7963c5-4d5d-44e9-ba65-dcbb68127b9f","key":"Runtime Error","value":"[PersonResourceV2:23] {\n \"name\": \"Jack\",\n \"surname\": \"Marques\",\n \"phoneNumber\": \"555-6162\n } could not be converted to a com.mezzanine.program.web.core.type.JsonType@577db227 value","millis":1536562229362,"appId":"a7c76024-b379-49e4-a3c0-bd2ef30d28ab","appUserId":null}
In addition to the above conversion error, the result of the conversion will be null
. This might lead to unexpected null pointer exceptions if not handled appropriately in DSL apps.
Although casting from string
 values to json
 and jsonarray
 might be quick and useful in some specific cases, its uses might be limited and, if the values are constructed "by hand", there is a high probability of undesirable runtime errors.Â
A better approach for constructing JSON values in a DSL app, is to use the supplied json
 manipulation built-in functions namely jsonPut
 and jsonGet
.
Manipulating json values using jsonPut and jsonGet
In order to use jsonPut
 and jsonGet
 a json
 variable needs to be declared and instantiated. If the variable is not instantiated, any subsequent operations might lead to null pointer exceptions. For instantiation, the implicit casting between string
 and json
 can again be utilised:
json jsonPersonObject = "{}";
jsonPut
 can be used to add fields and values to a json
 variable:
jsonPersonObject.jsonPut("name", "John"); jsonPersonObject.jsonPut("surname", "Smith"); jsonPersonObject.jsonPut("luckyNumber", 13); jsonPersonObject.jsonPut("dob", Date:fromString("1936-05-02")); jsonPersonObject.jsonPut("age", (Date:daysBetween(Date:fromString("1936-05-02"), Mez:now()))/365); jsonPersonObject.jsonPut("gender", GENDER.Male); jsonPersonObject.jsonPut("height", 1.83); jsonPersonObject.jsonPut("militaryService", true);
The arguments required for jsonPut
 is firstly the name of the json
 field, followed by the value for that field.
json
 values can also be nested:
// JSON representing contact details json jsonContactDetails = "{}"; jsonContactDetails.jsonPut("phoneNumber", "555-6162"); jsonContactDetails.jsonPut("emailAddress", "john.smith@gmail.com"); Â // Adding contact details JSON to a JSON person jsonPersonObject.jsonPut("contactDetails", jsonContactDetails);
Similarly, jsonGet
 can be used to retrieve values:
string name = jsonPersonObject.jsonGet("name"); string surname = jsonPersonObject.jsonGet("surname"); string phoneNumber = jsonPersonObject.jsonGet("phoneNumber"); int luckyNumber = jsonPersonObject.jsonGet("luckyNumber"); date dob = jsonPersonObject.jsonGet("dob"); int age = jsonPersonObject.jsonGet("age"); GENDER gender = jsonPersonObject.jsonGet("gender"); decimal height = jsonPersonObject.jsonGet("height"); bool milService = jsonPersonObject.jsonGet("militaryService"); json contactDetails = jsonPersonObject.jsonGet("contactDetails");
In the code segment above, note the implicit casting for the values being retrieved.
To reference a value that is nested more that one level deep, for example a person's contact detail phone number from the example above, one needs to make use of multiple jsonGet
 statements and intermediate variables. Chaining references to jsonGet
 is not valid syntax in the Helium DSL.
// First we get the contact details json object and assign it to an intermediate variable json contactDetails = jsonPersonObject.jsonGet("contactDetails"); Â // Now we can get specific field values for the contact details string phoneNum = contactDetails.jsonGet("phoneNumber"); string emailAddr = contactDetails.jsonGet("emailAddress");
Manipulating jsonarray values
In the above example we added a single contact detail object to a person where each field represents a channel of communication. Consider an example where we want to add multiple contact details for a person. For example, a collection of emergency contacts.
At present, no built-in functions are provided to help with the construction of jsonarray
 variables. Implicit casting between any primitive array and jsonarray
 and between arrays of json
 and jsonarray
 is, however, provided:
// Create parent as emergency contact json parentEmergencyContact = "{}"; parentEmergencyContact.jsonPut("description", "parent"); parentEmergencyContact.jsonPut("mobileNumber", "27763300000"); Â // Create spouse as emergency contact json spouseEmergencyContact = "{}"; spouseEmergencyContact.jsonPut("description", "spouse"); spouseEmergencyContact.jsonPut("mobileNumber", "27763300001"); Â // Create a json[] of the above json[] emergencyContacts; emergencyContacts.append(parentEmergencyContact); emergencyContacts.append(spouseEmergencyContact); Â // Cast it to jsonarray so we can add it to the json object jsonarray emergencyContactsArray = emergencyContacts; Â // Add it to the person object jsonPersonObject.jsonPut("emergencyContacts", emergencyContactsArray);
To retrieve the individual values one can make use of jsonGet
 and implicit casting to json[]
:
// Retrieve the emergency contacts jsonarray retrievedEmergencyContacts = jsonPersonObject.jsonGet("emergencyContacts"); Â // Cast back to json[] json[] convertedEmergencyContacts = retrievedEmergencyContacts; Â // Iterate over or reference as normal DSL collection foreach(json currentEmergencyContact: convertedEmergencyContacts) { . . . } Â json firstEmergencyContact = convertedEmergencyContacts.get(0);
Note that casting from jsonarray
 to json[]
 is a relatively expensive operation and if possible, it should be avoided for large arrays.
The example below describes the same concepts as discussed above.
json pet1 = /% { "name": "Jasmine", "type": "Dog" } %/; json pet2 = /% { "name": "Markus", "type": "Mole-rat" } %/; Â json[] petsArray; petsArray.append(pet1); petsArray.append(pet2); Â // json[] converted to jsonarray jsonarray petsJsonArray = petsArray; Â // jsonarray converted json[] json[] convertedPetsArray = petsJsonArray;
In the above examples the implicit casting between json[]
 and jsonarray
 is shown. Casting to primitive arrays is also supported:
int[] numbersPrimitiveArray; numbersPrimitiveArray.append(1); numbersPrimitiveArray.append(2); numbersPrimitiveArray.append(3); Â // int[] converted to jsonarray jsonarray numbersJsonArray = numbersPrimitiveArray; Â // jsonarray converted back to int[] int[] numbersPrimitiveArray2 = numbersJsonArray;
// string representing a number array in json format converted to jsonarray jsonarray primeNumbersJsonArray = /%[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]%/; Â // jsonarray converted to int[] int[] primeNumbersPrimitiveArray = primeNumbersJsonArray;
Reading json values from the app schema
 At present, only json
is supported as valid object attribute type. Values for json
types are represented in the app schema by the PostgresSQL jsonb type and can be retrieved in any way that other attribute values can.
No compile time validation is done when converting string
values to json
or jsonarray
. This means that if any incorrectly formatted JSON is used, it will result in a runtime error. These errors will be logged and can be inspected using the Helium Logging Service.
Helium does not support chaining of the jsonGet
built-in function. To get values for a json
field that is more than one level deep in a top level json
object, multiple jsonGet
statements and intermediate variables need to be used.
JSON Values as Object Attributes
Lets consider a different example where a Person
 object is created to represent most of the attributes of a person while including a single json
 attribute to represent the contact details of a person:
@NotTracked persistent object Person { string name; string surname; date dob; json contactDetails; }
Currently integration between Helium and Journey does not support JSON types. Note the inclusion of the NotTracked annotation above to avoid any possible syncing for this object. This is compulsory for any persistent object that contains a json
 attribute. If the NotTracked annotation is not included for persistent objects containing json
 attributes, a compiler error will generated:
Loading source files... Compiling the application... |--------------------------------------------------------------------------------------------------------------------------------------------| | The module "Web" generated the following errors | |--------------------------------------------------------------------------------------------------------------------------------------------| | # | Message | |--------------------------------------------------------------------------------------------------------------------------------------------| | 1 | [Person:2] The custom object Person is persistent, thus needs to be marked with @NotTracked if json attribute types are declared. | |--------------------------------------------------------------------------------------------------------------------------------------------|
Inspecting the database table created by Helium for the Person object, reveals the contactdetails column represented by the jsonb type in PostgreSQL:
helium-app-1=# \d person Table "snapshot_1535373223392_001.person" Column | Type | Modifiers ----------------+-----------------------------+-------------------------------------------------------------- _id_ | uuid | not null _tstamp_ | timestamp without time zone | not null person | text | _tx_id_ | bigint | not null default txid_current() _change_type_ | __he_obj_change_type__ | not null default 'create'::__he_obj_change_type__ _change_seq_ | bigint | not null default nextval('__he_sync_change_seq__'::regclass) name | text | surname | text | dob | date | contactdetails | jsonb | Indexes: "person_pkey" PRIMARY KEY, btree (_id_) "__idx_he_sync_person_txid__" btree (_tx_id_)
Any method of populating attributes in general is also applicable to JSON attributes. This includes the use of database functions.
Helium Journey integration does not currently support objects with json
attributes. For this reason, any persistent object with json
attributes needs to be annotated with NotTracked. Failure to do so will result in a compiler error.
Using Native JSON with an Inbound API
The native JSON types json
 and jsonarray,Â
as discussed in this document, are supported as valid return types for any inbound API function. They are also supported as valid parameters for put and post API functions. More details regarding the inbound API annotations and supported parameter and return types for API functions can be found here.