Become a member!

Supporto JSON in Delphi: Guida Completa con Esempi (2026)

🌐
Questo articolo è disponibile anche in altre lingue:
🇬🇧 English  •  🇪🇸 Español  •  🇩🇪 Deutsch  •  🇫🇷 Français

JSON è lo standard de-facto per scambiare dati tra applicazioni. Che tu stia costruendo REST API, leggendo file di configurazione o parlando con un web service, ti serve sapere come funziona in Delphi.

Questa guida copre il supporto JSON in Delphi da cima a fondo, con esempi compilabili che puoi riusare nei tuoi progetti.

Ho testato e verificato tutti gli esempi di codice di questo articolo con Delphi 13 Florence.
📝
Questo articolo si concentra sul parser JSON in stile DOM (TJSONObject, TJSONArray). Per il parsing in stile streaming/SAX, consulta le unit System.JSON.Readers e System.JSON.Writers.

Compatibilità tra le Versioni di Delphi

Il supporto JSON si è evoluto significativamente tra le varie versioni di Delphi:

Versione Unit Caratteristiche Principali
Delphi 2009 DBXJSON Supporto JSON iniziale con classi base
Delphi XE6 System.JSON Unit rinominata, API migliorata
Delphi 10.1 Berlin System.JSON API fluent TJSONObjectBuilder, miglioramenti a TryGetValue<T>
Delphi 10.3 Rio System.JSON Metodo Format(), EJSONParseException con dettagli, miglioramenti delle prestazioni
Delphi 11-12 System.JSON Ulteriori ottimizzazioni e perfezionamenti
Delphi 13 Florence System.JSON Ultimi miglioramenti e supporto continuo
💡
Tutti gli esempi in questo articolo sono compatibili con Delphi XE7 e successivi salvo diversa indicazione. Le funzionalità specifiche per versione sono chiaramente contrassegnate.

Cos’è JSON?

JSON è un formato di scambio dati leggero, basato su testo. Lo leggono e scrivono facilmente gli umani, lo analizzano e generano altrettanto facilmente le macchine. Un documento JSON può contenere:

  • Oggetti: Coppie chiave-valore racchiuse tra parentesi graffe {}
  • Array: Liste ordinate di valori racchiuse tra parentesi quadre []
  • Valori: Stringhe, numeri, booleani (true/false), null, oggetti o array

Esempio di struttura JSON:

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

Panoramica delle Classi JSON di Delphi

Delphi fornisce supporto JSON integrato tramite l’unit System.JSON. Le classi principali sono:

Classe Descrizione
TJSONValue Classe base per tutti i tipi di valori JSON
TJSONObject Rappresenta un oggetto JSON (coppie chiave-valore)
TJSONArray Rappresenta un array JSON (lista ordinata)
TJSONString Rappresenta un valore stringa JSON
TJSONNumber Rappresenta un valore numerico JSON
TJSONBool Rappresenta un valore booleano JSON
TJSONNull Rappresenta un valore null JSON
TJSONPair Rappresenta una coppia chiave-valore in un oggetto

Creazione di Oggetti JSON

Il punto di partenza: creare oggetti JSON e aggiungere proprietà.

Creazione Base di Oggetti JSON

La base è creare un TJSONObject e aggiungere coppie chiave-valore. AddPair ha overload per stringhe, interi, booleani e double: non devi avvolgere i valori primitivi in classi JSON apposite. Ecco un oggetto con qualche tipo diverso:

program JSONCreateBasic;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
begin
  LJSONObject := TJSONObject.Create;
  try
    // Aggiungi proprietà stringa
    LJSONObject.AddPair('firstName', 'Daniele');
    LJSONObject.AddPair('lastName', 'Teti');

    // Aggiungi proprietà numerica (sono disponibili overload per Integer, Int64, Double)
    LJSONObject.AddPair('age', 45);

    // Aggiungi proprietà booleana
    LJSONObject.AddPair('active', True);

    // Aggiungi proprietà null (nessun overload - deve usare TJSONNull)
    LJSONObject.AddPair('middleName', TJSONNull.Create);

    // Stampa il JSON
    // Nota: Format() disponibile da Delphi 10.3 Rio
    {$IF CompilerVersion >= 33.0} // Delphi 10.3 Rio
    WriteLn(LJSONObject.Format());
    {$ELSE}
    WriteLn(LJSONObject.ToString);
    {$ENDIF}
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Output:

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

Creazione di Array JSON

Un array JSON è una collezione ordinata: stringhe, numeri, booleani, anche altri array o oggetti, mescolati come vuoi. Quando aggiungi un TJSONArray a un TJSONObject con AddPair, l’oggetto genitore ne prende possesso: liberi solo la root. Qui costruisco un array di stringhe e uno a tipo misto:

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');

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

    // Crea array con tipi misti
    LContacts := TJSONArray.Create;
    LJSONObject.AddPair('contacts', LContacts);
    LContacts.Add('daniele@example.com');  // stringa
    LContacts.Add(123456);                 // numero
    LContacts.Add(True);                   // booleano

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

  ReadLn;
end.

Output:

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

Creazione di un Array di Oggetti

Il pattern più comune nel JSON vero è un array di oggetti: una lista di utenti, di prodotti, di record. Ogni oggetto ha le sue proprietà. Crei ogni oggetto a parte e lo aggiungi all’array con Add; l’array possiede tutti quelli che gli passi:

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);

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

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

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

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

  ReadLn;
