JSON in TypeScript-Typen

Füge eine API-Antwort oder eine .json-Datei ein: Du erhältst die TypeScript-Typen als Interfaces (oder Type-Aliase) und das JSON Schema 2020-12, mit optionalen Feldern, abgeleitet aus jedem Element einer Liste, nicht nur aus dem ersten. Das JSON verlässt nie deinen Browser.

Oder tippe hier oder ziehe eine .json-Datei hierher
Das JSON wird in deinem Browser gelesen und in Typen umgewandelt: Es wird nirgendwohin gesendet, du kannst also auch eine Antwort mit echten Daten einfügen.

Warum ein Blick auf das erste Element nicht reicht

Das JSON aus einer API hat fast nie lauter gleiche Objekte. In einer Liste von Bestellungen hat die erste notes und die zweite nicht; die dritte hat das Feld tracking mit einem Code, weil sie schon verschickt wurde. Ein Generator, der nur das erste Element ansieht, schreibt einen Typ mit Pflichtfeld notes und ohne tracking: Der Code kompiliert, und dann findest du mitten in der Ausführung undefined dort, wo der Typ einen String versprochen hat.

Hier wird jedes Element jeder Liste gelesen, auch Listen in Listen. Ein Schlüssel, der auch nur in einem Objekt fehlt, wird optional (tracking?: string). Ein Feld, das mal eine Zahl, mal ein String ist, wird zu number | string, und die Seite markiert es und sagt, wie oft jeder Typ vorkommt, denn oft sind es unsaubere Daten: ein Preis, der einmal in Anführungszeichen geschrieben wurde.

Ein fehlender Schlüssel und ein null-Wert sind nicht dasselbe, und der Typ unterscheidet sie auch. Ein Schlüssel, der manchmal fehlt, bekommt das Fragezeichen; ein Schlüssel, der immer da, aber manchmal null ist, bekommt | null; passiert beides, bekommt er beides, und mit --strict behandelt TypeScript die beiden Fälle unterschiedlich.

Die Entscheidungen, die du im Ergebnis findest

Eine Liste, die immer leer ist, wird zu unknown[] und nicht zu any[]. Über ihre Elemente weiß man nichts, und unknown zwingt dich, sie vor der Verwendung zu prüfen, während any die Prüfung genau dort abschalten würde, wo sie gebraucht wird. Ein Objekt, das immer leer ist, wird zu Record<string, unknown> (aus demselben Grund).

Schlüssel, die keine gültigen Namen sind, bleiben in Anführungszeichen: "date-of-birth", "first name", "2fa". Als Eigenschaftsnamen erlaubt sind dagegen reservierte Wörter wie default oder class, und sie bleiben ohne Anführungszeichen.

Die Namen verschachtelter Interfaces kommen vom Schlüssel: shipping_address wird zu ShippingAddress. Für die Elemente einer Liste kommt der Name in den Singular, nach einer offen dokumentierten Heuristik, die sich ansieht, wie das Wort im Italienischen, Englischen und Spanischen endet (utenti ergibt Utente, categories ergibt Category, direcciones ergibt Direccion). Endungen, die zweierlei bedeuten können, bleiben unangetastet: Das italienische -umi kommt sowohl von volume als auch von consumo, also wird aus legumi einfach Legumi, während die häufigsten Wörter einzeln ausgeschrieben sind (consumi ergibt Consumo). Was die Endung nicht klären kann, ist dasselbe Wort in zwei Sprachen: roles heißt auf Englisch role und auf Spanisch rol, und Englisch gewinnt. Deshalb wird jeder in den Singular gesetzte Name unter dem Ergebnis aufgelistet.

Zwei Objekte mit derselben Form werden zu einem einzigen Typ: Haben Liefer- und Rechnungsadresse dieselben Schlüssel, entsteht Address, das zweimal verwendet wird. Haben die Namen nichts gemeinsam, etwa bei author und category mit jeweils einer ID und einem Namen, bleibt der zweite ein Alias, type Category = Author, denn category: Author wäre irreführend. Und ein Name, den TypeScript oder der Browser schon für einen Typ verwendet (Date, Event, Location, AudioData), bekommt den Namen des Elternobjekts vorangestellt, damit er den echten Typ nicht in der ganzen Datei verdeckt.

interface oder type: was du nehmen solltest

Um Daten zu beschreiben, sind beide Formen gleichwertig: Nimm die, die dein Projekt schon verwendet. Interfaces lassen sich erweitern und sind das, was die TypeScript-Dokumentation für Objekte empfiehlt; Type-Aliase braucht man, wenn der Typ kein Objekt ist. Deshalb kommt eine Wurzel, die eine Liste oder ein einfacher Wert ist, als Type-Alias heraus, auch wenn du interface gewählt hast, zum Beispiel export type Root = RootItem[];, und die Seite sagt es dir. Jede Deklaration bekommt ein export.

