Serialise Caché Object to JSON String

Introduction
This article will provide an example of how a in-memory Caché Object can be serialised into a 'JavaScript ObjectScript Notation (JSON)' string. It has been written with Caché 2010. For more information on JSON, follow the hyperlinks below.

JSON for the masses JSON - Wikipedia, the free encyclopedia JSON - Introduction by Microsoft http://json.org/

Example
The following provides and example of how to create a JSON string from an in-memory object, using the 'Sample.Person' class in the 'SAMPLES' namespace.

Set person=##class(Sample.Person).%OpenId(3) Set json=##class(Utils.JSON).%New Set json.Pretty=1 Do json.SerialiseObject(person) Do json.Stream.OutputToDevice

The 'Pretty' property is optional and if not defined all whitespace is removed. The example script produces the following output.

{  "ID": 3, "Age": 70, "DOB": "03\/05\/1941", "FavoriteColors": [ "Purple", "Yellow" ],  "Home": { "City": "Queensbury", "State": "MO", "Street": "3588 Main Court", "Zip": "59808" },  "Name": "Xerxes,Lola J.", "Office": { "City": "Albany", "State": "ID", "Street": "1718 Clinton Court", "Zip": "71643" },  "SSN": "511-57-4430", "Spouse": null }

You can use the following web tool to validate this output.

JSONLint - The JSON Validator

Code
The code to serialise a Caché Object is shown below.

