Become a member!

JSON Support in Delphi: Complete Guide with Examples (2026)

๐ŸŒ
This article is also available in other languages:
๐Ÿ‡ฎ๐Ÿ‡น Italiano  โ€ข  ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol  โ€ข  ๐Ÿ‡ฉ๐Ÿ‡ช Deutsch  โ€ข  ๐Ÿ‡ซ๐Ÿ‡ท Franรงais

JSON (JavaScript Object Notation) is the de-facto standard for data interchange in modern applications. Whether you’re building REST APIs, reading configuration files, or communicating with web services, understanding how to work with JSON in Delphi is essential.

This comprehensive guide covers everything you need to know about JSON support in Delphi, with complete, compilable examples you can use in your projects.

โœ…
All code examples in this article have been tested and verified with Delphi 13 Florence.
๐Ÿ“
This article focuses on the DOM-style JSON parser (TJSONObject, TJSONArray). For streaming/SAX-style parsing, see the System.JSON.Readers and System.JSON.Writers units.

Delphi Version Compatibility

JSON support has evolved significantly across Delphi versions:

Version Unit Key Features
Delphi 2009 DBXJSON Initial JSON support with basic classes
Delphi XE6 System.JSON Unit renamed, improved API
Delphi 10.1 Berlin System.JSON TJSONObjectBuilder fluent API, TryGetValue<T> improvements
Delphi 10.3 Rio System.JSON Format() method, EJSONParseException with details, performance improvements
Delphi 11-12 System.JSON Further optimizations and refinements
Delphi 13 Florence System.JSON Latest improvements and continued support
๐Ÿ’ก
All examples in this article are compatible with Delphi XE7 and later unless otherwise noted. Version-specific features are clearly marked.

What is JSON?

JSON is a lightweight, text-based data interchange format. It’s easy for humans to read and write, and easy for machines to parse and generate. A JSON document can contain:

  • Objects: Key-value pairs enclosed in curly braces {}
  • Arrays: Ordered lists of values enclosed in square brackets []
  • Values: Strings, numbers, booleans (true/false), null, objects, or arrays

Example JSON structure:

{
  "name": "Daniele Teti",
  "age": 45,
  "active": true,
  "skills": ["Delphi", "Python", "SQL"],
  "address": {
    "city": "Rome",
    "country": "Italy"
  }
}

Delphi JSON Classes Overview

Delphi provides built-in JSON support through the System.JSON unit. The main classes are:

Class Description
TJSONValue Base class for all JSON value types
TJSONObject Represents a JSON object (key-value pairs)
TJSONArray Represents a JSON array (ordered list)
TJSONString Represents a JSON string value
TJSONNumber Represents a JSON numeric value
TJSONBool Represents a JSON boolean value
TJSONNull Represents a JSON null value
TJSONPair Represents a key-value pair in an object

Creating JSON Objects

Let’s start with the basics: creating JSON objects and adding properties.

Basic JSON Object Creation

The most fundamental operation is creating a TJSONObject and adding key-value pairs to it. Delphi provides convenient AddPair overloads that accept strings, integers, booleans, and doubles directly - no need to wrap primitive values in JSON-specific classes. The following example demonstrates how to build a simple JSON object containing personal information with various data types:

program JSONCreateBasic;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
begin
  LJSONObject := TJSONObject.Create;
  try
    // Add string property
    LJSONObject.AddPair('firstName', 'Daniele');
    LJSONObject.AddPair('lastName', 'Teti');

    // Add numeric property (Integer, Int64, Double overloads available)
    LJSONObject.AddPair('age', 45);

    // Add boolean property
    LJSONObject.AddPair('active', True);

    // Add null property (no overload - must use TJSONNull)
    LJSONObject.AddPair('middleName', TJSONNull.Create);

    // Output the JSON
    // Note: Format() available since Delphi 10.3 Rio
    {$IF CompilerVersion >= 33.0} // Delphi 10.3 Rio
    WriteLn(LJSONObject.Format());
    {$ELSE}
    WriteLn(LJSONObject.ToString);
    {$ENDIF}
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Output:

{
    "firstName": "Daniele",
    "lastName": "Teti",
    "age": 45,
    "active": true,
    "middleName": null
}

Creating JSON Arrays

JSON arrays are ordered collections that can hold any combination of values - strings, numbers, booleans, or even other arrays and objects. When you add a TJSONArray to a TJSONObject using AddPair, the parent object takes ownership of the array, so you only need to free the root object. This example shows how to create both a homogeneous array of strings and a mixed-type array:

program JSONCreateArray;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LContacts: TJSONArray;
  LSkills: TJSONArray;
begin
  LJSONObject := TJSONObject.Create;
  try
    LJSONObject.AddPair('name', 'Daniele Teti');

    // Create array of strings
    LSkills := TJSONArray.Create;
    LJSONObject.AddPair('skills', LSkills);
    LSkills.Add('Delphi');
    LSkills.Add('Python');
    LSkills.Add('SQL');

    // Create array with mixed types
    LContacts := TJSONArray.Create;
    LJSONObject.AddPair('contacts', LContacts);
    LContacts.Add('daniele@example.com');  // string
    LContacts.Add(123456);                 // number
    LContacts.Add(True);                   // boolean

    {$IF CompilerVersion >= 33.0}
    WriteLn(LJSONObject.Format());
    {$ELSE}
    WriteLn(LJSONObject.ToString);
    {$ENDIF}
  finally
    LJSONObject.Free; // Also frees LContacts and LSkills
  end;

  ReadLn;
end.

Output:

{
    "name": "Daniele Teti",
    "skills": [
        "Delphi",
        "Python",
        "SQL"
    ],
    "contacts": [
        "daniele@example.com",
        123456,
        true
    ]
}

Creating an Array of Objects

One of the most common patterns in real-world JSON is an array containing multiple objects - think of a list of users, products, or any collection of records. Each object in the array can have its own set of properties. When building this structure, you create each object separately and add it to the array using the Add method. The array then owns all the objects you add to it:

program JSONArrayOfObjects;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LRoot: TJSONObject;
  LUsers: TJSONArray;
  LUser: TJSONObject;
begin
  LRoot := TJSONObject.Create;
  try
    LUsers := TJSONArray.Create;
    LRoot.AddPair('users', LUsers);

    // First user
    LUser := TJSONObject.Create;
    LUsers.Add(LUser);
    LUser.AddPair('id', 1);
    LUser.AddPair('name', 'Alice');
    LUser.AddPair('email', 'alice@example.com');

    // Second user
    LUser := TJSONObject.Create;
    LUsers.Add(LUser);
    LUser.AddPair('id', 2);
    LUser.AddPair('name', 'Bob');
    LUser.AddPair('email', 'bob@example.com');

    // Third user
    LUser := TJSONObject.Create;
    LUsers.Add(LUser);
    LUser.AddPair('id', 3);
    LUser.AddPair('name', 'Charlie');
    LUser.AddPair('email', 'charlie@example.com');

    {$IF CompilerVersion >= 33.0}
    WriteLn(LRoot.Format());
    {$ELSE}
    WriteLn(LRoot.ToString);
    {$ENDIF}
  finally
    LRoot.Free;
  end;

  ReadLn;
end.

Output:

{
    "users": [
        {
            "id": 1,
            "name": "Alice",
            "email": "alice@example.com"
        },
        {
            "id": 2,
            "name": "Bob",
            "email": "bob@example.com"
        },
        {
            "id": 3,
            "name": "Charlie",
            "email": "charlie@example.com"
        }
    ]
}

