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 json and jsonarray values from strings
json
and jsonarray
variables can be populated by casting from strings. See, for example, how this can be done for string literals using multiline string declaration 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. In addition, if the values are constructed "by hand", there is a high probability of undesirable runtime errors.
Another approach for constructing JSON values in a DSL app, is to make use of objects and the existing built-in functions namely jsonGet, jsonPut, jsonRemove, jsonKeys, jsonContains.
See the sections below for details.
Creating json and jsonarray values from objects
json
and jsonarray
variables can be populated by casting from objects and object collections respectively.
Consider the following model object:
object Person { string name; string surname; date dob; json contactDetails; }
Casting to json can be done implicitly as follows:
Person person = getPersonFromDb(...); json personJson = person;
The above will create the following example json:
{ "_id": "b9bf8965-087f-47a1-8b9b-ab77a01a494d" "name": "John", "surname": "Doe", "dob": 1738846993000, "contactDetails": { "phoneNumber": "555-6162", "emailAddress": "john.doe@gmail.com" } }
Note that the object id is included under the property with name _id.
Relationships (many to one or one to one) will be included and the value or the related object's internal id will be used.
Similarly a jsonarray value can be created by using an object collection:
Person[] people = getPeopleFromDb(); jsonarray peopleJson = people;
Built-in methods for manipulating json
In order to use any of the built-in methods for json types, 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 of an empty json object, the implicit casting between string
and json can be used as follows:
json jsonPersonObject = "{}";
jsonPut
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
property, followed by the value for that property.
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);
jsonGet
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");
jsonContains
jsonContains can be used to determine whether a specific property is already present in the json variable.
// JSON representing contact details json jsonContactDetails = "{}"; jsonContactDetails.jsonPut("phoneNumber", "555-6162"); jsonContactDetails.jsonPut("emailAddress", "john.smith@gmail.com"); // This will evaluate to true since the property is present bool containsPhoneNumber = jsonContactDetails.jsonContains("phoneNumber"); // This will evaluate to false since the property is not present bool containsNickname = jsonContactDetails.jsonContains("nickname");
jsonRemove
To remove an individual property from a json variable, the jsonRemove built-in function can be used:
// JSON representing contact details json jsonContactDetails = "{}"; jsonContactDetails.jsonPut("phoneNumber", "555-6162"); jsonContactDetails.jsonPut("emailAddress", "john.smith@gmail.com"); // This will evaluate to true since the property is present bool containsPhoneNumber = jsonContactDetails.jsonContains("phoneNumber"); // This will remove the mobile number property jsonContactDetails.jsonRemove("phoneNumber"); // This will now evaluate to false since the property has been removed containsPhoneNumber = jsonContactDetails.jsonContains("phoneNumber");
jsonKeys
To get a collection of json property names for a json variable, the jsonKeys built-in function can be used:
// JSON representing contact details json jsonContactDetails = "{}"; jsonContactDetails.jsonPut("phoneNumber", "555-6162"); jsonContactDetails.jsonPut("emailAddress", "john.smith@gmail.com"); // This will return a collection of strings representing the property names // ["phoneNumber", "emailAddress"] string[] keys = jsonContactDetails.jsonKeys(); // Keys can be used to iterate over all the properties foreach(string key: keys) { string value = jsonContactDetails.jsonGet(key); }
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.
Manipulating jsonarray values
At present, no built-in functions are provided to help with the construction of jsonarray
variables. Implicit casting between any primitive or object collection and jsonarray
and between collections 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;
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
or jsonarray attribute.
If the NotTracked annotation is not included for persistent objects containing json
or jsonarray 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. Both json and jsonarray types will be represented on PostgreSQL as jsonb.
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
or
jsonarray
attributes. For this reason, any persistent objects with json
or
jsonarray
attributes need 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.