CodeGym /Kurse /C++ SELF /JSON — Objekt, Array, Primitive

JSON — Objekt, Array, Primitive

C++ SELF
Level 65, Lektion 0
Verfügbar

1. Warum JSON und wie man die Struktur sieht

Wenn ein Programm länger als fünf Minuten lebt, entwickelt es eine typische Gewohnheit: es muss irgendwo Daten speichern. Einstellungen, Aufgabenliste, Notizen, Benutzerprofil — all das möchte man in einer Datei ablegen, sodass es später wieder lesbar ist. Und hier beginnt der Kampf der Formate: „selbstgestrickte Lösung“ gegen „vernünftigen Standard“. JSON ist einer der populärsten Kompromisse: menschenlesbar, ausreichend strikt, nahezu überall unterstützt.

Damit die Beispiele zusammenhängen, stellen wir uns eine einfache Konsolenanwendung TaskPad vor: sie speichert Aufgabenlisten und ein paar Einstellungen (z. B. Benutzername und Theme). Heute werden wir nicht mit einer JSON-Bibliothek lesen — unser Ziel ist einfacher: lernen, die JSON-Struktur mit dem Auge zu erkennen und zu verstehen, wo Daten kaputtgehen.

JSON als Baum von Werten

Es ist sehr nützlich, aufzuhören, JSON als „Zeichenkette mit Klammern“ zu denken, und stattdessen es als Baum von Werten zu betrachten. Im Wurzelknoten des Baums liegt ein einzelner Wert. Er kann ein Objekt, ein Array oder ein Primitiv sein. Ein Objekt hat feldartige Zweige, ein Array hat elementartige Zweige, Primitiven haben keine Zweige — sie sind die „Blätter“. Dieses Denken hilft beim Debugging, wenn man fragt „warum ist hier nicht das richtige Feld?“ oder „warum meckert der Parser wegen eines Kommas?“.

Schematisch lässt sich das so darstellen:

flowchart TD
    Root["Wurzel des JSON-Werts"]
    Root --> Obj["object { ... }"]
    Root --> Arr["array [ ... ]"]
    Root --> Prim["primitive (string/number/bool/null)"]

    Obj --> Key1["Schlüssel: 'name'"]
    Obj --> Key2["Schlüssel: 'tasks'"]
    Key2 --> Arr2["Array von Aufgaben"]
    Arr2 --> TaskObj["Aufgaben-Objekt"]
    TaskObj --> Title["title: string"]
    TaskObj --> Done["done: bool"]

Wichtig: In JSON gibt es immer genau einen Wurzelwert. Manchmal ist das ein Objekt {...}, manchmal ein Array [...]. Wenn Sie „zwei Objekte hintereinander“ sehen — dann ist das kein JSON-Dokument mehr, sondern zwei JSON-Dokumente, aneinandergereiht ohne Regeln.

2. Wert-Familien: object, array, primitives

Wenn man alles zusammenfasst, ist JSON eigentlich sehr klein. Es gibt keine Klassen, keine Dates, keine „ints“, „longs“, keine Kommentare und all die anderen Annehmlichkeiten. Es gibt nur drei Familien von Knoten: Objekt, Array und Primitiven. Aus ihnen lässt sich fast jede Datenstruktur bauen — ungefähr wie mit LEGO-Steinen: wenige Teile, aber die Burgen werden trotzdem riesig.

Unten eine „Spickzettel“-Übersicht, die nützlich im Kopf zu behalten ist:

Familie Form Bedeutung
object
{ "key": value, ... }
Sammlung von benannten Feldern. Der Schlüssel ist eine String.
array
[ value, value, ... ]
Geordnete Liste von Werten.
primitive
"text", 123, true, null
Blatt des Baums: String, Zahl, Boolean oder null.

Weiter unten betrachten wir jede Familie genauer, mit greifbaren Beispielen.

JSON object: geschweifte Klammern und benannte Felder

Ein JSON-Objekt ist das, was viele intuitiv als „Dictionary“ oder „Struct“ wahrnehmen: es hat Felder, und jedes Feld hat einen Namen. Der Feldname in JSON ist immer ein String in doppelten Anführungszeichen. Der Wert kann alles Mögliche sein: Objekt, Array, String, Zahl, true/false oder null. Die Reihenfolge der Felder sollte keine semantische Bedeutung haben, auch wenn es optisch manchmal hübscher wirkt.

