Become a member!

JSON in Delphi: Kompletter Leitfaden mit Beispielen (2026)

🌐
Dieser Artikel ist auch in anderen Sprachen verfügbar:
🇬🇧 English  •  🇮🇹 Italiano  •  🇪🇸 Español  •  🇫🇷 Français

JSON (JavaScript Object Notation) ist der De-facto-Standard für den Datenaustausch. Ob du REST-APIs baust, Konfigurationsdateien liest oder mit Webdiensten sprichst: Du kommst an JSON in Delphi nicht vorbei.

Dieser Leitfaden zeigt dir die JSON-Unterstützung in Delphi, mit vollständigen, kompilierbaren Beispielen, die du direkt in deine Projekte übernehmen kannst.

Alle Codebeispiele in diesem Artikel sind mit Delphi 13 Florence getestet und geprüft.
📝
Dieser Artikel behandelt den DOM-artigen JSON-Parser (TJSONObject, TJSONArray). Für Streaming- bzw. SAX-artiges Parsen schau dir die Units System.JSON.Readers und System.JSON.Writers an.

Kompatibilität mit Delphi-Versionen

Die JSON-Unterstützung hat sich über die Delphi-Versionen hinweg deutlich weiterentwickelt:

Version Unit Wichtigste Neuerungen
Delphi 2009 DBXJSON Erste JSON-Unterstützung mit einfachen Basisklassen
Delphi XE6 System.JSON Unit umbenannt, verbesserte API
Delphi 10.1 Berlin System.JSON Fluent-API TJSONObjectBuilder, Verbesserungen an TryGetValue<T>
Delphi 10.3 Rio System.JSON Methode Format(), EJSONParseException mit Details, schnellere Verarbeitung
Delphi 11-12 System.JSON Weitere Optimierungen und Feinschliff
Delphi 13 Florence System.JSON Aktuelle Verbesserungen und weiterhin gepflegte Unterstützung
💡
Alle Beispiele in diesem Artikel laufen ab Delphi XE7, sofern nichts anderes dabeisteht. Versionsabhängige Features sind ausdrücklich gekennzeichnet.

Was ist JSON?

JSON ist ein schlankes, textbasiertes Format für den Datenaustausch. Menschen können es lesen und schreiben, Maschinen können es parsen und erzeugen. Ein JSON-Dokument kann enthalten:

  • Objekte: Schlüssel-Wert-Paare in geschweiften Klammern {}
  • Arrays: geordnete Listen von Werten in eckigen Klammern []
  • Werte: Strings, Zahlen, Booleans (true/false), null, Objekte oder Arrays

Beispiel für eine JSON-Struktur:

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

Die JSON-Klassen von Delphi im Überblick

Delphi bringt JSON von Haus aus mit, über die Unit System.JSON. Die wichtigsten Klassen:

Klasse Beschreibung
TJSONValue Basisklasse für alle JSON-Werttypen
TJSONObject steht für ein JSON-Objekt (Schlüssel-Wert-Paare)
TJSONArray steht für ein JSON-Array (geordnete Liste)
TJSONString steht für einen JSON-String-Wert
TJSONNumber steht für einen numerischen JSON-Wert
TJSONBool steht für einen booleschen JSON-Wert
TJSONNull steht für den JSON-Wert null
TJSONPair steht für ein Schlüssel-Wert-Paar in einem Objekt

JSON-Objekte erzeugen

Zuerst die Grundlagen: ein JSON-Objekt anlegen und Eigenschaften hinzufügen.

Ein einfaches JSON-Objekt anlegen

Der Grundfall: Du erzeugst ein TJSONObject und hängst Schlüssel-Wert-Paare dran. Delphi hat praktische Überladungen von AddPair, die Strings, Integer, Booleans und Doubles direkt entgegennehmen. Du musst primitive Werte also nicht erst in JSON-Klassen einpacken. Das folgende Beispiel baut ein einfaches JSON-Objekt mit Personendaten in verschiedenen Datentypen:

program JSONCreateBasic;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
begin
  LJSONObject := TJSONObject.Create;
  try
    // String-Eigenschaft hinzufügen
    LJSONObject.AddPair('firstName', 'Daniele');
    LJSONObject.AddPair('lastName', 'Teti');

    // Numerische Eigenschaft (Überladungen für Integer, Int64, Double vorhanden)
    LJSONObject.AddPair('age', 45);

    // Boolesche Eigenschaft hinzufügen
    LJSONObject.AddPair('active', True);

    // null-Eigenschaft (keine Überladung, TJSONNull ist Pflicht)
    LJSONObject.AddPair('middleName', TJSONNull.Create);

    // JSON ausgeben
    // Hinweis: Format() gibt es ab 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.

Ausgabe:

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

JSON-Arrays erzeugen

JSON-Arrays sind geordnete Sammlungen und können jede Kombination von Werten aufnehmen: Strings, Zahlen, Booleans, sogar weitere Arrays und Objekte. Wenn du ein TJSONArray per AddPair an ein TJSONObject hängst, übernimmt das Elternobjekt den Besitz, du gibst also nur das Wurzelobjekt frei. Dieses Beispiel zeigt ein homogenes String-Array und eines mit gemischten Typen:

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

    // String-Array anlegen
    LSkills := TJSONArray.Create;
    LJSONObject.AddPair('skills', LSkills);
    LSkills.Add('Delphi');
    LSkills.Add('Python');
    LSkills.Add('SQL');

    // Array mit gemischten Typen anlegen
    LContacts := TJSONArray.Create;
    LJSONObject.AddPair('contacts', LContacts);
    LContacts.Add('daniele@example.com');  // String
    LContacts.Add(123456);                 // Zahl
    LContacts.Add(True);                   // Boolean

    {$IF CompilerVersion >= 33.0}
    WriteLn(LJSONObject.Format());
    {$ELSE}
    WriteLn(LJSONObject.ToString);
    {$ENDIF}
  finally
    LJSONObject.Free; // gibt auch LContacts und LSkills frei
  end;

  ReadLn;
