Become a member!

JSON en Delphi : le guide complet avec exemples (2026)

🌐
Cet article est aussi disponible dans d'autres langues :
🇬🇧 English  •  🇮🇹 Italiano  •  🇪🇸 Español  •  🇩🇪 Deutsch

JSON (JavaScript Object Notation) est le standard de fait pour l’échange de données entre applications. Tu écris une API REST, tu lis un fichier de configuration, tu discutes avec un service web : dans les trois cas, il faut savoir manipuler du JSON en Delphi.

Ce guide couvre le support JSON de Delphi de bout en bout, avec des exemples complets et compilables, prêts à coller dans tes projets.

Tous les exemples de code de cet article ont été testés et vérifiés avec Delphi 13 Florence.
📝
Cet article traite du parser JSON de type DOM (TJSONObject, TJSONArray). Pour le parsing en streaming (style SAX), regarde les unités System.JSON.Readers et System.JSON.Writers.

Compatibilité entre versions de Delphi

Le support JSON a beaucoup bougé d’une version de Delphi à l’autre :

Version Unité Nouveautés
Delphi 2009 DBXJSON Premier support JSON, avec les classes de base
Delphi XE6 System.JSON Unité renommée, API améliorée
Delphi 10.1 Berlin System.JSON API fluide TJSONObjectBuilder, TryGetValue<T> amélioré
Delphi 10.3 Rio System.JSON Méthode Format(), EJSONParseException avec les détails, gains de performance
Delphi 11-12 System.JSON Optimisations et finitions supplémentaires
Delphi 13 Florence System.JSON Dernières améliorations, support poursuivi
💡
Tous les exemples de cet article fonctionnent à partir de Delphi XE7, sauf mention contraire. Les fonctionnalités liées à une version précise sont signalées.

C’est quoi, JSON ?

JSON est un format d’échange de données textuel et léger. Un humain le lit et l’écrit sans effort, une machine le parse et le génère aussi facilement. Un document JSON peut contenir :

  • Des objets : des paires clé-valeur entre accolades {}
  • Des tableaux : des listes ordonnées de valeurs entre crochets []
  • Des valeurs : chaînes, nombres, booléens (true/false), null, objets ou tableaux

Exemple de structure JSON :

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

Les classes JSON de Delphi

Delphi embarque son support JSON dans l’unité System.JSON. Les classes principales sont celles-ci :

Classe Description
TJSONValue Classe de base de tous les types de valeur JSON
TJSONObject Représente un objet JSON (paires clé-valeur)
TJSONArray Représente un tableau JSON (liste ordonnée)
TJSONString Représente une valeur chaîne
TJSONNumber Représente une valeur numérique
TJSONBool Représente une valeur booléenne
TJSONNull Représente la valeur null
TJSONPair Représente une paire clé-valeur dans un objet

Créer des objets JSON

Commence par la base : créer des objets JSON et leur ajouter des propriétés.

Création d’un objet JSON

L’opération de départ, c’est créer un TJSONObject et y ajouter des paires clé-valeur. Delphi fournit des surcharges de AddPair qui acceptent directement des chaînes, des entiers, des booléens et des doubles : pas besoin d’envelopper les valeurs primitives dans des classes JSON dédiées. Un objet avec des informations personnelles et plusieurs types de données :

program JSONCreateBasic;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
begin
  LJSONObject := TJSONObject.Create;
  try
    // Ajoute une propriété chaîne
    LJSONObject.AddPair('firstName', 'Daniele');
    LJSONObject.AddPair('lastName', 'Teti');

    // Ajoute une propriété numérique (surcharges Integer, Int64, Double disponibles)
    LJSONObject.AddPair('age', 45);

    // Ajoute une propriété booléenne
    LJSONObject.AddPair('active', True);

    // Ajoute une propriété null (pas de surcharge, il faut passer par TJSONNull)
    LJSONObject.AddPair('middleName', TJSONNull.Create);

    // Affiche le JSON
    // Note : Format() est disponible depuis 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.

Sortie :

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

Créer des tableaux JSON

Un tableau JSON est une collection ordonnée qui accepte n’importe quelle combinaison de valeurs : chaînes, nombres, booléens, et même d’autres tableaux ou objets. Quand tu ajoutes un TJSONArray à un TJSONObject avec AddPair, l’objet parent devient propriétaire du tableau : tu ne libères que l’objet racine. Un tableau de chaînes, puis un tableau aux types mélangés :

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

    // Crée un tableau de chaînes
    LSkills := TJSONArray.Create;
    LJSONObject.AddPair('skills', LSkills);
    LSkills.Add('Delphi');
    LSkills.Add('Python');
    LSkills.Add('SQL');

    // Crée un tableau avec des types mixtes
    LContacts := TJSONArray.Create;
    LJSONObject.AddPair('contacts', LContacts);
    LContacts.Add('daniele@example.com');  // chaîne
    LContacts.Add(123456);                 // nombre
    LContacts.Add(True);                   // booléen

    {$IF CompilerVersion >= 33.0}
    WriteLn(LJSONObject.Format());
    {$ELSE}
    WriteLn(LJSONObject.ToString);
    {$ENDIF}
  finally
    LJSONObject.Free; // Libère aussi LContacts et LSkills
  end;

  ReadLn;
end.

Sortie :

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

Créer un tableau d’objets

Un des motifs les plus courants dans le JSON de la vraie vie, c’est un tableau qui contient plusieurs objets : une liste d’utilisateurs, de produits, ou n’importe quelle collection d’enregistrements. Chaque objet du tableau a ses propres propriétés. Tu crées chaque objet séparément et tu l’ajoutes au tableau avec la méthode Add. Le tableau devient alors propriétaire de tout ce que tu lui ajoutes :

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

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

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

    // Troisième utilisateur
    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.