Stellen wir uns die Einstellungen von TaskPad als JSON vor:

{
  "user": "Alice",
  "theme": "dark",
  "autosave": true
}

Und ein kleines C++-Beispiel: wir speichern JSON vorerst einfach als String und geben es aus, damit man sich an das Aussehen und die Anführungszeichen gewöhnt. Beachten Sie, wie praktisch raw-Strings sind, damit der Code nicht zu einem Festival aus \" und \\ wird.

#include <iostream>
#include <string>

int main() {
    const std::string configText = R"({
        "user": "Alice",
        "theme": "dark",
        "autosave": true
    })";

    std::cout << configText << '\n';
}
// (druckt JSON so wie es ist)

Wichtiges Detail, das Anfänger oft übersehen: Schlüssel sollten semantisch eindeutig sein, aber das JSON-Format garantiert nicht immer, wie Duplikate behandelt werden. Wenn Sie so schreiben:

{ "user": "Alice", "user": "Bob" }

dann sind das bereits „daten mit zweifelhaftem Sinn“: manche Parser nehmen den letzten Wert, manche den ersten, manche geben einen Fehler. Deshalb sind doppelte Schlüssel eine typische Ursache für „merkwürdige Bugs“, wenn Sie sicher sind, etwas geschrieben zu haben, und das Programm etwas anderes liest.

JSON array: eckige Klammern und Reihenfolge der Elemente

Ein Array in JSON ist eine Liste von Werten in eckigen Klammern. Im Gegensatz zum Objekt ist die Reihenfolge im Array wichtig. Wenn Objekt „nach Namen“ ist, ist Array „nach Position“. Hier entsteht eine gute Praxis: Arrays möglichst homogen halten. Wenn es ein Aufgaben-Array ist, sollte jedes Element ein Aufgabenobjekt sein und nicht „mal String, mal Zahl, mal Objekt“.

Die Aufgabenliste von TaskPad könnte so aussehen:

[
  { "title": "Buy milk", "done": false },
  { "title": "Read C++ book", "done": true }
]

Und wieder ein C++-Snippet — wir geben das Beispiel einfach aus, damit das Auge das Muster „eckige Klammern, Kommata zwischen Elementen, Objekte im Array“ lernt.

#include <iostream>
#include <string>

int main() {
    const std::string tasksText = R"([
        { "title": "Buy milk", "done": false },
        { "title": "Read C++ book", "done": true }
    ])";

    std::cout << tasksText << '\n';
}
// (druckt JSON-Array)

Wenn Arrays heterogen werden (z. B. [1, "two", {"three": 3}]), ist das fast immer ein Hinweis auf „schnellen Prototyp“ oder „kein Formatvertrag“. In Lern- und Praxisprojekten sollte so etwas vermieden werden: später müssen Sie sich selbst erklären, warum das dritte Element ein Objekt statt eines Strings ist — Spoiler: das waren Sie vor einer Woche.

JSON primitives: vier Daten-„Atome“

Primitive in JSON sind die Blätter des Baums. Sie enthalten keine verschachtelten Strukturen. Es gibt nur vier Typen: String, Zahl, Boolean und null. Hier beginnt der Spaß: in C++ sind die Typen streng, aber JSON kennt nur „number“. Daher ist es wichtig, als Formatdesigner zu entscheiden, was erwartet wird: Integer, Float, Wertebereich, ob null erlaubt ist usw.

Beispiel eines „gemischten“ Aufgabenobjekts, das alle Primitiven zeigt:

{
  "title": "Write lecture",
  "done": false,
  "priority": 2,
  "note": null
}

Merken wir uns: in JSON gibt es keinen Typ «char», kein «date», kein «int64». Es gibt nur «number». Alles Weitere ist Abmachung zwischen Ihrem Format und Ihrem Programm.

3. Strings und Escaping: wo Anführungszeichen kaputtgehen

Das häufigste Problem mit JSON bei Einsteigern sind Strings. Ein JSON-String muss in doppelten Anführungszeichen stehen, und innerhalb des Strings müssen Anführungszeichen und Sonderzeichen escaped werden. Hier merken viele, dass Anführungszeichen nicht nur im Alltag („er sagte…“) vorkommen, sondern auch in den Daten, und sie wollen ebenfalls leben.

