Question
· Jun 10, 2021

%JSONImport: How to import JSON object with an array item that can be "anyof" 3 different schemas

Hello,

I am writing a POST API using IRIS. My POST API Endpoint invokes a Business Service -> Business Process -> Business Operation in an IRIS production .

I am trying to import the JSON payload into a JSON enabled class and work with the JSON class in my Business Process and invoke different Business operation(s) based on the data supplied. This works fine for simpler JSON schemas.

The POST API I am writing now needs to handle a complex schema. I.e. one of the Item on my JSON schema ("recipient") can be an array of "anyof" 5 different schemas.

e.g.

Here, recipient can be an array of any of the 5 different schemas like Reference, Contact, Patient, Practitioner or PractitionerRole.

So in my JSON enabled class, representing this particular schema, I tried defining Property recipient as list of %DynamicObject; but when I then try to import the JSON object in this class using %JSONImport, it fails because %DyanmicObject doesn't have %JSONNew method.

  0 JŠu<METHOD DOES NOT EXIST>%JSONImportInternal+359^mapi.core.msg.shared.Communication.1 *%JSONNew,%Library.DynamicObjectÉMAPIÀA^%JSONImportInternal+359^mapi.core.msg.shared.Communication.1^1$e^%JSONImport+12^%JSON.Adaptor.1^1,e^zPOSTComminucation+104^mah.utils.JWT.1^2&d^zDebugStub+40^%Debugger.System.1^1d^^^0
 

Has anyone done anything similar before? What should I use as "type" for my recipient property ?

Appreciate any ideas or input.

Regards,

Utsavi

Product version: IRIS 2020.1
Discussion (10)1
Log in or sign up to continue

This is a little messy and I'm going to report part of the answer as a bug internally. But regardless, here's one way to make it work - in short, have all of the things that could be listed as a recipient extend a common parent class, and in that class override %JSONNew to detect which type it is.

Class DC.Demo.Container Extends (%RegisteredObject, %JSON.Adaptor)
{
Property recipient As DC.Demo.Recipient;
ClassMethod Demo()
{
    for json = {"recipient":{"dob":"2021-06-10"}}, {"recipient":{"reference":"foo"}} {
        set inst = ..%New()
        do inst.%JSONImport(json)
        write !,json.%ToJSON(),!,$classname(inst.recipient),!
    }
}
}

Class DC.Demo.Recipient Extends (%RegisteredObject, %JSON.Adaptor)
{
/// Get an instance of an JSON enabled class.<br><br>
/// 
/// You may override this method to do custom processing (such as initializing
/// the object instance) before returning an instance of this class.
/// However, this method should not be called directly from user code.<br>
/// Arguments:<br>
///     dynamicObject is the dynamic object with thee values to be assigned to the new object.<br>
///     containerOref is the containing object instance when called from JSONImport.
ClassMethod %JSONNew(dynamicObject As %DynamicObject, containerOref As %RegisteredObject = "") As %RegisteredObject
{
    // This is weird: shouldn't need to reference .recipient here
    if dynamicObject.recipient.%IsDefined("dob") {
        quit ##class(DC.Demo.Patient).%New()
    } elseif dynamicObject.recipient.%IsDefined("reference") {
        quit ##class(DC.Demo.Reference).%New()
    } else {
        quit ..%New()
    }
}
}

Class DC.Demo.Reference Extends DC.Demo.Recipient
{
Property reference As %String;
}

Class DC.Demo.Patient Extends DC.Demo.Recipient
{
Property dob As %Date;
}

Output is:

 d ##class(DC.Demo.Container).Demo()
{"recipient":{"dob":"2021-06-10"}}
DC.Demo.Patient
{"recipient":{"reference":"foo"}}
DC.Demo.Reference

Only problem is, %JSONNew (as advertised in class reference documentation) should get the %DynamicObject representing the object itself, not the parent %DynamicObject. This would only really work if each type is used in exactly one context like this, which seems unlikely.

Hi

I believe that I have a solution for this.