Sortie :

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

Objets JSON imbriqués

Dès que les données sont un peu complexes, il leur faut une hiérarchie : une personne a une adresse, l’adresse a une ville et un pays, et ainsi de suite. En Delphi, tu construis ces structures en ajoutant des instances de TJSONObject comme valeurs d’autres objets. Comme pour les tableaux, l’objet parent devient propriétaire de ses enfants, ce qui simplifie la gestion mémoire. Une personne avec une adresse et une société imbriquées :

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

    // Crée l'objet adresse imbriqué
    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');

    // Crée un autre objet imbriqué
    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.

Sortie :

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

Utiliser TJSONObjectBuilder (Delphi 10.1 Berlin+)

Si tu préfères une syntaxe déclarative et chaînable pour construire ton JSON, Delphi 10.1 Berlin a introduit TJSONObjectBuilder. Cette API fluide te permet d’écrire une structure complexe en une seule expression, en enchaînant BeginObject, BeginArray, Add et EndObject/EndArray. Le builder écrit dans un TJsonTextWriter, qui écrit lui-même dans un TStringBuilder. La mise en place est plus verbeuse, mais sur les structures compliquées le code se lit mieux :

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
          // Construit le JSON avec l'API fluide
          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.

Sortie :

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

Parser une chaîne JSON

Quand tu reçois du JSON d’un service web, d’un fichier ou d’ailleurs, il faut le transformer en objets Delphi utilisables. C’est le travail de la méthode de classe TJSONObject.ParseJSONValue. Elle renvoie un TJSONValue (la classe de base), tu dois donc vérifier que c’est bien le type attendu, en général TJSONObject ou TJSONArray. Si le JSON est mal formé, la méthode renvoie nil : vérifie-le toujours avant d’aller plus loin.

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 renvoie un TJSONValue, transtype vers le type attendu
  LJSONValue := TJSONObject.ParseJSONValue(JSON_STRING);

  if LJSONValue = nil then
  begin
    WriteLn('ERROR: Invalid JSON!');
    ReadLn;
    Exit;
  end;

  try
    // Vérifie que c'est un objet (à la racine, ça pourrait être un tableau)
    if not (LJSONValue is TJSONObject) then
    begin
      WriteLn('ERROR: Expected JSON object at root level');
      Exit;
    end;

    LJSONObject := TJSONObject(LJSONValue); // Transtypage direct, sûr après le test "is"
    WriteLn('Parsed successfully!');
    WriteLn('Number of pairs: ', LJSONObject.Count);

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

  ReadLn;
end.
⚠️
Vérifie toujours si ParseJSONValue renvoie nil : ça veut dire que le JSON n'est pas valide.

Gérer les erreurs de parsing (Delphi 10.3+)

Quand le parsing échoue, savoir pourquoi change tout pour le débogage. Depuis Delphi 10.3 Rio, tu passes True en deuxième paramètre de ParseJSONValue et la méthode lève une EJSONParseException au lieu de renvoyer nil. L’exception porte le message, le chemin où le parsing a lâché et l’offset du caractère, trois informations qui servent vraiment quand le JSON est complexe ou fourni par quelqu’un d’autre :

program JSONParseErrors;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

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

var
  LJSONValue: TJSONValue;
begin
  {$IF CompilerVersion >= 33.0} // Delphi 10.3 Rio
  try
    // L'option RaiseExc donne l'exception avec les détails
    LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON, True);
    try
      WriteLn('Parsed: ', LJSONValue.ToString);
    finally
      LJSONValue.Free;
    end;
  except
    on E: EJSONParseException do
    begin
      WriteLn('Parse error!');
      WriteLn('  Message: ', E.Message);
      WriteLn('  Path: ', E.Path);
      WriteLn('  Offset: ', E.Offset);
    end;
  end;
  {$ELSE}
  // Avant 10.3 : impossible de faire mieux que tester nil
  LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON);
  if LJSONValue = nil then
    WriteLn('Invalid JSON - no details available')
  else
    LJSONValue.Free;
  {$ENDIF}

  ReadLn;
end.

Lire des valeurs JSON

Delphi propose plusieurs façons de lire une valeur dans un objet JSON, chacune avec son compromis entre confort et sécurité. Choisis la bonne au bon moment, et une donnée manquante ou inattendue ne fait pas planter le programme.

Méthode 1 : GetValue avec type générique (Delphi XE7+)

La façon la plus simple de lire une valeur, c’est la méthode générique GetValue<T>. Tu indiques le type attendu en paramètre de type et Delphi fait la conversion. Attention : elle lève une exception si la clé n’existe pas, donc réserve-la aux cas où tu es sûr que la clé est là.

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> lève une exception si la clé n'est pas trouvée
    WriteLn('Name: ', LJSONObject.GetValue<string>('name'));
    WriteLn('Age: ', LJSONObject.GetValue<Integer>('age'));
    WriteLn('Active: ', LJSONObject.GetValue<Boolean>('active'));
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Méthode 2 : TryGetValue, la lecture sûre (recommandée)

En production, TryGetValue<T> est la bonne approche. Elle renvoie False si la clé manque ou si la valeur ne se convertit pas dans le type demandé : tu traites la donnée absente sans passer par une exception. C’est particulièrement pratique sur du JSON venu de l’extérieur, où tu ne peux pas garantir que tous les champs sont là.

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 renvoie False si la clé n'est pas trouvée (aucune exception)
    if LJSONObject.TryGetValue<string>('name', LName) then
      WriteLn('Name: ', LName)
    else
      WriteLn('Name not found');

    if LJSONObject.TryGetValue<Integer>('age', LAge) then
      WriteLn('Age: ', LAge)
    else
      WriteLn('Age not found');

    // Cette clé n'existe pas, aucune exception n'est levée
    if LJSONObject.TryGetValue<string>('middleName', LMiddleName) then
      WriteLn('Middle Name: ', LMiddleName)
    else
      WriteLn('Middle Name: (not specified)');
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Méthode 3 : FindValue, qui renvoie nil si rien n’est trouvé

