Zum Inhalt springen
jsonbeautifiers
Deutsch

JSON hat keine Kommentare, und das war Absicht

Kommentare wurden aus JSON gestrichen, um die Interoperabilität zu schützen, und diese Entscheidung bezahlt seither jede Konfigurationsdatei.

Jede Aussage auf dieser Seite ist entweder gemessen oder belegt. Wo sie keines von beidem ist, steht das dabei.

Sie fügen einer Konfigurationsdatei eine Zeile hinzu, die erklärt, warum ein Timeout 45 Sekunden beträgt und nicht 30, das Deployment schlägt fehl, und die Meldung hilft überhaupt nicht weiter:

JSON.parse('{\n  // 45s: upstream p99 is 38s\n  "timeout": 45\n}')
// Expected property name or '}' in JSON at position 4 (line 2 column 3)

Der Parser sah einen Schrägstrich, wo ein Schlüssel stehen sollte, und gab auf. Python ist über die Ursache nicht klarer:

json.loads('{\n  // 45s: upstream p99 is 38s\n  "timeout": 45\n}')
# JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 3 (char 4)

Keine der beiden Meldungen erwähnt Kommentare, denn was die Grammatik angeht, gibt es nichts zu erwähnen. RFC 8259 definiert genau vier Zeichen, die zwischen Token stehen dürfen: Leerzeichen, Tabulator, Wagenrücklauf und Zeilenvorschub. Alles andere ist entweder Teil eines Werts oder ein Syntaxfehler.

Warum sie entfernt wurden

Kommentare waren in frühen Versionen von JSON enthalten, und Douglas Crockford nahm sie heraus. Seine erklärte Begründung ist der interessante Teil: Die Leute benutzten sie nicht für Prosa, sondern um Parser-Direktiven zu transportieren. Ein Hinweis auf die Kodierung, ein Verweis auf ein Schema, in einen Kommentar geschrieben, den ein bestimmter Konsument lesen und ausführen würde. An diesem Punkt ist der Kommentar kein Kommentar mehr. Er ist ein zweiter, undokumentierter Datenkanal, der in einem Format mitreist, dessen ganzes Verkaufsargument darin bestand, dass jeder Parser überall dieselben Werte aus denselben Bytes liest.

Der Ausweg, den Crockford selbst vorschlug, war, die kommentierte Datei durch einen Minifier zu schicken, bevor man sie einem Parser übergibt. Das ist immer noch die richtige Form der Antwort, und der Rest dieses Artikels handelt hauptsächlich davon, es richtig zu machen.

Für das, was JSON in seinen frühen Jahren war, ließ sich die Entscheidung verteidigen: ein Transportformat, um einen Wert zwischen zwei Programmen zu bewegen, die sich über seine Bedeutung bereits einig waren. Niemand kommentiert ein Netzwerkpaket.

Warum es trotzdem wehtut

JSON blieb kein Transportformat. Es wurde zur Standard-Konfigurationssprache der gesamten Toolchain, und Konfiguration ist genau der Fall, in dem die Begründung hinter einem Wert wichtiger ist als der Wert. Ein retries: 0 ohne Erklärung wird von der nächsten Person in bester Absicht „repariert“. Ein retries: 0 mit // Absicht, dieser Endpunkt ist nicht idempotent darüber nicht.

Also hat sich jedes Ökosystem, das JSON für Konfiguration übernommen hat, seinen eigenen Aufsatz gebaut, und die sind untereinander nicht kompatibel.

Die fünf Optionen

Ein _comment-Schlüssel

{
  "_comment": "45s, weil das p99 stromaufwärts 38s beträgt",
  "timeout": 45
}

Das ist striktes JSON, es lässt sich überall parsen und braucht keinerlei Werkzeuge. Die Probleme sind allerdings real. Ihr Schema muss ihn jetzt erlauben, sonst weist Ihr Validator ihn zurück. Er ist ein Datum, geht also an Clients raus, landet in Logs und erscheint in Diffs als Wertänderung statt als Kommentaränderung. Und Sie haben genau einen pro Objekt: RFC 8259 sagt, Schlüssel SOLLTEN eindeutig sein, und lässt Duplikate undefiniert, wobei JavaScript und Python beide den letzten nehmen, sodass ein zweiter _comment auf derselben Ebene den ersten stillschweigend verschluckt. Leute umgehen das mit _comment1, _comment2, und genau da lohnt sich der Ansatz nicht mehr.

Nehmen Sie ihn für eine Kopfnotiz am Anfang einer Datei. Nehmen Sie ihn nicht, um zeilenweise zu annotieren.

JSONC

JSONC ist JSON plus zwei Dinge: //- und /* */-Kommentare sowie nachgestellte Kommas. Mehr nicht. Das benutzt VS Code für seine eigenen settings.json und keybindings.json, und das akzeptiert TypeScript in tsconfig.json.

{
  // das p99 stromaufwärts beträgt 38s
  "timeout": 45,
  "retries": 0, // dieser Endpunkt ist nicht idempotent
}

Man sollte über den Status offen sein: Es gibt keine unabhängige JSONC-Spezifikation. Keine RFC, keine Versionsnummer, keine Konformitäts-Suite. Es ist eine Konvention mit einer editorförmigen Implementierung dahinter, und die Dialekte weichen an den Rändern voneinander ab (ob ein nachgestelltes Komma nach dem letzten Array-Element erlaubt ist, ob Kommentare einen Round-Trip überleben). Es ist die sicherste Option, wenn Ihr Konsument ohnehin ein Werkzeug ist, das es unterstützt, und eine schlechte Option für alles, was Sie an Dritte übergeben.

JSON5