end.

Ausgabe:

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

Ein Array aus Objekten erzeugen

Eines der häufigsten Muster in echtem JSON ist ein Array mit mehreren Objekten: eine Liste von Benutzern, Produkten oder was auch immer du an Datensätzen hast. Jedes Objekt im Array hat seine eigenen Eigenschaften. Beim Aufbau erzeugst du jedes Objekt einzeln und hängst es mit Add an das Array. Das Array besitzt danach alle Objekte, die du hineingelegt hast:

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

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

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

    // Dritter Benutzer
    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.

Ausgabe:

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

Verschachtelte JSON-Objekte

Komplexe Daten brauchen meistens eine Hierarchie: eine Person hat eine Adresse, die Adresse hat Stadt und Land, und so weiter. In Delphi baust du verschachtelte Strukturen, indem du TJSONObject-Instanzen als Werte in andere Objekte legst. Wie bei Arrays übernimmt das Elternobjekt den Besitz seiner Kinder, das macht die Speicherverwaltung einfach. Dieses Beispiel legt eine Person mit verschachteltem Adress- und Firmenobjekt an:

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

    // Verschachteltes Adressobjekt anlegen
    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');

    // Noch ein verschachteltes Objekt
    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.

Ausgabe:

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

TJSONObjectBuilder verwenden (ab Delphi 10.1 Berlin)

Wenn du lieber deklarativ und verkettet baust: Delphi 10.1 Berlin hat TJSONObjectBuilder eingeführt. Mit dieser Fluent-API baust du komplexe JSON-Strukturen in einem einzigen Ausdruck, per Methodenkette aus BeginObject, BeginArray, Add und EndObject/EndArray. Der Builder schreibt in einen TJsonTextWriter, der wiederum in einen TStringBuilder ausgibt. Das Setup ist länger, aber bei komplexen Strukturen wird der Code sauberer und besser lesbar:

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
          // JSON mit der Fluent-API bauen
          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.

Ausgabe:

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

JSON-Strings parsen

Wenn du JSON von einem Webdienst, aus einer Datei oder von sonstwo bekommst, musst du es in Delphi-Objekte umwandeln, mit denen du arbeiten kannst. Dafür gibt es die Klassenmethode TJSONObject.ParseJSONValue. Sie liefert ein TJSONValue zurück (die Basisklasse), du musst also prüfen, ob es der erwartete Typ ist, normalerweise TJSONObject oder TJSONArray. Ist das JSON kaputt, kommt nil zurück, also prüfe das immer, bevor du weitermachst:

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 liefert TJSONValue, auf den passenden Typ casten
  LJSONValue := TJSONObject.ParseJSONValue(JSON_STRING);

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

  try
    // Prüfen, ob es ein Objekt ist (die Wurzel kann auch ein Array sein)
    if not (LJSONValue is TJSONObject) then
    begin
      WriteLn('ERROR: Expected JSON object at root level');
      Exit;
    end;

    LJSONObject := TJSONObject(LJSONValue); // Harter Cast, nach dem "is" sicher
    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.
⚠️
Prüfe immer, ob ParseJSONValue nil zurückliefert: Das heißt, das JSON ist ungültig.

Parse-Fehler behandeln (ab Delphi 10.3)

Wenn das Parsen schiefgeht, hilft beim Debuggen vor allem eins: zu wissen, warum. Ab Delphi 10.3 Rio kannst du ParseJSONValue als zweiten Parameter ein True mitgeben, dann wirft die Methode eine EJSONParseException, statt nil zurückzugeben. Die Exception enthält die Fehlermeldung, den Pfad, an dem das Parsen gescheitert ist, und den Zeichen-Offset. Bei komplexem oder fremdem JSON ist das Gold wert:

program JSONParseErrors;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

const
  INVALID_JSON = '{"name": "Test", "value": }'; // Ungültig!

var
  LJSONValue: TJSONValue;
begin
  {$IF CompilerVersion >= 33.0} // Delphi 10.3 Rio
  try
    // Mit der Option RaiseExc kommt eine Exception mit Details
    LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON, True);
    try
      WriteLn('Parsed: ', LJSONValue.ToString);
    finally
      LJSONValue.Free;
    end;
  except
    on E: EJSONParseException do
    begin
      WriteLn('Parse error!');
      WriteLn('  Message: ', E.Message);
      WriteLn('  Path: ', E.Path);
      WriteLn('  Offset: ', E.Offset);
    end;
  end;
  {$ELSE}
  // Vor 10.3: nur auf nil prüfen
  LJSONValue := TJSONObject.ParseJSONValue(INVALID_JSON);
  if LJSONValue = nil then
    WriteLn('Invalid JSON - no details available')
  else
    LJSONValue.Free;
  {$ENDIF}

  ReadLn;
end.

JSON-Werte lesen

Es gibt mehrere Wege, Werte aus JSON-Objekten zu lesen, jeder mit einem anderen Kompromiss zwischen Bequemlichkeit und Sicherheit. Wenn du weißt, wann du welchen nimmst, schreibst du robusteren Code, der mit fehlenden oder unerwarteten Daten klarkommt.