Nested JSON Objects

Complex data often requires hierarchical organization - a person has an address, an address has city and country, and so on. In Delphi, you create nested structures by adding TJSONObject instances as values within other objects. Just like with arrays, the parent object takes ownership of its children, simplifying memory management. This example creates a person with nested address and company objects:

program JSONNested;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LAddress: TJSONObject;
  LCompany: TJSONObject;
begin
  LJSONObject := TJSONObject.Create;
  try
    LJSONObject.AddPair('name', 'Daniele Teti');

    // Create nested address object
    LAddress := TJSONObject.Create;
    LJSONObject.AddPair('address', LAddress);
    LAddress.AddPair('street', 'Via Roma 123');
    LAddress.AddPair('city', 'Rome');
    LAddress.AddPair('country', 'Italy');
    LAddress.AddPair('zipCode', '00100');

    // Create another nested object
    LCompany := TJSONObject.Create;
    LJSONObject.AddPair('company', LCompany);
    LCompany.AddPair('name', 'bit Time Professionals');
    LCompany.AddPair('website', 'https://www.bittime.it');

    {$IF CompilerVersion >= 33.0}
    WriteLn(LJSONObject.Format());
    {$ELSE}
    WriteLn(LJSONObject.ToString);
    {$ENDIF}
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Output:

{
    "name": "Daniele Teti",
    "address": {
        "street": "Via Roma 123",
        "city": "Rome",
        "country": "Italy",
        "zipCode": "00100"
    },
    "company": {
        "name": "bit Time Professionals",
        "website": "https://www.bittime.it"
    }
}

Using TJSONObjectBuilder (Delphi 10.1 Berlin+)

If you prefer a more declarative, chainable syntax for building JSON, Delphi 10.1 Berlin introduced TJSONObjectBuilder. This fluent API lets you construct complex JSON structures in a single expression using method chaining with BeginObject, BeginArray, Add, and EndObject/EndArray calls. The builder writes to a TJsonTextWriter, which outputs to a TStringBuilder. While more verbose in setup, this approach produces cleaner, more readable code for complex structures:

program JSONBuilderExample;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.Classes,
  System.JSON.Types,
  System.JSON.Writers,
  System.JSON.Builders;

var
  LBuilder: TJSONObjectBuilder;
  LWriter: TJsonTextWriter;
  LStringWriter: TStringWriter;
  LStringBuilder: TStringBuilder;
begin
  LStringBuilder := TStringBuilder.Create;
  try
    LStringWriter := TStringWriter.Create(LStringBuilder);
    try
      LWriter := TJsonTextWriter.Create(LStringWriter);
      try
        LWriter.Formatting := TJsonFormatting.Indented;
        LBuilder := TJSONObjectBuilder.Create(LWriter);
        try
          // Build JSON using fluent API
          LBuilder
            .BeginObject
              .Add('firstName', 'Daniele')
              .Add('lastName', 'Teti')
              .Add('age', 45)
              .Add('active', True)
              .BeginObject('address')
                .Add('city', 'Rome')
                .Add('country', 'Italy')
              .EndObject
              .BeginArray('skills')
                .Add('Delphi')
                .Add('Python')
                .Add('SQL')
              .EndArray
            .EndObject;

          WriteLn(LStringBuilder.ToString);
        finally
          LBuilder.Free;
        end;
      finally
        LWriter.Free;
      end;
    finally
      LStringWriter.Free;
    end;
  finally
    LStringBuilder.Free;
  end;

  ReadLn;
end.

Output:

{
    "firstName": "Daniele",
    "lastName": "Teti",
    "age": 45,
    "active": true,
    "address": {
        "city": "Rome",
        "country": "Italy"
    },
    "skills": [
        "Delphi",
        "Python",
        "SQL"
    ]
}

Parsing JSON Strings

When you receive JSON data from a web service, file, or any other source, you need to parse it into Delphi objects you can work with. The TJSONObject.ParseJSONValue class method handles this conversion. It returns a TJSONValue (the base class), so you’ll need to check if it’s the expected type - typically TJSONObject or TJSONArray. If the JSON is malformed, the method returns nil, so always check for this before proceeding:

program JSONParsing;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  JSON_STRING =
    '{"name":"Daniele","age":45,"skills":["Delphi","Python"]}';

var
  LJSONValue: TJSONValue;
  LJSONObject: TJSONObject;
begin
  // ParseJSONValue returns TJSONValue, cast to appropriate type
  LJSONValue := TJSONObject.ParseJSONValue(JSON_STRING);

  if LJSONValue = nil then
  begin
    WriteLn('ERROR: Invalid JSON!');
    ReadLn;
    Exit;
  end;

  try
    // Check if it's an object (could be array at root level)
    if not (LJSONValue is TJSONObject) then
    begin
      WriteLn('ERROR: Expected JSON object at root level');
      Exit;
    end;

    LJSONObject := TJSONObject(LJSONValue); // Hard cast - safe after "is" check
    WriteLn('Parsed successfully!');
    WriteLn('Number of pairs: ', LJSONObject.Count);

    {$IF CompilerVersion >= 33.0}
    WriteLn(LJSONObject.Format());
    {$ELSE}
    WriteLn(LJSONObject.ToString);
    {$ENDIF}
  finally
    LJSONValue.Free;
  end;

  ReadLn;
end.
โš ๏ธ
Always check if ParseJSONValue returns nil, which indicates invalid JSON.

Handling Parse Errors (Delphi 10.3+)

When parsing fails, knowing why it failed helps debugging tremendously. Starting from Delphi 10.3 Rio, you can pass True as the second parameter to ParseJSONValue to make it raise an EJSONParseException instead of returning nil. This exception includes the error message, the path where parsing failed, and the character offset - invaluable information when dealing with complex or externally-provided JSON:

program JSONParseErrors;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  INVALID_JSON = '{"name": "Test", "value": }'; // Invalid!

var
  LJSONValue: TJSONValue;
begin
  {$IF CompilerVersion >= 33.0} // Delphi 10.3 Rio
  try
    // Use RaiseExc option to get exception with details
    LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON, True);
    try
      WriteLn('Parsed: ', LJSONValue.ToString);
    finally
      LJSONValue.Free;
    end;
  except
    on E: EJSONParseException do
    begin
      WriteLn('Parse error!');
      WriteLn('  Message: ', E.Message);
      WriteLn('  Path: ', E.Path);
      WriteLn('  Offset: ', E.Offset);
    end;
  end;
  {$ELSE}
  // Pre-10.3: just check for nil
  LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON);
  if LJSONValue = nil then
    WriteLn('Invalid JSON - no details available')
  else
    LJSONValue.Free;
  {$ENDIF}

  ReadLn;
end.

Reading JSON Values

Delphi provides multiple ways to read values from JSON objects, each with different trade-offs between convenience and safety. Understanding when to use each approach will help you write more robust code that handles missing or unexpected data gracefully.

Method 1: GetValue with Generic Type (Delphi XE7+)

The simplest way to read a value is using the generic GetValue<T> method. You specify the expected type as a type parameter, and Delphi handles the conversion automatically. However, this method raises an exception if the key doesn’t exist, so use it only when you’re certain the key is present:

program JSONReadGetValue;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  JSON_DATA = '{"name":"Daniele","age":45,"active":true}';

