Hi, Community!
In Part 1 of this series, we built a production using the HL7 Messaging type and created a BPL Business Process that enriches an incoming HL7 message with a %DynamicObject before passing it to the routing engine. If you have not read Part 1 yet, start there. It fully covers the production setup, context properties, and the Build Dynamic Object Code activity.
This article focuses on what happens after you start using %DynamicObject in production. Seven specific property-access pitfalls catch every developer sooner or later. They all share the same frustrating characteristic: no error, no warning in the Event Log, just an empty value that causes your routing rule to fall through to the default action with no clue why.
To fix that, we added a second Code activity to the same Demo.HL7Router BPL from Part 1. Then we demonstrated all seven pitfalls with working code and verified the results in the Event Log and Visual Trace.
TL;DR
- All seven common %DynamicObject pitfalls produce the same symptom: empty value, no error, no warning.
- Underscore in property names, wrong case, and nested object access are the top three; use
%Get()for all of them. - Use
%IsDefined()to check if a property exists,%GetIterator()to loop over properties, and%GetTypeOf()to distinguish null from an empty string. - Numeric JSON values require numeric comparison; string comparison with a number silently fails to match.
Who This Article Is For
This guide is for IRIS Interoperability developers who work with %DynamicObject in BPL Business Processes and have experienced unexpected empty values or routing rule fall-throughs that are difficult to diagnose.
Prerequisites
Complete Part 1 of this series first. You should have a running Demo.HL7RulesDemo production with the following:
-
HL7FileServiceconfigured and pointing toHL7Router -
Demo.HL7RouterBPL with theBuild Dynamic ObjectCode activity verified in the Event Log and working -
MsgRouterandHL7FileOperationconfigured and running
Adding the Property Pitfalls Code Activity
Open Demo.HL7Router in the Business Process Designer.
Click Add Activity and select Code. Drag it between Build Dynamic Object and the Call activity.
Reconnect the arrows:
- Remove the arrow from
Build Dynamic Objectto the Call activity. - Connect
Build Dynamic Objectto the new Code activity. - Connect the new Code activity to the Call activity.
Label it Property Pitfalls.
Click on the Code activity to select it. In the right-side panel, find the Code field and click the magnifier icon. Enter the following code; each pitfall is clearly labelled in the comments:
// -------------------------------------------------------
// PITFALL 1: Underscore in property name
// _ is string concatenation in ObjectScript
// context.MetaData.patient_id reads as (context.MetaData.patient) _ (id)
// -------------------------------------------------------
Try {
Set wrongValue = context.MetaData.patient_id
} Catch ex {
Set wrongValue = ""
}
$$$LOGINFO("Pitfall 1 - Wrong (underscore) : [" _ wrongValue _ "]")
// CORRECT: use %Get() for underscore-named properties
Set context.PatientId = context.MetaData.%Get("patient_id")
$$$LOGINFO("Pitfall 1 - Correct : " _ context.PatientId)
// -------------------------------------------------------
// PITFALL 2: Case sensitivity
// Patient_Id and patient_id are different properties
// -------------------------------------------------------
Set wrongCase = context.MetaData.%Get("Patient_Id")
$$$LOGINFO("Pitfall 2 - Wrong case : [" _ wrongCase _ "]")
Set correctCase = context.MetaData.%Get("patient_id")
$$$LOGINFO("Pitfall 2 - Correct case : " _ correctCase)
// -------------------------------------------------------
// PITFALL 3: Nested object access
// Cannot chain dot notation for nested %DynamicObject
// -------------------------------------------------------
Set addressObj = ##class(%DynamicObject).%New()
Set addressObj.city = "Lahore"
Set addressObj.country = "PK"
Do context.MetaData.%Set("address", addressObj)
// Wrong way - fails silently
Set wrongNested = context.MetaData.address.city
$$$LOGINFO("Pitfall 3 - Wrong nested access: [" _ wrongNested _ "]")
// CORRECT: get nested object first then access its property
Set addr = context.MetaData.%Get("address")
Set city = addr.%Get("city")
$$$LOGINFO("Pitfall 3 - Correct nested : " _ city)
// -------------------------------------------------------
// PITFALL 4: Checking if a property exists
// %Get() returns empty for both missing and empty properties
// Use %IsDefined() to check existence reliably
// -------------------------------------------------------
If context.MetaData.%IsDefined("patient_id") {
$$$LOGINFO("Pitfall 4 - patient_id exists : " _ context.MetaData.%Get("patient_id"))
} Else {
$$$LOGINFO("Pitfall 4 - patient_id does not exist")
}
If context.MetaData.%IsDefined("ward_code") {
$$$LOGINFO("Pitfall 4 - ward_code exists : " _ context.MetaData.%Get("ward_code"))
} Else {
$$$LOGINFO("Pitfall 4 - ward_code does not exist - use default")
}
// -------------------------------------------------------
// PITFALL 5: Iterating over properties
// For loops do not work on %DynamicObject
// Use %GetIterator() instead
// -------------------------------------------------------
$$$LOGINFO("Pitfall 5 - Iterating properties:")
Set iter = context.MetaData.%GetIterator()
While iter.%GetNext(.key, .val) {
If $IsObject(val) {
$$$LOGINFO(" Key: " _ key _ " = [object]")
} Else {
$$$LOGINFO(" Key: " _ key _ " = " _ val)
}
}
// -------------------------------------------------------
// PITFALL 6: Null vs Empty String
// Both return empty from %Get() but are different types
// Use %GetTypeOf() to distinguish
// -------------------------------------------------------
Set nullJson = ##class(%DynamicObject).%FromJSON("{""nullProp"":null,""emptyProp"":""""}")
$$$LOGINFO("Pitfall 6 - emptyProp value : [" _ nullJson.%Get("emptyProp") _ "]")
$$$LOGINFO("Pitfall 6 - nullProp value : [" _ nullJson.%Get("nullProp") _ "]")
$$$LOGINFO("Pitfall 6 - emptyProp type : " _ nullJson.%GetTypeOf("emptyProp"))
$$$LOGINFO("Pitfall 6 - nullProp type : " _ nullJson.%GetTypeOf("nullProp"))
// -------------------------------------------------------
// PITFALL 7: Number type handling
// Numeric values in JSON are stored as numbers not strings
// Use numeric comparison or unary + for conversion
// -------------------------------------------------------
Set numObj = ##class(%DynamicObject).%FromJSON("{""age"":25}")
If numObj.%Get("age") = "25" {
$$$LOGINFO("Pitfall 7 - String comparison : matched (unexpected)")
} Else {
$$$LOGINFO("Pitfall 7 - String comparison : [no match - wrong approach]")
}
If numObj.%Get("age") = 25 {
$$$LOGINFO("Pitfall 7 - Numeric comparison : matched correctly")
}
If +numObj.%Get("age") = 25 {
$$$LOGINFO("Pitfall 7 - Unary + conversion : matched correctly")
}
// Store context values
Set context.PatientSex = context.MetaData.%Get("patient_sex")
// Send message to MsgRouter for routing decision
Set sc = ..%Process.SendRequestAsync("MsgRouter", request)
Click OK to close the editor, then click Save and Compile. Confirm that no errors appear before testing.
Test and Verify
Drop a new test file into your incoming folder. Go to Management Portal > Interoperability > View > Messages and click the message to open the Visual Trace.
Mark the Show Events checkbox at the top. This makes the $$$LOGINFO entries from your Code activities visible in line in the trace. Click on any event entry to expand it and see the full log message.
Go to Management Portal > Interoperability > View > Event Log. All the log entries from both Code activities appear here. Look for the pitfall entries to confirm the results match what you expect:
Understanding the Seven Pitfalls
All seven pitfalls share the same symptom: an empty value or an unexpected result with no error in the Event Log. That is what makes them hard to debug without knowing what to look for.
Pitfall 1: Underscore in Property Name
This is the most common silent failure when consuming REST APIs that use snake_case naming. When you write the following:
Set val = dynObj.patient_id
ObjectScript does not read patient_id as a single property name. The underscore _ is the string concatenation operator, so this line is read as the value of dynObj.patient concatenated with the string id. Since dynObj.patient does not exist, the result is empty, with no error or warning.
// Wrong - returns empty silently
Try {
Set val = dynObj.patient_id
} Catch ex {
Set val = ""
}
// Correct - use %Get() for underscore-named properties
Set val = dynObj.%Get("patient_id")
Pitfall 2: Case Sensitivity
%DynamicObject property names are fully case-sensitive. PATIENT_ID, patient_id, and Patient_Id are three completely different properties. This comes up when you consume third-party APIs with inconsistent casing or when you type the property name slightly differently in two places.
// Wrong - wrong case returns empty silently
Set val = dynObj.%Get("Patient_Id")
// Correct - exact case must match what was set
Set val = dynObj.%Get("patient_id")
Always check the raw JSON to confirm the exact casing before writing your access code. Log the property value immediately after setting it during development to confirm it is there.
Pitfall 3: Nested Object Access
Modern REST APIs almost always return nested JSON structures. When IRIS parses nested JSON, each nested object becomes its own %DynamicObject instance. Dot notation does not automatically traverse nested objects.
// Wrong - fails silently
Set city = dynObj.address.city
// Correct - get the nested object first, then access its property
Set addr = dynObj.%Get("address")
Set city = addr.%Get("city")
This pattern applies at every level of nesting:
Set patientObj = dynObj.%Get("patient")
Set contactObj = patientObj.%Get("contact")
Set phone = contactObj.%Get("phone")
Pitfall 4: Checking If a Property Exists
Many developers use %Get() and check if the result is empty to determine whether a property exists. However, this is unreliable because a property can exist with an empty value, and both cases return empty from %Get(). The correct method is %IsDefined():
// Wrong - cannot distinguish missing property from empty value
If dynObj.%Get("patient_id") = "" {
// Is it missing or just empty?
}
// Correct - explicitly checks if property exists
If dynObj.%IsDefined("patient_id") {
Set val = dynObj.%Get("patient_id")
} Else {
$$$LOGINFO("patient_id does not exist - using default")
Set val = "UNKNOWN"
}
In the Event Log, you will see ward_code logged as "does not exist" because we never set it, while patient_id is correctly found and returned.
Pitfall 5: Iterating Over Properties
ObjectScript For loops do not work on %DynamicObject. The correct way is %GetIterator():
// Wrong - does not work on %DynamicObject
For i = 1:1:dynObj.%Size() {
Write dynObj.%Get(i), !
}
// Correct - use %GetIterator()
Set iter = dynObj.%GetIterator()
While iter.%GetNext(.key, .val) {
If $IsObject(val) {
$$$LOGINFO("Key: " _ key _ " = [object]")
} Else {
$$$LOGINFO("Key: " _ key _ " = " _ val)
}
}
In the Event Log, you will see all properties listed in order, confirming that %GetIterator() traverses all of them correctly, including the nested address object.
Pitfall 6: Null vs. Empty String
%DynamicObject distinguishes between a property set to JSON null and a property set to an empty string "". Both return empty from %Get(), but they are different types. Use %GetTypeOf() to distinguish between them:
Set testObj = ##class(%DynamicObject).%FromJSON("{""nullProp"":null,""emptyProp"":""""}")
// Both return empty from %Get()
Write testObj.%Get("emptyProp"), ! // empty
Write testObj.%Get("nullProp"), ! // empty
// Use %GetTypeOf() to distinguish
Write testObj.%GetTypeOf("emptyProp"), ! // string
Write testObj.%GetTypeOf("nullProp"), ! // null
This matters when you need to know whether a field was intentionally left empty or was absent from the JSON payload entirely.
Pitfall 7: Number Type Handling
When JSON contains numeric values, %DynamicObject stores them as numbers, not strings. Comparing a numeric property with a string value silently fails to match:
Set numObj = ##class(%DynamicObject).%FromJSON("{""age"":25}")
// Wrong - string comparison does not match a numeric value
If numObj.%Get("age") = "25" { ... }
// Correct - use numeric comparison
If numObj.%Get("age") = 25 { ... }
// Also correct - force numeric conversion with unary +
If +numObj.%Get("age") = 25 { ... }
Common Mistakes
Using dot notation for anything other than simple property names: If the property name has an underscore, has inconsistent casing, or could be nested, always use %Get(). Making it your default for all %DynamicObject access prevents pitfalls 1, 2, and 3 with one habit.
Not logging values during development: All seven pitfalls return empty silently. Without $$$LOGINFO statements around your property access code, you will have no idea which pitfall you hit. Log every value you read from a %DynamicObject during development, then clean up before going to production.
Trusting the routing rule to tell you what went wrong: The routing rule does not throw an error when a condition evaluates to empty. It simply moves to the next rule or the default. If you are debugging a routing fall-through, always check the Event Log first to confirm what values your Code activity produced before looking at the routing rule.
Practical Recommendations
- Make
%Get()your default for all%DynamicObjectproperty access, not just for underscores. - Use
%Set()when setting any property with special characters, including underscores. - Exploit
%IsDefined()before%Get()whenever a property might be missing. - Utilize
%GetTypeOf()when you need to distinguish null from an empty string. - Employ numeric comparison (
= 25, not= "25") when working with JSON numeric values. - Log every dynamic object property during development and verify them in the Event Log before testing the routing rule.
- Remove debug
$$$LOGINFOstatements before going to production; they write to the Event Log database and consume storage over time.
FAQ
Q: Why does my routing rule fall through even though the condition looks correct?
A: The most likely cause is a silent empty value on the left side of the condition. Check the Event Log for the $$$LOGINFO entries from your Code activity. If the value you are routing on is empty, trace it back through the seven pitfalls starting with an underscore in the property name.
Q: Is there a safe way to read any %DynamicObject property regardless of what it is called?
A: Yes. Always use %Get("propertyName"). It handles underscore names, case-sensitive names, and works as the entry point for nested object traversal. Dot notation works reliably only for simple property names with no special characters.
Q: How do I know if a property name has the wrong case?
A: Log the raw JSON string before parsing it and compare it against your %Get() calls. In development, log every value immediately after setting it and check the Event Log to confirm the value is there before the routing rule evaluates it.
Q: Can I iterate over a %DynamicArray the same way?
A: Yes. %GetIterator() works on both %DynamicObject and %DynamicArray. For an array, the iterator returns numeric indices starting from 0 instead of property names. The %GetNext(.key, .val) pattern functions in an identical manner.
Q: What does %GetTypeOf() return for different value types?
A: It returns a string describing the type: "string", "number", "boolean", "null", "object", or "array". Use it whenever you need to make a decision based on what type of value a property holds rather than just whether it is empty or not.
Q: Should I remove the $$$LOGINFO statements after testing?
A: Yes, for production. $$$LOGINFO entries write to the Event Log database and consume storage over time. Keep only those entries that are genuinely useful for monitoring and remove the debug-level ones used during development.
Conclusion
All seven pitfalls covered in this article share one characteristic: they fail silently. That is what makes them so frustrating to debug. The routing rule falls through, the message lands on the default action, and there is nothing in the Event Log to point you in the right direction unless you already know what to log.
The fix for most of them, however, is a single habit: use %Get() for all %DynamicObject property access and log every value during development. Once you know the seven patterns, spotting them in code will take you seconds rather than hours.
Thanks for reading!




Top comments (0)