Become a member!

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

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.

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 Enterprise-grade, very fast GitHub

When to Use Third-Party Libraries

  • Large JSON files (>10MB): Consider streaming parsers or JsonDataObjects
  • High-frequency parsing: JsonDataObjects or mORMot2 offer better performance
  • BSON support needed: Grijjy Foundation
  • Object serialization: DelphiMVCFramework’s serializers or mORMot2

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

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.

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?

For simple cases, build the JSON manually with TJSONObject. For automatic serialization of objects and records, use DelphiMVCFramework’s serializers or third-party libraries like mORMot2. These can convert any Delphi object to JSON with a single method call.

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 doesn’t have a built-in AddPair overload for TDateTime. Convert it to a string first using a standard format like ISO 8601:

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.

Comments

comments powered by Disqus