var
  LJSONObject: TJSONObject;
begin
  LJSONObject := TJSONObject.ParseJSONValue(JSON_DATA) as TJSONObject;
  try
    // GetValue<T> - raises exception if key not found
    WriteLn('Name: ', LJSONObject.GetValue<string>('name'));
    WriteLn('Age: ', LJSONObject.GetValue<Integer>('age'));
    WriteLn('Active: ', LJSONObject.GetValue<Boolean>('active'));
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

For production code, TryGetValue<T> is the recommended approach. It returns False if the key is missing or the value cannot be converted to the requested type, allowing you to handle missing data without exception handling. This is particularly useful when parsing JSON from external sources where you can’t guarantee all fields are present:

program JSONReadTryGetValue;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  JSON_DATA = '{"name":"Daniele","age":45}';

var
  LJSONObject: TJSONObject;
  LName: string;
  LAge: Integer;
  LMiddleName: string;
begin
  LJSONObject := TJSONObject.ParseJSONValue(JSON_DATA) as TJSONObject;
  try
    // TryGetValue returns False if key not found (no exception)
    if LJSONObject.TryGetValue<string>('name', LName) then
      WriteLn('Name: ', LName)
    else
      WriteLn('Name not found');

    if LJSONObject.TryGetValue<Integer>('age', LAge) then
      WriteLn('Age: ', LAge)
    else
      WriteLn('Age not found');

    // This key doesn't exist - no exception raised
    if LJSONObject.TryGetValue<string>('middleName', LMiddleName) then
      WriteLn('Middle Name: ', LMiddleName)
    else
      WriteLn('Middle Name: (not specified)');
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Method 3: FindValue - Returns nil if Not Found

When you need access to the raw TJSONValue object rather than a converted value, use FindValue. This method returns nil if the key doesn’t exist, never raises an exception, and gives you full access to the JSON value’s properties and methods. It’s useful when you need to check the actual type of a value or when working with complex nested structures:

program JSONReadFindValue;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  JSON_DATA = '{"name":"Daniele","age":45}';

var
  LJSONObject: TJSONObject;
  LValue: TJSONValue;
begin
  LJSONObject := TJSONObject.ParseJSONValue(JSON_DATA) as TJSONObject;
  try
    // FindValue returns nil if not found (never raises exception)
    LValue := LJSONObject.FindValue('name');
    if LValue <> nil then
      WriteLn('Name: ', LValue.Value);

    LValue := LJSONObject.FindValue('nonexistent');
    if LValue = nil then
      WriteLn('Key "nonexistent" not found');
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Method 4: Path Notation for Nested Values

One of Delphi’s most convenient features is path notation - you can access deeply nested values using dot-separated paths like 'person.address.city' instead of navigating through multiple intermediate objects. This works with TryGetValue, GetValue, and FindValue, making it much easier to extract specific values from complex JSON structures without writing verbose navigation code:

program JSONReadPath;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  JSON_DATA = '{' +
    '"person": {' +
    '  "name": "Daniele",' +
    '  "address": {' +
    '    "city": "Rome",' +
    '    "country": "Italy"' +
    '  }' +
    '}' +
  '}';

var
  LJSONObject: TJSONObject;
  LValue: string;
begin
  LJSONObject := TJSONObject.ParseJSONValue(JSON_DATA) as TJSONObject;
  try
    // Use dot notation to access nested values
    if LJSONObject.TryGetValue<string>('person.name', LValue) then
      WriteLn('Person Name: ', LValue);

    if LJSONObject.TryGetValue<string>('person.address.city', LValue) then
      WriteLn('City: ', LValue);

    if LJSONObject.TryGetValue<string>('person.address.country', LValue) then
      WriteLn('Country: ', LValue);
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Reading Arrays - Classic and Modern Approaches

When your JSON contains arrays, you’ll need to iterate over their elements to process each value. Delphi supports both traditional index-based loops using Items[I] and the more modern for-in syntax that works with any TJSONArray. The for-in approach is cleaner when you don’t need the index, while the classic loop gives you access to the position. For numeric arrays, cast each item to TJSONNumber to access methods like AsInt or AsDouble:

program JSONReadArrays;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  JSON_DATA = '{"skills":["Delphi","Python","SQL"],"scores":[95,87,92]}';

var
  LJSONObject: TJSONObject;
  LSkills: TJSONArray;
  LScores: TJSONArray;
  LItem: TJSONValue;
  I: Integer;
begin
  LJSONObject := TJSONObject.ParseJSONValue(JSON_DATA) as TJSONObject;
  try
    // Read string array - classic for loop
    if LJSONObject.TryGetValue<TJSONArray>('skills', LSkills) then
    begin
      WriteLn('Skills (classic loop):');
      for I := 0 to LSkills.Count - 1 do
        WriteLn('  ', I + 1, '. ', LSkills.Items[I].Value);
    end;

    WriteLn;

    // Read string array - modern for-in loop (Delphi XE+)
    if LJSONObject.TryGetValue<TJSONArray>('skills', LSkills) then
    begin
      WriteLn('Skills (for-in loop):');
      for LItem in LSkills do
        WriteLn('  - ', LItem.Value);
    end;

    WriteLn;

    // Read numeric array
    if LJSONObject.TryGetValue<TJSONArray>('scores', LScores) then
    begin
      WriteLn('Scores:');
      for LItem in LScores do
        WriteLn('  Score: ', (LItem as TJSONNumber).AsInt);
    end;
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Iterating Over JSON Object Pairs

Sometimes you need to process all properties in a JSON object without knowing the key names in advance - for example, when building a generic JSON viewer or when the structure is dynamic. The for-in loop works on TJSONObject just like on arrays, yielding TJSONPair instances. Each pair gives you access to both the key (via JsonString.Value) and the value (via JsonValue), plus you can inspect the runtime type using ClassName:

program JSONIteratePairs;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  JSON_DATA = '{"name":"Daniele","age":45,"city":"Rome","active":true}';

var
  LJSONObject: TJSONObject;
  LPair: TJSONPair;
begin
  LJSONObject := TJSONObject.ParseJSONValue(JSON_DATA) as TJSONObject;
  try
    WriteLn('All pairs in object:');
    WriteLn;

    // Iterate over all pairs using for-in
    for LPair in LJSONObject do
    begin
      WriteLn('Key: ', LPair.JsonString.Value);
      WriteLn('Value: ', LPair.JsonValue.ToString);
      WriteLn('Type: ', LPair.JsonValue.ClassName);
      WriteLn;
    end;
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Modifying JSON Objects

JSON objects in Delphi are fully mutable - you can add, remove, and update properties after creation. Understanding the memory management rules is crucial: when you call RemovePair, ownership of that pair transfers back to you, so you must free it. The Free method in Delphi is safe to call on nil, so the pattern RemovePair('key').Free works even if the key doesn’t exist.

Adding and Removing Pairs

This example demonstrates the complete lifecycle of modifying a JSON object: creating initial properties, removing one, and updating another. Notice that to update a value, you must remove the old pair first (freeing it) and then add a new one with the same key:

program JSONModify;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LRemovedPair: TJSONPair;
begin
  LJSONObject := TJSONObject.Create;
  try
    // Add initial pairs
    LJSONObject.AddPair('name', 'Daniele');
    LJSONObject.AddPair('city', 'Rome');
    LJSONObject.AddPair('temp', 'to be removed');

    WriteLn('Initial:');
    WriteLn(LJSONObject.ToString);
    WriteLn;

    // Remove a pair - RemovePair returns the removed pair (you own it!)
    LRemovedPair := LJSONObject.RemovePair('temp');
    LRemovedPair.Free; // Safe even if nil - Free checks Self <> nil

    WriteLn('After removing "temp":');
    WriteLn(LJSONObject.ToString);
    WriteLn;

    // To update a value: remove then add
    LRemovedPair := LJSONObject.RemovePair('city');
    LRemovedPair.Free;
    LJSONObject.AddPair('city', 'Milan');

    WriteLn('After updating "city":');
    WriteLn(LJSONObject.ToString);
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Output:

Initial:
{"name":"Daniele","city":"Rome","temp":"to be removed"}

After removing "temp":
{"name":"Daniele","city":"Rome"}

After updating "city":
{"name":"Daniele","city":"Milan"}

Cloning JSON Objects

When you need to create a modified version of a JSON object without affecting the original, use the Clone method. This creates a deep copy - a completely independent object tree where changes to the clone don’t affect the original and vice versa. This is essential when you receive JSON data that you need to transform before sending elsewhere while preserving the original:

program JSONClone;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LOriginal: TJSONObject;
  LClone: TJSONObject;
  LPair: TJSONPair;
begin
  LOriginal := TJSONObject.Create;
  try
    LOriginal.AddPair('name', 'Daniele');
    LOriginal.AddPair('city', 'Rome');

    // Clone creates an independent copy
    LClone := LOriginal.Clone as TJSONObject;
    try
      // Modify the clone - original is not affected
      LPair := LClone.RemovePair('city');
      LPair.Free;
      LClone.AddPair('city', 'Milan');

      WriteLn('Original: ', LOriginal.ToString);
      WriteLn('Clone: ', LClone.ToString);
    finally
      LClone.Free;
    end;
  finally
    LOriginal.Free;
  end;

  ReadLn;
end.

Output:

Original: {"name":"Daniele","city":"Rome"}
Clone: {"name":"Daniele","city":"Milan"}

Working with JSON Files

Persisting JSON to disk is a common requirement for configuration files, caching, and data export. Delphi’s System.IOUtils unit provides the TFile class with simple methods for reading and writing text files, which pairs perfectly with JSON’s string representation.

Saving JSON to File

To save a JSON object to a file, convert it to a string using Format() (for readable output) or ToString() (for compact output), then write that string to disk. Using TPath.GetDocumentsPath ensures your file goes to a writable location that works across different Windows configurations:

program JSONSaveToFile;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.IOUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LDatabase: TJSONObject;
  LFileName: string;
begin
  LFileName := TPath.Combine(TPath.GetDocumentsPath, 'config.json');

  LJSONObject := TJSONObject.Create;
  try
    LJSONObject.AddPair('appName', 'MyApplication');
    LJSONObject.AddPair('version', '1.0.0');
    LJSONObject.AddPair('debug', False);

    LDatabase := TJSONObject.Create;
    LJSONObject.AddPair('database', LDatabase);
    LDatabase.AddPair('host', 'localhost');
    LDatabase.AddPair('port', 5432);

    // Save to file
    {$IF CompilerVersion >= 33.0}
    TFile.WriteAllText(LFileName, LJSONObject.Format());
    {$ELSE}
    TFile.WriteAllText(LFileName, LJSONObject.ToString);
    {$ENDIF}

    WriteLn('Saved to: ', LFileName);
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Loading JSON from File

Reading JSON from a file is equally straightforward: read the file contents into a string, then parse it with ParseJSONValue. Always check if the file exists first to avoid exceptions, and verify that parsing succeeded before accessing the data. Path notation works just as well on parsed JSON as on manually constructed objects:

program JSONLoadFromFile;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.IOUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LJSONValue: TJSONValue;
  LContent: string;
  LFileName: string;
  LAppName: string;
  LPort: Integer;
begin
  LFileName := TPath.Combine(TPath.GetDocumentsPath, 'config.json');

  if not TFile.Exists(LFileName) then
  begin
    WriteLn('File not found: ', LFileName);
    ReadLn;
    Exit;
  end;

  LContent := TFile.ReadAllText(LFileName);
  LJSONValue := TJSONObject.ParseJSONValue(LContent);

  if LJSONValue = nil then
  begin
    WriteLn('Invalid JSON in file!');
    ReadLn;
    Exit;
  end;

  try
    LJSONObject := LJSONValue as TJSONObject;

    if LJSONObject.TryGetValue<string>('appName', LAppName) then
      WriteLn('App Name: ', LAppName);

    if LJSONObject.TryGetValue<Integer>('database.port', LPort) then
      WriteLn('Database Port: ', LPort);
  finally
    LJSONValue.Free;
  end;

  ReadLn;
end.

Turning Objects into JSON with REST.Json

Everything so far builds JSON by hand, pair by pair. That is the right thing when the shape of the document is the point. It is the wrong thing when you already have a class and you just want it on the wire.

For that, Delphi ships REST.Json. It has been in the box since XE5, it needs no third-party code, and most of the work is one call.

Object to JSON

uses
  REST.Json, REST.Json.Types;

type
  TAddress = class
  private
    FCity: string;
    FZipCode: string;
  public
    property City: string read FCity write FCity;
    property ZipCode: string read FZipCode write FZipCode;
  end;

  TCustomer = class
  private
    FId: Integer;
    FName: string;
    FActive: Boolean;
    FAddress: TAddress;
    [JSONMarshalled(False)]
    FInternalNote: string;
    [JSONName('vat_number')]
    FVatNumber: string;
  public
    constructor Create;
    destructor Destroy; override;
    property Id: Integer read FId write FId;
    property Name: string read FName write FName;
    property Active: Boolean read FActive write FActive;
    property Address: TAddress read FAddress write FAddress;
    property InternalNote: string read FInternalNote write FInternalNote;
    property VatNumber: string read FVatNumber write FVatNumber;
  end;

// ...

LCustomer.Id := 42;
LCustomer.Name := 'Daniele Teti';
LCustomer.Active := True;
LCustomer.VatNumber := 'IT01234567890';
LCustomer.InternalNote := 'do not send this to the client';
LCustomer.Address.City := 'Roma';
LCustomer.Address.ZipCode := '00100';

Writeln(TJson.ObjectToJsonString(LCustomer));

Output:

{"id":42,"name":"Daniele Teti","active":true,"address":{"city":"Roma","zipCode":"00100"},"vat_number":"IT01234567890"}

Three things happened there.

The nested TAddress was serialized too, without you asking. REST.Json walks the object graph.

InternalNote is not in the output. [JSONMarshalled(False)] is how you keep a field out of the document, and it is the attribute you want on anything the client has no business seeing.

The keys came out as id, name, zipCode. REST.Json reads the private fields, not the properties, drops the F prefix and lowercases the first letter. So FZipCode becomes zipCode, camelCase, whether you like it or not. When the other end insists on a different spelling, [JSONName('vat_number')] overrides it one field at a time.

JSON back into an object

const
  JSON_TEXT =
    '{"Id":7,"Name":"Anna Bianchi","Active":false,' +
    '"vat_number":"IT09876543210",' +
    '"Address":{"City":"Milano","ZipCode":"20100"}}';
var
  LCustomer: TCustomer;