JSON Schema, um die Daten beim Eintreffen zu prüfen

TypeScript-Typen verschwinden, wenn der Code läuft: Sie schützen den, der das Programm schreibt, nicht das Programm vor den Daten, die es bekommt. Um eine Antwort beim Eintreffen zu prüfen, brauchst du ein Schema, und die zweite Ausgabe ist ein JSON Schema draft 2020-12, gelesen von Ajv in JavaScript, jsonschema in Python und den meisten Tools, die Konfigurationen und APIs validieren.

Das Schema spiegelt die Typen: Dieselben Interfaces stehen in $defs, die Pflichtfelder in required, null-Werte und gemischte Felder in einem type mit mehreren Werten. Es unterscheidet integer von number an der Schreibweise der Zahl: 10 ist eine ganze Zahl, 10.0 nicht, denn wer den Punkt schreibt, sagt, dass dort Dezimalstellen kommen können.

Die Option Geschlossenes Schema ergänzt additionalProperties: false: Ein Schlüssel, der im Beispiel nicht vorkam, wird zum Fehler. Schalte sie bei Daten ein, die du selbst erzeugst; bei fremden APIs lass sie aus, denn die könnten morgen ein Feld hinzufügen, und dann würden korrekte Antworten abgelehnt.

Zu große Zahlen und doppelte Schlüssel

In JavaScript ist eine ganze Zahl exakt nur bis 9.007.199.254.740.991, also 2 hoch 53 minus 1. Darüber ändern sich die letzten Ziffern von selbst: 9007199254740993 wird, mit JSON.parse gelesen, zu 9007199254740992, stillschweigend. Das passiert bei Kennungen, deshalb schicken manche APIs die ID auch als String. Diese Seite liest das JSON mit einem eigenen Parser, der die Originalziffern behält, und sagt dir, wo eine solche Zahl steht und was aus ihr wird. Im Typ bleibt sie number; am besten lässt du sie dir als String schicken.

Dasselbe gilt für doppelte Schlüssel im selben Objekt: JSON.parse behält ohne Warnung den letzten, und hier werden sie mit Zeilenangabe gemeldet. Und ein kaputtes JSON bekommt Zeile, Spalte und den Grund in Worten: das überzählige Komma vor einer Klammer, einfache Anführungszeichen, einen Kommentar, ein True im Python-Stil.

Wie es getestet wurde

Ein Typgenerator kann sich auf eine Weise irren, die man nicht sieht: Typen, die alle optional oder alle any sind, kompilieren immer. Deshalb nimmt der Test Hunderte zufällig erzeugte JSON-Dateien (manche mit derselben Form an mehreren Stellen, jede mit ihrer eigenen Variante) und lässt den TypeScript-Compiler, mit --strict, die erzeugten Typen zusammen mit dem JSON selbst kompilieren. Dann entfernt der Test ein Pflichtfeld, ändert den Typ eines Werts oder fügt einen erfundenen Schlüssel hinzu und verlangt, dass die Datei vom Compiler abgelehnt wird.

Das Schema wird von einer fremden Bibliothek geprüft, jsonschema für Python: Das Ausgangs-JSON muss gültig sein, die kaputte Version nicht. Der JSON-Parser wird an Tausenden absichtlich beschädigten Texten mit dem von Python verglichen und gibt dieselben Antworten, mit zwei bewussten Unterschieden: Er toleriert die unsichtbare BOM, die manche Windows-Programme an den Dateianfang setzen, und er lehnt NaN und Infinity ab, die es in JSON nicht gibt.

Was es nicht kann

Es errät keine Bedeutungen. Ein Datum bleibt string, ebenso eine E-Mail: Am Text lässt sich nicht erkennen, ob morgen ein anderes Format kommt. Genauso bleibt der Status einer Bestellung bei drei beobachteten Werten string und wird nicht zur Liste dieser drei: Die erlaubten Werte schreibst du selbst.

Es erkennt keine Dictionaries. Ein Objekt, das Kennungen als Schlüssel nutzt, wird zu einem Interface mit diesen Schlüsseln; ist es eigentlich eine Map, ändere es in Record<string, User>. Und es erfindet keine rekursiven Typen: Ein Baum wird zu verschachtelten Interfaces, so tief wie das Beispiel.

Es schreibt keinen Validierungscode und keine Klassen, und es repariert kein kaputtes JSON: Dafür gibt es JSON reparieren, und um es neu zu formatieren und zu sehen, wo der Fehler steckt, gibt es JSON formatieren & validieren. Brauchst du eine Tabelle aus einem JSON, gibt es JSON in CSV, und ist die Datei YAML, etwa eine docker-compose-Datei oder ein Kubernetes-Manifest, nimm den Weg über YAML in JSON.

Ähnliche Tools