Quand tu veux l’objet TJSONValue brut plutôt qu’une valeur convertie, utilise FindValue. La méthode renvoie nil si la clé n’existe pas, ne lève jamais d’exception, et te donne accès à toutes les propriétés et méthodes de la valeur JSON. Pratique quand tu dois vérifier le type réel d’une valeur, ou quand tu travailles sur des structures imbriquées compliquées :

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 renvoie nil si rien n'est trouvé (jamais d'exception)
    LValue := LJSONObject.FindValue('name');
    if LValue <> nil then
      WriteLn('Name: ', LValue.Value);

    LValue := LJSONObject.FindValue('nonexistent');
    if LValue = nil then
      WriteLn('Key "nonexistent" not found');
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Méthode 4 : la notation par chemin pour les valeurs imbriquées

Un des trucs vraiment pratiques de Delphi, c’est la notation par chemin : tu atteins une valeur profondément imbriquée avec un chemin séparé par des points, comme 'person.address.city', au lieu de traverser tous les objets intermédiaires. Ça marche avec TryGetValue, GetValue et FindValue, et ça t’évite des lignes et des lignes de navigation sur du JSON complexe :

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
    // La notation par points atteint les valeurs imbriquées
    if LJSONObject.TryGetValue<string>('person.name', LValue) then
      WriteLn('Person Name: ', LValue);

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

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

  ReadLn;
end.

Lire des tableaux, à l’ancienne et à la moderne

Quand ton JSON contient des tableaux, il faut parcourir leurs éléments pour traiter chaque valeur. Delphi gère les deux : la boucle classique par index avec Items[I], et le for-in moderne, qui marche sur n’importe quel TJSONArray. Le for-in est plus propre quand l’index ne t’intéresse pas, la boucle classique te donne la position. Pour un tableau de nombres, transtype chaque élément en TJSONNumber pour utiliser AsInt ou 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
    // Lit un tableau de chaînes, boucle for classique
    if LJSONObject.TryGetValue<TJSONArray>('skills', LSkills) then
    begin
      WriteLn('Skills (classic loop):');
      for I := 0 to LSkills.Count - 1 do
        WriteLn('  ', I + 1, '. ', LSkills.Items[I].Value);
    end;

    WriteLn;

    // Lit un tableau de chaînes, boucle for-in moderne (Delphi XE+)
    if LJSONObject.TryGetValue<TJSONArray>('skills', LSkills) then
    begin
      WriteLn('Skills (for-in loop):');
      for LItem in LSkills do
        WriteLn('  - ', LItem.Value);
    end;

    WriteLn;

    // Lit un tableau numérique
    if LJSONObject.TryGetValue<TJSONArray>('scores', LScores) then
    begin
      WriteLn('Scores:');
      for LItem in LScores do
        WriteLn('  Score: ', (LItem as TJSONNumber).AsInt);
    end;
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Parcourir les paires d’un objet JSON

Parfois tu dois traiter toutes les propriétés d’un objet JSON sans connaître les noms des clés à l’avance : un visualiseur JSON générique, ou une structure dynamique. Le for-in marche sur TJSONObject comme sur les tableaux, et à chaque tour tu récupères un TJSONPair. Chaque paire te donne la clé (via JsonString.Value) et la valeur (via JsonValue), et tu peux regarder le type à l’exécution avec ClassName :

program JSONIteratePairs;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

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

var
  LJSONObject: TJSONObject;
  LPair: TJSONPair;
begin
  LJSONObject := TJSONObject.ParseJSONValue(JSON_DATA) as TJSONObject;
  try
    WriteLn('All pairs in object:');
    WriteLn;

    // Parcourt toutes les paires avec for-in
    for LPair in LJSONObject do
    begin
      WriteLn('Key: ', LPair.JsonString.Value);
      WriteLn('Value: ', LPair.JsonValue.ToString);
      WriteLn('Type: ', LPair.JsonValue.ClassName);
      WriteLn;
    end;
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Modifier des objets JSON

En Delphi, un objet JSON est entièrement modifiable : tu ajoutes, tu enlèves et tu mets à jour des propriétés après coup. Les règles de gestion mémoire comptent ici : quand tu appelles RemovePair, la propriété de la paire te revient, c’est donc à toi de la libérer. Appeler Free sur nil ne pose aucun problème en Delphi : l’écriture RemovePair('key').Free marche donc même si la clé n’existe pas.

Ajouter et enlever des paires

Le cycle complet de modification d’un objet JSON : créer les propriétés de départ, en supprimer une, en mettre une autre à jour. Regarde bien la mise à jour : tu enlèves d’abord l’ancienne paire (et tu la libères), puis tu en ajoutes une nouvelle avec la même clé.

program JSONModify;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LRemovedPair: TJSONPair;
begin
  LJSONObject := TJSONObject.Create;
  try
    // Ajoute les paires de départ
    LJSONObject.AddPair('name', 'Daniele');
    LJSONObject.AddPair('city', 'Rome');
    LJSONObject.AddPair('temp', 'to be removed');

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

    // Enlève une paire, RemovePair renvoie la paire enlevée (elle est à toi !)
    LRemovedPair := LJSONObject.RemovePair('temp');
    LRemovedPair.Free; // Sûr même si nil, Free teste Self <> nil

    WriteLn('After removing "temp":');
    WriteLn(LJSONObject.ToString);
    WriteLn;

    // Pour mettre à jour une valeur : enlever puis ajouter
    LRemovedPair := LJSONObject.RemovePair('city');
    LRemovedPair.Free;
    LJSONObject.AddPair('city', 'Milan');

    WriteLn('After updating "city":');
    WriteLn(LJSONObject.ToString);
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Sortie :