begin
  LCustomer := TJson.JsonToObject<TCustomer>(JSON_TEXT);
  try
    Writeln(Format('Id=%d Name=%s Active=%s Vat=%s City=%s',
      [LCustomer.Id, LCustomer.Name, BoolToStr(LCustomer.Active, True),
       LCustomer.VatNumber, LCustomer.Address.City]));
  finally
    LCustomer.Free;
  end;
end;
Id=7 Name=Anna Bianchi Active=False Vat=IT09876543210 City=Milano

Note that the input used Id and Name with a capital letter and it still worked: reading is case-insensitive, writing is not. The object comes back fully built, nested address included, and it is yours to free.

Fields that the JSON does not mention keep whatever the constructor left them:

LCustomer := TJson.JsonToObject<TCustomer>('{"Name":"Only a name"}');
Id=0 Name=Only a name Active=False

No exception. If a missing Id means something is wrong upstream, you have to check for it yourself.

The one that will cost you an afternoon

Declare those classes in the .dpr and TJson.JsonToObject fails:

EConversionError: Internal: Cannot instantiate type restjson.TCustomer

The serializer needs linked RTTI for the class, and a type declared in the program file does not get it. Move the declarations into a unit and the same code works. Serialization out of the object never complains, so you meet this only on the way back in, usually after you have convinced yourself the JSON is malformed.

One more, specific to recent versions: TJson.Format is deprecated in Delphi 13 Florence. The compiler tells you what to use instead:

W1000 Symbol 'Format' is deprecated: 'Use TJSONAncestor.Format instead'

So pretty printing an object is now:

LJson := TJson.ObjectToJsonObject(LCustomer);
try
  Writeln(LJson.Format);   // TJSONAncestor.Format
finally
  LJson.Free;
end;
{
    "id": 1,
    "name": "Pretty",
    "active": false,
    "address": {
        "city": "Napoli",
        "zipCode": ""
    },
    "vat_number": ""
}

Where REST.Json stops

Serialize a list and you find the edge:

LArray := TJSONArray.Create;
try
  for LCustomer in LList do
    LArray.AddElement(TJson.ObjectToJsonObject(LCustomer));
  Writeln(LArray.ToJSON);
finally
  LArray.Free;
end;

It works, and it is already a loop you wrote by hand. Going the other way, from a JSON array back into a TObjectList<TCustomer>, REST.Json has nothing to offer at all: you parse the array yourself and call JsonToObject per element.

REST.Json is very good at one object at a time, with names it chooses for you. Past that, you want a serializer built for the job.

When you need more: the DelphiMVCFramework serializers

DelphiMVCFramework ships a serializer you can use on its own, with no server and no controller in sight. It is one unit and one interface.

A list, in one call, in both directions:

uses
  MVCFramework.Serializer.Intf,
  MVCFramework.Serializer.Commons,
  MVCFramework.Serializer.JsonDataObjects;

var
  LSer: IMVCSerializer;
begin
  LSer := TMVCJsonDataObjectsSerializer.Create;
  Writeln(LSer.SerializeCollection(LOrders));
[{"id":1,"description":"Order 1","placedat":"2026-09-01T10:30:00.000+02:00"},{"id":2,"description":"Order 2","placedat":"2026-09-02T10:30:00.000+02:00"},{"id":3,"description":"Order 3","placedat":"2026-09-03T10:30:00.000+02:00"}]

And back:

LOrders := TObjectList<TOrder>.Create(True);
try
  LSer.DeserializeCollection(JSON_TEXT, LOrders, TOrder);
objects rebuilt: 2
  id=10 desc=From JSON placed=01/09/2026 10:30:00
  id=11 desc=Second one placed=02/09/2026 10:30:00

Two calls where REST.Json gave you two loops. Notice also that TDateTime went out and came back as a real date, in ISO 8601 with the offset, which is the subject of the next section.

The key case is a decision here, not a rule imposed on you. Put the attribute on the class and the whole class follows it:

[MVCNameCase(ncSnakeCase)]
TSnakeOrder = class
  // ...
end;

[MVCNameCase(ncPascalCase)]
TPascalOrder = class
  // ...
end;
ncSnakeCase : {"order_id":42,"customer_name":"Daniele Teti"}
ncPascalCase: {"OrderId":42,"CustomerName":"Daniele Teti"}

That is the one that decides it for most people. If the API you have to speak to wants order_id, REST.Json gives you one [JSONName] per field, forever; the DMVCFramework serializer gives you one attribute per class.

There is a trap in it, though, and it is quiet. The name case applies when reading too. The serializer defaults to ncLowerCase, so it emits placedat and expects placedat. Feed it the same payload with placedAt and:

key written as "placedAt", serializer expects "placedat":
  id=10 desc=From JSON placed=30/12/1899
  no exception, the date is simply gone

30/12/1899 is TDateTime zero. No error, no warning, just a field that quietly did not arrive. When you are consuming an API you do not control, set the name case to match it and test one payload end to end before you believe anything.

The same RTTI rule applies, by the way. Declare TOrder in the .dpr and you get:

Exception: Cannot find RTTI for dmvcser.TOrder. Hint: Is the specified classtype linked in the module?

Different serializer, different message, same cause: types go in units.

Dates in JSON, and the hour you will lose

JSON has no date type. Whatever you do, a TDateTime leaves your process as a string, and everybody has agreed on which string: ISO 8601. Delphi gives you the conversion in System.DateUtils, and it gives you a default that is wrong for most of the code you are writing.

The default is UTC

uses
  System.DateUtils;

const
  FIXED: TDateTime = 45000.5;   // 2023-03-15 12:00:00

Writeln('local value        : ', DateTimeToStr(FIXED));
Writeln('DateToISO8601(v)   : ', DateToISO8601(FIXED));
Writeln('DateToISO8601(v,F) : ', DateToISO8601(FIXED, False));
local value        : 15/03/2023 12:00:00
DateToISO8601(v)   : 2023-03-15T12:00:00.000Z
DateToISO8601(v,F) : 2023-03-15T12:00:00.000+01:00

The second parameter is AInputIsUTC and it defaults to True. So DateToISO8601(SomeDate) tells the world that the value you passed is already UTC. If it came from Now, from a TDateTimePicker or from a database column written by a local application, it is not UTC, and you have just stamped Z on a local time.

Nothing raises and the document is valid. The hour is simply wrong.

Write and read with the same flag

LText := DateToISO8601(LOriginal, False);
LBack := ISO8601ToDate(LText, False);
Writeln('written  : ', LText);
Writeln('read back: ', DateTimeToStr(LBack));
Writeln('identical: ', BoolToStr(SameDateTime(LOriginal, LBack), True));
written  : 2023-03-15T12:00:00.000+01:00
read back: 15/03/2023 12:00:00
identical: True

Mix the flags and the value moves by your offset from UTC, silently:

LText := DateToISO8601(LOriginal, False);   // local
LBack := ISO8601ToDate(LText);              // default, treats it as UTC
written with False, read with the default:
  15/03/2023 12:00:00 -> 15/03/2023 11:00:00
  drift in minutes: 60

One hour, on a machine in Italy in March. In August it is two. On a machine in UTC it is zero, which is exactly why this survives testing and shows up at a customer.

In a document

