Serialise Caché Object to JSON String

From CacheWiki
Revision as of 21:31, 26 April 2011 by DavidWhitten (Talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

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))
		
}

}
Personal tools