JSON Support in Delphi: Complete Guide with Examples (2026)
๐ฎ๐น 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.
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 |
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.
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.
Method 2: TryGetValue - Safe Reading (Recommended)
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.JSONfurther down this article - BSON support needed: Grijjy Foundation
- Object serialization:
REST.Jsonfor 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
DBXJSONunit - Delphi XE6: Renamed to
System.JSONwith API improvements - Delphi 10.1 Berlin:
TJSONObjectBuilderfluent API - Delphi 10.3 Rio: Added
Format()method,EJSONParseExceptionwith 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:
- Use
TJSONObjectandTJSONArrayfor creating and parsing JSON - Always check for nil when parsing JSON strings
- Use
TryGetValuefor safe value reading with optional fields - Use path notation (
'parent.child') for nested values - Remember memory management: parent objects own their children;
RemovePairreturns ownership to you - Consider third-party libraries only for specific performance needs
- Use
Format()for readable output (Delphi 10.3+),ToString()for compact output - 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:
- Building Delphi Web Applications with DMVCFramework and TemplatePro - Complete web development guide
- How to Serialize a TList of Objects with Delphi - Advanced serialization techniques
- Custom Marshalling/Unmarshalling in Delphi - Deep dive into custom JSON handling
Comments
comments powered by Disqus