Beispiel eines korrekten JSON-Strings:

{ "title": "He said: \"Hello\"" }

Innerhalb des Strings wird das doppelte Anführungszeichen zu \". Wenn Sie den Backslash vergessen, „endet“ der JSON-String zu früh und der Parser meckert, als hätten Sie ihm das Schicksal gebrochen.

Ein weiterer typischer Punkt sind Zeilenumbrüche. In einem JSON-String kann man nicht einfach Enter drücken und denken „das ist Text“. Ein Zeilenumbruch im String muss als \n geschrieben werden. Daher werden mehrzeilige Texte oft entweder mit \n gespeichert oder in ein Array von Strings ausgelagert.

Und ein kleines C++-Beispiel: auch in C++ leben wir in der Welt der Strings und des Escapings. Raw-Strings helfen, JSON in C++ nicht extra zu escapen (sonst bekämen wir „Escapen des Escapings“ und würden anfangen zu zweifeln, ob die Realität eine Simulation ist).

#include <iostream>
#include <string>

int main() {
    const std::string text = R"({"title":"He said: \"Hello\""})";
    std::cout << text << '\n'; // {"title":"He said: \"Hello\""}
}

Beachten Sie den interessanten Fakt: innerhalb einer raw-String-Syntax R"( ... )" müssen Sie in C++ die Anführungszeichen nicht escapen, aber innerhalb des JSON gelten trotzdem die JSON-Regeln für Escaping. Eine raw-String hebt die JSON-Regeln nicht auf, sie erleichtert nur das Aufschreiben.

4. Zahlen, bool und null: „"42"“, 42 und 42.0 — verschiedene Tiere

Auf den ersten Blick wirken Zahlen und Booleans in JSON einfach. Sobald Sie die Daten aber in ein Programm einlesen, zeigt sich, dass die Einfachheit trügerisch war. JSON unterscheidet nicht zwischen int und double wie C++: dort gibt es nur number. In Ihrem Datenmodell möchten Sie aber vermutlich Strenge: Alter ist integer, Rating ist float, Flag ist boolean, fehlender Wert ist null oder Feld fehlt.

Die bekannteste Falle: Zahl als String.

{ "age": "42" }

und

{ "age": 42 }

Sie sehen ähnlich aus, aber die Bedeutung ist grundlegend verschieden. Im ersten Fall ist es Text "42", im zweiten eine Zahl 42. Wenn Sie später vergleichen oder sortieren müssen, wird der Unterschied plötzlich groß: der String "100" ist lexikographisch „kleiner“ als "42", weil '1' < '4'. Und schon sortieren Sie Menschen nach Alter falsch.

Boolean-Werte in JSON sind strikt true und false in Kleinbuchstaben. Varianten wie True, FALSE, yes oder 0/1 sind „kein JSON“ bzw. „nicht vertragskonform“.

null ist ein eigener Primitivwert. Er steht nicht in Anführungszeichen. Er ist nicht der String "null" und nicht „leerer String“. Er bedeutet wirklich „nichts“.

{ "note": null }

Wie das in unserem „Text-C++“-Beispiel aussieht:

#include <iostream>
#include <string>

int main() {
    const std::string example1 = R"({"age": 42})";
    const std::string example2 = R"({"age": "42"})";
    const std::string example3 = R"({"note": null, "done": true})";

    std::cout << example1 << '\n'; // {"age": 42}
    std::cout << example2 << '\n'; // {"age": "42"}
    std::cout << example3 << '\n'; // {"note": null, "done": true}
}

Solange wir nicht parsen, üben wir bereits eine wichtige Fähigkeit: den Typ anhand der Syntax zu erkennen. Anführungszeichen — String. Keine Anführungszeichen — Zahl/true/false/null (oder ein Fehler).

5. „Feld fehlt“ und „Feld = null“: zwei verschiedene Situationen

Das ist eines der fehlerträchtigsten Themen in jedem Datenformat: „kein Wert vorhanden“ von „Wert explizit als leer gesetzt“ zu unterscheiden. In JSON sind diese Zustände klar getrennt: wenn das Feld fehlt — dann fehlt es. Wenn das Feld vorhanden ist und den Wert null hat — dann existiert es und signalisiert etwas anderes.

Vergleichen Sie zwei Aufgaben.