Initial:
{"name":"Daniele","city":"Rome","temp":"to be removed"}

After removing "temp":
{"name":"Daniele","city":"Rome"}

After updating "city":
{"name":"Daniele","city":"Milan"}

Cloner un objet JSON

Quand tu veux une version modifiée d’un objet JSON sans toucher à l’original, utilise Clone. Tu obtiens une copie profonde : un arbre d’objets totalement indépendant, où les modifications du clone ne touchent pas l’original, et réciproquement. Indispensable quand tu reçois du JSON que tu dois transformer avant de le renvoyer ailleurs tout en gardant l’original intact :

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 crée une copie indépendante
    LClone := LOriginal.Clone as TJSONObject;
    try
      // Modifie le clone, l'original n'est pas touché
      LPair := LClone.RemovePair('city');
      LPair.Free;
      LClone.AddPair('city', 'Milan');

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

  ReadLn;
end.

Sortie :

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

Travailler avec des fichiers JSON

Écrire du JSON sur disque, c’est le quotidien : fichiers de configuration, cache, export de données. L’unité System.IOUtils de Delphi fournit la classe TFile avec des méthodes simples de lecture et d’écriture de fichiers texte, ce qui tombe bien : un document JSON n’est que du texte.

Enregistrer du JSON dans un fichier

Pour enregistrer un objet JSON dans un fichier, convertis-le en chaîne avec Format() (sortie lisible) ou ToString() (sortie compacte), puis écris cette chaîne sur disque. TPath.GetDocumentsPath t’assure un emplacement accessible en écriture quelle que soit la configuration 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);

    // Enregistre dans le fichier
    {$IF CompilerVersion >= 33.0}
    TFile.WriteAllText(LFileName, LJSONObject.Format());
    {$ELSE}
    TFile.WriteAllText(LFileName, LJSONObject.ToString);
    {$ENDIF}

    WriteLn('Saved to: ', LFileName);
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Charger du JSON depuis un fichier

La lecture est tout aussi directe : tu lis le contenu du fichier dans une chaîne et tu la passes à ParseJSONValue. Vérifie d’abord que le fichier existe, pour éviter une exception, puis que le parsing a réussi avant d’accéder aux données. La notation par chemin marche sur du JSON parsé exactement comme sur un objet construit à la main :

program JSONLoadFromFile;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.IOUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LJSONValue: TJSONValue;
  LContent: string;
  LFileName: string;
  LAppName: string;
  LPort: Integer;
begin
  LFileName := TPath.Combine(TPath.GetDocumentsPath, 'config.json');

  if not TFile.Exists(LFileName) then
  begin
    WriteLn('File not found: ', LFileName);
    ReadLn;
    Exit;
  end;

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

  if LJSONValue = nil then
  begin
    WriteLn('Invalid JSON in file!');
    ReadLn;
    Exit;
  end;

  try
    LJSONObject := LJSONValue as TJSONObject;

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

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

  ReadLn;
end.

Transformer des objets en JSON avec REST.Json

Tout ce qui précède construit le JSON à la main, paire par paire. C’est ce qu’il faut faire quand la forme du document est le sujet. C’est ce qu’il ne faut pas faire quand tu as déjà une classe et que tu veux juste l’envoyer sur le réseau.

Pour ça, il y a REST.Json, livrée avec Delphi depuis XE5. Aucun code tiers à ajouter, et l’essentiel du travail tient en un appel.

De l’objet au 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));

Sortie :

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

Il s’est passé trois choses.

Le TAddress imbriqué a été sérialisé lui aussi, sans que tu le demandes. REST.Json parcourt le graphe d’objets.

InternalNote n’est pas dans la sortie. [JSONMarshalled(False)] est la façon de garder un champ hors du document, et c’est l’attribut que tu veux sur tout ce que le client n’a pas à voir.

Les clés ressortent en id, name et zipCode. REST.Json lit les champs privés, pas les propriétés, enlève le préfixe F et met la première lettre en minuscule. Donc FZipCode devient zipCode, camelCase, que ça te plaise ou non. Quand l’autre bout de la ligne exige une autre orthographe, [JSONName('vat_number')] l’impose, un champ à la fois.

Du JSON vers l’objet

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

Remarque que l’entrée utilisait Id et Name avec une majuscule et que ça a marché quand même : la lecture est insensible à la casse, l’écriture non. L’objet revient entièrement construit, adresse imbriquée comprise, et c’est à toi de le libérer.

Les champs dont le JSON ne parle pas gardent ce que le constructeur y a laissé :

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

Pas d’exception. Si un Id absent veut dire que quelque chose ne va pas en amont, c’est à toi de le vérifier.

Celui qui va te coûter un après-midi

Déclare ces classes dans le .dpr et TJson.JsonToObject échoue :

EConversionError: Internal: Cannot instantiate type restjson.TCustomer

Le sérialiseur a besoin du RTTI de la classe, lié dans le module, et un type déclaré dans le fichier programme n’en produit pas. Déplace les déclarations dans une unité et le même code marche. La sérialisation vers le JSON ne se plaint jamais, donc tu ne rencontres ça qu’au retour, en général après t’être convaincu que le JSON est malformé.

Encore un, propre aux versions récentes : TJson.Format est déprécié dans Delphi 13 Florence. Le compilateur te dit quoi utiliser à la place :

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

L’affichage indenté d’un objet, c’est donc maintenant :

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

Là où REST.Json s’arrête

Sérialise une liste et tu tombes sur la limite :

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