end.

Output:

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

Oggetti JSON Annidati

I dati complessi sono quasi sempre gerarchici: una persona ha un indirizzo, l’indirizzo ha città e paese. In Delphi annidi le strutture aggiungendo TJSONObject come valori dentro altri oggetti. Come per gli array, il genitore possiede i figli, e la gestione della memoria resta semplice. Qui una persona con indirizzo e azienda annidati:

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');

    // Crea oggetto indirizzo annidato
    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');

    // Crea un altro oggetto annidato
    LCompany := TJSONObject.Create;
    LJSONObject.AddPair('company', LCompany);
    LCompany.AddPair('name', 'bit Time Professionals');
    LCompany.AddPair('website', 'https://www.bittime.it');

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

  ReadLn;
end.

Output:

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

Uso di TJSONObjectBuilder (Delphi 10.1 Berlin+)

Se preferisci una sintassi fluent, Delphi 10.1 Berlin ha introdotto TJSONObjectBuilder: costruisci tutta la struttura in un’unica espressione concatenando BeginObject, BeginArray, Add, EndObject, EndArray. Il builder scrive su un TJsonTextWriter, che scrive su un TStringBuilder. La configurazione iniziale è più verbosa, ma su strutture complesse il risultato si legge meglio:

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
          // Costruisci JSON usando l'API fluent
          LBuilder
            .BeginObject
              .Add('firstName', 'Daniele')
              .Add('lastName', 'Teti')
              .Add('age', 45)
              .Add('active', True)
              .BeginObject('address')
                .Add('city', 'Rome')
                .Add('country', 'Italy')
              .EndObject
              .BeginArray('skills')
                .Add('Delphi')
                .Add('Python')
                .Add('SQL')
              .EndArray
            .EndObject;

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

  ReadLn;
end.

Output:

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

Parsing di Stringhe JSON

JSON che arriva da un web service, un file o qualunque altra fonte va analizzato in oggetti Delphi prima di poterci lavorare. Se ne occupa TJSONObject.ParseJSONValue, un metodo di classe che restituisce un TJSONValue, la classe base: controlla tu se è del tipo che ti aspetti, tipicamente TJSONObject o TJSONArray. Su JSON malformato restituisce nil, quindi controllalo sempre prima di andare avanti:

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 restituisce TJSONValue, converti al tipo appropriato
  LJSONValue := TJSONObject.ParseJSONValue(JSON_STRING);

  if LJSONValue = nil then
  begin
    WriteLn('ERRORE: JSON non valido!');
    ReadLn;
    Exit;
  end;

  try
    // Controlla se è un oggetto (potrebbe essere un array a livello root)
    if not (LJSONValue is TJSONObject) then
    begin
      WriteLn('ERRORE: Atteso oggetto JSON a livello root');
      Exit;
    end;

    LJSONObject := TJSONObject(LJSONValue); // Cast diretto - sicuro dopo il controllo "is"
    WriteLn('Analisi riuscita!');
    WriteLn('Numero di coppie: ', LJSONObject.Count);

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

  ReadLn;
end.
⚠️
Controlla sempre se ParseJSONValue restituisce nil, che indica JSON non valido.

Gestione degli Errori di Parsing (Delphi 10.3+)

Quando il parsing fallisce, sapere perché ti fa risparmiare tempo. Da Delphi 10.3 Rio, passa True come secondo parametro a ParseJSONValue e ottieni un’eccezione EJSONParseException invece di un nil muto. Dentro trovi il messaggio di errore, il percorso dove ha fallito e l’offset del carattere: utile su JSON complesso o arrivato da fuori:

program JSONParseErrors;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

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

var
  LJSONValue: TJSONValue;
begin
  {$IF CompilerVersion >= 33.0} // Delphi 10.3 Rio
  try
    // Usa l'opzione RaiseExc per ottenere l'eccezione con i dettagli
    LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON, True);
    try
      WriteLn('Analizzato: ', LJSONValue.ToString);
    finally
      LJSONValue.Free;
    end;
  except
    on E: EJSONParseException do
    begin
      WriteLn('Errore di parsing!');
      WriteLn('  Messaggio: ', E.Message);
      WriteLn('  Percorso: ', E.Path);
      WriteLn('  Offset: ', E.Offset);
    end;
  end;
  {$ELSE}
  // Pre-10.3: controlla solo nil
  LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON);
  if LJSONValue = nil then
    WriteLn('JSON non valido - nessun dettaglio disponibile')
  else
    LJSONValue.Free;
  {$ENDIF}

  ReadLn;
end.

Lettura di Valori JSON

Delphi ti dà diversi modi per leggere un valore da un oggetto JSON, ognuno con un compromesso diverso tra comodità e sicurezza. Sapere quale usare quando ti fa scrivere codice che non esplode sui dati mancanti.

Metodo 1: GetValue con Tipo Generico (Delphi XE7+)

Il modo più diretto è GetValue<T>: specifichi il tipo che ti aspetti e Delphi converte per te. Solleva un’eccezione se la chiave non esiste, quindi usalo solo quando sai per certo che c’è:

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> - solleva eccezione se la chiave non è trovata
    WriteLn('Nome: ', LJSONObject.GetValue<string>('name'));
    WriteLn('Età: ', LJSONObject.GetValue<Integer>('age'));
    WriteLn('Attivo: ', LJSONObject.GetValue<Boolean>('active'));
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Metodo 2: TryGetValue - Lettura Sicura (Consigliato)

