Become a member!

Soporte JSON en Delphi: Guía Completa con Ejemplos (2026)

🌐
Este artículo también está disponible en otros idiomas:
🇬🇧 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.

Todos los ejemplos de código en este artículo han sido probados y verificados con Delphi 13 Florence.
📝
Este artículo se centra en el analizador JSON estilo DOM (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
💡
Todos los ejemplos en este artículo son compatibles con Delphi XE7 y versiones posteriores a menos que se indique lo contrario. Las características específicas de versión están claramente marcadas.

¿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.
⚠️
Siempre verifica si 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.JSON más abajo en este artículo
  • Soporte BSON necesario: Grijjy Foundation
  • Serialización de objetos: REST.Json para 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.JSON con mejoras en la API
  • Delphi 10.1 Berlin: API fluida con TJSONObjectBuilder
  • Delphi 10.3 Rio: Se agregó el método Format(), EJSONParseException con 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:

  1. Usa TJSONObject y TJSONArray para crear y analizar JSON
  2. Siempre verifica nil al analizar cadenas JSON
  3. Usa TryGetValue para lectura segura de valores con campos opcionales
  4. Usa notación de ruta ('parent.child') para valores anidados
  5. Recuerda la gestión de memoria: los objetos padre poseen a sus hijos; RemovePair devuelve la posesión a ti
  6. Considera bibliotecas de terceros solo para necesidades específicas de rendimiento
  7. Usa Format() para salida legible (Delphi 10.3+), ToString() para salida compacta
  8. 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