Weg 1: GetValue mit generischem Typ (ab Delphi XE7)

Am einfachsten liest du einen Wert mit der generischen Methode GetValue<T>. Du gibst den erwarteten Typ als Typparameter an, Delphi kümmert sich um die Umwandlung. Allerdings fliegt eine Exception, wenn der Schlüssel nicht existiert, nimm sie also nur, wenn du sicher bist, dass der Schlüssel da ist:

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> wirft eine Exception, wenn der Schlüssel fehlt
    WriteLn('Name: ', LJSONObject.GetValue<string>('name'));
    WriteLn('Age: ', LJSONObject.GetValue<Integer>('age'));
    WriteLn('Active: ', LJSONObject.GetValue<Boolean>('active'));
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Weg 2: TryGetValue, sicher lesen (empfohlen)

Im Produktivcode ist TryGetValue<T> der Weg der Wahl. Fehlt der Schlüssel oder lässt sich der Wert nicht in den gewünschten Typ umwandeln, kommt False zurück, und du behandelst den Fall ohne Exception-Handling. Besonders praktisch bei JSON aus fremder Quelle, wo du nicht garantieren kannst, dass alle Felder da sind:

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 liefert False, wenn der Schlüssel fehlt (keine 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');

    // Diesen Schlüssel gibt es nicht, trotzdem keine Exception
    if LJSONObject.TryGetValue<string>('middleName', LMiddleName) then
      WriteLn('Middle Name: ', LMiddleName)
    else
      WriteLn('Middle Name: (not specified)');
  finally
    LJSONObject.Free;
  end;

  ReadLn;
end.

Weg 3: FindValue liefert nil, wenn nichts da ist

Wenn du nicht den umgewandelten Wert brauchst, sondern das rohe TJSONValue-Objekt, nimm FindValue. Die Methode liefert nil, wenn der Schlüssel fehlt, wirft nie eine Exception und gibt dir vollen Zugriff auf Eigenschaften und Methoden des JSON-Werts. Praktisch, wenn du den tatsächlichen Typ eines Werts prüfen willst oder mit komplexen verschachtelten Strukturen arbeitest:

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 liefert nil, wenn nichts gefunden wird (wirft nie eine 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.

Weg 4: Pfadnotation für verschachtelte Werte

Eines der bequemsten Features in Delphi ist die Pfadnotation: Du greifst auf tief verschachtelte Werte über punktgetrennte Pfade wie 'person.address.city' zu, statt dich durch mehrere Zwischenobjekte zu hangeln. Das geht mit TryGetValue, GetValue und FindValue und erspart dir eine Menge Navigationscode, wenn du einzelne Werte aus komplexem JSON ziehst:

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
    // Mit Punktnotation auf verschachtelte Werte zugreifen
    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.

Arrays lesen, klassisch und modern

Enthält dein JSON Arrays, musst du über die Elemente laufen, um jeden Wert zu verarbeiten. Delphi kann beides: die klassische indexbasierte Schleife über Items[I] und die modernere for-in-Syntax, die mit jedem TJSONArray funktioniert. For-in ist sauberer, solange du den Index nicht brauchst, die klassische Schleife gibt dir die Position. Bei numerischen Arrays castest du jedes Element auf TJSONNumber, um an Methoden wie AsInt oder AsDouble zu kommen:

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
    // String-Array lesen, klassische for-Schleife
    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;

    // String-Array lesen, moderne for-in-Schleife (ab 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;

    // Numerisches Array lesen
    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.

Über die Paare eines JSON-Objekts iterieren

Manchmal musst du alle Eigenschaften eines JSON-Objekts verarbeiten, ohne die Schlüsselnamen vorher zu kennen, etwa für einen generischen JSON-Viewer oder wenn die Struktur dynamisch ist. Die for-in-Schleife funktioniert auf TJSONObject genauso wie auf Arrays und liefert TJSONPair-Instanzen. Jedes Paar gibt dir den Schlüssel (über JsonString.Value) und den Wert (über JsonValue), und über ClassName siehst du den Laufzeittyp:

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;

    // Mit for-in über alle Paare laufen
    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.

JSON-Objekte ändern

JSON-Objekte sind in Delphi voll veränderbar: Du kannst nach dem Erzeugen Eigenschaften hinzufügen, entfernen und aktualisieren. Entscheidend sind die Regeln zur Speicherverwaltung: Rufst du RemovePair auf, geht der Besitz des Paars an dich zurück, du musst es also freigeben. Free darf in Delphi auf nil aufgerufen werden, deshalb funktioniert das Muster RemovePair('key').Free auch dann, wenn es den Schlüssel gar nicht gibt.

Paare hinzufügen und entfernen

Dieses Beispiel zeigt den ganzen Ablauf beim Ändern eines JSON-Objekts: Eigenschaften anlegen, eine entfernen, eine andere aktualisieren. Beachte: Um einen Wert zu aktualisieren, entfernst du zuerst das alte Paar (und gibst es frei) und legst dann ein neues mit demselben Schlüssel an:

program JSONModify;
{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.JSON;

var
  LJSONObject: TJSONObject;
  LRemovedPair: TJSONPair;
begin
  LJSONObject := TJSONObject.Create;
  try
    // Erste Paare anlegen
    LJSONObject.AddPair('name', 'Daniele');
    LJSONObject.AddPair('city', 'Rome');
    LJSONObject.AddPair('temp', 'to be removed');

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

    // Ein Paar entfernen, RemovePair liefert es zurück (jetzt gehört es dir!)
    LRemovedPair := LJSONObject.RemovePair('temp');
    LRemovedPair.Free; // auch bei nil sicher, Free prüft Self <> nil

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

    // Wert aktualisieren: erst entfernen, dann hinzufügen
    LRemovedPair := LJSONObject.RemovePair('city');
    LRemovedPair.Free;
    LJSONObject.AddPair('city', 'Milan');

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

  ReadLn;
end.

Ausgabe:

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

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

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

JSON-Objekte klonen

Wenn du eine geänderte Fassung eines JSON-Objekts brauchst, ohne das Original anzufassen, nimm Clone. Das erzeugt eine tiefe Kopie, einen komplett eigenständigen Objektbaum: Änderungen am Klon wirken sich nicht auf das Original aus und umgekehrt. Genau das brauchst du, wenn du empfangenes JSON umbauen und weiterschicken willst, das Original aber behalten musst:

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 erzeugt eine unabhängige Kopie
    LClone := LOriginal.Clone as TJSONObject;
    try
      // Den Klon ändern, das Original bleibt unberührt
      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.

Ausgabe:

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

Mit JSON-Dateien arbeiten

JSON auf die Platte zu schreiben brauchst du dauernd: Konfigurationsdateien, Caches, Datenexport. Die Unit System.IOUtils bringt die Klasse TFile mit, die Textdateien mit einfachen Methoden liest und schreibt. Das passt perfekt zur String-Darstellung von JSON.

JSON in eine Datei speichern

Um ein JSON-Objekt in eine Datei zu schreiben, wandelst du es mit Format() (lesbare Ausgabe) oder ToString() (kompakte Ausgabe) in einen String um und schreibst diesen auf die Platte. Mit TPath.GetDocumentsPath landet die Datei an einem beschreibbaren Ort, der unter allen Windows-Konfigurationen funktioniert:

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

    // In die Datei speichern
    {$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.

JSON aus einer Datei laden

Umgekehrt ist es genauso einfach: Dateiinhalt in einen String lesen, dann mit ParseJSONValue parsen. Prüfe immer zuerst, ob die Datei existiert, damit dir keine Exception um die Ohren fliegt, und ob das Parsen geklappt hat, bevor du an die Daten gehst. Die Pfadnotation funktioniert auf geparstem JSON genauso wie auf selbst gebauten Objekten:

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.

Objekte in JSON verwandeln mit REST.Json

Bis hierher haben wir JSON von Hand gebaut, Paar für Paar. Das ist genau richtig, wenn es auf die Form des Dokuments ankommt. Es ist falsch, wenn du schon eine Klasse hast und sie einfach nur über die Leitung schicken willst.

Dafür liefert Delphi REST.Json mit. Seit XE5 dabei, kein Fremdcode nötig, und die meiste Arbeit erledigt ein einziger Aufruf.

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

Ausgabe:

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

Drei Dinge sind hier passiert.

Das verschachtelte TAddress wurde gleich mitserialisiert, ohne dass du darum gebeten hast. REST.Json durchläuft den Objektgraphen.

InternalNote steht nicht in der Ausgabe. [JSONMarshalled(False)] hält ein Feld aus dem Dokument heraus, und das ist das Attribut, das du auf alles setzen willst, was den Client nichts angeht.

Herausgekommen sind die Schlüssel id, name und zipCode. REST.Json liest die privaten Felder, nicht die Eigenschaften, lässt das F-Präfix weg und macht den ersten Buchstaben klein. Aus FZipCode wird also zipCode, camelCase, ob dir das passt oder nicht. Wenn die Gegenstelle auf einer anderen Schreibweise besteht, überschreibt [JSONName('vat_number')] diese Regel, Feld für Feld.

JSON zurück ins Objekt

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

Beachte, dass im JSON Id und Name mit großem Anfangsbuchstaben stehen und es trotzdem geklappt hat: Beim Lesen spielt Groß- und Kleinschreibung keine Rolle, beim Schreiben schon. Das Objekt kommt fertig gebaut zurück, samt verschachtelter Adresse, und du gibst es frei.

Felder, die im JSON nicht vorkommen, behalten das, was der Konstruktor hinterlassen hat:

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

Keine Exception. Wenn eine fehlende Id heißt, dass vorher in der Kette etwas schiefgelaufen ist, musst du selbst danach schauen.

Der Fehler, der dich einen Nachmittag kostet

Deklariere diese Klassen im .dpr, und TJson.JsonToObject scheitert:

EConversionError: Internal: Cannot instantiate type restjson.TCustomer

Der Serializer braucht gelinktes RTTI für die Klasse, und ein Typ, der in der Programmdatei steht, bekommt keins. Verschiebe die Deklarationen in eine Unit, und derselbe Code läuft. Beim Serialisieren aus dem Objekt heraus beschwert sich nichts, deshalb merkst du es erst auf dem Rückweg, meistens nachdem du dich selbst davon überzeugt hast, dass das JSON kaputt ist.

Noch etwas, das nur die neueren Versionen betrifft: TJson.Format ist in Delphi 13 Florence deprecated. Der Compiler sagt dir, was du stattdessen nehmen sollst:

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

Hübsch formatiert gibst du ein Objekt jetzt also so aus:

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

Wo REST.Json aufhört

Serialisiere eine Liste, und du stößt an die Grenze:

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

Es funktioniert, und es ist schon eine Schleife, die du selbst schreiben musst. In die andere Richtung, von einem JSON-Array zurück in eine TObjectList<TCustomer>, hat REST.Json überhaupt nichts anzubieten: Du parst das Array selbst und rufst JsonToObject pro Element auf.

REST.Json ist sehr gut darin, ein Objekt nach dem anderen zu serialisieren, mit Namen, die es für dich aussucht. Für alles darüber hinaus brauchst du einen Serializer, der für die Aufgabe gebaut ist.

Wenn es mehr sein muss: die Serializer von DelphiMVCFramework

DelphiMVCFramework bringt einen Serializer mit, den du auch für sich allein benutzen kannst, ganz ohne Server und ohne Controller. Eine Unit und ein Interface.

Eine Liste, in einem Aufruf, in beide Richtungen:

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

Und zurück:

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

Zwei Aufrufe, wo REST.Json dir zwei Schleifen gegeben hat. Beachte außerdem, dass TDateTime als echtes Datum rausgegangen und wieder hereingekommen ist, in ISO 8601 mit Offset. Genau darum geht es im nächsten Abschnitt.

Die Schreibweise der Schlüssel ist hier eine Entscheidung, keine Regel, die dir aufgezwungen wird. Setz das Attribut auf die Klasse, und die ganze Klasse folgt ihm:

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

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

Das ist der Punkt, der es für die meisten entscheidet. Wenn die API, mit der du reden musst, order_id will, gibt dir REST.Json ein [JSONName] pro Feld, für immer; der Serializer von DelphiMVCFramework gibt dir ein Attribut pro Klasse.

Es steckt allerdings eine Falle darin, und sie macht sich nicht bemerkbar. Die Schreibweise gilt auch beim Lesen. Der Serializer steht per Default auf ncLowerCase, schreibt also placedat und erwartet placedat. Gib ihm dieselbe Payload mit placedAt, und:

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 ist der Nullwert von TDateTime. Kein Fehler, keine Warnung, nur ein Feld, das einfach nicht angekommen ist. Wenn du eine API anbindest, die dir nicht gehört, stell die Schreibweise passend ein und teste eine Payload einmal von Anfang bis Ende, bevor du irgendetwas glaubst.

Dieselbe RTTI-Regel gilt hier übrigens auch. Deklariere TOrder im .dpr, und du bekommst:

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

Anderer Serializer, andere Meldung, dieselbe Ursache: Typen gehören in Units.

Das Datum in JSON und die Stunde, die du verlierst

JSON kennt keinen Datumstyp. Was immer du tust, ein TDateTime verlässt deinen Prozess als String, und alle haben sich auf denselben String geeinigt: ISO 8601. Delphi gibt dir die Umwandlung in System.DateUtils, und es gibt dir eine Voreinstellung, die für den meisten Code falsch ist, den du schreibst.

Die Voreinstellung ist 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

Der zweite Parameter heißt AInputIsUTC und steht per Default auf True. DateToISO8601(SomeDate) erzählt der Welt also, dass der Wert, den du übergeben hast, schon UTC ist. Kommt er von Now, aus einem TDateTimePicker oder aus einer Datenbankspalte, die eine lokale Anwendung geschrieben hat, dann ist er nicht UTC, und du hast gerade ein Z auf eine Ortszeit gestempelt.

Es fliegt keine Exception, und das Dokument ist gültig. Die Stunde ist einfach falsch.

Schreiben und lesen mit demselben 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

Mische die Flags, und der Wert verschiebt sich unbemerkt um deinen Abstand zu UTC:

LText := DateToISO8601(LOriginal, False);   // lokal
LBack := ISO8601ToDate(LText);              // Default, behandelt ihn als UTC
written with False, read with the default:
  15/03/2023 12:00:00 -> 15/03/2023 11:00:00
  drift in minutes: 60

Eine Stunde, auf einem Rechner in Italien im März. Im August sind es zwei. Auf einem Rechner, der in UTC läuft, sind es null, und genau deshalb übersteht das die Tests und taucht beim Kunden auf.

In einem Dokument

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

Eingaben, die nicht von dir stammen

ISO8601ToDate wirft eine Exception bei allem, was es nicht lesen kann. Für eine Payload, die über das Netz gekommen ist, nimm die Try-Variante:

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

Dieselbe Form wie TryGetValue weiter oben in diesem Artikel und derselbe Grund, es vorzuziehen.

Entscheide dich einmal für UTC oder Ortszeit, für die ganze Anwendung, und übergib das Flag jedes einzelne Mal explizit. Der Default ist nicht der, den du gemeint hast.

Praxisbeispiel: REST-API-Client

Ein echtes Szenario bringt jetzt alles zusammen: eine REST-API aufrufen und die JSON-Antwort verarbeiten. Dieses Beispiel spricht mit JSONPlaceholder (einer kostenlosen Test-API), holt eine Benutzerliste und parst jeden Eintrag in einen Delphi-Record. Achte darauf, dass hier überall TryGetValue steht, damit fehlende Felder nichts kaputt machen. Bei fremden APIs, die sich jederzeit ändern können, ist das Pflicht.

📝
THTTPClient braucht Delphi XE8 oder neuer.
program JSONRestApiClient;
{$APPTYPE CONSOLE}

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

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

function ParseUser(AJSONObject: TJSONObject): TUser;
begin
  // Sicherheitshalber TryGetValue
  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;

    // Die JSON-Array-Antwort parsen
    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.

Praxisbeispiel: Verwaltung einer Konfigurationsdatei

Das letzte Beispiel ist eine vollständige, wiederverwendbare Klasse für die Konfiguration einer Anwendung. TConfigManager kapselt den ganzen Kram rund ums Laden, Speichern und Zugreifen und gibt dir dafür eine saubere, typsichere API. Drin sind Lazy Loading (die Datei wird erst beim ersten Zugriff gelesen), Standardwerte für fehlende Schlüssel und das automatische Anlegen der Datei. Nimm das Muster als Startpunkt für dein eigenes Konfigurationssystem:

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;

// Beispielnutzung
var
  Config: TConfigManager;
  LConfigFile: string;
begin
  LConfigFile := TPath.Combine(TPath.GetDocumentsPath, 'appsettings.json');
  WriteLn('Config file: ', LConfigFile);
  WriteLn;

  Config := TConfigManager.Create(LConfigFile);
  try
    // Ein paar Werte setzen (die Schlüssel sind flach, keine verschachtelten Objekte)
    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;

    // Werte wieder auslesen
    WriteLn('Database Host: ', Config.GetString('databaseHost'));
    WriteLn('Database Port: ', Config.GetInteger('databasePort'));
    WriteLn('Logging Enabled: ', Config.GetBoolean('loggingEnabled'));

    // Mit Standardwert lesen
    WriteLn('Timeout (default 30): ', Config.GetInteger('timeout', 30));
  finally
    Config.Free;
  end;

  ReadLn;
end.

Ausgabe:

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

Configuration saved!

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

JSON-Bibliotheken von Drittanbietern

Der eingebaute JSON-Parser von Delphi reicht für die allermeisten Fälle völlig. In ein paar Szenarien lohnt sich trotzdem ein Blick auf Bibliotheken von Drittanbietern:

Bibliothek Am besten für URL
JsonDataObjects hohe Geschwindigkeit, wird von DelphiMVCFramework benutzt GitHub
Grijjy Foundation großer Funktionsumfang, inklusive BSON GitHub
mORMot2 Full-Stack-Framework (ORM, SOA, REST), das seine eigene JSON-Schicht mitbringt GitHub

Wann eine Drittanbieter-Bibliothek sinnvoll ist

  • Große JSON-Dateien (>10MB): Schau dir Streaming-Parser oder JsonDataObjects an
  • Sehr häufiges Parsen: JsonDataObjects, weiter unten in diesem Artikel gegen System.JSON gemessen
  • BSON wird gebraucht: Grijjy Foundation
  • Objektserialisierung: REST.Json für die einfachen Fälle, die Serializer von DelphiMVCFramework für Listen, Datasets und Kontrolle über die Schreibweise der Schlüssel

Für die meisten Anwendungen reicht das eingebaute System.JSON, und es hat den Vorteil, dass keine externe Abhängigkeit dazukommt.

Wie schnell ist System.JSON wirklich?

“Nimm eine Drittanbieter-Bibliothek, wenn du Performance brauchst” ist leicht geschrieben und schwer umzusetzen. Hier sind Zahlen.

Der Test baut ein Array aus 50.000 Records, jeder mit einem Integer, zwei Strings, einem Boolean, einer Zahl und einem ISO-8601-Zeitstempel: 14 MB UTF-16-Text, die Form eines echten Exports. Dann misst er zwei Aufgaben. Parsen und aus jedem Record einen Integer lesen, das macht ein Client. Und parsen und direkt wieder rausschreiben, das macht ein Proxy.

Vor jedem Lauf steht ein Warm-up, das verworfen wird, und berichtet wird der beste von sieben Läufen. Die Zahlen sind also die Untergrenze und kein Mittelwert über das, was der Rechner sonst noch zu tun hatte.

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, Optimierung an, Bereichs- und Überlaufprüfungen aus, auf einem Core i9-13980HX unter Windows 11.

Also: JsonDataObjects parst rund viermal schneller und schafft den Roundtrip rund zweieinhalbmal schneller. Dieser Abstand ist echt und über die Läufe hinweg stabil.

System.JSON hat die 14 MB trotzdem in etwa einer Sechstelsekunde durchgekaut. Wenn dein JSON ein paar hundert Kilobyte groß ist, und das trifft auf die meisten REST-Antworten und fast jede Konfigurationsdatei zu, wählst du zwischen zwei Millisekunden und einer halben. Das ist keine Entscheidung, das ist ein Rundungsfehler, und System.JSON ist schon installiert.

Greif zu JsonDataObjects, wenn die Payload in Megabyte gemessen wird, oder wenn du in einer Schleife parst, die tausende Male läuft, oder wenn du auf einem Gerät arbeitest, auf dem Rechenzeit nicht umsonst ist. Sonst kostet dich die Abhängigkeit mehr, als sie bringt.

Ein praktischer Hinweis, falls du sie doch reinholst: JsonDataObjects deklariert seine eigenen TJSONObject und TJSONArray. In einer Unit, die beide benutzt, gewinnt das, was in der uses-Klausel zuletzt steht, und du bekommst Fehler, die keinen Sinn ergeben, bis du es siehst:

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

Schreib die Typnamen qualifiziert hin, System.JSON.TJSONObject und JsonDataObjects.TJsonArray, und die Mehrdeutigkeit ist weg.

mORMot2 ist in diesem Vergleich nicht dabei. Es ist ein komplettes Framework und keine JSON-Bibliothek. Es fair zu messen hieße, das ganze Ding reinzuholen und zu konfigurieren, und das ist ein anderer Artikel.

REST-APIs mit JSON bauen

Wenn du in Delphi REST-APIs baust: DelphiMVCFramework hat eine sehr gute JSON-Unterstützung mit automatischer Serialisierung:

[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); // Automatische JSON-Serialisierung
end;

Vollständige Beispiele findest du in den DelphiMVCFramework-Samples, und wenn du die Serializer lieber erklärt bekommst, als sie zu erraten, im offiziellen Guide.

Häufige Fragen

Wie parse ich einen JSON-String in Delphi?

Mit TJSONObject.ParseJSONValue() aus der 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')); // Ausgabe: John
    finally
      LJSONObject.Free;
    end;
  end;
end;

Wie gehe ich mit null-Werten im JSON um?

Mit TryGetValue fängst du fehlende und null-Werte gefahrlos ab:

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

Wie iteriere ich über ein JSON-Array?

Mit der modernen for-in-Schleife:

var
  LArray: TJSONArray;
  LItem: TJSONValue;
begin
  if LJSONObject.TryGetValue<TJSONArray>('items', LArray) then
  begin
    for LItem in LArray do
      WriteLn(LItem.Value);
  end;
end;

Was ist der Unterschied zwischen Format() und ToString()?

  • Format(): liefert eingerücktes, für Menschen lesbares JSON (nur ab Delphi 10.3)
  • ToString(): liefert kompaktes JSON ohne Leerraum (besser für die Übertragung im Netz, gibt es in allen Versionen)

Wie ändere ich ein bestehendes JSON-Objekt?

Mit RemovePair und danach AddPair. RemovePair liefert das entfernte Paar zurück (oder nil, wenn es den Schlüssel nicht gibt), es gehört dann dir und du musst es freigeben:

begin
  // Remove liefert das Paar zurück, du musst es freigeben!
  // Free darf auf nil aufgerufen werden (es prüft intern Self <> nil)
  LJSONObject.RemovePair('name').Free;
  // Neuen Wert hinzufügen
  LJSONObject.AddPair('name', 'New Value');
end;

Welche Delphi-Version hat JSON eingeführt?

  • Delphi 2009: erste JSON-Unterstützung in der Unit DBXJSON
  • Delphi XE6: umbenannt in System.JSON, mit Verbesserungen an der API
  • Delphi 10.1 Berlin: Fluent-API TJSONObjectBuilder
  • Delphi 10.3 Rio: Methode Format() dazu, EJSONParseException mit detaillierten Fehlerinfos

Was ist der Unterschied zwischen GetValue, FindValue und TryGetValue?

Methode Liefert Wenn der Schlüssel fehlt
GetValue<T>('key') Wert vom Typ T wirft eine Exception
FindValue('key') TJSONValue oder nil liefert nil
TryGetValue<T>('key', outVar) Boolean liefert False

Empfehlung: Im Produktivcode TryGetValue nehmen, das ist der sicherste Weg.

Wie erzeuge ich eine tiefe Kopie eines JSON-Objekts?

Mit der Methode Clone:

var
  LOriginal, LCopy: TJSONObject;
begin
  LOriginal := TJSONObject.ParseJSONValue('{"name":"test"}') as TJSONObject;
  try
    LCopy := LOriginal.Clone as TJSONObject;
    try
      // LCopy ist unabhängig, Änderungen wirken sich nicht auf LOriginal aus
    finally
      LCopy.Free;
    end;
  finally
    LOriginal.Free;
  end;
end;

Wie prüfe ich, ob ein JSON-Wert null ist?

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;

Kann ich mit der Pfadnotation auf Array-Elemente zugreifen?

Ja, mit eckigen Klammern und dem Index:

var
  LFirstSkill: string;
begin
  // Auf das erste Element des skills-Arrays zugreifen
  if LJSONObject.TryGetValue<string>('skills[0]', LFirstSkill) then
    WriteLn('First skill: ', LFirstSkill);
end;

Wie wandle ich ein Delphi-Objekt in JSON um?

Mit TJson.ObjectToJsonString aus REST.Json, das bei Delphi dabei ist. Es durchläuft den Objektgraphen, liest die privaten Felder und gibt dir camelCase-Schlüssel; [JSONName] benennt ein Feld um, [JSONMarshalled(False)] lässt eines weg. Für Listen, Kontrolle über die Schreibweise und Datasets nimmst du die Serializer von DelphiMVCFramework. Siehe Objekte in JSON verwandeln mit REST.Json weiter oben.

Ist System.JSON schnell genug?

Für fast alles ja. Bei 14 MB JSON, 50.000 Records, parst und liest System.JSON in etwa 158 ms; JsonDataObjects macht dieselbe Arbeit in 42 ms, also rund viermal schneller. Bei einer Payload von ein paar hundert Kilobyte, also bei den meisten REST-Antworten und bei jeder Konfigurationsdatei, ist der Unterschied ein Bruchteil einer Millisekunde. Wechsle die Bibliothek, wenn die Dokumente in Megabyte gemessen werden oder du in einer engen Schleife parst, nicht einfach so. Die Zahlen und die Methode stehen im Abschnitt Wie schnell ist System.JSON wirklich? weiter oben.

Wie serialisiere ich eine TObjectList nach JSON?

REST.Json kann keine Listen: Du gehst sie in einer Schleife durch, rufst TJson.ObjectToJsonObject pro Element auf und hängst jedes an ein TJSONArray. In die andere Richtung hat es gar nichts, also parst du das Array und rufst JsonToObject pro Element auf. Der Serializer von DelphiMVCFramework macht beides in je einem Aufruf, SerializeCollection und DeserializeCollection.

Warum wirft TJson.JsonToObject “Cannot instantiate type”?

Weil die Klasse in der .dpr-Programmdatei deklariert ist, und die bekommt kein gelinktes RTTI. Verschiebe die Typdeklaration in eine Unit, und derselbe Code läuft. Beim Serialisieren beschwert sich nichts, deshalb taucht der Fehler nur auf dem Rückweg auf. Der Serializer von DelphiMVCFramework scheitert aus demselben Grund, nur mit einer anderen Meldung, Cannot find RTTI for ....

Ist System.JSON threadsicher?

Nein, TJSONObject und die verwandten Klassen sind nicht threadsicher. Wenn mehrere Threads auf dasselbe JSON-Objekt zugreifen, musst du selbst synchronisieren (Critical Sections, Locks und so weiter). Für reinen Lesezugriff nach dem Parsen kannst du das Objekt gefahrlos zwischen Threads teilen, solange niemand etwas ändert.

Wie serialisiere ich ein TDateTime nach JSON?

TJSONObject hat keine AddPair-Überladung für TDateTime. Wandle den Wert zuerst in einen ISO-8601-String um und übergib AInputIsUTC explizit, denn der Default ist True und stempelt dir eine Ortszeit als UTC. Siehe Das Datum in JSON und die Stunde, die du verlierst:

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

Wie groß darf das JSON sein, das Delphi parsen kann?

Es gibt keine harte Grenze, aber System.JSON lädt das ganze Dokument in den Speicher. Bei sehr großen Dateien (>100MB) nimmst du besser Streaming-Parser wie TJsonTextReader aus System.JSON.Readers oder Bibliotheken, die auf große Dokumente ausgelegt sind.

Was ist der Unterschied zwischen System.JSON und DBXJSON?

Es ist dieselbe Bibliothek, nur umbenannt. DBXJSON war der ursprüngliche Unit-Name in Delphi 2009 bis XE5. Ab Delphi XE6 heißt sie System.JSON, passend zu den neuen Namenskonventionen. Die API ist im Wesentlichen gleich geblieben, alten Code umzustellen ist also unspektakulär.

Wie gebe ich JSON hübsch formatiert aus?

Mit der Methode Format() (ab Delphi 10.3), die eingerücktes, lesbares JSON liefert:

WriteLn(LJSONObject.Format());  // Hübsch eingerückt
WriteLn(LJSONObject.ToString);  // Kompakt, eine Zeile

In älteren Delphi-Versionen brauchst du dafür eine Fremdbibliothek oder eine eigene Formatierung.

Wie gehe ich mit Sonderzeichen und Unicode im JSON um?

System.JSON kümmert sich beim Erzeugen von JSON selbst um Unicode und escapt Sonderzeichen. Beim Parsen werden Sequenzen wie \n, \t und \uXXXX korrekt zurückgewandelt. Du musst nichts von Hand machen:

LJSONObject.AddPair('message', 'Line 1'#13#10'Line 2');  // Zeilenumbrüche werden automatisch escaped
LJSONObject.AddPair('emoji', '🚀');  // Unicode geht direkt

Wie führe ich zwei JSON-Objekte zusammen?

Eine eingebaute Merge-Funktion gibt es nicht. Lauf über das eine Objekt und häng seine Paare an das andere:

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

Achtung: Du musst die Werte klonen, denn sie können immer nur zu einem Elternobjekt gehören.

Wie validiere ich JSON vor dem Parsen?

ParseJSONValue liefert bei ungültigem JSON nil, das ist die einfachste Form der Validierung. Für eine Schema-Validierung (Struktur, Pflichtfelder, Typen) brauchst du eine Fremdbibliothek, denn JSON Schema bringt Delphi nicht mit.

Wie komme ich an tief verschachtelte Arrays?

Kombiniere Pfadnotation und Array-Index:

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

Kann ich JSON mit FireDAC-Datasets verwenden?

Ja, aber es gibt keine direkte Integration. Entweder du läufst von Hand über das Dataset und baust das JSON, oder du nimmst eine Serialisierungsbibliothek. DelphiMVCFramework und mORMot2 bringen beide die Serialisierung von Datasets nach JSON von Haus aus mit.

Wie gehe ich mit doppelten Schlüsseln im JSON um?

JSON erlaubt doppelte Schlüssel formal, auch wenn davon abgeraten wird. TJSONObject speichert alle Paare, aber GetValue und TryGetValue liefern nur den ersten Treffer. Um an alle Werte mit demselben Schlüssel zu kommen, läufst du mit der for-in-Schleife darüber.

Zusammenfassung

Delphi hat mit der Unit System.JSON eine solide JSON-Unterstützung ab Werk. Das Wichtigste:

  1. Nimm TJSONObject und TJSONArray, um JSON zu erzeugen und zu parsen
  2. Prüfe beim Parsen von JSON-Strings immer auf nil
  3. Nimm TryGetValue, um Werte sicher zu lesen, gerade bei optionalen Feldern
  4. Nimm die Pfadnotation ('parent.child') für verschachtelte Werte
  5. Denk an die Speicherverwaltung: Elternobjekte besitzen ihre Kinder, RemovePair gibt dir den Besitz zurück
  6. Zieh Fremdbibliotheken erst in Betracht, wenn du sie wegen der Geschwindigkeit wirklich brauchst
  7. Format() für lesbare Ausgabe (ab Delphi 10.3), ToString() für kompakte Ausgabe
  8. Nimm for-in-Schleifen, damit iterierst du sauberer über Arrays und Objektpaare

Wenn du in Delphi moderne REST-APIs bauen willst, schau dir DelphiMVCFramework an: Es hat eine fortgeschrittene JSON-Serialisierung eingebaut und läuft weltweit bei Firmen im Produktivbetrieb.

Verwandte Artikel:

Comments

comments powered by Disqus