LJson := TJSONObject.Create;
try
  LJson.AddPair('event', 'invoice.created');
  LJson.AddPair('created_at', DateToISO8601(FIXED, False));
  Writeln(LJson.ToJSON);
  LWhen := ISO8601ToDate(LJson.GetValue<string>('created_at'), False);
  Writeln('parsed back: ', DateTimeToStr(LWhen));
finally
  LJson.Free;
end;
{"event":"invoice.created","created_at":"2023-03-15T12:00:00.000+01:00"}
parsed back: 15/03/2023 12:00:00

Input you did not write

ISO8601ToDate raises on anything it cannot read. For a payload that arrived over the network, use the Try version:

if TryISO8601ToDate('2026-13-45T99:00:00', LWhen, False) then
  Writeln('parsed: ', DateTimeToStr(LWhen))
else
  Writeln('TryISO8601ToDate returned False, no exception raised');
TryISO8601ToDate returned False, no exception raised

Same shape as TryGetValue earlier in this article, and the same reason to prefer it.

Pick UTC or local once, for the whole application, and pass the flag explicitly every single time. The default will not be the one you meant.

Practical Example: REST API Client

Now let’s put everything together in a real-world scenario: calling a REST API and processing the JSON response. This example connects to JSONPlaceholder (a free testing API), fetches a list of users, and parses each one into a Delphi record. Notice how we use TryGetValue throughout to handle potentially missing fields gracefully - a must when dealing with external APIs that might change.

๐Ÿ“
THTTPClient requires Delphi XE8 or later.
program JSONRestApiClient;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON,
  System.Net.HttpClient;  // Requires Delphi XE8+

type
  TUser = record
    ID: Integer;
    Name: string;
    Email: string;
    Username: string;
  end;

function ParseUser(AJSONObject: TJSONObject): TUser;
begin
  // Using TryGetValue for safety
  if not AJSONObject.TryGetValue<Integer>('id', Result.ID) then
    Result.ID := 0;
  if not AJSONObject.TryGetValue<string>('name', Result.Name) then
    Result.Name := '';
  if not AJSONObject.TryGetValue<string>('email', Result.Email) then
    Result.Email := '';
  if not AJSONObject.TryGetValue<string>('username', Result.Username) then
    Result.Username := '';
end;

var
  LClient: THTTPClient;
  LResponse: IHTTPResponse;
  LJSONValue: TJSONValue;
  LJSONArray: TJSONArray;
  LUserJSON: TJSONObject;
  LUser: TUser;
  I: Integer;
begin
  WriteLn('Fetching users from JSONPlaceholder API...');
  WriteLn;

  LClient := THTTPClient.Create;
  try
    LResponse := LClient.Get('https://jsonplaceholder.typicode.com/users');

    if LResponse.StatusCode <> 200 then
    begin
      WriteLn('HTTP Error: ', LResponse.StatusCode);
      ReadLn;
      Exit;
    end;

    // Parse JSON array response
    LJSONValue := TJSONObject.ParseJSONValue(LResponse.ContentAsString);
    if LJSONValue = nil then
    begin
      WriteLn('Invalid JSON response');
      ReadLn;
      Exit;
    end;

    try
      if not (LJSONValue is TJSONArray) then
      begin
        WriteLn('Expected JSON array');
        Exit;
      end;

      LJSONArray := LJSONValue as TJSONArray;
      WriteLn('Found ', LJSONArray.Count, ' users:');
      WriteLn(StringOfChar('-', 50));

      for I := 0 to LJSONArray.Count - 1 do
      begin
        LUserJSON := LJSONArray.Items[I] as TJSONObject;
        LUser := ParseUser(LUserJSON);

        WriteLn('ID: ', LUser.ID);
        WriteLn('Name: ', LUser.Name);
        WriteLn('Email: ', LUser.Email);
        WriteLn('Username: ', LUser.Username);
        WriteLn(StringOfChar('-', 50));
      end;

    finally
      LJSONValue.Free;
    end;

  finally
    LClient.Free;
  end;

  ReadLn;
end.

Practical Example: Configuration File Manager

This final example shows a complete, reusable class for managing application configuration. The TConfigManager encapsulates all the complexity of loading, saving, and accessing settings while providing a clean, type-safe API. It demonstrates lazy loading (only reads the file when first needed), default values for missing keys, and automatic file creation. You can use this pattern as a starting point for your own configuration systems:

program JSONConfigManager;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.IOUtils,
  System.JSON;

type
  TConfigManager = class
  private
    FFileName: string;
    FJSONObject: TJSONObject;
    FModified: Boolean;
    procedure EnsureLoaded;
  public
    constructor Create(const AFileName: string);
    destructor Destroy; override;

    procedure Load;
    procedure Save;

    function GetString(const AKey: string; const ADefault: string = ''): string;
    function GetInteger(const AKey: string; const ADefault: Integer = 0): Integer;
    function GetBoolean(const AKey: string; const ADefault: Boolean = False): Boolean;

    procedure SetValue(const AKey: string; const AValue: string); overload;
    procedure SetValue(const AKey: string; const AValue: Integer); overload;
    procedure SetValue(const AKey: string; const AValue: Boolean); overload;

    property FileName: string read FFileName;
    property Modified: Boolean read FModified;
  end;

constructor TConfigManager.Create(const AFileName: string);
begin
  inherited Create;
  FFileName := AFileName;
  FJSONObject := nil;
  FModified := False;
end;

destructor TConfigManager.Destroy;
begin
  FJSONObject.Free;
  inherited;
end;

procedure TConfigManager.EnsureLoaded;
begin
  if FJSONObject = nil then
    Load;
end;

procedure TConfigManager.Load;
var
  LContent: string;
  LJSONValue: TJSONValue;
begin
  FreeAndNil(FJSONObject);
  FModified := False;

  if TFile.Exists(FFileName) then
  begin
    LContent := TFile.ReadAllText(FFileName);
    LJSONValue := TJSONObject.ParseJSONValue(LContent);
    if (LJSONValue <> nil) and (LJSONValue is TJSONObject) then
      FJSONObject := TJSONObject(LJSONValue)
    else if LJSONValue <> nil then
      LJSONValue.Free;
  end;

  if FJSONObject = nil then
    FJSONObject := TJSONObject.Create;
end;

procedure TConfigManager.Save;
begin
  EnsureLoaded;
  {$IF CompilerVersion >= 33.0}
  TFile.WriteAllText(FFileName, FJSONObject.Format());
  {$ELSE}
  TFile.WriteAllText(FFileName, FJSONObject.ToString);
  {$ENDIF}
  FModified := False;
end;

function TConfigManager.GetString(const AKey, ADefault: string): string;
begin
  EnsureLoaded;
  if not FJSONObject.TryGetValue<string>(AKey, Result) then
    Result := ADefault;
end;

function TConfigManager.GetInteger(const AKey: string; const ADefault: Integer): Integer;
begin
  EnsureLoaded;
  if not FJSONObject.TryGetValue<Integer>(AKey, Result) then
    Result := ADefault;
end;

function TConfigManager.GetBoolean(const AKey: string; const ADefault: Boolean): Boolean;
begin
  EnsureLoaded;
  if not FJSONObject.TryGetValue<Boolean>(AKey, Result) then
    Result := ADefault;
end;

procedure TConfigManager.SetValue(const AKey: string; const AValue: string);
begin
  EnsureLoaded;
  FJSONObject.RemovePair(AKey).Free;
  FJSONObject.AddPair(AKey, AValue);
  FModified := True;
end;

