16.2 JSON Processing: JsonObject, JsonArray, JsonValue & JsonToken
Key Takeaways
- The AL JSON object hierarchy is structured around JsonToken as the universal base class, which is polymorphically specialized into JsonObject (key-value dictionary), JsonArray (ordered collection), and JsonValue (primitive scalar or null literal).
- JsonObject.ReadFrom(Text/InStream) and JsonToken.ReadFrom(Text/InStream) parse JSON payloads and return a Boolean indicating success, preventing unhandled runtime exceptions on malformed or empty payloads.
- Properties are retrieved using JsonObject.Get('key', JsonToken); the resulting token must be inspected with .IsObject(), .IsArray(), or .IsValue() and downcast using .AsObject(), .AsArray(), or .AsValue().
- Primitive values are extracted from JsonValue using .AsText(), .AsInteger(), .AsDecimal(), .AsBoolean(), .AsDate(), or .AsDateTime(); calling these methods on a null JsonValue immediately triggers a fatal runtime exception.
- Defensive JSON processing requires verifying property presence with JsonObject.Contains('key') and checking JsonToken.IsNull() or JsonValue.IsNull() before attempting type downcasting and scalar extraction.
16.2 JSON Processing: JsonObject, JsonArray, JsonValue & JsonToken
JavaScript Object Notation (JSON) is the universal data interchange format for modern RESTful web services, cloud APIs, microservice architectures, and cloud ERP integrations. Dynamics 365 Business Central provides a rich, strongly typed JSON object model built directly into the AL runtime. Unlike legacy platforms that relied on external COM libraries, .NET interoperability (Newtonsoft.Json), or XML DOM parsers, AL includes four native, high-performance reference data types: JsonToken, JsonObject, JsonArray, and JsonValue.
For the MB-820 (Microsoft Dynamics 365 Business Central Developer Associate) examination, developers must thoroughly master the polymorphic relationship between these types, parse complex nested JSON streams, extract and convert scalar values safely, construct and serialize outbound payloads, and prevent fatal runtime crashes caused by missing keys or JSON null literals.
1. AL JSON Type Hierarchy & Polymorphism
The AL JSON architecture is structured around JsonToken, which serves as the universal abstract base class for all JSON entities. Every JSON node—whether an entire JSON document, a nested dictionary, an array of records, a single string value, a numeric scalar, a boolean flag, or an explicit null literal—can be stored in and represented by a JsonToken.
[ JsonToken ]
(Universal Base Class Node)
│
┌─────────────────────────────┼─────────────────────────────┐
▼ ▼ ▼
[ JsonObject ] [ JsonArray ] [ JsonValue ]
(Key-Value Dictionary) (Ordered Collection) (Scalar / Primitive / Null)
- Contains(Key) - Count() - AsText()
- Get(Key, JsonToken) - Get(Index, JsonToken) - AsInteger()
- Add(Key, Value) - Add(JsonToken) - AsDecimal()
- Remove(Key) - RemoveAt(Index) - AsBoolean()
- Replace(Key, Value) - IndexOf(JsonToken) - AsDate() / AsDateTime()
- Keys() - Clear() - IsNull()
The Four JSON Types Compared
| AL Type | JSON Representation | Primary Purpose | Key Methods |
|---|---|---|---|
JsonToken | Any JSON element ({}, [], "text", 123, true, null) | The polymorphic container used when reading, navigating, or traversing unknown or dynamic JSON structures. | `ReadFrom(Text |
JsonObject | { "key": value, ... } | Represents an unordered collection of string key-value pairs (JSON Object). | ReadFrom(), WriteTo()<br/>Get(Key, JsonToken)<br/>Add(Key, Value)<br/>Contains(Key)<br/>Remove(Key)<br/>Replace(Key, Value)<br/>Keys() |
JsonArray | [ value1, value2, ... ] | Represents an ordered zero-indexed list of JsonToken items (JSON Array). | ReadFrom(), WriteTo()<br/>Get(Index, JsonToken)<br/>Add(JsonToken)<br/>Count()<br/>RemoveAt(Index)<br/>IndexOf(JsonToken) |
JsonValue | "string", 100, 45.50, true, false, null | Encapsulates a single primitive scalar value, date, or null literal. | AsText(), AsInteger(), AsDecimal()<br/>AsBoolean(), AsDate(), AsDateTime()<br/>SetValue(Value)<br/>IsNull() |
2. Parsing JSON Payloads and Navigating Structures
When processing JSON returned from an external REST API, developers read the payload into memory using ReadFrom() and traverse nested tokens using typed accessor methods.
Safe Ingestion with ReadFrom()
Both JsonObject and JsonToken provide ReadFrom() methods accepting either a Text variable or an InStream:
JsonObject.ReadFrom(JsonText: Text): BooleanJsonObject.ReadFrom(InStream: InStream): BooleanJsonToken.ReadFrom(JsonText: Text): BooleanJsonToken.ReadFrom(InStream: InStream): Boolean
ReadFrom() returns a Boolean indicating whether the text conforms to valid JSON syntax. If the input string is empty or contains malformed syntax, ReadFrom() returns false without raising an unhandled runtime exception, allowing AL developers to implement clean diagnostic logging and graceful error recovery.
Navigating and Downcasting JsonToken
In AL, properties cannot be accessed using dot-notation (e.g., Json.Customer.Name is invalid syntax). Instead, developers navigate properties using JsonObject.Get() and downcast the resulting JsonToken:
-
Inspect Token Type:
Token.IsObject(): Boolean— Returnstrueif the node is a{}dictionary.Token.IsArray(): Boolean— Returnstrueif the node is a[]list.Token.IsValue(): Boolean— Returnstrueif the node is a scalar primitive ornull.
-
Downcast Token:
Token.AsObject(): JsonObject— Casts token toJsonObject. Throws runtime error ifnot Token.IsObject().Token.AsArray(): JsonArray— Casts token toJsonArray. Throws runtime error ifnot Token.IsArray().Token.AsValue(): JsonValue— Casts token toJsonValue. Throws runtime error ifnot Token.IsValue().
-
Extract Primitive Data from
JsonValue:JsonValue.AsText(): TextJsonValue.AsInteger(): IntegerJsonValue.AsDecimal(): DecimalJsonValue.AsBoolean(): BooleanJsonValue.AsDate(): DateJsonValue.AsDateTime(): DateTime
Comprehensive AL Parsing Example: Ingesting Complex Nested Documents
Consider the following external JSON response from an e-commerce platform:
{
"orderId": "ORD-99201",
"customer": {
"number": "C00010",
"name": "Contoso Retail"
},
"posted": true,
"totalAmount": 1450.75,
"lines": [
{ "lineNo": 10000, "itemNo": "1001", "quantity": 2, "unitPrice": 500.00 },
{ "lineNo": 20000, "itemNo": "1002", "quantity": 1, "unitPrice": 450.75 }
]
}
The following AL procedure parses this complex document safely:
codeunit 50125 "JSON Ingestion Handler"
{
procedure ParseOrderResponse(ResponseJson: Text)
var
RootObj: JsonObject;
CustomerObj: JsonObject;
LinesArray: JsonArray;
LineToken: JsonToken;
LineObj: JsonObject;
Token: JsonToken;
OrderId: Text;
CustomerNo: Code[20];
TotalAmount: Decimal;
IsPosted: Boolean;
ItemNo: Code[20];
Quantity: Decimal;
begin
// 1. Safe parsing of root JSON object
if not RootObj.ReadFrom(ResponseJson) then
Error('Invalid JSON format received from server.');
// 2. Extract scalar string property
if RootObj.Get('orderId', Token) then
OrderId := Token.AsValue().AsText();
// 3. Extract boolean and decimal scalar properties
if RootObj.Get('posted', Token) then
IsPosted := Token.AsValue().AsBoolean();
if RootObj.Get('totalAmount', Token) then
TotalAmount := Token.AsValue().AsDecimal();
// 4. Navigate nested JsonObject ('customer')
if RootObj.Get('customer', Token) then begin
CustomerObj := Token.AsObject();
if CustomerObj.Get('number', Token) then
CustomerNo := CopyStr(Token.AsValue().AsText(), 1, MaxStrLen(CustomerNo));
end;
// 5. Navigate and iterate through JsonArray ('lines')
if RootObj.Get('lines', Token) then begin
LinesArray := Token.AsArray();
// Iterate collection using foreach over JsonToken
foreach LineToken in LinesArray do begin
LineObj := LineToken.AsObject();
if LineObj.Get('itemNo', Token) then
ItemNo := CopyStr(Token.AsValue().AsText(), 1, MaxStrLen(ItemNo));
if LineObj.Get('quantity', Token) then
Quantity := Token.AsValue().AsDecimal();
InsertSalesLine(OrderId, ItemNo, Quantity);
end;
end;
end;
local procedure InsertSalesLine(OrderId: Text; ItemNo: Code[20]; Qty: Decimal)
begin
// Database insert routine
end;
}
3. Constructing and Serializing JSON Payloads
When sending data from Business Central to external systems, AL developers construct JsonObject and JsonArray graphs programmatically and serialize them to string or stream variables.
Constructing Payloads with JsonObject.Add()
JsonObject.Add() is overloaded to accept a string key name alongside various value types:
JsonObject.Add(Key: Text, Value: Text)JsonObject.Add(Key: Text, Value: Integer)JsonObject.Add(Key: Text, Value: Decimal)JsonObject.Add(Key: Text, Value: Boolean)JsonObject.Add(Key: Text, Value: JsonToken)(attaches nestedJsonObjectorJsonArray)
Similarly, JsonArray.Add() accepts any JsonToken (or scalar value) and appends it to the end of the array.
Modifying Existing Objects with Replace and Remove
JsonObject.Replace(Key: Text, Value: Text | Integer | Decimal | Boolean | JsonToken): Boolean: Replaces the value of an existing key. If the key does not exist, it throws a runtime error or returnsfalse.JsonObject.Remove(Key: Text): Boolean: Removes the specified key and its associated value from theJsonObject. Returnstrueif the key was found and removed.
Serializing JSON with WriteTo()
To transform an in-memory JSON structure into an outbound payload:
JsonObject.WriteTo(JsonText: Text): Boolean: Serializes the JSON graph into an ALTextvariable.JsonObject.WriteTo(OutStream: OutStream): Boolean: Serializes directly into anOutStream. This is highly recommended when generating large payloads to avoid holding massive strings in memory.
Comprehensive Payload Construction Example
procedure GenerateBatchCustomerSyncPayload(var Customer: Record Customer): Text
var
RootObj: JsonObject;
MetadataObj: JsonObject;
CustomersArray: JsonArray;
CustObj: JsonObject;
SerializedPayload: Text;
begin
// 1. Build metadata header object
MetadataObj.Add('sourceSystem', 'Dynamics 365 Business Central');
MetadataObj.Add('exportTimestamp', CurrentDateTime());
MetadataObj.Add('companyName', CompanyName());
RootObj.Add('metadata', MetadataObj);
// 2. Iterate records and construct customer objects in array
if Customer.FindSet() then
repeat
Clear(CustObj);
CustObj.Add('customerNumber', Customer."No.");
CustObj.Add('displayName', Customer.Name);
CustObj.Add('balanceLCY', Customer."Balance (LCY)");
CustObj.Add('creditLimitLCY', Customer."Credit Limit (LCY)");
CustObj.Add('blocked', Format(Customer.Blocked));
CustomersArray.Add(CustObj);
until Customer.Next() = 0;
// 3. Attach array to root document
RootObj.Add('customerCount', CustomersArray.Count());
RootObj.Add('customers', CustomersArray);
// 4. Serialize to string
RootObj.WriteTo(SerializedPayload);
exit(SerializedPayload);
end;
4. Robust Null and Missing Property Handling
In real-world REST integrations, external APIs frequently omit optional properties or return JSON null literals (e.g., "taxExemptNumber": null). Failing to anticipate these edge cases causes fatal AL runtime errors.
Missing Key vs. JSON Null
- Missing Key: The key does not exist in the
JsonObjectat all ({ "name": "Contoso" }). CallingRootObj.Get('taxExemptNumber', Token)returnsfalsewithout modifyingToken. - JSON Null Literal: The key exists, but its value is explicitly
null({ "taxExemptNumber": null }). CallingRootObj.Get('taxExemptNumber', Token)returnstrue, andTokenis initialized with aJsonValuerepresentingnull.
Fatal Error Alert: If you call
.AsText(),.AsDecimal(), or.AsInteger()on aJsonValuethat is null, the AL runtime crashes immediately with an unhandled exception ("Cannot convert null value to..."). You must verify thatToken.IsNull()orJsonValue.IsNull()isfalsebefore invoking conversion methods.
Safe Property Extractor Pattern in AL
To write clean, enterprise-grade integration code, implement reusable helper functions that verify key existence and null status before attempting value conversion:
codeunit 50126 "JSON Safe Helpers"
{
procedure GetJsonValueAsText(var JObject: JsonObject; PropertyKey: Text; DefaultValue: Text): Text
var
JToken: JsonToken;
begin
if not JObject.Contains(PropertyKey) then
exit(DefaultValue);
if not JObject.Get(PropertyKey, JToken) then
exit(DefaultValue);
if JToken.IsNull() or JToken.IsUndefined() then
exit(DefaultValue);
if not JToken.IsValue() then
exit(DefaultValue);
if JToken.AsValue().IsNull() then
exit(DefaultValue);
exit(JToken.AsValue().AsText());
end;
procedure GetJsonValueAsDecimal(var JObject: JsonObject; PropertyKey: Text; DefaultValue: Decimal): Decimal
var
JToken: JsonToken;
begin
if not JObject.Contains(PropertyKey) then
exit(DefaultValue);
if not JObject.Get(PropertyKey, JToken) then
exit(DefaultValue);
if JToken.IsNull() or JToken.IsUndefined() or (not JToken.IsValue()) then
exit(DefaultValue);
if JToken.AsValue().IsNull() then
exit(DefaultValue);
exit(JToken.AsValue().AsDecimal());
end;
procedure GetJsonValueAsDate(var JObject: JsonObject; PropertyKey: Text; DefaultValue: Date): Date
var
JToken: JsonToken;
begin
if not JObject.Contains(PropertyKey) then
exit(DefaultValue);
if not JObject.Get(PropertyKey, JToken) then
exit(DefaultValue);
if JToken.IsNull() or JToken.IsUndefined() or (not JToken.IsValue()) then
exit(DefaultValue);
if JToken.AsValue().IsNull() then
exit(DefaultValue);
exit(JToken.AsValue().AsDate());
end;
}
A developer is writing an AL function to process an incoming JSON payload string stored in a Text variable. Which method should be used to parse the string into a JsonObject without throwing an unhandled runtime error if the JSON syntax is malformed?
An AL procedure parses an incoming JSON array containing purchase line items: [ { 'itemNo': '1000', 'qty': 5 }, { 'itemNo': '1001', 'qty': 2 } ]. Which code snippet correctly iterates over the JsonArray variable LinesArray in modern AL?
Which statement accurately describes the relationship and capabilities of JsonToken, JsonObject, JsonArray, and JsonValue in AL?
A developer is parsing a JSON response from an external payment gateway. When reading a response string, the property 'feeAmount' may contain a numeric value or a JSON null literal (e.g., {"feeAmount": null}). If the developer executes Token.AsValue().AsDecimal() directly on a null token, what happens at runtime, and what is the recommended practice?