JSON5 ist eine echte Spezifikation mit Versionshistorie und geht deutlich weiter als JSONC:

  • Objektschlüssel ohne Anführungszeichen, wenn der Schlüssel ein gültiger ES5-Bezeichner ist
  • Zeichenketten in einfachen Anführungszeichen
  • Nachgestellte Kommas in Objekten und Arrays
  • Zeilen- und Blockkommentare
  • Hexadezimalzahlen
  • Führende und nachgestellte Dezimalpunkte, also sind .5 und 5. Zahlen
  • Infinity, -Infinity und NaN

Der letzte Punkt ist der, den man gründlich durchdenken muss. NaN und die Unendlichkeiten haben in JSON keine Darstellung, ein JSON5-Dokument, das sie benutzt, lässt sich also nicht in JSON umwandeln, ohne verlustbehaftet zu entscheiden, was stattdessen dort steht. Die übrigen Erweiterungen sind kosmetisch und überstehen eine Umwandlung problemlos. Nehmen Sie JSON5, wenn der Hauptautor der Datei ein Mensch ist und eine .json5-Endung akzeptabel ist; nehmen Sie es nicht als API-Format.

Hören Sie auf, JSON zu benutzen

Wenn die Datei Konfiguration ist, die Sie durchgehend kontrollieren, und nichts von außen sie konsumiert, ist das Format eine freie Wahl, und JSON ist nicht offensichtlich die beste. YAML und TOML haben Kommentare erster Klasse. Beide haben ihre eigenen Kosten, und der Vergleich lohnt eine Lektüre, bevor Sie sich festlegen, denn gerade YAML beschert Ihnen das Norwegen-Problem: Unter der Semantik von YAML 1.1, die PyYAML und Rubys Psych implementieren, wird ein no ohne Anführungszeichen als der Boolesche Wert false geparst.

Entfernen Sie sie zur Build-Zeit

Behalten Sie die kommentierte Datei als Quelle der Wahrheit, entfernen Sie die Kommentare in der CI, veröffentlichen Sie striktes JSON. Das ist Crockfords Vorschlag, und er wird allem gerecht: Ihre Bearbeiter und Reviewer sehen die Kommentare, Ihr Parser zur Laufzeit sieht ein Dokument, das RFC 8259 erfüllt, und kein Konsument muss wissen, dass eines der beiden Formate existiert.

Kommentare entfernen, ohne URLs zu zerstören

Die naheliegende Implementierung ist ein regulärer Ausdruck, und der naheliegende reguläre Ausdruck ist falsch:

// Machen Sie das nicht.
text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');

Lassen Sie ihn hierauf laufen und sehen Sie zu, wie er einen Wert zerstört:

{
  "endpoint": "https://api.example.com/v2/orders", // Produktion
  "note": "siehe /* das Runbook */, bevor du das änderst"
}

Die erste Regel findet // innerhalb von https:// und löscht den Rest der Zeile, samt schließendem Anführungszeichen und Komma. Die zweite findet einen Blockkommentar innerhalb einer Zeichenkette. Sie enden mit einer nicht abgeschlossenen Zeichenkette und einem Fehler, der auf eine völlig unbeteiligte Stelle zeigt. Eine Regex kann diese Arbeit nicht leisten, weil sie nicht wissen kann, ob ein Schrägstrich innerhalb einer Zeichenkette steht, und der Zeichenketten-Kontext in JSON hängt vom Zählen der Escapes ab.

Sie brauchen einen Scanner, der genau einen Zustand mitführt:

function stripJsonComments(text) {
  let out = '';
  let inString = false;
  let inLine = false;
  let inBlock = false;

  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    const next = text[i + 1];

    if (inLine) {
      if (c === '\n') { inLine = false; out += c; }
      continue;
    }
    if (inBlock) {
      // Zeilenumbrüche behalten, damit die Zeilennummern weiter stimmen
      if (c === '*' && next === '/') { inBlock = false; i++; }
      else if (c === '\n') { out += c; }
      continue;
    }
    if (inString) {
      out += c;
      if (c === '\\') { out += next; i++; continue; }  // Escape, beide konsumieren
      if (c === '"') inString = false;
      continue;
    }
    if (c === '"') { inString = true; out += c; continue; }
    if (c === '/' && next === '/') { inLine = true; i++; continue; }
    if (c === '/' && next === '*') { inBlock = true; i++; continue; }
    out += c;
  }
  return out;
}

Der Escape-Zweig ist der Teil, den die Leute weglassen. Ohne ihn liest sich das escapte Anführungszeichen in "er sagte \"geh auf https://example.com\" heute" als Ende der Zeichenkette, das folgende // wird also für einen Kommentaranfang gehalten und der Rest der Zeile verschwindet.

Beachten Sie auch, was das nicht tut. Das Entfernen von Kommentaren lässt nachgestellte Kommas zurück, und die scheitern für sich genommen mit ihrem eigenen Fehler: V8 meldet Expected double-quoted property name in JSON at position 7 (line 1 column 8) für {"a":1,} und das völlig andere Unexpected token ']', "[1,2,]" is not valid JSON für [1,2,]. Eine Umwandlung von JSONC nach JSON muss beides behandeln.

Wenn Sie den Scanner lieber nicht mitschleppen wollen, fügen Sie die Datei in JSON reparieren ein, das Kommentare und nachgestellte Kommas in einem Durchgang entfernt und Ihnen striktes JSON zurückgibt, und bestätigen Sie das Ergebnis anschließend mit dem Validator. Beide laufen vollständig in Ihrem Browser, was zählt, wenn die Datei, die Sie reparieren, eine Produktionskonfiguration mit Zugangsdaten darin ist. Die übrigen Fehlerseiten behandeln, was zu tun ist, wenn sich der Fehlschlag als gar kein Kommentar entpuppt.