Serialise Caché Object to JSON String
From CacheWiki
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.
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))
}
}