In produzione usa TryGetValue<T>. Restituisce False se la chiave manca o il valore non converte al tipo richiesto, senza passare da un blocco except. Utile con JSON esterno, dove non hai garanzie su quali campi arrivano:

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 restituisce False se la chiave non è trovata (nessuna eccezione)
    if LJSONObject.TryGetValue<string>('name', LName) then
      WriteLn('Nome: ', LName)
    else
      WriteLn('Nome non trovato');

    if LJSONObject.TryGetValue<Integer>('age', LAge) then
      WriteLn('Età: ', LAge)
    else
      WriteLn('Età non trovata');

    // Questa chiave non esiste - nessuna eccezione sollevata
    if LJSONObject.TryGetValue<string>('middleName', LMiddleName) then
      WriteLn('Secondo Nome: ', LMiddleName)
    else
      WriteLn('Secondo Nome: (non specificato)');
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Metodo 3: FindValue - Restituisce nil se Non Trovato

Se ti serve il TJSONValue grezzo invece di un valore convertito, usa FindValue. Restituisce nil se la chiave non esiste, non solleva mai eccezioni, e ti dà accesso pieno a proprietà e metodi. Comodo quando devi controllare il tipo effettivo di un valore o navigare strutture annidate:

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 restituisce nil se non trovato (non solleva mai eccezioni)
    LValue := LJSONObject.FindValue('name');
    if LValue <> nil then
      WriteLn('Nome: ', LValue.Value);

    LValue := LJSONObject.FindValue('nonexistent');
    if LValue = nil then
      WriteLn('Chiave "nonexistent" non trovata');
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Metodo 4: Notazione a Percorso per Valori Annidati

La notazione a percorso è una delle comodità migliori di Delphi: accedi a valori annidati con un percorso separato da punti, tipo 'person.address.city', senza attraversare a mano ogni oggetto intermedio. Funziona con TryGetValue, GetValue e FindValue:

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
    // Usa la notazione a punto per accedere ai valori annidati
    if LJSONObject.TryGetValue<string>('person.name', LValue) then
      WriteLn('Nome Persona: ', LValue);

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

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

  ReadLn;
end.

Lettura di Array - Approcci Classico e Moderno

Su un array devi iterare gli elementi. Delphi supporta sia il ciclo classico per indice con Items[I] sia il for-in, che funziona su qualunque TJSONArray. Il for-in è più pulito quando non ti serve la posizione; il ciclo classico te la dà. Sugli array numerici, converti ogni elemento in TJSONNumber per usare 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
    // Leggi array di stringhe - ciclo for classico
    if LJSONObject.TryGetValue<TJSONArray>('skills', LSkills) then
    begin
      WriteLn('Competenze (ciclo classico):');
      for I := 0 to LSkills.Count - 1 do
        WriteLn('  ', I + 1, '. ', LSkills.Items[I].Value);
    end;

    WriteLn;

    // Leggi array di stringhe - ciclo for-in moderno (Delphi XE+)
    if LJSONObject.TryGetValue<TJSONArray>('skills', LSkills) then
    begin
      WriteLn('Competenze (ciclo for-in):');
      for LItem in LSkills do
        WriteLn('  - ', LItem.Value);
    end;

    WriteLn;

    // Leggi array numerico
    if LJSONObject.TryGetValue<TJSONArray>('scores', LScores) then
    begin
      WriteLn('Punteggi:');
      for LItem in LScores do
        WriteLn('  Punteggio: ', (LItem as TJSONNumber).AsInt);
    end;
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Iterazione sulle Coppie di un Oggetto JSON

A volte devi scorrere tutte le proprietà di un oggetto senza sapere in anticipo i nomi delle chiavi: un visualizzatore JSON generico, una struttura dinamica. Il for-in funziona su TJSONObject come sugli array, e produce istanze TJSONPair. Ogni coppia ti dà la chiave (JsonString.Value), il valore (JsonValue), e puoi controllare il tipo runtime con ClassName:

program JSONIteratePairs;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

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

var
  LJSONObject: TJSONObject;
  LPair: TJSONPair;
begin
  LJSONObject := TJSONObject.ParseJSONValue(JSON_DATA) as TJSONObject;
  try
    WriteLn('Tutte le coppie nell''oggetto:');
    WriteLn;

    // Itera su tutte le coppie usando for-in
    for LPair in LJSONObject do
    begin
      WriteLn('Chiave: ', LPair.JsonString.Value);
      WriteLn('Valore: ', LPair.JsonValue.ToString);
      WriteLn('Tipo: ', LPair.JsonValue.ClassName);
      WriteLn;
    end;
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Modifica di Oggetti JSON

Gli oggetti JSON in Delphi sono mutabili: aggiungi, rimuovi, aggiorni proprietà dopo la creazione. Ma la gestione della memoria ha una regola precisa: RemovePair ti trasferisce la proprietà della coppia rimossa, e tocca a te liberarla. Free in Delphi è sicuro su nil, quindi RemovePair('key').Free funziona anche se la chiave non esiste.

Aggiunta e Rimozione di Coppie

Ecco il ciclo completo: crei proprietà, ne rimuovi una, ne aggiorni un’altra. Per aggiornare un valore rimuovi prima la vecchia coppia (e la liberi), poi ne aggiungi una nuova con la stessa chiave:

program JSONModify;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LRemovedPair: TJSONPair;
begin
  LJSONObject := TJSONObject.Create;
  try
    // Aggiungi coppie iniziali
    LJSONObject.AddPair('name', 'Daniele');
    LJSONObject.AddPair('city', 'Rome');
    LJSONObject.AddPair('temp', 'da rimuovere');

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

    // Rimuovi una coppia - RemovePair restituisce la coppia rimossa (ne hai la proprietà!)
    LRemovedPair := LJSONObject.RemovePair('temp');
    LRemovedPair.Free; // Sicuro anche se nil - Free controlla Self <> nil

    WriteLn('Dopo la rimozione di "temp":');
    WriteLn(LJSONObject.ToString);
    WriteLn;

    // Per aggiornare un valore: rimuovi poi aggiungi
    LRemovedPair := LJSONObject.RemovePair('city');
    LRemovedPair.Free;
    LJSONObject.AddPair('city', 'Milan');

    WriteLn('Dopo l''aggiornamento di "city":');
    WriteLn(LJSONObject.ToString);
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Output:

Iniziale:
{"name":"Daniele","city":"Rome","temp":"da rimuovere"}

Dopo la rimozione di "temp":
{"name":"Daniele","city":"Rome"}

Dopo l'aggiornamento di "city":
{"name":"Daniele","city":"Milan"}

Clonazione di Oggetti JSON

Se devi modificare un oggetto JSON senza toccare l’originale, usa Clone. È una copia profonda: un albero indipendente, dove le modifiche su una copia non si vedono sull’altra. Serve quando ricevi dati che devi trasformare prima di rispedirli, mantenendo intatto l’originale:

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 indipendente
    LClone := LOriginal.Clone as TJSONObject;
    try
      // Modifica il clone - l'originale non è influenzato
      LPair := LClone.RemovePair('city');
      LPair.Free;
      LClone.AddPair('city', 'Milan');

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

  ReadLn;
end.

Output:

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

Lavorare con File JSON

Salvare JSON su disco ti serve per la configurazione, la cache, l’export dei dati. System.IOUtils ti dà TFile, con metodi semplici per leggere e scrivere file di testo: si sposa bene con JSON, che in fondo è solo una stringa.

Salvare JSON su File

Per salvare un oggetto JSON, convertilo in stringa con Format() (leggibile) o ToString() (compatto) e scrivi la stringa su disco. TPath.GetDocumentsPath ti garantisce una posizione scrivibile su qualunque configurazione di 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);

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

    WriteLn('Salvato in: ', LFileName);
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Caricamento di JSON da File

Leggere è altrettanto semplice: leggi il file in una stringa e la analizzi con ParseJSONValue. Controlla prima che il file esista, per evitare eccezioni, e che il parsing sia riuscito prima di toccare i dati. La notazione a percorso funziona uguale sia su JSON analizzato che su oggetti costruiti 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('File non trovato: ', LFileName);
    ReadLn;
    Exit;
  end;

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

  if LJSONValue = nil then
  begin
    WriteLn('JSON non valido nel file!');
    ReadLn;
    Exit;
  end;

  try
    LJSONObject := LJSONValue as TJSONObject;

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

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

  ReadLn;
end.

Da oggetti a JSON con REST.Json

Fino a qui il JSON lo hai costruito a mano, coppia per coppia. Ha senso quando il punto è la forma del documento. Non ne ha quando hai già una classe e vuoi solo spedirla in rete.

Per quello Delphi ha REST.Json. Fa parte di Delphi da XE5, non richiede codice di terze parti, e quasi tutto il lavoro lo fa una chiamata sola.

Dall’oggetto al JSON

uses
  REST.Json, REST.Json.Types;

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

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

// ...

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

Writeln(TJson.ObjectToJsonString(LCustomer));

Output:

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

Qui sono successe tre cose.

Il TAddress annidato è stato serializzato anche lui, senza che tu lo chiedessi. REST.Json percorre il grafo degli oggetti.

InternalNote nell’output non c’è. [JSONMarshalled(False)] è il modo per tenere un campo fuori dal documento, ed è l’attributo che vuoi su tutto quello che il client non deve vedere.

Le chiavi sono uscite come id, name, zipCode. REST.Json legge i campi privati, non le proprietà, toglie il prefisso F e mette in minuscolo la prima lettera. Così FZipCode diventa zipCode, camelCase, che ti piaccia o no. Quando dall’altra parte pretendono un nome diverso, [JSONName('vat_number')] te lo cambia un campo alla volta.

Dal JSON all’oggetto

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

Nota che l’input usava Id e Name con la maiuscola e ha funzionato lo stesso: in lettura le maiuscole non contano, in scrittura sì. L’oggetto torna indietro completo, indirizzo annidato compreso, e sta a te liberarlo.

I campi che il JSON non nomina restano come li ha lasciati il costruttore:

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

Nessuna eccezione. Se un Id mancante vuol dire che a monte qualcosa non va, controllarlo tocca a te.

Quello che ti costa un pomeriggio

Dichiara quelle classi nel .dpr e TJson.JsonToObject fallisce:

EConversionError: Internal: Cannot instantiate type restjson.TCustomer

Al serializzatore serve la RTTI linkata per la classe, e un tipo dichiarato nel file di programma non ce l’ha. Sposta le dichiarazioni in una unit e lo stesso codice funziona. In uscita la serializzazione non protesta mai, quindi lo incontri solo al ritorno, di solito dopo che ti sei convinto che il JSON sia malformato.

Ce n’è un’altra, specifica delle versioni recenti: TJson.Format è deprecata in Delphi 13 Florence. Il compilatore ti dice cosa usare al suo posto:

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

Quindi il pretty printing di un oggetto adesso è:

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": ""
}

Dove REST.Json si ferma

Serializza una lista e trovi il limite:

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

Funziona, ed è già un ciclo che hai scritto tu a mano. Nel verso opposto, da un array JSON a una TObjectList<TCustomer>, REST.Json non ha proprio niente da offrire: l’array lo analizzi tu e chiami JsonToObject elemento per elemento.

REST.Json è molto bravo con un oggetto alla volta, con i nomi che sceglie lui. Oltre quello ti serve un serializzatore fatto apposta.

Quando serve di più: i serializzatori di DelphiMVCFramework

DelphiMVCFramework ha un serializzatore che puoi usare da solo, senza un server e senza nemmeno un controller. È una unit e un’interfaccia.

Una lista, in una chiamata, in tutti e due i versi:

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"}]

E al ritorno:

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

Due chiamate dove REST.Json ti dava due cicli. Nota anche che il TDateTime è uscito ed è tornato come data vera, in ISO 8601 con l’offset, che è l’argomento della prossima sezione.

Il name case qui è una decisione, non una regola che ti viene imposta. Metti l’attributo sulla classe e tutta la classe lo segue:

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

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

È il motivo per cui quasi tutti finiscono su questo serializzatore. Se l’API con cui devi parlare vuole order_id, REST.Json ti dà un [JSONName] per campo, per sempre; il serializzatore di DMVCFramework ti dà un attributo per classe.

Dentro però c’è una trappola, ed è silenziosa. Il name case vale anche in lettura. Il serializzatore usa ncLowerCase per default, quindi emette placedat e si aspetta placedat. Dagli lo stesso payload con placedAt e:

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 è lo zero di TDateTime. Nessun errore, nessun warning, solo un campo che non è arrivato, in silenzio. Quando consumi un’API che non controlli, allinea il name case al suo e prova un payload da un capo all’altro prima di credere a quello che vedi.

Vale anche qui la stessa regola sulla RTTI, tra l’altro. Dichiara TOrder nel .dpr e ottieni:

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

Serializzatore diverso, messaggio diverso, stessa causa: i tipi vanno nelle unit.

Le date in JSON, e l’ora che ci perderai

JSON non ha un tipo data. Qualunque cosa tu faccia, un TDateTime esce dal processo come stringa, e su quale stringa usare si sono messi d’accordo tutti: ISO 8601. Delphi ti dà la conversione in System.DateUtils, e ti dà un default sbagliato per la maggior parte del codice che stai scrivendo.

Il default è 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

Il secondo parametro è AInputIsUTC e per default vale True. Quindi DateToISO8601(SomeDate) dice al mondo che il valore che gli hai passato è già UTC. Se arriva da Now, da un TDateTimePicker o da una colonna di database scritta da un’applicazione locale, UTC non è, e hai appena stampato una Z sopra un’ora locale.

Non solleva niente e il documento è valido. È l’ora a essere sbagliata.

Scrivi e leggi con lo stesso 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

Mescola i flag e il valore si sposta del tuo offset da UTC, in silenzio:

LText := DateToISO8601(LOriginal, False);   // locale
LBack := ISO8601ToDate(LText);              // default, lo tratta come UTC
written with False, read with the default:
  15/03/2023 12:00:00 -> 15/03/2023 11:00:00
  drift in minutes: 60

Un’ora, su una macchina in Italia a marzo. Ad agosto sono due. Su una macchina in UTC sono zero, ed è esattamente per questo che sopravvive ai test e salta fuori da un cliente.

Dentro 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

Input che non hai scritto tu

ISO8601ToDate solleva un’eccezione su tutto quello che non riesce a leggere. Per un payload arrivato dalla rete, usa la versione 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

Stessa forma di TryGetValue più su in questo articolo, e stesso motivo per preferirla.

Scegli UTC o locale una volta sola, per tutta l’applicazione, e passa il flag esplicitamente ogni singola volta. Il default non sarà quello che intendevi tu.

Esempio Pratico: Client REST API

Ecco uno scenario vero che mette insieme i pezzi visti finora: chiami una REST API ed elabori la risposta JSON. L’esempio si connette a JSONPlaceholder, un’API di test gratuita, prende una lista di utenti e la traduce in record Delphi. Nota che TryGetValue è ovunque: con un’API esterna che può cambiare da un giorno all’altro, è quello che ti salva quando un campo sparisce.

📝
THTTPClient richiede Delphi XE8 o successivo.
program JSONRestApiClient;
{$APPTYPE CONSOLE}

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

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

function ParseUser(AJSONObject: TJSONObject): TUser;
begin
  // Uso di TryGetValue per sicurezza
  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('Recupero utenti dall''API JSONPlaceholder...');
  WriteLn;

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

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

    // Analizza la risposta array JSON
    LJSONValue := TJSONObject.ParseJSONValue(LResponse.ContentAsString);
    if LJSONValue = nil then
    begin
      WriteLn('Risposta JSON non valida');
      ReadLn;
      Exit;
    end;

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

      LJSONArray := LJSONValue as TJSONArray;
      WriteLn('Trovati ', LJSONArray.Count, ' utenti:');
      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('Nome: ', LUser.Name);
        WriteLn('Email: ', LUser.Email);
        WriteLn('Username: ', LUser.Username);
        WriteLn(StringOfChar('-', 50));
      end;

    finally
      LJSONValue.Free;
    end;

  finally
    LClient.Free;
  end;

  ReadLn;
end.

Esempio Pratico: Gestore File di Configurazione

Ultimo esempio: una classe completa per gestire la configurazione. TConfigManager incapsula caricamento, salvataggio e accesso alle impostazioni dietro un’API pulita e type-safe. Carica il file solo quando serve, usa valori predefiniti per le chiavi mancanti, e crea il file da solo se manca. Prendilo come punto di partenza per la tua configurazione:

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;

// Dimostrazione di utilizzo
var
  Config: TConfigManager;
  LConfigFile: string;
begin
  LConfigFile := TPath.Combine(TPath.GetDocumentsPath, 'appsettings.json');
  WriteLn('File di configurazione: ', LConfigFile);
  WriteLn;

  Config := TConfigManager.Create(LConfigFile);
  try
    // Imposta alcuni valori (le chiavi sono piatte - non oggetti annidati)
    Config.SetValue('databaseHost', 'localhost');
    Config.SetValue('databasePort', 5432);
    Config.SetValue('databaseName', 'myapp');
    Config.SetValue('loggingEnabled', True);
    Config.SetValue('loggingMaxFiles', 10);
    Config.Save;

    WriteLn('Configurazione salvata!');
    WriteLn;

    // Rileggi i valori
    WriteLn('Host Database: ', Config.GetString('databaseHost'));
    WriteLn('Porta Database: ', Config.GetInteger('databasePort'));
    WriteLn('Logging Abilitato: ', Config.GetBoolean('loggingEnabled'));

    // Leggi con valore predefinito
    WriteLn('Timeout (predefinito 30): ', Config.GetInteger('timeout', 30));
  finally
    Config.Free;
  end;

  ReadLn;
end.

Output:

File di configurazione: C:\Users\yourname\Documents\appsettings.json

Configurazione salvata!

Host Database: localhost
Porta Database: 5432
Logging Abilitato: TRUE
Timeout (predefinito 30): 30

Librerie JSON di Terze Parti

Il parser integrato copre quasi tutto. In alcuni casi però conviene una libreria di terze parti:

Libreria Migliore Per URL
JsonDataObjects Alte prestazioni, usata da DelphiMVCFramework GitHub
Grijjy Foundation Ricca di funzionalità, include supporto BSON GitHub
mORMot2 Framework full-stack (ORM, SOA, REST) che si porta dietro il suo strato JSON GitHub

Quando Usare Librerie di Terze Parti

  • File JSON grandi (>10MB): Considera parser streaming o JsonDataObjects
  • Parsing ad alta frequenza: JsonDataObjects, confrontato con System.JSON più avanti in questo articolo
  • Necessità supporto BSON: Grijjy Foundation
  • Serializzazione oggetti: REST.Json per i casi semplici, i serializzatori di DelphiMVCFramework per liste, dataset e controllo del name case

Per la maggior parte delle applicazioni System.JSON integrato basta, e in più non porta dipendenze esterne.

Quanto è veloce davvero System.JSON

“Usa una libreria di terze parti se ti servono le prestazioni” è facile da scrivere e difficile da mettere in pratica. Ecco i numeri.

Il test costruisce un array di 50.000 record, ognuno con un intero, due stringhe, un booleano, un numero e un timestamp ISO 8601: 14 MB di testo UTF-16, la forma di un export vero. Poi misura due lavori: analizzare e leggere un intero da ogni record, che è quello che fa un client, e analizzare e riserializzare subito in uscita, che è quello che fa un proxy.

Ogni misura è preceduta da un warm-up che viene scartato, e quella riportata è la migliore di sette, quindi i numeri sono il minimo, non la media di qualunque altra cosa stesse facendo la macchina.

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, ottimizzazione attiva, controlli di range e overflow disattivati, su un Core i9-13980HX con Windows 11.

Quindi: JsonDataObjects analizza circa quattro volte più in fretta, e fa il giro completo circa due volte e mezzo più in fretta. Il divario è reale ed è stabile da un’esecuzione all’altra.

System.JSON ha comunque masticato 14 MB in circa un sesto di secondo. Se il tuo JSON è di qualche centinaio di kilobyte, che copre quasi tutte le risposte REST e praticamente ogni file di configurazione, stai scegliendo tra due millisecondi e mezzo millisecondo. Non è una decisione, è un errore di arrotondamento, e System.JSON è già installato.

Passa a JsonDataObjects quando il payload si misura in megabyte, o quando analizzi dentro un ciclo che gira migliaia di volte, o quando sei su un dispositivo dove la CPU non è gratis. Altrimenti la dipendenza ti costa più di quanto ti dia.