Variante A — Feld fehlt:

{ "title": "Buy milk", "done": false }

Variante B — Feld ist vorhanden, aber null:

{ "title": "Buy milk", "done": false, "deadline": null }

Aus Sicht des „Formatvertrags“ können das unterschiedliche Bedeutungen sein. Beispielsweise kann das Fehlen von deadline bedeuten „wir haben noch keinen Termin“, während deadline: null „es gibt grundsätzlich keinen Termin“. Oder umgekehrt. Wichtig ist: klären Sie vorher, was das bedeutet, sonst wird das Lesen der Daten zur Kaffeesatzleserei.

Es gibt eine dritte Falle: das Feld ist vorhanden und ist ein leerer String.

{ "deadline": "" }

Das ist nicht dasselbe wie null und erst recht nicht dasselbe wie „Feld fehlt“. Manchmal ist ein leerer String ein erlaubter Wert, manchmal ein Datenfehler. Wenn Sie das nicht spezifizieren, entstehen später „magische“ Prüfungen im Code wie if (s == ""), und das Leben wird weniger fröhlich.

Ein kleines C++-Beispiel, das zeigt, dass „Feld fehlt“ und „Feld = null“ schon im Text verschieden sind, noch bevor irgendein Parser läuft:

#include <iostream>
#include <string>

int main() {
    const std::string noField = R"({"title":"Buy milk"})";
    const std::string nullField = R"({"title":"Buy milk","deadline":null})";

    std::cout << noField << '\n';    // {"title":"Buy milk"}
    std::cout << nullField << '\n';  // {"title":"Buy milk","deadline":null}
}

Unser heutiges Ziel: Merken Sie sich, dass Fehlen eines Feldes und null nicht dasselbe sind. In späteren Vorlesungen wird das beim Lesen und Validieren von Daten zentral.

6. Typische JSON-Fehler und wie man sie mit dem Auge erkennt

JSON ist strikt. Es ist nicht böse — es toleriert nur nicht „das habt ihr schon gemeint“. Daher lassen sich die meisten Fehler durch genaues Hinschauen am Text entdecken. Das ist eine nützliche Fähigkeit: je früher Sie lernen, „mit dem Auge zu debuggen“, desto weniger Zeit verschwenden Sie mit endlosem „warum meckert der Parser?“.

Ein sehr häufiger Fehler ist ein überflüssiges Komma am Ende einer Feldliste oder eines Array-Elements. In einigen Sprachen (und bestimmten Konfigurationsformaten) sind trailing commas erlaubt, aber im Standard-JSON sind sie verboten.

Schlechtes Beispiel:

{ "user": "Alice", }

Dasselbe im Array:

[ 1, 2, 3, ]

Ein Klassiker sind falsche Anführungszeichen. JSON akzeptiert nur doppelte Anführungszeichen "...". Einzelne Anführungszeichen '...' sind kein JSON, auch wenn manche so tun, als wäre es „fast das Gleiche“.

Schlechtes Beispiel:

{ 'user': 'Alice' }

Ein weiterer oft gesehener Fehler ist das Hinzufügen von Kommentaren. JSON unterstützt keine Kommentare. Kein // und keine /* ... */. Wenn Sie Kommentare brauchen, ist das ein anderes Format (z. B. JSON5), und ein Standard-JSON-Parser weigert sich zu verarbeiten.

Schlechtes Beispiel:

{
  "user": "Alice" // Benutzer
}

Weiter geht’s mit „Pseudo-JSON“ aus anderen Welten: True/False statt true/false, NULL statt null, oder sogar NaN und Infinity. In manchen Systemen tauchen diese Varianten auf, aber das ist dann nicht mehr JSON. Wenn Sie so etwas sehen, stammen die Daten nicht aus der JSON-Welt, sondern aus einer Welt, „in der alle beliebig verabredet haben“.

Schlechte Beispiele:

{ "done": True }
{ "value": NaN }

Ein extra Schmerzpunkt sind nicht geschlossene Klammern und Anführungszeichen. Eine einfache Gewohnheit hilft: bei größeren Dokumenten von „außen nach innen“ prüfen. Zuerst sicherstellen, dass die Wurzel ein Paar {...} oder [...] hat. Dann prüfen, dass alles innen symmetrisch ist, und erst danach die Felder im Detail lesen.