Ça marche, et c’est déjà une boucle que tu as écrite à la main. Dans l’autre sens, d’un tableau JSON vers une TObjectList<TCustomer>, REST.Json n’a rien à proposer du tout : tu parses le tableau toi-même et tu appelles JsonToObject sur chaque élément.

REST.Json est très bonne pour un objet à la fois, avec les noms qu’elle choisit à ta place. Au-delà, il te faut un sérialiseur fait pour ça.

Quand il t’en faut plus : les sérialiseurs de DelphiMVCFramework

DelphiMVCFramework livre un sérialiseur utilisable seul, sans serveur ni contrôleur en vue. C’est une unité et une interface.

Une liste, en un appel, dans les deux sens :

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

Et le retour :

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

Deux appels là où REST.Json te donnait deux boucles. Remarque aussi que le TDateTime est sorti et revenu comme une vraie date, en ISO 8601 avec le décalage, ce qui est le sujet de la section suivante.

La casse des clés est une décision, ici, pas une règle qu’on t’impose. Mets l’attribut sur la classe et toute la classe suit :

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

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

C’est celui-là qui tranche, pour la plupart des gens. Si l’API à laquelle tu dois parler veut order_id, REST.Json te donne un [JSONName] par champ, à vie ; le sérialiseur DMVCFramework te donne un attribut par classe.

Il y a un piège là-dedans, et il est silencieux. La casse des noms s’applique aussi en lecture. Le sérialiseur est en ncLowerCase par défaut, donc il émet placedat et attend placedat. Donne-lui le même payload avec placedAt et :

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, c’est le zéro de TDateTime. Pas d’erreur, pas d’avertissement, juste un champ qui n’est pas arrivé, sans bruit. Quand tu consommes une API que tu ne contrôles pas, règle la casse des noms sur la sienne et teste un payload de bout en bout avant de croire quoi que ce soit.

La même règle RTTI s’applique, au passage. Déclare TOrder dans le .dpr et tu obtiens :

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

Sérialiseur différent, message différent, même cause : les types vont dans des unités.

Les dates en JSON, et l’heure que tu vas perdre

JSON n’a pas de type date. Quoi que tu fasses, un TDateTime sort de ton processus sous forme de chaîne, et tout le monde s’est mis d’accord sur laquelle : ISO 8601. Delphi te donne la conversion dans System.DateUtils, et il te donne un défaut qui est faux pour l’essentiel du code que tu écris.

Le défaut, c’est 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

Le deuxième paramètre s’appelle AInputIsUTC et il vaut True par défaut. Donc DateToISO8601(SomeDate) annonce au monde entier que la valeur que tu as passée est déjà en UTC. Si elle vient de Now, d’un TDateTimePicker ou d’une colonne de base de données écrite par une application locale, elle n’est pas en UTC, et tu viens de tamponner un Z sur une heure locale.

Aucune exception, et le document est valide. L’heure est juste fausse.

Écrire et lire avec le même 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

Mélange les flags et la valeur se déplace de ton décalage par rapport à UTC, en silence :

LText := DateToISO8601(LOriginal, False);   // locale
LBack := ISO8601ToDate(LText);              // défaut, la traite comme de l'UTC
written with False, read with the default:
  15/03/2023 12:00:00 -> 15/03/2023 11:00:00
  drift in minutes: 60

Une heure, sur une machine en Italie au mois de mars. En août, deux. Sur une machine en UTC, zéro, et c’est exactement pour ça que le décalage passe les tests et ressort chez un client.

Dans un document

LJson := TJSONObject.Create;
try
  LJson.AddPair('event', 'invoice.created');
  LJson.AddPair('created_at', DateToISO8601(FIXED, False));
  Writeln(LJson.ToJSON);
  LWhen := ISO8601ToDate(LJson.GetValue<string>('created_at'), False);
  Writeln('parsed back: ', DateTimeToStr(LWhen));
finally
  LJson.Free;
end;
{"event":"invoice.created","created_at":"2023-03-15T12:00:00.000+01:00"}
parsed back: 15/03/2023 12:00:00

Une entrée que tu n’as pas écrite

ISO8601ToDate lève une exception sur tout ce qu’elle n’arrive pas à lire. Pour un payload arrivé par le réseau, utilise la version 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

Même forme que TryGetValue plus haut dans cet article, et la même raison de la préférer.

Choisis UTC ou local une fois pour toutes, pour toute l’application, et passe le flag explicitement à chaque appel. Le défaut ne sera pas celui que tu voulais.

Exemple concret : un client d’API REST

Assemble tout ça dans un cas réel : appelle une API REST et traite la réponse JSON. L’exemple se connecte à JSONPlaceholder (une API de test gratuite), récupère une liste d’utilisateurs et transforme chacun en record Delphi. Remarque l’usage systématique de TryGetValue pour absorber les champs éventuellement absents, obligatoire face à une API externe qui peut changer sans te prévenir.

📝
THTTPClient demande Delphi XE8 ou plus récent.
program JSONRestApiClient;
{$APPTYPE CONSOLE}

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

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

function ParseUser(AJSONObject: TJSONObject): TUser;
begin
  // Passe par TryGetValue par sécurité
  if not AJSONObject.TryGetValue<Integer>('id', Result.ID) then
    Result.ID := 0;
  if not AJSONObject.TryGetValue<string>('name', Result.Name) then
    Result.Name := '';
  if not AJSONObject.TryGetValue<string>('email', Result.Email) then
    Result.Email := '';
  if not AJSONObject.TryGetValue<string>('username', Result.Username) then
    Result.Username := '';
end;

var
  LClient: THTTPClient;
  LResponse: IHTTPResponse;
  LJSONValue: TJSONValue;
  LJSONArray: TJSONArray;
  LUserJSON: TJSONObject;
  LUser: TUser;
  I: Integer;
begin
  WriteLn('Fetching users from JSONPlaceholder API...');
  WriteLn;

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

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

    // Parse la réponse, un tableau JSON
    LJSONValue := TJSONObject.ParseJSONValue(LResponse.ContentAsString);
    if LJSONValue = nil then
    begin
      WriteLn('Invalid JSON response');
      ReadLn;
      Exit;
    end;

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

      LJSONArray := LJSONValue as TJSONArray;
      WriteLn('Found ', LJSONArray.Count, ' users:');
      WriteLn(StringOfChar('-', 50));

      for I := 0 to LJSONArray.Count - 1 do
      begin
        LUserJSON := LJSONArray.Items[I] as TJSONObject;
        LUser := ParseUser(LUserJSON);

        WriteLn('ID: ', LUser.ID);
        WriteLn('Name: ', LUser.Name);
        WriteLn('Email: ', LUser.Email);
        WriteLn('Username: ', LUser.Username);
        WriteLn(StringOfChar('-', 50));
      end;

    finally
      LJSONValue.Free;
    end;

  finally
    LClient.Free;
  end;

  ReadLn;
end.

Exemple concret : un gestionnaire de fichier de configuration

Dernier exemple : une classe complète et réutilisable pour gérer la configuration d’une application. TConfigManager encapsule toute la mécanique de chargement, d’enregistrement et de lecture des paramètres derrière une API propre et typée, avec chargement paresseux (le fichier n’est lu qu’au premier besoin), valeurs par défaut pour les clés absentes et création automatique du fichier. Prends-le comme point de départ pour ton propre système de configuration :

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;

// Démonstration d'usage
var
  Config: TConfigManager;
  LConfigFile: string;
begin
  LConfigFile := TPath.Combine(TPath.GetDocumentsPath, 'appsettings.json');
  WriteLn('Config file: ', LConfigFile);
  WriteLn;

  Config := TConfigManager.Create(LConfigFile);
  try
    // Écrit quelques valeurs (les clés sont à plat, pas d'objets imbriqués)
    Config.SetValue('databaseHost', 'localhost');
    Config.SetValue('databasePort', 5432);
    Config.SetValue('databaseName', 'myapp');
    Config.SetValue('loggingEnabled', True);
    Config.SetValue('loggingMaxFiles', 10);
    Config.Save;

    WriteLn('Configuration saved!');
    WriteLn;

    // Relit les valeurs
    WriteLn('Database Host: ', Config.GetString('databaseHost'));
    WriteLn('Database Port: ', Config.GetInteger('databasePort'));
    WriteLn('Logging Enabled: ', Config.GetBoolean('loggingEnabled'));

    // Lecture avec valeur par défaut
    WriteLn('Timeout (default 30): ', Config.GetInteger('timeout', 30));
  finally
    Config.Free;
  end;

  ReadLn;
end.

Sortie :

Config file: C:\Users\yourname\Documents\appsettings.json

Configuration saved!

Database Host: localhost
Database Port: 5432
Logging Enabled: TRUE
Timeout (default 30): 30

Bibliothèques JSON tierces

Le parser JSON intégré de Delphi fait très bien le travail dans la majorité des cas, mais certaines situations gagnent à passer par une bibliothèque tierce :

Bibliothèque Idéale pour URL
JsonDataObjects Les performances, utilisée par DelphiMVCFramework GitHub
Grijjy Foundation Complète, avec le support BSON GitHub
mORMot2 Framework complet (ORM, SOA, REST) qui apporte sa propre couche JSON GitHub

Quand sortir une bibliothèque tierce

  • Gros fichiers JSON (plus de 10 Mo) : regarde du côté des parsers en streaming ou de JsonDataObjects
  • Parsing à haute fréquence : JsonDataObjects, mesurée face à System.JSON plus bas dans cet article
  • Besoin de BSON : Grijjy Foundation
  • Sérialisation d’objets : REST.Json pour les cas simples, les sérialiseurs de DelphiMVCFramework pour les listes, les datasets et le contrôle de la casse des noms

Pour la plupart des applications, System.JSON suffit, et tu n’ajoutes aucune dépendance externe.

System.JSON, quelle vitesse au juste

« Prends une bibliothèque tierce si tu as besoin de performance », c’est facile à écrire et difficile à appliquer. Voilà des chiffres.

Le test construit un tableau de 50 000 records, chacun avec un entier, deux chaînes, un booléen, un nombre et un horodatage ISO 8601 : 14 Mo de texte UTF-16, la forme d’un vrai export. Puis il mesure deux opérations. Parser et lire un entier dans chaque record, ce que fait un client. Et parser puis sérialiser à nouveau dans la foulée, ce que fait un proxy.

Chaque exécution est précédée d’un préchauffage qui part à la poubelle, et c’est le meilleur des sept qui est retenu, donc les chiffres sont le plancher, pas une moyenne de ce que la machine faisait à côté.

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, optimisations activées, contrôles de plage et de débordement désactivés, sur un Core i9-13980HX sous Windows 11.

Donc : JsonDataObjects parse à peu près quatre fois plus vite, et fait l’aller-retour à peu près deux fois et demie plus vite. L’écart est réel et il tient d’une exécution à l’autre.

System.JSON a quand même avalé 14 Mo en un sixième de seconde environ. Si ton JSON fait quelques centaines de kilo-octets, ce qui couvre la plupart des réponses REST et quasiment tous les fichiers de configuration, tu choisis entre deux millisecondes et une demi-milliseconde. Ce n’est pas une décision, c’est une erreur d’arrondi, et System.JSON est déjà installée.

Sors JsonDataObjects quand le payload se mesure en méga-octets, ou quand tu parses dans une boucle qui tourne des milliers de fois, ou quand tu es sur un appareil où le CPU n’est pas gratuit. Sinon la dépendance te coûte plus qu’elle ne te rapporte.

