Soporte JSON en Delphi: Guía Completa con Ejemplos (2026)
🇬🇧 English • 🇮🇹 Italiano • 🇩🇪 Deutsch • 🇫🇷 Français
JSON (JavaScript Object Notation) es el estándar de facto para intercambiar datos. Lo usas en APIs REST, en archivos de configuración, en la comunicación con cualquier servicio web. Si trabajas en Delphi, tarde o temprano tienes que dominarlo.
Aquí tienes TJSONObject y TJSONArray construyendo y leyendo JSON a mano, REST.Json serializando objetos completos, la trampa de las fechas en UTC, y System.JSON medido contra JsonDataObjects con números reales, no promesas de rendimiento.
TJSONObject, TJSONArray). Para análisis en modo streaming/SAX, consulta las unidades System.JSON.Readers y System.JSON.Writers.
Compatibilidad de versiones de Delphi
El soporte de JSON en Delphi ha cambiado bastante de una versión a otra:
| Versión | Unidad | Características Clave |
|---|---|---|
| Delphi 2009 | DBXJSON |
Soporte JSON inicial con clases básicas |
| Delphi XE6 | System.JSON |
Unidad renombrada, API mejorada |
| Delphi 10.1 Berlin | System.JSON |
API fluida con TJSONObjectBuilder, mejoras en TryGetValue<T> |
| Delphi 10.3 Rio | System.JSON |
Método Format(), EJSONParseException con detalles, mejoras de rendimiento |
| Delphi 11-12 | System.JSON |
Optimizaciones adicionales y refinamientos |
| Delphi 13 Florence | System.JSON |
Últimas mejoras y soporte continuo |
¿Qué es JSON?
JSON es un formato de texto ligero para intercambiar datos: fácil de leer y escribir para una persona, fácil de analizar y generar para una máquina. Un documento JSON puede contener:
- Objetos: Pares clave-valor encerrados entre llaves
{} - Arrays: Listas ordenadas de valores encerradas entre corchetes
[] - Valores: Cadenas, números, booleanos (
true/false),null, objetos o arrays
Ejemplo de estructura JSON:
{
"name": "Daniele Teti",
"age": 45,
"active": true,
"skills": ["Delphi", "Python", "SQL"],
"address": {
"city": "Rome",
"country": "Italy"
}
}
Descripción general de las clases JSON de Delphi
Delphi trae el soporte de JSON integrado en la unidad System.JSON. Las clases principales:
| Clase | Descripción |
|---|---|
TJSONValue |
Clase base para todos los tipos de valores JSON |
TJSONObject |
Representa un objeto JSON (pares clave-valor) |
TJSONArray |
Representa un array JSON (lista ordenada) |
TJSONString |
Representa un valor de cadena JSON |
TJSONNumber |
Representa un valor numérico JSON |
TJSONBool |
Representa un valor booleano JSON |
TJSONNull |
Representa un valor null JSON |
TJSONPair |
Representa un par clave-valor en un objeto |
Creación de objetos JSON
Empieza por lo básico: crear objetos JSON y agregar propiedades.
Creación básica de objetos JSON
Lo más básico es crear un TJSONObject y agregarle pares clave-valor. AddPair tiene sobrecargas para cadenas, enteros, booleanos y doubles, así que no hace falta envolver los valores primitivos en ninguna clase JSON. Así se construye un objeto con datos de una persona, mezclando tipos:
program JSONCreateBasic;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
System.JSON;
var
LJSONObject: TJSONObject;
begin
LJSONObject := TJSONObject.Create;
try
// Agregar propiedad de cadena
LJSONObject.AddPair('firstName', 'Daniele');
LJSONObject.AddPair('lastName', 'Teti');
// Agregar propiedad numérica (sobrecargas disponibles para Integer, Int64, Double)
LJSONObject.AddPair('age', 45);
// Agregar propiedad booleana
LJSONObject.AddPair('active', True);
// Agregar propiedad null (sin sobrecarga - debe usar TJSONNull)
LJSONObject.AddPair('middleName', TJSONNull.Create);
// Mostrar el JSON
// Nota: Format() disponible desde 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.
Salida:
{
"firstName": "Daniele",
"lastName": "Teti",
"age": 45,
"active": true,
"middleName": null
}
Creación de arrays JSON
Un array JSON es una colección ordenada, y admite cualquier combinación de valores: cadenas, números, booleanos, incluso otros arrays u objetos. Cuando agregas un TJSONArray a un TJSONObject con AddPair, el objeto padre se queda con la propiedad del array, así que solo liberas la raíz. Aquí tienes un array homogéneo de cadenas y uno de tipo mixto:
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');
// Crear array de cadenas
LSkills := TJSONArray.Create;
LJSONObject.AddPair('skills', LSkills);
LSkills.Add('Delphi');
LSkills.Add('Python');
LSkills.Add('SQL');
// Crear array con tipos mixtos
LContacts := TJSONArray.Create;
LJSONObject.AddPair('contacts', LContacts);
LContacts.Add('daniele@example.com'); // cadena
LContacts.Add(123456); // número
LContacts.Add(True); // booleano
{$IF CompilerVersion >= 33.0}
WriteLn(LJSONObject.Format());
{$ELSE}
WriteLn(LJSONObject.ToString);
{$ENDIF}
finally
LJSONObject.Free; // También libera LContacts y LSkills
end;
ReadLn;
end.
Salida:
{
"name": "Daniele Teti",
"skills": [
"Delphi",
"Python",
"SQL"
],
"contacts": [
"daniele@example.com",
123456,
true
]
}
Creación de un array de objetos
Un patrón muy común en JSON real es un array de objetos: una lista de usuarios, de productos, de cualquier colección de registros. Cada objeto puede tener sus propias propiedades. Los creas uno por uno y los agregas al array con Add, que se queda con la propiedad de todos ellos:
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);
// Primer usuario
LUser := TJSONObject.Create;
LUsers.Add(LUser);
LUser.AddPair('id', 1);
LUser.AddPair('name', 'Alice');
LUser.AddPair('email', 'alice@example.com');
// Segundo usuario
LUser := TJSONObject.Create;
LUsers.Add(LUser);
LUser.AddPair('id', 2);
LUser.AddPair('name', 'Bob');
LUser.AddPair('email', 'bob@example.com');
// Tercer usuario
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.
Salida:
{
"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"
}
]
}
Objetos JSON anidados
Los datos complejos piden jerarquía: una persona tiene una dirección, una dirección tiene ciudad y país. En Delphi anidas objetos agregando instancias de TJSONObject como valores dentro de otros objetos. Igual que con los arrays, el padre se queda con la propiedad de los hijos, así que la memoria se gestiona sola. Aquí una persona con dirección y empresa anidadas:
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');
// Crear objeto de dirección anidado
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');
// Crear otro objeto anidado
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.
Salida:
{
"name": "Daniele Teti",
"address": {
"street": "Via Roma 123",
"city": "Rome",
"country": "Italy",
"zipCode": "00100"
},
"company": {
"name": "bit Time Professionals",
"website": "https://www.bittime.it"
}
}
Uso de TJSONObjectBuilder (Delphi 10.1 Berlin+)
Delphi 10.1 Berlin metió TJSONObjectBuilder para quien prefiere una sintaxis declarativa, encadenada. Con BeginObject, BeginArray, Add y EndObject/EndArray construyes toda la estructura en una sola expresión. El builder escribe en un TJsonTextWriter, que vuelca la salida a un TStringBuilder. La configuración inicial es más verbosa, pero el resultado queda más limpio en estructuras complejas:
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
// Construir JSON usando API fluida
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.
Salida:
{
"firstName": "Daniele",
"lastName": "Teti",
"age": 45,
"active": true,
"address": {
"city": "Rome",
"country": "Italy"
},
"skills": [
"Delphi",
"Python",
"SQL"
]
}
Análisis de cadenas JSON
Cuando te llega JSON de un servicio web, de un archivo o de cualquier otra fuente, tienes que analizarlo en objetos Delphi con los que trabajar. Eso lo hace el método de clase TJSONObject.ParseJSONValue. Devuelve un TJSONValue, la clase base, así que después compruebas el tipo real: normalmente TJSONObject o TJSONArray. Si el JSON está mal formado, devuelve nil, y eso lo compruebas siempre antes de seguir:
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 devuelve TJSONValue, convertir al tipo apropiado
LJSONValue := TJSONObject.ParseJSONValue(JSON_STRING);
if LJSONValue = nil then
begin
WriteLn('ERROR: ¡JSON inválido!');
ReadLn;
Exit;
end;
try
// Verificar si es un objeto (podría ser un array en el nivel raíz)
if not (LJSONValue is TJSONObject) then
begin
WriteLn('ERROR: Se esperaba un objeto JSON en el nivel raíz');
Exit;
end;
LJSONObject := TJSONObject(LJSONValue); // Conversión directa - segura después de verificar "is"
WriteLn('¡Analizado exitosamente!');
WriteLn('Número de pares: ', LJSONObject.Count);
{$IF CompilerVersion >= 33.0}
WriteLn(LJSONObject.Format());
{$ELSE}
WriteLn(LJSONObject.ToString);
{$ENDIF}
finally
LJSONValue.Free;
end;
ReadLn;
end.
ParseJSONValue devuelve nil, lo cual indica JSON inválido.
Manejo de errores de análisis (Delphi 10.3+)
Cuando el análisis falla, saber por qué ahorra tiempo depurando. Desde Delphi 10.3 Rio pasas True como segundo parámetro de ParseJSONValue y, en vez de nil, obtienes una EJSONParseException. Trae el mensaje de error, la ruta donde falló y el desplazamiento en caracteres: justo lo que necesitas con JSON complejo o que no has escrito tú:
program JSONParseErrors;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
System.JSON;
const
INVALID_JSON = '{"name": "Test", "value": }'; // ¡Inválido!
var
LJSONValue: TJSONValue;
begin
{$IF CompilerVersion >= 33.0} // Delphi 10.3 Rio
try
// Usar opción RaiseExc para obtener excepción con detalles
LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON, True);
try
WriteLn('Analizado: ', LJSONValue.ToString);
finally
LJSONValue.Free;
end;
except
on E: EJSONParseException do
begin
WriteLn('¡Error de análisis!');
WriteLn(' Mensaje: ', E.Message);
WriteLn(' Ruta: ', E.Path);
WriteLn(' Desplazamiento: ', E.Offset);
end;
end;
{$ELSE}
// Pre-10.3: solo verificar nil
LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON);
if LJSONValue = nil then
WriteLn('JSON inválido - no hay detalles disponibles')
else
LJSONValue.Free;
{$ENDIF}
ReadLn;
end.
Lectura de valores JSON
Delphi te da varias formas de leer valores de un objeto JSON, cada una con su propio equilibrio entre comodidad y seguridad. Saber cuál usar en cada caso es lo que separa el código que se cae ante un dato que falta del que no.
Método 1: GetValue con tipo genérico (Delphi XE7+)
La forma más simple de leer un valor es GetValue<T>. Le pasas el tipo esperado como parámetro de tipo y Delphi hace la conversión sola. El problema: lanza una excepción si la clave no existe, así que úsalo solo cuando estés seguro de que está ahí:
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> - lanza excepción si la clave no se encuentra
WriteLn('Nombre: ', LJSONObject.GetValue<string>('name'));
WriteLn('Edad: ', LJSONObject.GetValue<Integer>('age'));
WriteLn('Activo: ', LJSONObject.GetValue<Boolean>('active'));
finally
LJSONObject.Free;
end;
ReadLn;
end.
Método 2: TryGetValue - lectura segura (recomendado)
En producción usa TryGetValue<T>. Devuelve False si la clave falta o el valor no se puede convertir al tipo pedido, y así manejas los datos que faltan sin try/except. Es lo que necesitas cuando analizas JSON de fuera y no puedes garantizar que todos los campos estén ahí:
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 devuelve False si la clave no se encuentra (sin excepción)
if LJSONObject.TryGetValue<string>('name', LName) then
WriteLn('Nombre: ', LName)
else
WriteLn('Nombre no encontrado');
if LJSONObject.TryGetValue<Integer>('age', LAge) then
WriteLn('Edad: ', LAge)
else
WriteLn('Edad no encontrada');
// Esta clave no existe - no se lanza excepción
if LJSONObject.TryGetValue<string>('middleName', LMiddleName) then
WriteLn('Segundo Nombre: ', LMiddleName)
else
WriteLn('Segundo Nombre: (no especificado)');
finally
LJSONObject.Free;
end;
ReadLn;
end.
Método 3: FindValue - devuelve nil si no se encuentra
Cuando necesitas el TJSONValue en bruto, no un valor ya convertido, usa FindValue. Devuelve nil si la clave no existe, nunca lanza una excepción, y te da acceso completo a las propiedades y métodos del valor JSON. Te sirve para comprobar el tipo real de algo o para moverte por estructuras anidadas complicadas:
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 devuelve nil si no se encuentra (nunca lanza excepción)
LValue := LJSONObject.FindValue('name');
if LValue <> nil then
WriteLn('Nombre: ', LValue.Value);
LValue := LJSONObject.FindValue('nonexistent');
if LValue = nil then
WriteLn('Clave "nonexistent" no encontrada');
finally
LJSONObject.Free;
end;
ReadLn;
end.
Método 4: notación de ruta para valores anidados
Una de las cosas más cómodas de Delphi es la notación de ruta: accedes a un valor anidado con una ruta separada por puntos, como 'person.address.city', en vez de navegar objeto por objeto. Funciona con TryGetValue, GetValue y FindValue, y te ahorra el código de navegación verboso en estructuras JSON complejas:
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
// Usar notación de puntos para acceder a valores anidados
if LJSONObject.TryGetValue<string>('person.name', LValue) then
WriteLn('Nombre de Persona: ', LValue);
if LJSONObject.TryGetValue<string>('person.address.city', LValue) then
WriteLn('Ciudad: ', LValue);
if LJSONObject.TryGetValue<string>('person.address.country', LValue) then
WriteLn('País: ', LValue);
finally
LJSONObject.Free;
end;
ReadLn;
end.
Lectura de arrays - enfoques clásico y moderno
Cuando tu JSON trae arrays, tienes que iterar sus elementos para procesar cada valor. Delphi soporta el bucle clásico por índice con Items[I] y el for-in moderno, que funciona con cualquier TJSONArray. El for-in es más limpio cuando no te hace falta el índice; el bucle clásico te da la posición. En arrays numéricos, convierte cada elemento a TJSONNumber para usar AsInt o 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
// Leer array de cadenas - bucle for clásico
if LJSONObject.TryGetValue<TJSONArray>('skills', LSkills) then
begin
WriteLn('Habilidades (bucle clásico):');
for I := 0 to LSkills.Count - 1 do
WriteLn(' ', I + 1, '. ', LSkills.Items[I].Value);
end;
WriteLn;
// Leer array de cadenas - bucle for-in moderno (Delphi XE+)
if LJSONObject.TryGetValue<TJSONArray>('skills', LSkills) then
begin
WriteLn('Habilidades (bucle for-in):');
for LItem in LSkills do
WriteLn(' - ', LItem.Value);
end;
WriteLn;
// Leer array numérico
if LJSONObject.TryGetValue<TJSONArray>('scores', LScores) then
begin
WriteLn('Puntuaciones:');
for LItem in LScores do
WriteLn(' Puntuación: ', (LItem as TJSONNumber).AsInt);
end;
finally
LJSONObject.Free;
end;
ReadLn;
end.
Iteración sobre pares de objetos JSON
A veces tienes que recorrer todas las propiedades de un objeto JSON sin saber de antemano cómo se llaman las claves: por ejemplo, para un visor JSON genérico, o cuando la estructura es dinámica. El for-in funciona en TJSONObject igual que en un array, y te da instancias de TJSONPair. Cada par trae la clave (JsonString.Value) y el valor (JsonValue), y con ClassName inspeccionas el tipo en tiempo de ejecución:
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('Todos los pares en el objeto:');
WriteLn;
// Iterar sobre todos los pares usando for-in
for LPair in LJSONObject do
begin
WriteLn('Clave: ', LPair.JsonString.Value);
WriteLn('Valor: ', LPair.JsonValue.ToString);
WriteLn('Tipo: ', LPair.JsonValue.ClassName);
WriteLn;
end;
finally
LJSONObject.Free;
end;
ReadLn;
end.
Modificación de objetos JSON
Un objeto JSON en Delphi es del todo mutable: agregas, eliminas y actualizas propiedades después de crearlo. Lo que sí importa es la gestión de memoria: RemovePair te devuelve la propiedad de ese par, así que te toca liberarlo a ti. Free es seguro llamarlo sobre nil, así que el patrón RemovePair('key').Free funciona aunque la clave no exista.
Agregar y eliminar pares
Aquí tienes el ciclo completo de modificar un objeto JSON: creas las propiedades iniciales, eliminas una, actualizas otra. Para actualizar un valor primero eliminas el par viejo (y lo liberas), y luego agregas uno nuevo con la misma clave:
program JSONModify;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
System.JSON;
var
LJSONObject: TJSONObject;
LRemovedPair: TJSONPair;
begin
LJSONObject := TJSONObject.Create;
try
// Agregar pares iniciales
LJSONObject.AddPair('name', 'Daniele');
LJSONObject.AddPair('city', 'Rome');
LJSONObject.AddPair('temp', 'a ser eliminado');
WriteLn('Inicial:');
WriteLn(LJSONObject.ToString);
WriteLn;
// Eliminar un par - RemovePair devuelve el par eliminado (¡tú lo posees!)
LRemovedPair := LJSONObject.RemovePair('temp');
LRemovedPair.Free; // Seguro incluso si es nil - Free verifica Self <> nil
WriteLn('Después de eliminar "temp":');
WriteLn(LJSONObject.ToString);
WriteLn;
// Para actualizar un valor: eliminar y luego agregar
LRemovedPair := LJSONObject.RemovePair('city');
LRemovedPair.Free;
LJSONObject.AddPair('city', 'Milan');
WriteLn('Después de actualizar "city":');
WriteLn(LJSONObject.ToString);
finally
LJSONObject.Free;
end;
ReadLn;
end.
Salida:
Inicial:
{"name":"Daniele","city":"Rome","temp":"a ser eliminado"}
Después de eliminar "temp":
{"name":"Daniele","city":"Rome"}
Después de actualizar "city":
{"name":"Daniele","city":"Milan"}
Clonación de objetos JSON
Cuando necesitas una versión modificada de un objeto JSON sin tocar el original, usa Clone. Es una copia profunda: un árbol de objetos completamente independiente, donde los cambios en el clon no tocan al original ni al revés. Te hace falta justo cuando recibes JSON que tienes que transformar antes de mandarlo a otro sitio, conservando el 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 crea una copia independiente
LClone := LOriginal.Clone as TJSONObject;
try
// Modificar el clon - el original no se ve afectado
LPair := LClone.RemovePair('city');
LPair.Free;
LClone.AddPair('city', 'Milan');
WriteLn('Original: ', LOriginal.ToString);
WriteLn('Clon: ', LClone.ToString);
finally
LClone.Free;
end;
finally
LOriginal.Free;
end;
ReadLn;
end.
Salida:
Original: {"name":"Daniele","city":"Rome"}
Clon: {"name":"Daniele","city":"Milan"}
Trabajo con archivos JSON
Guardar JSON en disco es el pan de cada día en archivos de configuración, caché y exportación de datos. System.IOUtils trae la clase TFile, con métodos simples para leer y escribir texto, y encaja perfecto con la representación de cadena de JSON.
Guardar JSON en archivo
Para guardar un objeto JSON en archivo, lo conviertes a cadena con Format() (salida legible) o ToString() (salida compacta) y escribes esa cadena en disco. Con TPath.GetDocumentsPath el archivo va a una carpeta con permiso de escritura, sea cual sea la configuración de Windows:
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);
// Guardar en archivo
{$IF CompilerVersion >= 33.0}
TFile.WriteAllText(LFileName, LJSONObject.Format());
{$ELSE}
TFile.WriteAllText(LFileName, LJSONObject.ToString);
{$ENDIF}
WriteLn('Guardado en: ', LFileName);
finally
LJSONObject.Free;
end;
ReadLn;
end.
Cargar JSON desde archivo
Leer JSON de un archivo es igual de sencillo: lees el contenido en una cadena y lo analizas con ParseJSONValue. Comprueba primero que el archivo existe, para no toparte con una excepción, y comprueba que el análisis salió bien antes de tocar los datos. La notación de ruta funciona igual en JSON analizado que en objetos construidos a mano:
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('Archivo no encontrado: ', LFileName);
ReadLn;
Exit;
end;
LContent := TFile.ReadAllText(LFileName);
LJSONValue := TJSONObject.ParseJSONValue(LContent);
if LJSONValue = nil then
begin
WriteLn('¡JSON inválido en el archivo!');
ReadLn;
Exit;
end;
try
LJSONObject := LJSONValue as TJSONObject;
if LJSONObject.TryGetValue<string>('appName', LAppName) then
WriteLn('Nombre de Aplicación: ', LAppName);
if LJSONObject.TryGetValue<Integer>('database.port', LPort) then
WriteLn('Puerto de Base de Datos: ', LPort);
finally
LJSONValue.Free;
end;
ReadLn;
end.
Convertir objetos en JSON con REST.Json
Todo lo anterior construye el JSON a mano, par por par. Es lo correcto cuando la forma del documento es el objetivo. Es lo incorrecto cuando ya tienes una clase y solo quieres mandarla por la red.
Para eso, Delphi trae REST.Json. Está incluido desde XE5, no necesita código de terceros y casi todo se resuelve en una sola llamada.
De objeto a 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));
Salida:
{"id":42,"name":"Daniele Teti","active":true,"address":{"city":"Roma","zipCode":"00100"},"vat_number":"IT01234567890"}
Ahí pasaron tres cosas.
El TAddress anidado también se serializó, sin que se lo pidieras. REST.Json recorre el grafo de objetos.
InternalNote no aparece en la salida. [JSONMarshalled(False)] es la forma de dejar un campo fuera del documento, y es el atributo que quieres en todo lo que el cliente no tiene por qué ver.
Las claves salieron como id, name, zipCode. REST.Json lee los campos privados, no las propiedades, quita el prefijo F y pone en minúscula la primera letra. Así FZipCode se convierte en zipCode, camelCase, te guste o no. Cuando el otro extremo insiste en otro nombre, [JSONName('vat_number')] lo cambia campo por campo.
De JSON a objeto
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
Fíjate en que la entrada usó Id y Name con mayúscula inicial y aun así funcionó: al leer no se distinguen mayúsculas de minúsculas, al escribir sí. El objeto vuelve entero, con la dirección anidada dentro, y liberarlo es cosa tuya.
Los campos que el JSON no menciona se quedan con lo que les dejó el constructor:
LCustomer := TJson.JsonToObject<TCustomer>('{"Name":"Only a name"}');
Id=0 Name=Only a name Active=False
Ninguna excepción. Si la falta de un Id significa que algo va mal en el origen, te toca comprobarlo a ti.
El que te va a costar una tarde
Declara esas clases en el .dpr y TJson.JsonToObject falla:
EConversionError: Internal: Cannot instantiate type restjson.TCustomer
El serializador necesita la RTTI enlazada de la clase, y un tipo declarado en el archivo de programa no la tiene. Mueve las declaraciones a una unidad y el mismo código funciona. Serializar hacia fuera no se queja nunca, así que esto te lo encuentras solo en el camino de vuelta, normalmente después de haberte convencido de que el JSON está mal formado.
Una más, propia de las versiones recientes: TJson.Format está obsoleto en Delphi 13 Florence. El compilador te dice qué usar en su lugar:
W1000 Symbol 'Format' is deprecated: 'Use TJSONAncestor.Format instead'
Así que ahora imprimir un objeto con formato es:
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": ""
}
Hasta dónde llega REST.Json
Serializa una lista y das con el límite:
LArray := TJSONArray.Create;
try
for LCustomer in LList do
LArray.AddElement(TJson.ObjectToJsonObject(LCustomer));
Writeln(LArray.ToJSON);
finally
LArray.Free;
end;
Funciona, y ya es un bucle que has escrito tú. En el otro sentido, de un array JSON de vuelta a un TObjectList<TCustomer>, REST.Json no ofrece nada de nada: el array lo analizas tú y llamas a JsonToObject por cada elemento.
REST.Json es muy bueno con un objeto cada vez, y con los nombres que elige él. Más allá de eso, necesitas un serializador pensado para ese trabajo.
Cuando necesitas más: los serializadores de DelphiMVCFramework
DelphiMVCFramework trae un serializador que puedes usar por separado, sin un servidor ni un controlador a la vista. Es una unidad y una interfaz.
Una lista, en una llamada, en los dos sentidos:
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"}]
Y de vuelta:
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
Dos llamadas donde REST.Json te daba dos bucles. Fíjate también en que el TDateTime salió y volvió como una fecha de verdad, en ISO 8601 y con el desfase horario, que es el tema de la sección siguiente.
Aquí el formato de los nombres es una decisión tuya, no una regla que te imponen. Pon el atributo en la clase y se aplica a toda la clase:
[MVCNameCase(ncSnakeCase)]
TSnakeOrder = class
// ...
end;
[MVCNameCase(ncPascalCase)]
TPascalOrder = class
// ...
end;
ncSnakeCase : {"order_id":42,"customer_name":"Daniele Teti"}
ncPascalCase: {"OrderId":42,"CustomerName":"Daniele Teti"}
Ese es el detalle que lo decide para casi todo el mundo. Si la API con la que tienes que hablar quiere order_id, REST.Json te da un [JSONName] por campo, para siempre; el serializador de DelphiMVCFramework te da un atributo por clase.
Pero tiene una trampa, y es silenciosa. El formato de los nombres se aplica también al leer. El serializador usa ncLowerCase por defecto, así que emite placedat y espera placedat. Dale el mismo payload con placedAt y:
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 es el cero de TDateTime. Ningún error, ningún aviso, solo un campo que no llegó y no lo dijo. Cuando consumes una API que no controlas, ajusta el formato de los nombres al suyo y prueba un payload de punta a punta antes de creerte nada.
Y se aplica la misma regla de la RTTI, por cierto. Declara TOrder en el .dpr y obtienes:
Exception: Cannot find RTTI for dmvcser.TOrder. Hint: Is the specified classtype linked in the module?
Otro serializador, otro mensaje, la misma causa: los tipos van en unidades.
Las fechas en JSON, y la hora que vas a perder
JSON no tiene tipo fecha. Hagas lo que hagas, un TDateTime sale de tu proceso como una cadena, y todo el mundo se ha puesto de acuerdo en cuál: ISO 8601. Delphi te da la conversión en System.DateUtils, y te da un valor por defecto que está mal para casi todo el código que escribes.
El valor por defecto es 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
El segundo parámetro es AInputIsUTC y por defecto vale True. O sea que DateToISO8601(SomeDate) le dice al mundo que el valor que le pasaste ya está en UTC. Si viene de Now, de un TDateTimePicker o de una columna de base de datos escrita por una aplicación local, no está en UTC, y acabas de estamparle una Z a una hora local.
No salta nada y el documento es válido. Solo que la hora está mal.
Escribe y lee con el mismo 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
Mezcla los flags y el valor se desplaza según tu desfase con UTC, en silencio:
LText := DateToISO8601(LOriginal, False); // local
LBack := ISO8601ToDate(LText); // por defecto, lo trata como UTC
written with False, read with the default:
15/03/2023 12:00:00 -> 15/03/2023 11:00:00
drift in minutes: 60
Una hora, en una máquina italiana en marzo. En agosto son dos. En una máquina en UTC es cero, y por eso mismo esto sobrevive a las pruebas y aparece en casa de un cliente.
Dentro de un documento
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
Entrada que no escribiste tú
ISO8601ToDate lanza excepción con cualquier cosa que no sepa leer. Para un payload que llegó por la red, usa la versión Try:
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
La misma forma que TryGetValue más arriba en este artículo, y la misma razón para preferirla.
Elige UTC o local una vez, para toda la aplicación, y pasa el flag de forma explícita todas y cada una de las veces. El valor por defecto no va a ser el que querías.
Ejemplo práctico: cliente de API REST
Ahora júntalo todo en un caso real: llamar a una API REST y procesar la respuesta JSON. Este ejemplo se conecta a JSONPlaceholder (una API de prueba gratuita), trae una lista de usuarios y analiza cada uno en un registro Delphi. Fíjate en que usa TryGetValue en todo momento: con una API externa que puede cambiar, un campo que falte no debería tirarte el programa.
THTTPClient requiere Delphi XE8 o posterior.
program JSONRestApiClient;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
System.JSON,
System.Net.HttpClient; // Requiere Delphi XE8+
type
TUser = record
ID: Integer;
Name: string;
Email: string;
Username: string;
end;
function ParseUser(AJSONObject: TJSONObject): TUser;
begin
// Usando TryGetValue por seguridad
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('Obteniendo usuarios de la API JSONPlaceholder...');
WriteLn;
LClient := THTTPClient.Create;
try
LResponse := LClient.Get('https://jsonplaceholder.typicode.com/users');
if LResponse.StatusCode <> 200 then
begin
WriteLn('Error HTTP: ', LResponse.StatusCode);
ReadLn;
Exit;
end;
// Analizar respuesta de array JSON
LJSONValue := TJSONObject.ParseJSONValue(LResponse.ContentAsString);
if LJSONValue = nil then
begin
WriteLn('Respuesta JSON inválida');
ReadLn;
Exit;
end;
try
if not (LJSONValue is TJSONArray) then
begin
WriteLn('Se esperaba un array JSON');
Exit;
end;
LJSONArray := LJSONValue as TJSONArray;
WriteLn('Se encontraron ', LJSONArray.Count, ' usuarios:');
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('Nombre: ', LUser.Name);
WriteLn('Email: ', LUser.Email);
WriteLn('Usuario: ', LUser.Username);
WriteLn(StringOfChar('-', 50));
end;
finally
LJSONValue.Free;
end;
finally
LClient.Free;
end;
ReadLn;
end.
Ejemplo práctico: gestor de archivos de configuración
Este último ejemplo es una clase completa y reutilizable para gestionar la configuración de una aplicación. TConfigManager esconde toda la complejidad de cargar, guardar y acceder a la configuración detrás de una API limpia y con tipos seguros. Trae carga perezosa (el archivo solo se lee la primera vez que hace falta), valores predeterminados para las claves que faltan y creación automática del archivo. Úsalo como punto de partida para tu propio sistema de configuración:
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;
// Uso de demostración
var
Config: TConfigManager;
LConfigFile: string;
begin
LConfigFile := TPath.Combine(TPath.GetDocumentsPath, 'appsettings.json');
WriteLn('Archivo de configuración: ', LConfigFile);
WriteLn;
Config := TConfigManager.Create(LConfigFile);
try
// Establecer algunos valores (las claves son planas - no objetos anidados)
Config.SetValue('databaseHost', 'localhost');
Config.SetValue('databasePort', 5432);
Config.SetValue('databaseName', 'myapp');
Config.SetValue('loggingEnabled', True);
Config.SetValue('loggingMaxFiles', 10);
Config.Save;
WriteLn('¡Configuración guardada!');
WriteLn;
// Leer valores de vuelta
WriteLn('Host de Base de Datos: ', Config.GetString('databaseHost'));
WriteLn('Puerto de Base de Datos: ', Config.GetInteger('databasePort'));
WriteLn('Logging Habilitado: ', Config.GetBoolean('loggingEnabled'));
// Leer con valor predeterminado
WriteLn('Tiempo de espera (predeterminado 30): ', Config.GetInteger('timeout', 30));
finally
Config.Free;
end;
ReadLn;
end.
Salida:
Archivo de configuración: C:\Users\yourname\Documents\appsettings.json
¡Configuración guardada!
Host de Base de Datos: localhost
Puerto de Base de Datos: 5432
Logging Habilitado: TRUE
Tiempo de espera (predeterminado 30): 30
Bibliotecas JSON de terceros
El analizador JSON integrado de Delphi te sirve para casi todo, pero hay escenarios donde una biblioteca de terceros te conviene más:
| Biblioteca | Mejor Para | URL |
|---|---|---|
| JsonDataObjects | Alto rendimiento, usado por DelphiMVCFramework | GitHub |
| Grijjy Foundation | Con todas las características, incluye soporte BSON | GitHub |
| mORMot2 | Framework full-stack (ORM, SOA, REST) que trae su propia capa JSON | GitHub |
Cuándo usar bibliotecas de terceros
- Archivos JSON grandes (>10MB): Considera analizadores de streaming o JsonDataObjects
- Análisis de alta frecuencia: JsonDataObjects, medido contra
System.JSONmás abajo en este artículo - Soporte BSON necesario: Grijjy Foundation
- Serialización de objetos:
REST.Jsonpara los casos simples, los serializadores de DelphiMVCFramework para listas, datasets y control del formato de los nombres
Para la mayoría de las aplicaciones te basta con System.JSON: no trae dependencias externas.
¿Cuán rápido es System.JSON, en realidad?
«Usa una biblioteca de terceros si necesitas rendimiento» es fácil de escribir y difícil de aplicar. Aquí van números.
La prueba construye un array de 50.000 registros, cada uno con un entero, dos cadenas, un booleano, un número y una marca de tiempo ISO 8601: 14 MB de texto UTF-16, la forma de una exportación real. Después mide dos tareas. Analizar y leer un entero de cada registro, que es lo que hace un cliente. Y analizar y volver a serializar sin más, que es lo que hace un proxy.
Cada ejecución va precedida de un calentamiento que se descarta, y se toma la mejor de siete, así que los números son el mínimo, no una media de lo que la máquina estuviera haciendo al mismo tiempo.
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, optimización activada, comprobaciones de rango y de desbordamiento desactivadas, en un Core i9-13980HX con Windows 11.
Entonces: JsonDataObjects analiza unas cuatro veces más rápido, y hace el viaje de ida y vuelta unas dos veces y media más rápido. Esa diferencia es real y se mantiene estable entre ejecuciones.
System.JSON aun así se comió 14 MB en algo así como un sexto de segundo. Si tu JSON ocupa unos cientos de kilobytes, que cubre la mayoría de las respuestas REST y casi todos los archivos de configuración, estás eligiendo entre dos milisegundos y medio milisegundo. Eso no es una decisión, es un error de redondeo, y System.JSON ya lo tienes instalado.
Recurre a JsonDataObjects cuando el payload se mida en megabytes, o cuando analices dentro de un bucle que se ejecuta miles de veces, o cuando estés en un dispositivo donde la CPU no sea gratis. Si no, la dependencia te cuesta más de lo que te da.
Una nota práctica por si la añades al proyecto: JsonDataObjects declara sus propios TJSONObject y TJSONArray. En una unidad que usa las dos, gana la que va última en la cláusula uses, y salen errores que no tienen ningún sentido hasta que lo ves:
E2003 Undeclared identifier: 'ParseJSONValue'
E2010 Incompatible types: 'System.JSON.TJSONValue' and 'JsonDataObjects.TJsonArray'
Cualifica los nombres de tipo, System.JSON.TJSONObject y JsonDataObjects.TJsonArray, y la ambigüedad desaparece.
mORMot2 no está en esta comparación. Es más un framework completo que una biblioteca JSON, y medirlo con justicia obliga a traerlo entero y configurarlo, que da para otro artículo.
Construcción de APIs REST con JSON
Si construyes APIs REST en Delphi, DelphiMVCFramework trae serialización JSON automática:
[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); // Serialización JSON automática
end;
Consulta los ejemplos de DelphiMVCFramework para ejemplos completos, y la guía oficial si prefieres que te expliquen los serializadores en vez de adivinarlos.
Preguntas frecuentes
¿Cómo analizo una cadena JSON en Delphi?
Usa TJSONObject.ParseJSONValue() de la unidad System.JSON:
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')); // Salida: John
finally
LJSONObject.Free;
end;
end;
end;
¿Cómo manejo valores null en JSON?
Usa TryGetValue para manejar de forma segura valores faltantes o null:
var
LValue: string;
begin
if LJSONObject.TryGetValue<string>('optionalField', LValue) then
WriteLn('Valor: ', LValue)
else
WriteLn('El campo falta o es null');
end;
¿Cómo itero sobre un array JSON?
Usa la sintaxis moderna de bucle for-in:
var
LArray: TJSONArray;
LItem: TJSONValue;
begin
if LJSONObject.TryGetValue<TJSONArray>('items', LArray) then
begin
for LItem in LArray do
WriteLn(LItem.Value);
end;
end;
¿Cuál es la diferencia entre Format() y ToString()?
Format(): Devuelve JSON indentado y legible para humanos (solo Delphi 10.3+)ToString(): Devuelve JSON compacto sin espacios en blanco (mejor para transferencia de red, funciona en todas las versiones)
¿Cómo modifico un objeto JSON existente?
Usa RemovePair y luego AddPair. RemovePair devuelve el par eliminado (o nil si no se encuentra) - tú lo posees y debes liberarlo:
begin
// Remove devuelve el par - ¡debes liberarlo!
// Free es seguro de llamar en nil (verifica Self <> nil internamente)
LJSONObject.RemovePair('name').Free;
// Agregar nuevo valor
LJSONObject.AddPair('name', 'Nuevo Valor');
end;
¿Qué versión de Delphi introdujo el soporte JSON?
- Delphi 2009: Soporte JSON inicial en la unidad
DBXJSON - Delphi XE6: Renombrado a
System.JSONcon mejoras en la API - Delphi 10.1 Berlin: API fluida con
TJSONObjectBuilder - Delphi 10.3 Rio: Se agregó el método
Format(),EJSONParseExceptioncon información de error detallada
¿Cuál es la diferencia entre GetValue, FindValue y TryGetValue?
| Método | Devuelve | Si la Clave No se Encuentra |
|---|---|---|
GetValue<T>('key') |
Valor de tipo T | Lanza excepción |
FindValue('key') |
TJSONValue o nil | Devuelve nil |
TryGetValue<T>('key', outVar) |
Boolean | Devuelve False |
Recomendación: Usa TryGetValue para código de producción ya que es el enfoque más seguro.
¿Cómo creo una copia profunda de un objeto JSON?
Usa el método Clone:
var
LOriginal, LCopy: TJSONObject;
begin
LOriginal := TJSONObject.ParseJSONValue('{"name":"test"}') as TJSONObject;
try
LCopy := LOriginal.Clone as TJSONObject;
try
// LCopy es independiente - las modificaciones no afectan a LOriginal
finally
LCopy.Free;
end;
finally
LOriginal.Free;
end;
end;
¿Cómo verifico si un valor JSON es null?
var
LValue: TJSONValue;
begin
LValue := LJSONObject.FindValue('myField');
if LValue = nil then
WriteLn('El campo no existe')
else if LValue is TJSONNull then
WriteLn('El campo existe pero es null')
else
WriteLn('El campo tiene un valor: ', LValue.Value);
end;
¿Puedo usar notación de ruta para acceder a elementos de array?
Sí, usa notación de corchetes con el índice:
var
LFirstSkill: string;
begin
// Acceder al primer elemento del array de habilidades
if LJSONObject.TryGetValue<string>('skills[0]', LFirstSkill) then
WriteLn('Primera habilidad: ', LFirstSkill);
end;
¿Cómo convierto un objeto Delphi a JSON?
Con TJson.ObjectToJsonString de REST.Json, que viene con Delphi. Recorre el grafo de objetos, lee los campos privados y te da claves en camelCase; [JSONName] renombra un campo y [JSONMarshalled(False)] deja uno fuera. Para listas, control del formato de los nombres y datasets, usa los serializadores de DelphiMVCFramework. Mira Convertir objetos en JSON con REST.Json más arriba.
¿Es System.JSON lo bastante rápido?
Para casi todo, sí. Con 14 MB de JSON y 50.000 registros, System.JSON analiza y lee en unos 158 ms; JsonDataObjects hace el mismo trabajo en 42 ms, unas cuatro veces más rápido. Con un payload de unos cientos de kilobytes, que cubre la mayoría de las respuestas REST y todos los archivos de configuración, la diferencia es una fracción de milisegundo. Cambia de biblioteca cuando los documentos se midan en megabytes o cuando analices dentro de un bucle que se ejecuta miles de veces, no por defecto. Los números y el método están en ¿Cuán rápido es System.JSON, en realidad?.
¿Cómo serializo un TObjectList a JSON?
REST.Json no sabe de listas: haces el bucle, llamas a TJson.ObjectToJsonObject por elemento y agregas cada uno a un TJSONArray. En el otro sentido no tiene nada de nada, así que analizas el array y llamas a JsonToObject por cada elemento. El serializador de DelphiMVCFramework hace cada cosa en una sola llamada, SerializeCollection y DeserializeCollection.
¿Por qué TJson.JsonToObject lanza “Cannot instantiate type”?
Porque la clase está declarada en el archivo de programa .dpr, que no tiene RTTI enlazada. Mueve la declaración del tipo a una unidad y el mismo código funciona. Serializar hacia fuera no se queja nunca, así que el error solo aparece en el camino de vuelta. El serializador de DelphiMVCFramework falla por la misma causa con otro mensaje, Cannot find RTTI for ....
¿Es System.JSON seguro para hilos?
No, TJSONObject y las clases relacionadas no son seguras para hilos. Si varios hilos necesitan tocar el mismo objeto JSON, la sincronización (secciones críticas, bloqueos) la pones tú. Para acceso de solo lectura después del análisis inicial, puedes compartirlo entre hilos sin problema, mientras nadie lo modifique.
¿Cómo serializo un TDateTime a JSON?
TJSONObject no tiene una sobrecarga AddPair para TDateTime. Conviértelo primero a una cadena ISO 8601, y pasa AInputIsUTC de forma explícita, porque por defecto vale True y va a etiquetar una hora local como UTC. Mira Las fechas en JSON, y la hora que vas a perder:
LJSONObject.AddPair('createdAt', FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', Now));
¿Cuál es el tamaño máximo de JSON que Delphi puede analizar?
No hay un límite estricto, pero System.JSON carga todo el documento en memoria. Para archivos muy grandes (>100MB), considera analizadores de streaming como TJsonTextReader de System.JSON.Readers, o bibliotecas de terceros optimizadas para documentos grandes.
¿Cuál es la diferencia entre System.JSON y DBXJSON?
Son la misma biblioteca - solo renombrada. DBXJSON fue el nombre original de la unidad en Delphi 2009-XE5. A partir de Delphi XE6, se renombró a System.JSON para seguir las nuevas convenciones de nomenclatura. La API es esencialmente la misma, por lo que migrar código antiguo es sencillo.
¿Cómo imprimo JSON de forma elegante en Delphi?
Usa el método Format() (Delphi 10.3+) que devuelve JSON indentado y legible para humanos:
WriteLn(LJSONObject.Format()); // Impreso de forma elegante con indentación
WriteLn(LJSONObject.ToString); // Compacto, una sola línea
Para versiones antiguas de Delphi, usa bibliotecas de terceros o implementa formato personalizado.
¿Cómo manejo caracteres especiales y Unicode en JSON?
System.JSON maneja automáticamente Unicode y escapa caracteres especiales al generar JSON. Al analizar, las secuencias escapadas como \n, \t y \uXXXX se convierten correctamente. No se necesita manejo manual:
LJSONObject.AddPair('message', 'Línea 1'#13#10'Línea 2'); // Saltos de línea auto-escapados
LJSONObject.AddPair('emoji', '🚀'); // Unicode funciona directamente
¿Cómo fusiono dos objetos JSON?
No hay función de fusión integrada. Itera sobre un objeto y agrega sus pares al otro:
for LPair in LSource do
LTarget.AddPair(LPair.JsonString.Value, LPair.JsonValue.Clone as TJSONValue);
Nota: Debes clonar los valores ya que solo pueden pertenecer a un objeto padre.
¿Cómo valido JSON antes de analizarlo?
ParseJSONValue devuelve nil para JSON inválido, lo que sirve como validación básica. Para validación de esquema (verificar estructura, campos requeridos, tipos), necesitarás bibliotecas de terceros ya que Delphi no incluye soporte integrado para JSON Schema.
¿Cómo accedo a arrays profundamente anidados?
Combina notación de ruta con indexación de array:
// Acceder: {"data": {"users": [{"name": "Alice"}, {"name": "Bob"}]}}
if LJSONObject.TryGetValue<string>('data.users[1].name', LValue) then
WriteLn(LValue); // Salida: Bob
¿Puedo usar JSON con conjuntos de datos FireDAC?
Sí, pero no hay integración directa. Puedes iterar manualmente sobre un conjunto de datos y construir JSON, o usar bibliotecas de serialización. Tanto DelphiMVCFramework como mORMot2 proporcionan serialización de conjunto de datos a JSON lista para usar.
¿Cómo manejo JSON con claves duplicadas?
JSON técnicamente permite claves duplicadas, aunque está desaconsejado. TJSONObject almacena todos los pares, pero GetValue/TryGetValue solo devuelven la primera coincidencia. Para acceder a todos los valores con la misma clave, itera usando el bucle for-in.
Resumen
Delphi trae soporte de JSON integrado y sólido en la unidad System.JSON. Lo que hay que recordar:
- Usa
TJSONObjectyTJSONArraypara crear y analizar JSON - Siempre verifica nil al analizar cadenas JSON
- Usa
TryGetValuepara lectura segura de valores con campos opcionales - Usa notación de ruta (
'parent.child') para valores anidados - Recuerda la gestión de memoria: los objetos padre poseen a sus hijos;
RemovePairdevuelve la posesión a ti - Considera bibliotecas de terceros solo para necesidades específicas de rendimiento
- Usa
Format()para salida legible (Delphi 10.3+),ToString()para salida compacta - Usa bucles for-in para iteración más limpia sobre arrays y pares de objetos
Para construir APIs REST modernas en Delphi, mira DelphiMVCFramework: trae serialización JSON avanzada, y es el framework que mantengo yo.
Comments
comments powered by Disqus