If you have spent any time on the Developer Community, you have seen the many questions return in different costumes: How do I turn a persistent object into JSON? How do I loop over a JSON payload whose shape I don't know? Why does Required do nothing on my %DynamicArray property? Why is my date coming back as 31390?
Noticing that, we decided to write an article summarizing all the questions that come with something that the documentation alone couldn't provide: practice.
Let's start with the foundation
InterSystems IRIS gives you two classes for schema-less data: %DynamicObject and %DynamicArray. Both inherit from %DynamicAbstractObject, and instances of either are called dynamic entities. They map cleanly onto JSON: an object is a set of key/value pairs, an array is an ordered list. Unlike a persistent or registered class, a dynamic object keeps no predefined list of valid property names — every string key is legal, and you add or remove members at runtime.
The most pleasant part is the literal syntax, which will look familiar to anyone coming from JavaScript:
Set person = {"name":"Ada","active":true,"roles":["admin","dev"]}
Set scores = [90, 85, 77]
You can also build them field by field. %Set() returns the entity it modified, so calls chain:
Set obj = {}.%Set("a",1).%Set("b",2) // now obj is {"a":1,"b":2}
Do scores.%Push(100) // now scores is [90, 85, 77, 100]
Set last = scores.%Pop() // now scores is [90, 85, 77] and last is 100
%Push() and %Pop() exist only on arrays, but everything else works on both. To turn text into an entity, use %FromJSON() and to serialize back, use %ToJSON():
Set jsonString = "{""field"": ""value""}"
Set obj = {}.%FromJSON(jsonString) // now obj is {"field": "value"}
Write obj.%ToJSON() // this outputs "{""field"": ""value""}"
%FromJSON() also accepts a stream, and %FromJSONFile() reads straight from a filename (note: a filename string, rather than a %File object — a common trip-up).
Iterating over structure you don't control
When a payload arrives from another system, you often don't know how many elements it has or what they're called. Don't reach for a for loop by index. Dynamic arrays can be sparse, which means that an element can exist positionally without ever having been assigned, and a for loop will happily hand you those empty slots. The correct tool is %GetIterator(), which returns a %Iterator.Object or %Iterator.Array, driven by %GetNext():
Set iter = obj.%GetIterator()
While iter.%GetNext(.key, .value, .type)
{
Write !, key, " = ", value, " (", type, ")"
}
%GetNext() skips unassigned elements automatically, which is exactly why it is preferred. For an object, key is the property name. For an array, key is the index. To walk a nested structure, recurse whenever $IsObject(value) is true, as that will return true for both sub-objects and sub-arrays.
That third argument, .type, is easy to ignore, but worth understanding. When present it does two useful things. First, it returns the element's original JSON datatype as a string. Second — and this is the practical part — it changes the conversion rules so you avoid <MAXSTRING> errors: a very long JSON string is handed back as a read-only stream object instead of being force-fit into an ObjectScript string, and JSON numbers are returned in their original textual form rather than being coerced. If you're processing arbitrary external JSON, passing .type is cheap insurance.
Type discovery inside a dynamic entity
Because dynamic entities are untyped containers, IRIS gives you tools to interrogate them. %GetTypeOf(key) reports what a value actually is: number, string, boolean, object, array, null, oref, or unassigned:
Set a = [1, "test", true, {"v":1}, [1,2,3]]
// the indexes are:
// 0, 1, 2, 3, 4
Write a.%GetTypeOf(2) // true: boolean
Write a.%GetTypeOf(3) // {"v":1}: object
Write a.%GetTypeOf(9) // unassigned - it finishes at index 4
This matters because ObjectScript flattens JSON's richer type system on the way in. JSON true, false, and null all become ObjectScript-friendly values 1, 0, and "" when you read them with dot syntax or %Get(). If you need to tell a genuine null apart from an empty string apart from a key that was never set, %GetTypeOf() is the reliable discriminator. There is also %IsDefined(), but it returns false for unassigned members and true for both "" and null.
The date gotcha
Type flattening is behind one of the community's recurring puzzles. Export a persistent object whose DOB is 1926-12-11 and you may see "DOB":31390 in the result — that 31390 is the internal $HOROLOG day count, not a corrupted value. The same logic bites the other direction in dynamic SQL: if you pass a query a literal like '1926-12-11' and get zero rows, it's usually because the column expects $HOROLOG internal format. The fix is to convert on the way in with $ZDATEH("1926-12-11", 3). Whenever a date crosses the boundary between JSON, SQL, and stored objects, ask which representation each side expects.
Two things both called "array"
Here is a distinction that quietly causes bugs. %DynamicArray is a positional list, indexed from 0. But ObjectScript also has typed collection properties, and the array of collection is not a positional array at all — it's a dictionary (a keyed map). Compare:
Property Tags As array of %String; // a dictionary: key -> value
Property Notes As list of %String; // an ordered, positional list
You access an array of collection by key, not by position:
Do obj.Tags.SetAt("high", "priority")Write obj.Tags.GetAt("priority") // high
Set key = ""
For
{
Set value = obj.Tags.GetNext(.key) Quit:key=""
Write !, key, ": ", value
}
When a class using %JSON.Adapter serializes an array of property, it comes out as a JSON object{"priority":"high"}, whereas a list of comes out as a JSON array ["high"]. So "typed array" can mean two very different shapes on the wire depending on which collection you chose. If you want positional JSON, use list of (or a %DynamicArray); if you genuinely want a keyed lookup, array of is your dictionary.
Bridging persistent objects and dynamic objects
A frequent need is converting a stored object into a free-form dynamic one — for instance, to trim fields before returning them from a REST method. If your class extends %JSON.Adapter, the clean, non-deprecated path is a two-step that fits on one line:
Set sc = person.%JSONExportToString(.json)
Set dynObj = {}.%FromJSON(json)
(For large objects, swap in %JSONExportToStream() so you never hit the string length limit.) Two alternatives are worth knowing. Embedded SQL's JSON_OBJECT() lets you cherry-pick and rename columns when you only want a subset:
&sql(SELECT JSON_OBJECT('name':Name,'dob':DOB) INTO :json WHERE ID = 1)
Set dynObj = {}.%FromJSON(json)
And going the other way, %JSONImport() populates a persistent object from a dynamic one.
Where dynamic freedom ends: validation
Finally, the caveat that surprises people building JSON request validators. The Required property keyword works for literals, collections, streams, and object-valued properties — but it is silently ignored for %DynamicArray and %DynamicObject properties. The reason is mechanical: the generated getter defaults these to [] and {}, so even assigning "" gets overwritten with a non-empty default, and %ValidateObject() never sees a missing value. If you need to enforce presence or shape on dynamic properties, don't rely on Required — implement a %OnValidateObject() callback and check them yourself:
Method %OnValidateObject() As %Status
{
If ..fieldOptions.%Size() = 0
{
Return $$ERROR($$GeneralError, "fieldOptions is required")
}Return $$OK
}
Choosing well
Dynamic entities are the right tool when structure is unknown, external, or genuinely fluid — parsing payloads, assembling responses, staging data. Typed persistent classes remain the right tool when you want the database, indexes, and validation to enforce a contract. Most real systems use both, meeting at the %JSON.Adapter boundary. Keep three habits and you'll avoid the classic pitfalls: iterate with %GetNext() rather than by index, reach for %GetTypeOf() whenever a value's type actually matters, and remember that array of is a dictionary, not a list.
Disclaimer: this article has been written by human hands, but reviewed with Claude AI for (1) enhancing my English (not my first language) and (2) reminding me of topics I hadn't covered in the initial versions. The search and studies for sources and practice were all made by a human (me :D), and the agent used to review is trained only on my own articles, so that it copies my tone without plagiarism, with respect to my fellow community members. The final revision was done completely without AI.




Top comments (0)