Une note pratique si tu l’ajoutes : JsonDataObjects déclare ses propres TJSONObject et TJSONArray. Dans une unité qui utilise les deux, c’est le dernier de la clause uses qui gagne, et tu récoltes des erreurs qui n’ont aucun sens tant que tu n’as pas vu ça :

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

Qualifie les noms de types, System.JSON.TJSONObject et JsonDataObjects.TJsonArray, et l’ambiguïté disparaît.

mORMot2 n’est pas dans cette comparaison. C’est un framework complet plutôt qu’une bibliothèque JSON, et le mesurer honnêtement suppose d’embarquer et de configurer l’ensemble, ce qui ferait un autre article.

Construire des API REST avec JSON

Si tu écris des API REST en Delphi, DelphiMVCFramework apporte un très bon support JSON avec sérialisation automatique :

[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); // Sérialisation JSON automatique
end;

Regarde les exemples de DelphiMVCFramework pour du code complet, et le guide officiel si tu préfères qu’on t’explique les sérialiseurs plutôt que de les deviner.

Questions fréquentes

Comment parser une chaîne JSON en Delphi ?

Avec TJSONObject.ParseJSONValue(), de l’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')); // Affiche : John
    finally
      LJSONObject.Free;
    end;
  end;
end;

Comment gérer les valeurs null en JSON ?

Passe par TryGetValue, qui gère les valeurs absentes ou null sans lever d’exception :

var
  LValue: string;
begin
  if LJSONObject.TryGetValue<string>('optionalField', LValue) then
    WriteLn('Value: ', LValue)
  else
    WriteLn('Field is missing or null');
end;

Comment parcourir un tableau JSON ?

Avec la syntaxe moderne du 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;

Quelle différence entre Format() et ToString() ?

  • Format() : renvoie du JSON indenté, lisible par un humain (Delphi 10.3+ uniquement)
  • ToString() : renvoie du JSON compact, sans espaces (mieux pour le transfert réseau, marche dans toutes les versions)

Comment modifier un objet JSON existant ?

Avec RemovePair puis AddPair. RemovePair renvoie la paire enlevée (ou nil si elle n’existe pas) : elle t’appartient, c’est à toi de la libérer.

begin
  // Remove renvoie la paire, tu dois la libérer !
  // Free est sûr sur nil (il teste Self <> nil en interne)
  LJSONObject.RemovePair('name').Free;
  // Ajoute la nouvelle valeur
  LJSONObject.AddPair('name', 'New Value');
end;

Quelle version de Delphi a introduit le support JSON ?

  • Delphi 2009 : premier support JSON, dans l’unité DBXJSON
  • Delphi XE6 : l’unité est renommée System.JSON, avec une API améliorée
  • Delphi 10.1 Berlin : l’API fluide TJSONObjectBuilder
  • Delphi 10.3 Rio : la méthode Format() et EJSONParseException avec le détail de l’erreur

Quelle différence entre GetValue, FindValue et TryGetValue ?

Méthode Renvoie Si la clé n’existe pas
GetValue<T>('key') Une valeur de type T Lève une exception
FindValue('key') TJSONValue ou nil Renvoie nil
TryGetValue<T>('key', outVar) Boolean Renvoie False

Recommandation : en production, utilise TryGetValue, c’est la plus sûre.

Comment faire une copie profonde d’un objet JSON ?

Avec la méthode Clone :

var
  LOriginal, LCopy: TJSONObject;
begin
  LOriginal := TJSONObject.ParseJSONValue('{"name":"test"}') as TJSONObject;
  try
    LCopy := LOriginal.Clone as TJSONObject;
    try
      // LCopy est indépendant, le modifier ne touche pas LOriginal
    finally
      LCopy.Free;
    end;
  finally
    LOriginal.Free;
  end;
end;

Comment savoir si une valeur JSON est null ?

var
  LValue: TJSONValue;
begin
  LValue := LJSONObject.FindValue('myField');
  if LValue = nil then
    WriteLn('Field does not exist')
  else if LValue is TJSONNull then
    WriteLn('Field exists but is null')
  else
    WriteLn('Field has a value: ', LValue.Value);
end;

La notation par chemin marche-t-elle sur les éléments d’un tableau ?

Oui, avec les crochets et l’index :

var
  LFirstSkill: string;
begin
  // Accède au premier élément du tableau skills
  if LJSONObject.TryGetValue<string>('skills[0]', LFirstSkill) then
    WriteLn('First skill: ', LFirstSkill);
end;

Comment convertir un objet Delphi en JSON ?

Avec TJson.ObjectToJsonString de REST.Json, livrée avec Delphi. Elle parcourt le graphe d’objets, lit les champs privés et te donne des clés en camelCase ; [JSONName] renomme un champ et [JSONMarshalled(False)] en exclut un. Pour les listes, le contrôle de la casse des noms et les datasets, passe par les sérialiseurs de DelphiMVCFramework. Voir Transformer des objets en JSON avec REST.Json plus haut.

System.JSON est-elle assez rapide ?

Pour presque tout, oui. Sur 14 Mo de JSON, 50 000 records, System.JSON parse et lit en 158 ms environ ; JsonDataObjects fait le même travail en 42 ms, à peu près quatre fois plus vite. Sur un payload de quelques centaines de kilo-octets, ce qui couvre la plupart des réponses REST et tous les fichiers de configuration, la différence est une fraction de milliseconde. Change de bibliothèque quand les documents se mesurent en méga-octets ou que tu parses dans une boucle serrée, pas par défaut. Les chiffres et la méthode sont dans System.JSON, quelle vitesse au juste.

Comment sérialiser une TObjectList en JSON ?