I worked on the basis that there is a 'Parent' object that has a property Schemas of type  %ArrayOfObjectsWithClassName. This allows you to create an array of Objects where 'key' is the schema name and the 'id' is the instance.%Oid()

I then defined 4 classes:

Reference, Contact, Patient, Practitioner

I then created a method to Build N instances of the ParentClass. That code reads as follows:

ClassMethod BuildData(pCount As %Integer = 1) As %Status
{
    set tSC=$$$OK
    set array(1)="DFI.Common.JSON.Contact"
    set array(2)="DFI.Common.JSON.Patient"
    set array(3)="DFI.Common.JSON.Practitioner"
    set array(4)="DFI.Common.JSON.Reference"
    try {
        for i=1:1:pCount {
            set obj=##class(DFI.Common.JSON.ParentClass).%New()
            set obj.Schemas.ElementType="%Persistent"
            set count=$r(10)
            for j=1:1:count {
                 set k=$r(4)+1
                 set schema=$classmethod(array(k),"%New"),tSC=schema.%Save() quit:'tSC

                 do obj.Schemas.SetObjectAt(schema.%Oid(),$p(array(k),".",4))
             }
        set tSC=obj.%Save() quit:'tSC
        }
    }
    catch ex {set tSC=ex.AsStatus()}
    write !,"Status: "_$s(tSC:"OK",1:$$$GetErrorText(tSC))
    quit tSC
}

Initially I wanted to see if I could (a) insert different object types into the Array and (b) Export the Parent Object to JSON and so to make life easier I specified [ initialexpression = {some expression}] to generate a value for the field. Sort of like %Populate would do but I didn't want to pre-create instances in the 4 schema tables and then manually go and link them together.

When I ran my Method to create 10 Parents it created them and as you can see in the logic I generate a random number of schemas.

That all worked and I then exported to JSON to String resulting in this:

{"%seriesCount":"1","parentId":"Parent36","parentName":"Example: 38","schemas":{"Contact_1":{"%seriesCount":"1","contactGivenName":"Zeke","contactSurname":"Zucherro"},"Contact_11":{"%seriesCount":"1","contactGivenName":"Mark","contactSurname":"Nagel"},"Contact_3":{"%seriesCount":"1","contactGivenName":"Brendan","contactSurname":"King"},"Contact_8":{"%seriesCount":"1","contactGivenName":"George","contactSurname":"O'Brien"},"Patient_10":{"%seriesCount":"1","patientId":"PAT-000-251","patientDateOfBirth":"2021-05-05T03:38:33Z"},"Patient_2":{"%seriesCount":"1","patientId":"PAT-000-401","patientDateOfBirth":"2017-09-30T21:56:00Z"},"Patient_4":{"%seriesCount":"1","patientId":"PAT-000-305","patientDateOfBirth":"2019-04-19T14:04:11Z"},"Patient_5":{"%seriesCount":"1","patientId":"PAT-000-366","patientDateOfBirth":"2017-07-03T18:57:58Z"},"Patient_7":{"%seriesCount":"1","patientId":"PAT-000-50","patientDateOfBirth":"2016-11-26T03:39:36Z"},"Patient_9":{"%seriesCount":"1","patientId":"PAT-000-874","patientDateOfBirth":"2019-03-28T15:22:37Z"},"Practitioner_6":{"%seriesCount":"1","practitionerId":{"%seriesCount":"1","practitionerId":"PR0089","practitionerTitle":"Dr.","practitionerGivenName":"Angela","practitionerSurname":"Noodleman","practitionerSpeciality":"GP"},"practitionerIsActive":false}}}

Because I am using effectively an array of Objects the array is subscripted by 'key' and so if there are multiple instances of say "Patient" then each instance of "Patient" would over write the existing "Patient" in the array and so in creating the array I concatenated the counter 'j' to the Schema Name.

in object terms if you open an Instance of ParentClass and you use the GetAt('key') method on the Schemas array you will be returned with a full object Oid() and from that you can extract the ClassName and the %Id()

