Iterate over dynamic object
Here's a sample code to display JSON or dynamic object.
It shows how to iterate over object, get property values and their paths.
Class JSON.Test
{
/// do ##class(JSON.Test).Test()
ClassMethod Test()
{
set json = "{""a"":1,""b"":2,""c"":{""c1"":3,""c2"":4}, ""d"": [5, {""e_e"":6}, 7]}"
set obj = {}.%FromJSON(json)
do ..Iterate(obj)
}
ClassMethod Iterate(obj As %DynamicAbstractObject, level = 0, path = "obj")
{
set indent = $j("", level * 4)
#dim iterator As %Iterator.Array
set iterator = obj.%GetIterator()
while iterator.%GetNext(.key, .value) {
set type = obj.%GetTypeOf(key)
write indent, "Key: ", key, !
write indent, "Type: ", type, !
if $classname(obj) = "%Library.DynamicArray" {
set newPath = path _ ".%GetAt(" _ key _ ")"
} else {
if $zname(key, 6) = 1 {
set newPath = path _ "." _ key
} else {
set newPath = path _ ".""" _ key _ """"
}
}
write indent, "Path: ", newPath, !
if $isObject(value) {
write indent, "Value: ", !
do ..Iterate(value, level + 1, newPath)
} else {
write indent, "Value: ", value, !
}
write !
}
}
}Output from running Test method:
Key: a
Type: number
Path: obj.a
Value: 1
Key: b
Type: number
Path: obj.b
Value: 2
Key: c
Type: object
Path: obj.c
Value:
Key: c1
Type: number
Path: obj.c.c1
Value: 3
Key: c2
Type: number
Path: obj.c.c2
Value: 4
Key: d
Type: array
Path: obj.d
Value:
Key: 0
Type: number
Path: obj.d.%GetAt(0)
Value: 5
Key: 1
Type: object
Path: obj.d.%GetAt(1)
Value:
Key: e_e
Type: number
Path: obj.d.%GetAt(1)."e_e"
Value: 6
Key: 2
Type: number
Path: obj.d.%GetAt(2)
Value: 7Comments
Thank you Eduard.
💡 This article is considered as InterSystems Data Platform Best Practice.
This is very neat, thankyou Eduard.
hi, I tried to modify the above ClassMethod iterate using a local variable sessionId inside the while loop.
Doing this I found out, the behavior of this method is not like normal ClassMethods.
The sessionId set in one cycle of the loop in the next cycleis invalid or - if I set it to a default value before the loop - it has this default value again.
As a temporary solution I then set a global ^SESSIONID and used that inside the loop. In case I kill this global at the end of the method, I have the same effect again. The ^SESSIONID global is undefined inside the loop again.
It looks like my solution is working when I kill the temporary Global outside this method. But this way is needing a lock of the global, which I could avoid if I could use a local variable.
So: Is there a solution for this problem or is this an error in the %DynamicAbstractObject class and I have to live with this behaviour?
Post a minimal code snippet illustrating your issue please.
Gud post, Thanks!
this looks ok. Maybe try
zw myobjbefore entering the loop, just to make sure that the file stream has linked up ok?
And also try
zw sc
after
Set sc=stream.LinkToFile("test_test.json")
set stream = ##class(%Stream.FileCharacter).%OpenId("/Users/.../data/continents-en.json")Did you know that you can just open a file as a stream?
And for iterating - this has been in the product for a while now. This is the code for my :pp alias.
ClassMethod pp(setAs%AbstractSet, offset As%Integer = 0)
{
#define QUOTE(%val) $zu(144,1,%val)
try {
set isLabeled = set."_isLabeled"()
if (isLabeled) {
write"{"setclose = "}"
} else {
write"["setclose = "]"
}
set it = set.iterator()
while it.hasNext() {
set next = it.next()
if$isobject(next.value) {
write !,?(offset+2)
write:isLabeled $$$QUOTE(next.key),": "do..pp(next.value, offset + 2)
} else {
write !,?(offset+2),$select(isLabeled:$$$QUOTE(next.key)_": ",1:""),$$$QUOTE(next.value)
}
if it.hasNext() {
write","
}
}
write !,?offset,close
} catch exc {
write !,"Exception caught: ",exc.AsSQLMessage()
}
return
}This is in %ASQ.SetUtils. My alias, pp - pretty print, is this:
pp do ##class(%ASQ.SetUtils).pp($*)
How do we take the JSON response, iterate through it to put it into a data class structure that could be used? Using JSON2Persistent, I took a JSON response and converted it into a Data Class Structure.
set obj = ##class(YourPackage.YourClass).%New()
set sc = obj.%JSONImport(jsonstring)If the Response you get from a REST call is a %DynamicAbstractObject, how do you know how many levels to go through?
I tried using JSON2Persistant however the naming convention of the tree that it is building it too long for me to upload it into github, so since I have two values that I need to only retrieve I am attempting to iterate through the JSON and just extract those two values
for example, if I only want to pull the pureId and the portalURL from the response object, how can I iterate through the JSON response to get those values only?
{
"count": 0,
"pageInformation": {
"offset": 0,
"size": 0
},
"items": [
{
"pureId": 0,
"uuid": "196ab1c9-6e60-4000-88cb-4b1795761180",
"createdBy": "string",
"createdDate": "1970-01-01T00:00:00.000Z",
"modifiedBy": "string",
"modifiedDate": "1970-01-01T00:00:00.000Z",
"portalUrl": "string",
"prettyUrlIdentifiers": [
"string"
],
"previousUuids": [
"string"
],
"version": "string",
"startDateAsResearcher": "1970-01-01",
"affiliationNote": "string",
"dateOfBirth": "1970-01-01",
"employeeStartDate": "1970-01-01",
"employeeEndDate": "1970-01-01",
"externalPositions": [
{
"pureId": 0,
"appointment": {
"uri": "string",
"term": {
"en_GB": "Some text"
}
},
"appointmentString": {
"en_GB": "Some text"
},
"period": {
"startDate": {
"year": 0,
"month": 1,
"day": 1
},
"endDate": {
"year": 0,
"month": 1,
"day": 1
}
},
"externalOrganization": {
"uuid": "196ab1c9-6e60-4000-8b89-29269178a480",
"systemName": "string"
}
}
],when I attempted
set itr = responseData.%GetIterator()
while itr.%GetNext(.key, .value) {
if key = "pureID"{
set pResponse.pureID = value
} elseif key = "portalURL" {
set pResponse.portalURL = value
}
}both the pureID and portalURL came back blank.
You case is simpler, you do know the json structure and all you need is to iterate the "items" array and find all the portalUrl and pureID properties. Note that since there can be many "items", there may be many portalUrl and pureID.
Set itr=responseData.items.%GetIterator()
while itr.%GetNext(.key, .value) {
Write value.portalUrl,!
Write value.pureID,!
}
P.S.: it's unlikely that the Response you get from a REST call is a %DynamicAbstractObject since, by definition, that's an abstract class and cannot be instantiated, what you actually get is a subclass of %DynamicAbstractObject, a %DynamicObject in this case or a %DynamicArray when the response is an array.
In case you want only the portalUrl and pureID of the first element of "item" array, then all you need is:
Write responseData.items.%Get(0).portalUrl
Write responseData.items.%Get(0).pureID
The name of the properties you are searching for are camel case, you need to change values used in "if" condition:
set itr = responseData.%GetIterator()
while itr.%GetNext(.key, .value) {
if key = "pureId"{
set pResponse.pureID = value
} elseif key = "portalUrl" {
set pResponse.portalURL = value
}
}You can still use classes, just add %JSONIGNOREINVALIDFIELD to ignore unknown properties:
Parameter%JSONIGNOREINVALIDFIELDAs BOOLEAN = 1;Here's how your example can look like:
Class dc.Item Extends (%RegisteredObject, %JSON.Adaptor)
{
Parameter%JSONIGNOREINVALIDFIELDAs BOOLEAN = 1;Property pureId As%Integer;Property portalUrl As%VarString;
}and the main class:
Class dc.Response Extends (%RegisteredObject, %JSON.Adaptor)
{
Parameter%JSONIGNOREINVALIDFIELDAs BOOLEAN = 1;Property items As list Of Item;/// do ##class(dc.Response).Test()ClassMethod Test()
{
set json = ..Sample()
set obj = ..%New()
$$$TOE(sc, obj.%JSONImport(json))
do obj.DisplayItems()
}
Method DisplayItems()
{
for i=1:1:..items.Count() {
set item = ..items.GetAt(i)
zw item
}
}
ClassMethod Sample() As%String [ CodeMode = expression ]
{
{
"count": 0,
"pageInformation": {
"offset": 0,
"size": 0
},
"items": [
{
"pureId": 0,
"uuid": "196ab1c9-6e60-4000-88cb-4b1795761180",
"createdBy": "string",
"createdDate": "1970-01-01T00:00:00.000Z",
"modifiedBy": "string",
"modifiedDate": "1970-01-01T00:00:00.000Z",
"portalUrl": "string",
"prettyUrlIdentifiers": [
"string"
],
"previousUuids": [
"string"
],
"version": "string",
"startDateAsResearcher": "1970-01-01",
"affiliationNote": "string",
"dateOfBirth": "1970-01-01",
"employeeStartDate": "1970-01-01",
"employeeEndDate": "1970-01-01",
"externalPositions": [
{
"pureId": 0,
"appointment": {
"uri": "string",
"term": {
"en_GB": "Some text"
}
},
"appointmentString": {
"en_GB": "Some text"
},
"period": {
"startDate": {
"year": 0,
"month": 1,
"day": 1
},
"endDate": {
"year": 0,
"month": 1,
"day": 1
}
},
"externalOrganization": {
"uuid": "196ab1c9-6e60-4000-8b89-29269178a480",
"systemName": "string"
}
}
]
}
]
}.%ToJSON()
}
}Very useful, thanks!
Thanks!
How would I do nested iterations if I need to get something from say example "external Positions"
"externalPositions": [
{
"pureId": 0,
"appointment": {
"uri": "string",
"term": {
"en_GB": "Some text"
}
},
"appointmentString": {
"en_GB": "Some text"
},
"period": {
"startDate": {
"year": 0,
"month": 1,
"day": 1
},
"endDate": {
"year": 0,
"month": 1,
"day": 1
}
},Our JPL implementation, called "ASQ", is not very fast (yet, we are working on that) but it works well for small sets of data.
LATEST:USER>:pp obj.apply("$[*].items[*].externalPositions[*]")
do ##class(%ASQ.SetUtils).pp(obj.apply("$[*].items[*].externalPositions[*]"))
[
{
"pureId": 0,
"appointment": {
"uri": "string",
"term": {
"en_GB": "Some text"
}
},
"appointmentString": {
"en_GB": "Some text"
},
"period": {
"startDate": {
"year": 0,
"month": 1,
"day": 1
},
"endDate": {
"year": 0,
"month": 1,
"day": 1
}
},
"externalOrganization": {
"uuid": "196ab1c9-6e60-4000-8b89-29269178a480",
"systemName": "string"
}
}
]