Una nota pratica se lo porti dentro: JsonDataObjects dichiara i propri TJSONObject e TJSONArray. In una unit che le usa tutte e due vince quella che viene per ultima nella clausola uses, e ti prendi errori che non hanno senso finché non te ne accorgi:

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

Qualifica i nomi dei tipi, System.JSON.TJSONObject e JsonDataObjects.TJsonArray, e l’ambiguità sparisce.

mORMot2 in questo confronto non c’è. È un framework intero più che una libreria JSON, e misurarlo in modo onesto vuol dire tirarselo dentro e configurarlo tutto, che è un altro articolo.

Costruire REST API con JSON

Se costruisci REST API in Delphi, DelphiMVCFramework ti dà serializzazione JSON automatica:

[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); // Serializzazione JSON automatica
end;

Vedi i sample di DelphiMVCFramework per esempi completi, e la guida ufficiale se preferisci che i serializzatori te li spieghino invece di indovinarli.

Domande Frequenti (FAQ)

Come faccio a fare il parsing di una stringa JSON in Delphi?

Usa TJSONObject.ParseJSONValue() dall’unit 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')); // Output: John
    finally
      LJSONObject.Free;
    end;
  end;
end;

Come gestisco i valori null in JSON?

Usa TryGetValue per gestire in sicurezza valori mancanti o null:

var
  LValue: string;
begin
  if LJSONObject.TryGetValue<string>('optionalField', LValue) then
    WriteLn('Valore: ', LValue)
  else
    WriteLn('Campo mancante o null');
end;

Come itero su un array JSON?

Usa la sintassi moderna del ciclo 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;

Qual è la differenza tra Format() e ToString()?

  • Format(): Restituisce JSON indentato e leggibile (Solo Delphi 10.3+)
  • ToString(): Restituisce JSON compatto senza spazi (meglio per il trasferimento di rete, funziona in tutte le versioni)

Come modifico un oggetto JSON esistente?

Usa RemovePair poi AddPair. RemovePair restituisce la coppia rimossa (o nil se non trovata) - ne hai la proprietà e devi liberarla:

begin
  // Remove restituisce la coppia - devi liberarla!
  // Free è sicuro da chiamare su nil (controlla internamente Self <> nil)
  LJSONObject.RemovePair('name').Free;
  // Aggiungi nuovo valore
  LJSONObject.AddPair('name', 'Nuovo Valore');
end;

Quale versione di Delphi ha introdotto il supporto JSON?

  • Delphi 2009: Supporto JSON iniziale nell’unit DBXJSON
  • Delphi XE6: Rinominata in System.JSON con miglioramenti API
  • Delphi 10.1 Berlin: API fluent TJSONObjectBuilder
  • Delphi 10.3 Rio: Aggiunto metodo Format(), EJSONParseException con informazioni dettagliate sull’errore

Qual è la differenza tra GetValue, FindValue e TryGetValue?

Metodo Restituisce Se Chiave Non Trovata
GetValue<T>('key') Valore di tipo T Solleva eccezione
FindValue('key') TJSONValue o nil Restituisce nil
TryGetValue<T>('key', outVar) Boolean Restituisce False

Raccomandazione: usa TryGetValue in produzione. È l’approccio più sicuro.

Come creo una copia profonda di un oggetto JSON?

Usa il metodo Clone:

var
  LOriginal, LCopy: TJSONObject;
begin
  LOriginal := TJSONObject.ParseJSONValue('{"name":"test"}') as TJSONObject;
  try
    LCopy := LOriginal.Clone as TJSONObject;
    try
      // LCopy è indipendente - le modifiche non influenzano LOriginal
    finally
      LCopy.Free;
    end;
  finally
    LOriginal.Free;
  end;
end;

Come controllo se un valore JSON è null?

var
  LValue: TJSONValue;
begin
  LValue := LJSONObject.FindValue('myField');
  if LValue = nil then
    WriteLn('Il campo non esiste')
  else if LValue is TJSONNull then
    WriteLn('Il campo esiste ma è null')
  else
    WriteLn('Il campo ha un valore: ', LValue.Value);
end;

Posso usare la notazione a percorso per accedere agli elementi di un array?

Sì, usa la notazione tra parentesi quadre con l’indice:

var
  LFirstSkill: string;
begin
  // Accedi al primo elemento dell'array skills
  if LJSONObject.TryGetValue<string>('skills[0]', LFirstSkill) then
    WriteLn('Prima competenza: ', LFirstSkill);
end;

Come converto un oggetto Delphi in JSON?

Con TJson.ObjectToJsonString di REST.Json, che Delphi include già. Percorre il grafo degli oggetti, legge i campi privati e ti dà chiavi camelCase; [JSONName] rinomina un campo e [JSONMarshalled(False)] ne tiene uno fuori. Per liste, controllo del name case e dataset, usa i serializzatori di DelphiMVCFramework. Vedi Da oggetti a JSON con REST.Json più su.

System.JSON è abbastanza veloce?

Per quasi tutto, sì. Su 14 MB di JSON, 50.000 record, System.JSON analizza e legge in circa 158 ms; JsonDataObjects fa lo stesso lavoro in 42 ms, circa quattro volte più in fretta. Su un payload di qualche centinaio di kilobyte, che copre quasi tutte le risposte REST e ogni file di configurazione, la differenza è una frazione di millisecondo. Cambia libreria quando i documenti si misurano in megabyte o quando analizzi dentro un ciclo stretto, non per default. I numeri e il metodo sono in Quanto è veloce davvero System.JSON.