The only way I can see around not having to uniquely identify the 'Schema' %dynamicObject in the JSON string is in the Parent class you need to have an array for each schema type. i.e. Array of Patient, Array of Contact.

In terms of nesting you will see that Patient has a Practitioner and Practioner is linked to a Table of Practitioners and in the JSON above you can see that it picks up the Patient, Practitioner and the Practitioner Details from the Table Practitioners

I havent tried importing the JSON as I would have to remove all of the code that I put in the Schema classes to generate values if the field is NULL but that can be overcome by setting the attribute  %JSONIGNORENULL to 0 and then make sure that you specify NULL for the property that has no value.

I would carry on experimenting but we are in the middle of a Power Cut (Thank you South African State Utility Company)

If you want to see the classes I wrote and play with them let me know and I'll email them as I can't upload them

Nigel

Hi

I should have included the class definition for Parent

Include DFIInclude Class DFI.Common.JSON.ParentClass Extends (%Persistent, %JSON.Adaptor, %XML.Adaptor, %ZEN.DataModel.Adaptor)
{ Property ParentId As %String(%JSONFIELDNAME = "parentId", %JSONIGNORENULL = 1, %JSONINCLUDE = "INOUT") [ InitialExpression = {"Parent"_$i(^Parent)} ];

Property ParentName As %String(%JSONFIELDNAME = "parentName", %JSONIGNORENULL = 1, %JSONINCLUDE = "INOUT") [ InitialExpression = {..ParentName()} ];

Property Schemas As %ArrayOfObjectsWithClassName(%JSONFIELDNAME = "schemas", %JSONIGNORENULL = 1, %JSONINCLUDE = "INOUT", CLASSNAME = 2, ELEMENTQUALIFIED = 1, REFELEMENTQUALIFIED = 1);

ClassMethod ParentName() As %String
{
quit "Example: "_$i(^Example)
}

ClassMethod BuildData(pCount As %Integer = 1) As %Status
{
set tSC=$$$OK
set array(1)="DFI.Common.JSON.Contact"
set array(2)="DFI.Common.JSON.Patient"
set array(3)="DFI.Common.JSON.Practitioner"
set array(4)="DFI.Common.JSON.Reference"
try {
for i=1:1:pCount {
set obj=##class(DFI.Common.JSON.ParentClass).%New()
set obj.Schemas.ElementType="%Persistent"
set count=$r(12)
for j=1:1:count {
set k=$r(4)+1
set schema=$classmethod(array(k),"%New"),tSC=schema.%Save() quit:'tSC do obj.Schemas.SetObjectAt(schema.%Oid(),$p(array(k),".",4)_"_"_j)
}
quit:'tSC
set tSC=obj.%Save() quit:'tSC
}
}
catch ex {set tSC=ex.AsStatus()}
write !,"Status: "_$s(tSC:"OK",1:$$$GetErrorText(tSC))
quit tSC
}

Nigel

Hi

I have done some more experienting and in the Contact class I have a ContactPhoneNumbers which I defined as %ListOfDataTypes and I noticed that they were being generated but not exported to JSON so I changed the type to %ArrayOfDataTpes and that didn't work either. I played around with the %JSON attributes to no avail. I read the documentation on the %JSON.Adapter class and there are strict rules about Arrays and Lists must contain literals or objects and  so I wrapped the Phone Numbers in quotes even though I was generating them as +27nn nnn nnn but that made no difference. I suspect that the Attribute ElementType should be set. In the ParentClass I specify that the array of object Oid's has an ElementType of %Persistent (the default is %RegisteredObject) and I think that I should do the same with the Phone Number array/list.

Nigel

Hi Utsavi

I have sent you an email with the classes.

I have managed to get all of the scemas to be generated correctly and exported to JSON correctly. the only issue is that the if I just use the class name as the 'key' in the array you will only get one instance of each schema however if I append the counter to the classname then it will create multiple instances of each schema in the class

 
JSON

I haven't explored the XDATA mapping associated with %JSON nor investigated the other %JSON classes.

One solution if you were importing the json would be to pre-process the json string and remove the counter appended to the class name but that is a bit cludgy

Nigel