Zur Übung des Strukturlesens ist es nützlich, das Mini-JSON unseres TaskPad (Einstellungen + Aufgaben) anzuschauen und sich Fragen zu stellen: Ist die Wurzel ein Objekt oder Array? Wo ist das Aufgaben-Array? Wo sind die Primitiven? Welche Felder sind Pflicht?

#include <iostream>
#include <string>

int main() {
    const std::string saveText = R"({
        "user": "Alice",
        "tasks": [
            { "title": "Buy milk", "done": false },
            { "title": "Read C++ book", "done": true }
        ]
    })";

    std::cout << saveText << '\n';
}

Ja, das ist bisher nur Text. Aber jetzt sollten Sie das Muster erkennen: Wurzel ist ein Objekt, es hat das Feld tasks, der Wert dieses Feldes ist ein Array, und die Elemente des Arrays sind Aufgabenobjekte.

7. Typische Fehler

Fehler Nr. 1: JSON mit „fast JSON“ verwechseln (Kommentare, überflüssige Kommata, einfache Anführungszeichen).
Das wirkt harmlos, weil für Menschen klar ist, was gemeint war. Der Parser arbeitet jedoch nicht „nach Sinn“, sondern nach strengen Regeln. Am Ende bekommen Sie eine Fehlermeldung „irgendwo stimmt etwas nicht“, obwohl es sich für Sie richtig anfühlt. Abhilfe schafft Disziplin: wenn das Format JSON heißt, dann keine Erweiterungen ohne ausdrückliche Entscheidung.

Fehler Nr. 2: Zahlen als Strings speichern („"42"“ statt 42) und dann korrekte Rechenoperationen erwarten.
Beim Schreiben der Daten scheint „alles als String speichern ist einfacher“. Später müssen Sie aber vergleichen, sortieren oder rechnen, und dann sind Ihre Daten nur noch Texte. Besonders sichtbar wird das beim Sortieren, wenn "100" „kleiner“ ist als "42". Für Zahlen nutzen Sie number, Strings bleiben Strings.

Fehler Nr. 3: Sich auf die Reihenfolge der Schlüssel in einem object verlassen.
Manche Menschen betrachten ein JSON-Objekt als „Struktur mit fester Feldreihenfolge“, weil die Datei schön aussieht: erst id, dann name, dann tasks. Semantisch ist ein JSON-Object jedoch eine Menge benannter Felder, und die Reihenfolge sollte keine Rolle spielen. Wenn Ihr Code auf Reihenfolge angewiesen ist, bricht das beim ersten Speichern mit einem anderen Werkzeug oder Parser.

Fehler Nr. 4: „Feld fehlt“ und „Feld = null“ nicht unterscheiden und später mit dem Format streiten.
Diese beiden Zustände sind unterschiedlich, und JSON bietet beide. Wenn Sie nicht vorher im Vertrag festlegen, was jeweils bedeutet, entstehen widersprüchliche Dateien im Projekt: mal schreibt man das Feld nicht, mal schreibt man null, mal einen leeren String. Später müssen solche Probleme per Migration behoben werden, und das ist unangenehm. Besser vorher klare Regeln.

Fehler Nr. 5: Arrays ohne zwingenden Grund heterogen machen.
Ein Array [ {Aufgabe}, {Aufgabe}, {Aufgabe} ] lässt sich vorhersehbar lesen und validieren. Ein Array [ "Buy milk", 7, true, null, {"done":false} ] ist zwar technisch erlaubt, wird aber fast immer zu Brei: Sie müssen viele Typprüfungen schreiben, dem Benutzer das Format erklären und ständig fürchten, dass jemand einen neuen „Sonderfall“ hinzufügt. Für ein nachhaltiges Format sind Arrays in der Regel homogen.

1
Aufgabe
C++ SELF, Level 65, Lektion 0
Gesperrt
Anwendungseinstellungen
Anwendungseinstellungen
1
Aufgabe
C++ SELF, Level 65, Lektion 0
Gesperrt
Benutzerprofil
Benutzerprofil
1
Aufgabe
C++ SELF, Level 65, Lektion 0
Gesperrt
Primitiv-Erkenner
Primitiv-Erkenner
1
Aufgabe
C++ SELF, Level 65, Lektion 0
Gesperrt
Deadline-Status
Deadline-Status
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION