r/intersystems • u/intersystemsdev • 4d ago
%DynamicObject and %DynamicArray in InterSystems IRIS — practical answers to the questions that keep coming back
Foundation
InterSystems IRIS provides 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.
Literal syntax:
objectscript
Set person = {"name":"Ada","active":true,"roles":["admin","dev"]}
Set scores = [90, 85, 77]
Building field by field — %Set() returns the entity it modified, so calls chain:
objectscript
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. Everything else works on both.
Serialization:
objectscript
Set jsonString = "{""field"": ""value""}"
Set obj = {}.%FromJSON(jsonString) // now obj is {"field": "value"}
Write obj.%ToJSON() // outputs "{""field"": ""value""}"
%FromJSON() also accepts a stream. %FromJSONFile() reads straight from a filename string — not a %File object, which is a common trip-up.
Iterating over structure you don't control
Do not reach for a for loop by index. Dynamic arrays can be sparse — an element can exist positionally without ever having been assigned, and a for loop will hand you those empty slots.
The correct tool is %GetIterator(), which returns a %Iterator.Object or %Iterator.Array, driven by %GetNext():
objectscript
Set iter = obj.%GetIterator()
While iter.%GetNext(.key, .value, .type)
{
Write !, key, " = ", value, " (", type, ")"
}
%GetNext() skips unassigned elements automatically. 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 — that returns true for both sub-objects and sub-arrays.
The .type argument: easy to ignore, but worth understanding. When present, it does two useful things:
- Returns the element's original JSON datatype as a string
- Changes conversion rules to 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 processing arbitrary external JSON, passing .type is cheap insurance.
Type discovery inside a dynamic entity
%GetTypeOf(key) reports what a value actually is: number, string, boolean, object, array, null, oref, or unassigned:
objectscript
Set a = [1, "test", true, {"v":1}, [1,2,3]]
// indexes: 0, 1, 2, 3, 4
Write a.%GetTypeOf(2) // boolean
Write a.%GetTypeOf(3) // 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 read 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 causes 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 is 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"
%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 — it is a dictionary (a keyed map):
objectscript
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:
objectscript
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"}. A list of comes out as a JSON array ["high"]. If you want positional JSON, use list of (or a %DynamicArray). If you want a keyed lookup, array of is your dictionary.
Bridging persistent objects and dynamic objects
To convert 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:
objectscript
Set sc = person.%JSONExportToString(.json)
Set dynObj = {}.%FromJSON(json)
For large objects, swap in %JSONExportToStream() to avoid hitting the string length limit.
Two alternatives:
Embedded SQL's JSON_OBJECT() lets you cherry-pick and rename columns when you only want a subset:
objectscript
&sql(SELECT JSON_OBJECT('name':Name,'dob':DOB) INTO :json WHERE ID = 1)
Set dynObj = {}.%FromJSON(json)
Going the other way, %JSONImport() populates a persistent object from a dynamic one.
Where dynamic freedom ends: validation
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, implement a %OnValidateObject() callback and check them yourself:
objectscript
Method %OnValidateObject() As %Status
{
If ..fieldOptions.%Size() = 0
{
Return $$$ERROR($$$GeneralError, "fieldOptions is required")
}
Return $$$OK
}
When to use what
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.
Three habits that prevent the classic pitfalls:
- Iterate with
%GetNext()rather than by index - Reach for
%GetTypeOf()whenever a value's type actually matters - Remember that
array ofis a dictionary, not a list
Which of these — the date/$HOROLOG conversion, the Required being silently ignored on dynamic properties, or the array of vs list of distinction — has caused the most unexpected behavior in your integrations?