procedure TConfigManager.SetValue(const AKey: string; const AValue: Integer);
begin
  EnsureLoaded;
  FJSONObject.RemovePair(AKey).Free;
  FJSONObject.AddPair(AKey, AValue);
  FModified := True;
end;

procedure TConfigManager.SetValue(const AKey: string; const AValue: Boolean);
begin
  EnsureLoaded;
  FJSONObject.RemovePair(AKey).Free;
  FJSONObject.AddPair(AKey, AValue);
  FModified := True;
end;

// Demo usage
var
  Config: TConfigManager;
  LConfigFile: string;
begin
  LConfigFile := TPath.Combine(TPath.GetDocumentsPath, 'appsettings.json');
  WriteLn('Config file: ', LConfigFile);
  WriteLn;

  Config := TConfigManager.Create(LConfigFile);
  try
    // Set some values (keys are flat - not nested objects)
    Config.SetValue('databaseHost', 'localhost');
    Config.SetValue('databasePort', 5432);
    Config.SetValue('databaseName', 'myapp');
    Config.SetValue('loggingEnabled', True);
    Config.SetValue('loggingMaxFiles', 10);
    Config.Save;

    WriteLn('Configuration saved!');
    WriteLn;

    // Read values back
    WriteLn('Database Host: ', Config.GetString('databaseHost'));
    WriteLn('Database Port: ', Config.GetInteger('databasePort'));
    WriteLn('Logging Enabled: ', Config.GetBoolean('loggingEnabled'));

    // Read with default value
    WriteLn('Timeout (default 30): ', Config.GetInteger('timeout', 30));
  finally
    Config.Free;
  end;

  ReadLn;
end.

Output:

Config file: C:\Users\yourname\Documents\appsettings.json

Configuration saved!

Database Host: localhost
Database Port: 5432
Logging Enabled: TRUE
Timeout (default 30): 30

Third-Party JSON Libraries

While Delphi’s built-in JSON parser is excellent for most use cases, some scenarios may benefit from third-party libraries:

Library Best For URL
JsonDataObjects High performance, used by DelphiMVCFramework GitHub
Grijjy Foundation Full-featured, includes BSON support GitHub
mORMot2 Full-stack framework (ORM, SOA, REST) that brings its own JSON layer GitHub

When to Use Third-Party Libraries

  • Large JSON files (>10MB): Consider streaming parsers or JsonDataObjects
  • High-frequency parsing: JsonDataObjects, measured against System.JSON further down this article
  • BSON support needed: Grijjy Foundation
  • Object serialization: REST.Json for the simple cases, DelphiMVCFramework’s serializers for lists, datasets and name-case control

For most applications, the built-in System.JSON is sufficient and has the advantage of no external dependencies.

How fast is System.JSON, really

“Use a third-party library if you need performance” is easy to write and hard to act on. Here are numbers.

The test builds an array of 50,000 records, each with an integer, two strings, a boolean, a number and an ISO 8601 timestamp: 14 MB of UTF-16 text, the shape of a real export. Then it measures two jobs. Parsing and reading one integer out of every record, which is what a client does. And parsing and serializing straight back out, which is what a proxy does.

Every run is preceded by a discarded warm-up, and the best of seven is reported, so the numbers are the floor, not an average of whatever else the machine was doing.

payload: 50000 records, 14657 KB of UTF-16 text
best of 7 runs, one warm-up discarded

System.JSON  parse + read ids         158 ms      90,6 MB/s
JsonDataObjects  parse + read ids      42 ms     340,8 MB/s

System.JSON  parse + serialize        170 ms      84,2 MB/s
JsonDataObjects  parse + serialize     63 ms     227,2 MB/s

Delphi 13 Florence, Win32, optimization on, range and overflow checks off, on a Core i9-13980HX running Windows 11.

So: JsonDataObjects parses about four times faster, and round-trips about two and a half times faster. That gap is real and it is stable across runs.

System.JSON still chewed through 14 MB in about a sixth of a second. If your JSON is a few hundred kilobytes, which covers most REST responses and nearly every configuration file, you are choosing between two milliseconds and half a millisecond. That is not a decision, it is a rounding error, and System.JSON is already installed.

Reach for JsonDataObjects when the payload is measured in megabytes, or when you parse in a loop that runs thousands of times, or when you are on a device where the CPU is not free. Otherwise the dependency costs you more than it buys.

One practical note if you do bring it in: JsonDataObjects declares its own TJSONObject and TJSONArray. In a unit that uses both, whichever comes last in the uses clause wins, and you get errors that make no sense until you see it:

E2003 Undeclared identifier: 'ParseJSONValue'
E2010 Incompatible types: 'System.JSON.TJSONValue' and 'JsonDataObjects.TJsonArray'

Qualify the type names, System.JSON.TJSONObject and JsonDataObjects.TJsonArray, and the ambiguity goes away.

mORMot2 is not in this comparison. It is a full framework rather than a JSON library, and measuring it fairly means bringing in and configuring the whole thing, which is a different article.

Building REST APIs with JSON

If you’re building REST APIs in Delphi, DelphiMVCFramework provides excellent JSON support with automatic serialization:

[MVCPath('/api/customers')]
TCustomersController = class(TMVCController)
public
  [MVCPath]
  [MVCHTTPMethod([httpGET])]
  procedure GetCustomers;

  [MVCPath('/($id)')]
  [MVCHTTPMethod([httpGET])]
  procedure GetCustomer(id: Integer);
end;

procedure TCustomersController.GetCustomers;
var
  LCustomers: TObjectList<TCustomer>;
begin
  LCustomers := TCustomerService.GetAll;
  Render(LCustomers); // Automatic JSON serialization
end;

See the DelphiMVCFramework samples for complete examples, and the official guide if you would rather have the serializers explained than guessed at.

Frequently Asked Questions

How do I parse a JSON string in Delphi?

Use TJSONObject.ParseJSONValue() from the System.JSON unit:

uses System.JSON;

var
  LJSONObject: TJSONObject;
  LValue: TJSONValue;
begin
  LValue := TJSONObject.ParseJSONValue('{"name":"John"}');
  if (LValue <> nil) and (LValue is TJSONObject) then
  begin
    LJSONObject := TJSONObject(LValue);
    try
      WriteLn(LJSONObject.GetValue<string>('name')); // Output: John
    finally
      LJSONObject.Free;
    end;
  end;
end;

How do I handle null values in JSON?

Use TryGetValue to safely handle missing or null values:

var
  LValue: string;
begin
  if LJSONObject.TryGetValue<string>('optionalField', LValue) then
    WriteLn('Value: ', LValue)
  else
    WriteLn('Field is missing or null');
end;

How do I iterate over a JSON array?

Use the modern for-in loop syntax:

var
  LArray: TJSONArray;
  LItem: TJSONValue;
begin
  if LJSONObject.TryGetValue<TJSONArray>('items', LArray) then
  begin
    for LItem in LArray do
      WriteLn(LItem.Value);
  end;
end;

What’s the difference between Format() and ToString()?

  • Format(): Returns indented, human-readable JSON (Delphi 10.3+ only)
  • ToString(): Returns compact JSON without whitespace (better for network transfer, works in all versions)

How do I modify an existing JSON object?

Use RemovePair then AddPair. RemovePair returns the removed pair (or nil if not found) - you own it and must free it:

begin
  // Remove returns the pair - you must free it!
  // Free is safe to call on nil (it checks Self <> nil internally)
  LJSONObject.RemovePair('name').Free;
  // Add new value
  LJSONObject.AddPair('name', 'New Value');
end;

Which Delphi version introduced JSON support?

  • Delphi 2009: Initial JSON support in DBXJSON unit
  • Delphi XE6: Renamed to System.JSON with API improvements
  • Delphi 10.1 Berlin: TJSONObjectBuilder fluent API
  • Delphi 10.3 Rio: Added Format() method, EJSONParseException with detailed error info

What is the difference between GetValue, FindValue, and TryGetValue?

Method Returns On Key Not Found
GetValue<T>('key') Value of type T Raises exception
FindValue('key') TJSONValue or nil Returns nil
TryGetValue<T>('key', outVar) Boolean Returns False

Recommendation: Use TryGetValue for production code as it’s the safest approach.

How do I create a deep copy of a JSON object?

Use the Clone method:

var
  LOriginal, LCopy: TJSONObject;
begin
  LOriginal := TJSONObject.ParseJSONValue('{"name":"test"}') as TJSONObject;
  try
    LCopy := LOriginal.Clone as TJSONObject;
    try
      // LCopy is independent - modifications don't affect LOriginal
    finally
      LCopy.Free;
    end;
  finally
    LOriginal.Free;
  end;
end;

How do I check if a JSON value is null?

var
  LValue: TJSONValue;
begin
  LValue := LJSONObject.FindValue('myField');
  if LValue = nil then
    WriteLn('Field does not exist')
  else if LValue is TJSONNull then
    WriteLn('Field exists but is null')
  else
    WriteLn('Field has a value: ', LValue.Value);
end;

Can I use path notation to access array elements?

Yes, use bracket notation with the index:

var
  LFirstSkill: string;
begin
  // Access first element of skills array
  if LJSONObject.TryGetValue<string>('skills[0]', LFirstSkill) then
    WriteLn('First skill: ', LFirstSkill);
end;

How do I convert a Delphi object to JSON?

With TJson.ObjectToJsonString from REST.Json, which ships with Delphi. It walks the object graph, reads the private fields, and gives you camelCase keys; [JSONName] renames one field and [JSONMarshalled(False)] keeps one out. For lists, name-case control and datasets, use DelphiMVCFramework’s serializers. See Turning Objects into JSON with REST.Json above.

Is System.JSON fast enough?

For almost everything, yes. On 14 MB of JSON, 50,000 records, System.JSON parses and reads in about 158 ms; JsonDataObjects does the same work in 42 ms, roughly four times faster. On a payload of a few hundred kilobytes, which covers most REST responses and every configuration file, the difference is a fraction of a millisecond. Change library when the documents are measured in megabytes or you parse in a tight loop, not by default. The numbers and the method are in How fast is System.JSON, really.

How do I serialize a TObjectList to JSON?

REST.Json has no list support: you loop, call TJson.ObjectToJsonObject per item and add each one to a TJSONArray. Going the other way it has nothing at all, so you parse the array and call JsonToObject per element. The DelphiMVCFramework serializer does both in one call, SerializeCollection and DeserializeCollection.

Why does TJson.JsonToObject raise “Cannot instantiate type”?

Because the class is declared in the .dpr program file, which does not get linked RTTI. Move the type declaration into a unit and the same code works. Serializing out never complains, so the error only appears on the way back in. The DelphiMVCFramework serializer fails on the same cause with a different message, Cannot find RTTI for ....

Is System.JSON thread-safe?

No, TJSONObject and related classes are not thread-safe. If multiple threads need to access the same JSON object, you must implement your own synchronization (critical sections, locks, etc.). For read-only access after initial parsing, you can safely share the object across threads as long as no modifications occur.

How do I serialize a TDateTime to JSON?

TJSONObject has no AddPair overload for TDateTime. Convert to an ISO 8601 string first, and pass AInputIsUTC explicitly, because it defaults to True and will label a local time as UTC. See Dates in JSON, and the hour you will lose:

LJSONObject.AddPair('createdAt', FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', Now));

What’s the maximum JSON size Delphi can parse?

There’s no hard limit, but System.JSON loads the entire document into memory. For very large files (>100MB), consider streaming parsers like TJsonTextReader from System.JSON.Readers, or third-party libraries optimized for large documents.

What is the difference between System.JSON and DBXJSON?

They are the same library - just renamed. DBXJSON was the original unit name in Delphi 2009-XE5. Starting with Delphi XE6, it was renamed to System.JSON to follow the new naming conventions. The API is essentially the same, so migrating old code is straightforward.

How do I pretty print JSON in Delphi?

Use the Format() method (Delphi 10.3+) which returns indented, human-readable JSON:

WriteLn(LJSONObject.Format());  // Pretty printed with indentation
WriteLn(LJSONObject.ToString);  // Compact, single line

For older Delphi versions, use third-party libraries or implement custom formatting.

How do I handle special characters and Unicode in JSON?

System.JSON automatically handles Unicode and escapes special characters when generating JSON. When parsing, escaped sequences like \n, \t, and \uXXXX are correctly converted. No manual handling is needed:

LJSONObject.AddPair('message', 'Line 1'#13#10'Line 2');  // Newlines auto-escaped
LJSONObject.AddPair('emoji', '๐Ÿš€');  // Unicode works directly

How do I merge two JSON objects?

There’s no built-in merge function. Iterate over one object and add its pairs to the other:

for LPair in LSource do
  LTarget.AddPair(LPair.JsonString.Value, LPair.JsonValue.Clone as TJSONValue);

Note: You must clone the values since they can only belong to one parent object.

How do I validate JSON before parsing?

ParseJSONValue returns nil for invalid JSON, which serves as basic validation. For schema validation (checking structure, required fields, types), you’ll need third-party libraries as Delphi doesn’t include built-in JSON Schema support.

How do I access deeply nested arrays?

Combine path notation with array indexing:

// Access: {"data": {"users": [{"name": "Alice"}, {"name": "Bob"}]}}
if LJSONObject.TryGetValue<string>('data.users[1].name', LValue) then
  WriteLn(LValue);  // Output: Bob

Can I use JSON with FireDAC datasets?

Yes, but there’s no direct integration. You can manually iterate over a dataset and build JSON, or use serialization libraries. DelphiMVCFramework and mORMot2 both provide dataset-to-JSON serialization out of the box.

How do I handle JSON with duplicate keys?

JSON technically allows duplicate keys, though it’s discouraged. TJSONObject stores all pairs, but GetValue/TryGetValue only return the first match. To access all values with the same key, iterate using the for-in loop.

Summary

Delphi provides robust, built-in JSON support through the System.JSON unit. Key takeaways:

  1. Use TJSONObject and TJSONArray for creating and parsing JSON
  2. Always check for nil when parsing JSON strings
  3. Use TryGetValue for safe value reading with optional fields
  4. Use path notation ('parent.child') for nested values
  5. Remember memory management: parent objects own their children; RemovePair returns ownership to you
  6. Consider third-party libraries only for specific performance needs
  7. Use Format() for readable output (Delphi 10.3+), ToString() for compact output
  8. Use for-in loops for cleaner iteration over arrays and object pairs

For building modern REST APIs in Delphi, check out DelphiMVCFramework - it includes advanced JSON serialization and is used in production by companies worldwide.

Related articles:

Comments

comments powered by Disqus