Come serializzo una TObjectList in JSON?

REST.Json non ha supporto per le liste: fai un ciclo, chiami TJson.ObjectToJsonObject per ogni elemento e aggiungi ognuno a un TJSONArray. Nel verso opposto non ha proprio niente, quindi l’array lo analizzi tu e chiami JsonToObject elemento per elemento. Il serializzatore di DelphiMVCFramework fa tutte e due le cose in una chiamata, SerializeCollection e DeserializeCollection.

Perché TJson.JsonToObject solleva “Cannot instantiate type”?

Perché la classe è dichiarata nel file di programma .dpr, e lì la RTTI non viene linkata. Sposta la dichiarazione del tipo in una unit e lo stesso codice funziona. In uscita la serializzazione non protesta mai, quindi l’errore compare solo al ritorno. Il serializzatore di DelphiMVCFramework fallisce per la stessa causa con un messaggio diverso, Cannot find RTTI for ....

System.JSON è thread-safe?

No. TJSONObject e le classi collegate non sono thread-safe. Se più thread toccano lo stesso oggetto, la sincronizzazione (sezioni critiche, lock) la scrivi tu. In sola lettura, dopo il parsing, puoi condividerlo tra thread senza problemi, finché nessuno lo modifica.

Come serializzo un TDateTime in JSON?

TJSONObject non ha un overload di AddPair per TDateTime. Converti prima in una stringa ISO 8601, e passa AInputIsUTC esplicitamente, perché per default vale True ed etichetta un’ora locale come UTC. Vedi Le date in JSON, e l’ora che ci perderai:

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

Qual è la dimensione massima di JSON che Delphi può analizzare?

Non c’è un limite rigido, ma System.JSON carica l’intero documento in memoria. Per file molto grandi (>100MB), considera parser streaming come TJsonTextReader da System.JSON.Readers, o librerie di terze parti ottimizzate per documenti grandi.

Qual è la differenza tra System.JSON e DBXJSON?

Stessa libreria, solo rinominata. DBXJSON era il nome originale, da Delphi 2009 a XE5; da XE6 si chiama System.JSON, per seguire le nuove convenzioni. L’API è praticamente identica, quindi migrare vecchio codice è banale.

Come faccio a formattare in modo leggibile (pretty print) JSON in Delphi?

Usa il metodo Format() (Delphi 10.3+) che restituisce JSON indentato e leggibile:

WriteLn(LJSONObject.Format());  // Formattato con indentazione
WriteLn(LJSONObject.ToString);  // Compatto, singola riga

Per le versioni più vecchie di Delphi, usa librerie di terze parti o implementa una formattazione personalizzata.

Come gestisco i caratteri speciali e Unicode in JSON?

System.JSON gestisce Unicode e l’escape dei caratteri speciali da solo, quando genera JSON. In lettura, converte \n, \t, \uXXXX senza che tu debba fare niente:

LJSONObject.AddPair('message', 'Riga 1'#13#10'Riga 2');  // Newline con escape automatico
LJSONObject.AddPair('emoji', '🚀');  // Unicode funziona direttamente

Come unisco due oggetti JSON?

Non c’è una funzione di merge integrata. Itera su un oggetto e aggiungi le sue coppie all’altro:

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

Nota: clona i valori, perché possono appartenere a un solo oggetto genitore.

Come valido JSON prima del parsing?

ParseJSONValue restituisce nil su JSON non valido: è già una validazione di base. Per validare uno schema (struttura, campi obbligatori, tipi) ti serve una libreria di terze parti: Delphi non ha JSON Schema integrato.

Come accedo ad array profondamente annidati?

Combina la notazione a percorso con l’indicizzazione dell’array:

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

Posso usare JSON con i dataset FireDAC?

Sì, ma non c’è integrazione diretta. O iteri il dataset a mano e costruisci il JSON, o usi una libreria di serializzazione: sia DelphiMVCFramework che mORMot2 hanno il dataset-to-JSON già pronto.

Come gestisco JSON con chiavi duplicate?

JSON tecnicamente permette chiavi duplicate, anche se è una pessima idea. TJSONObject le memorizza tutte, ma GetValue/TryGetValue restituiscono solo la prima corrispondenza. Per prenderle tutte, itera con un for-in.

Delphi ha un supporto JSON solido, integrato nell’unit System.JSON:

  1. Usa TJSONObject e TJSONArray per creare e analizzare JSON
  2. Controlla sempre nil quando analizzi stringhe JSON
  3. Usa TryGetValue per la lettura sicura dei valori con campi opzionali
  4. Usa la notazione a percorso ('parent.child') per valori annidati
  5. Ricorda la gestione della memoria: gli oggetti genitori possiedono i loro figli; RemovePair restituisce la proprietà a te
  6. Considera librerie di terze parti solo per esigenze specifiche di prestazioni
  7. Usa Format() per output leggibile (Delphi 10.3+), ToString() per output compatto
  8. Usa cicli for-in per iterazione più pulita su array e coppie di oggetti

Per REST API moderne in Delphi, dai un’occhiata a DelphiMVCFramework: serializzazione JSON avanzata, in produzione da anni in progetti reali.

Comments

comments powered by Disqus