REST.Json ne gère pas les listes : tu boucles, tu appelles TJson.ObjectToJsonObject sur chaque élément et tu les ajoutes un par un à un TJSONArray. Dans l’autre sens, elle n’a rien du tout, donc tu parses le tableau et tu appelles JsonToObject sur chaque élément. Le sérialiseur DelphiMVCFramework fait les deux en un appel, SerializeCollection et DeserializeCollection.

Pourquoi TJson.JsonToObject lève-t-elle « Cannot instantiate type » ?

Parce que la classe est déclarée dans le fichier programme .dpr, qui ne produit pas de RTTI lié. Déplace la déclaration du type dans une unité et le même code marche. La sérialisation vers le JSON ne se plaint jamais, donc l’erreur n’apparaît qu’au retour. Le sérialiseur DelphiMVCFramework échoue sur la même cause avec un autre message, Cannot find RTTI for ....

System.JSON est-elle thread-safe ?

Non. TJSONObject et les classes associées ne sont pas thread-safe. Si plusieurs threads doivent toucher au même objet JSON, la synchronisation est à ta charge (sections critiques, verrous, etc.). En lecture seule après le parsing initial, tu peux partager l’objet entre threads tant que personne ne le modifie.

Comment sérialiser un TDateTime en JSON ?

TJSONObject n’a pas de surcharge AddPair pour TDateTime. Convertis d’abord en chaîne ISO 8601, et passe AInputIsUTC explicitement, parce qu’il vaut True par défaut et va étiqueter une heure locale comme de l’UTC. Voir Les dates en JSON, et l’heure que tu vas perdre :

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

Quelle taille de JSON Delphi peut-il parser ?

Il n’y a pas de limite fixe, mais System.JSON charge tout le document en mémoire. Pour les très gros fichiers (plus de 100 Mo), regarde les parsers en streaming comme TJsonTextReader de System.JSON.Readers, ou les bibliothèques tierces optimisées pour les gros documents.

Quelle différence entre System.JSON et DBXJSON ?

C’est la même bibliothèque, simplement renommée. DBXJSON était le nom de l’unité de Delphi 2009 à XE5. À partir de Delphi XE6, elle est devenue System.JSON pour suivre les nouvelles conventions de nommage. L’API est pour ainsi dire identique, migrer du vieux code ne pose pas de difficulté.

Comment afficher du JSON indenté en Delphi ?

Avec la méthode Format() (Delphi 10.3+), qui renvoie du JSON indenté et lisible :

WriteLn(LJSONObject.Format());  // Indenté, lisible
WriteLn(LJSONObject.ToString);  // Compact, sur une seule ligne

Sur les versions plus anciennes de Delphi, il faut passer par une bibliothèque tierce ou écrire ton propre formatage.

Comment gérer les caractères spéciaux et l’Unicode en JSON ?

System.JSON gère l’Unicode tout seul et échappe les caractères spéciaux à la génération. Au parsing, il convertit sans problème les séquences échappées comme \n, \t et \uXXXX. Tu n’as rien à faire à la main :

LJSONObject.AddPair('message', 'Line 1'#13#10'Line 2');  // Sauts de ligne échappés automatiquement
LJSONObject.AddPair('emoji', '🚀');  // L'Unicode passe directement

Comment fusionner deux objets JSON ?

Il n’y a pas de fonction de fusion intégrée. Parcours un objet et ajoute ses paires à l’autre :

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

Note : tu dois cloner les valeurs, parce qu’elles ne peuvent appartenir qu’à un seul objet parent.

Comment valider du JSON avant de le parser ?

ParseJSONValue renvoie nil sur du JSON invalide, ce qui te donne une validation de base. Pour valider un schéma (structure, champs obligatoires, types), il te faut une bibliothèque tierce : Delphi n’embarque pas le support de JSON Schema.

Comment accéder à des tableaux profondément imbriqués ?

Combine la notation par chemin et l’indexation :

// Accès à : {"data": {"users": [{"name": "Alice"}, {"name": "Bob"}]}}
if LJSONObject.TryGetValue<string>('data.users[1].name', LValue) then
  WriteLn(LValue);  // Affiche : Bob

Peut-on utiliser JSON avec les datasets FireDAC ?

Oui, mais il n’y a pas d’intégration directe. Tu peux parcourir le dataset à la main et construire le JSON, ou passer par une bibliothèque de sérialisation. DelphiMVCFramework et mORMot2 fournissent tous les deux la sérialisation dataset vers JSON en standard.

Comment gérer du JSON avec des clés en double ?

Techniquement, JSON autorise les clés en double, même si c’est déconseillé. TJSONObject stocke toutes les paires, mais GetValue/TryGetValue ne renvoient que la première trouvée. Pour récupérer toutes les valeurs d’une même clé, parcours l’objet avec une boucle for-in.

Résumé

Delphi embarque un support JSON solide avec l’unité System.JSON. À retenir :

  1. TJSONObject et TJSONArray pour créer et parser du JSON
  2. Teste toujours nil quand tu parses une chaîne JSON
  3. TryGetValue pour lire sans risque, surtout des champs optionnels
  4. La notation par chemin ('parent.child') pour les valeurs imbriquées
  5. La mémoire : l’objet parent possède ses enfants, RemovePair te rend la propriété
  6. Les bibliothèques tierces seulement pour un besoin de performance précis
  7. Format() pour une sortie lisible (Delphi 10.3+), ToString() pour une sortie compacte
  8. Les boucles for-in pour parcourir proprement tableaux et paires

Pour écrire des API REST modernes en Delphi, va voir DelphiMVCFramework : il embarque une sérialisation JSON avancée et tourne en production dans des entreprises du monde entier.

Articles liés :

Comments

comments powered by Disqus