Class Utils.JSON Extends %RegisteredObject {

Property Stream As %Stream.GlobalCharacter;

Property Pretty As %Boolean [ InitialExpression = 0 ];

Property Indent As %Integer [ InitialExpression = 0, Private ];

Property Deep As %Integer [ Private ];

Property Last As %String [ Private ];

Method SerialiseObject(obj As %RegisteredObject) As %Status {	// check if 'obj' is an object and get class name If '$IsObject(obj) Quit $$$ERROR($$$GeneralError, "Not an object instance.") // look at all the class properties Quit ..ObjectMembers(obj) }

Method Boolean(bool As %String) As %Status {	If $IsValidNum(bool) { Quit ..Write($Select(bool:"true", 1:"false")) }	Set bool=$ZConvert(bool, "L") Quit ..Write($Select(bool="true":"true", 1:"false")) }

Method Number(num As %Numeric) As %Status {	If num="" Quit ..Write("null") Quit ..Write($Number(num)) }

Method String(str As %String) As %Status {	If str="" Quit ..Write("null") Quit ..Write($Char(34)_..Escape(str)_$Char(34)) }

Method StartObject As %Status {	Quit ..Start("{") }

Method NewMember(str As %String) As %Status {	Quit ..New(str) }

Method EndObject As %Status {	Quit ..End("}") }

Method StartArray As %Status {	Quit ..Start("[") }

Method NewElement As %Status {	Quit ..New }

Method EndArray As %Status {	Quit ..End("]") }

Method Start(str) As %Status [ Private ] {	If ..Pretty { If '((..Last=":") || (..Last=" ")) { If ..Indent Do ..Stream.WriteLine Do ..Write($Justify("", ..Indent)) }		Quit ..Write(str_" ", 2) }	Quit ..Write(str_" ") }

Method New(str) As %Status [ Private ] {	If '((..Last="{") || (..Last="[")) Do ..Write(", ") If ..Pretty { Do ..Stream.WriteLine Do ..Write($Justify("", ..Indent)) }	If $Data(str) { Do ..String(str) Quit ..Write(": ") }	Quit $$$OK }

Method End(str) As %Status [ Private ] {	If ..Pretty { Do ..WriteLine("", -2) Quit ..Write($Justify("", ..Indent)_str) }	Quit ..Write(" "_str) }

Method Write(str As %String, ind As %Integer) As %Status [ Private ] {	Set ..Last=$Extract(str, $Length(str)-1) If $Data(ind) Set ..Indent=..Indent+ind Quit ..Stream.Write(str) }

Method WriteLine(str As %String, ind As %Integer) As %Status [ Private ] {	Set ..Last=$Extract(str, $Length(str)-1) If $Data(ind) Set ..Indent=..Indent+ind Quit ..Stream.WriteLine(str) }

Method Escape(str As %String) As %String [ Private ] {	/*		http://www.json.org/ " - quotation mark (34)		b - backspace (8)		f - formfeed (12)		n - newline (10)		r - carriage return (13)		t - horizontal tab (9)	*/	For list="\\", "//", $C(34)_$C(34), $C(8)_"b", $C(12)_"f", $C(10)_"n", $C(13)_"r", $C(9)_"t" {		While (str'="") && (str[$Extract(list)) {			Set str=$Piece(str, $Extract(list))_$Char(2,3)_$Piece(str, $Extract(list), 2, 999)		}		If str[$Char(3) Set str=$Translate(str, $Char(2, 3), "\"_$Extract(list, 2))	}	// return 'escaped' value	Quit str }

Method ObjectMembers(obj As %RegisteredObject, key As %String = "") As %Status [ Private ] {

// [Documentation] > [Development Guides] > [Using Caché Objects] > [Class Definition Classes] // http://docs.intersystems.com/cache20102/csp/docbook/DocBook.UI.Page.cls?KEY=GOBJ_classdef // check stream size If ..Stream.Size>1048576 Quit $$$ERROR($$$GeneralError, "Stream is too large (over a MB).")

// start a JSON object Do ..StartObject // define the fully qualified class name Set classname=obj.%ClassName(1)

// output object id if a persistent class // (class types: persistent, serial, stream, datatype) Set type=##class(%Dictionary.CompiledClass).%OpenId(classname).ClassType If type="persistent" { Set id=obj.%Id Do ..NewMember("ID") Do $Case($IsValidNum(id), 1:..Number(id), :..String(id)) }

// new JSON object member (if collection) If $Length(key) { Do ..NewMember(obj.%ClassName_"_Key") Do $Case($IsValidNum(key), 1:..Number(key), :..String(key)) }	// instantiate the relevant class definition object Set cdef=##class(%Dictionary.ClassDefinition).%OpenId(classname)

// work through the class properties Set count = cdef.Properties.Count For i=1:1:count { // define property object Set prop=cdef.Properties.GetAt(i) // create a new object member Do ..NewMember(prop.Name) // get property value Set value=$Property(obj, prop.Name) // collection property? Do $Case(prop.Collection, 			"list": ..ListCollection(obj, prop, value),			"array": ..ArrayCollection(obj, prop, value),			: ..ObjectProperty(obj, prop, value)) }	// end a JSON object Do ..EndObject // end of method Quit $$$OK }

Method ListCollection(obj As %RegisteredObject, prop As %RegisteredObject, value As %CacheString) As %Status [ Private ] {	// only start array if values present Set count=value.Count If count { // start a JSON array Do ..StartArray // work through array For i=1:1:count { // start new array element Do ..NewElement

// get the array entry Set entry=$Method(value, "GetAt", i)			// output property value Do ..ObjectProperty(obj, prop, entry) }

// end a JSON array Do ..EndArray }	// end of method Quit $$$OK }

Method ArrayCollection(obj As %RegisteredObject, prop As %RegisteredObject, value As %CacheString) As %Status [ Private ] {

// only start array if values present Set key=$Method(value, "Next", "") If key'="" { // start a JSON array Do ..StartArray // work through array While key'="" { // start new array element Do ..NewElement

// get the array entry Set entry=$Method(value, "GetAt", key) // output property value Do ..ObjectProperty(obj, prop, entry, key) // read next key Set key=$Method(value, "Next", key) }

// end a JSON array Do ..EndArray }	// end of method Quit $$$OK }

Method ObjectProperty(obj As %RegisteredObject, prop As %RegisteredObject, value As %CacheString, key As %String = "") As %Status [ Private ] {	// recurse if property value is an object If $IsObject(value) { Quit ..ObjectMembers(value, key) }	// not an object, so get data type Set type=prop.Type // TODO: how do we test for LogicalToDisplay method on 'datatype' class properly? If $Extract(type)="%", $Piece(type, ".")'="%Library" Set $Extract(type)="%Library." Set mdef=##class(%Dictionary.MethodDefinition).%OpenId(type_"||LogicalToDisplay") If type'["Integer", $IsObject(mdef) Set value=$Method(obj, prop.Name_"LogicalToDisplay", value) // output data type value Quit $Case(type, 		"%Library.Boolean": ..Boolean(value), 		"%Library.Integer": ..Number(value),		"%Library.Numeric": ..Number(value),		"%Library.Float": ..Number(value),		"%Library.Double": ..Number(value),		"%Library.Currency": ..Number(value),		: ..String(value)) }

}