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.
Last updated: August 2026

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 TypeJSON RepresentationPrimary PurposeKey Methods
JsonTokenAny 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, nullEncapsulates 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): Boolean
  • JsonObject.ReadFrom(InStream: InStream): Boolean
  • JsonToken.ReadFrom(JsonText: Text): Boolean
  • JsonToken.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:

  1. Inspect Token Type:

    • Token.IsObject(): Boolean — Returns true if the node is a {} dictionary.
    • Token.IsArray(): Boolean — Returns true if the node is a [] list.
    • Token.IsValue(): Boolean — Returns true if the node is a scalar primitive or null.
  2. Downcast Token:

    • Token.AsObject(): JsonObject — Casts token to JsonObject. Throws runtime error if not Token.IsObject().
    • Token.AsArray(): JsonArray — Casts token to JsonArray. Throws runtime error if not Token.IsArray().
    • Token.AsValue(): JsonValue — Casts token to JsonValue. Throws runtime error if not Token.IsValue().
  3. Extract Primitive Data from JsonValue:

    • JsonValue.AsText(): Text
    • JsonValue.AsInteger(): Integer
    • JsonValue.AsDecimal(): Decimal
    • JsonValue.AsBoolean(): Boolean
    • JsonValue.AsDate(): Date
    • JsonValue.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;
}
Loading diagram...
AL JsonToken Polymorphic Navigation and Value Downcasting Pipeline

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 nested JsonObject or JsonArray)

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 returns false.
  • JsonObject.Remove(Key: Text): Boolean: Removes the specified key and its associated value from the JsonObject. Returns true if 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 AL Text variable.
  • JsonObject.WriteTo(OutStream: OutStream): Boolean: Serializes directly into an OutStream. 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

  1. Missing Key: The key does not exist in the JsonObject at all ({ "name": "Contoso" }). Calling RootObj.Get('taxExemptNumber', Token) returns false without modifying Token.
  2. JSON Null Literal: The key exists, but its value is explicitly null ({ "taxExemptNumber": null }). Calling RootObj.Get('taxExemptNumber', Token) returns true, and Token is initialized with a JsonValue representing null.

Fatal Error Alert: If you call .AsText(), .AsDecimal(), or .AsInteger() on a JsonValue that is null, the AL runtime crashes immediately with an unhandled exception ("Cannot convert null value to..."). You must verify that Token.IsNull() or JsonValue.IsNull() is false before 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;
}
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

Which statement accurately describes the relationship and capabilities of JsonToken, JsonObject, JsonArray, and JsonValue in AL?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D