JSON Schema automatisch aus TypeScript-Typen generieren
AI generated
type
TypeScript · JSON Schema · API-Verträge
JSON Schema aus TypeScript-Typen generieren
Eine Quelle der Wahrheit für Typen, Validierung und API-Dokumentation statt doppelter Pflege

Wer TypeScript-Interfaces und JSON Schema getrennt pflegt, verliert früher oder später die Synchronität zwischen beiden: Ein Feld wird im Typ umbenannt, das Schema bleibt beim alten Namen stehen, und ein Konsument validiert gegen Regeln, die nicht mehr zur echten API passen. Werkzeuge wie ts-json-schema-generator und zod-to-json-schema leiten das Schema direkt aus dem TypeScript-Code ab und machen den Typ zur einzigen Quelle der Wahrheit.

9 Min. Lesezeit JSON Schema Validierung API-Verträge

1. Das Synchronisationsproblem zwischen Typen und Schema

In vielen Projekten existieren TypeScript-Interfaces für den internen Gebrauch und ein separates JSON Schema für externe API-Verträge, Validierung eingehender Requests oder OpenAPI-Dokumentation, oft von Hand in zwei verschiedenen Dateien gepflegt. Sobald ein Feld hinzukommt oder sich ein Typ ändert, muss diese Änderung an zwei Stellen nachgezogen werden, und genau das wird in der Praxis regelmäßig vergessen.

Das Resultat ist ein Schema, das der Realität des Codes hinterherhinkt: Ein optionales Feld wird im TypeScript-Typ verpflichtend, im JSON Schema bleibt es weiterhin optional, und ein Client, der sich auf das Schema verlässt, sendet Requests, die zur Laufzeit an einer Stelle scheitern, die eigentlich schon zur Build-Zeit hätte auffallen müssen.

Automatische Generierung dreht diese Beziehung um: Der TypeScript-Typ ist die einzige Quelle, aus der sowohl der Compile-Zeit-Typ als auch das Laufzeit-Schema abgeleitet werden, entweder durch Analyse der TypeScript-AST oder durch eine Bibliothek, die Schema und Typ gemeinsam aus einer einzigen Definition erzeugt.

Dieses Prinzip ist besonders wertvoll an Systemgrenzen, an denen mehrere Teams oder sogar mehrere Unternehmen auf einen gemeinsamen Vertrag angewiesen sind, etwa bei einer öffentlichen API oder einem internen Event-Bus mit vielen Konsumenten. Dort führt jede manuell gepflegte Doppelung des Schemas früher oder später zu Inkonsistenzen, die erst beim Kunden oder in der Produktion auffallen, während eine generierte Quelle diese Klasse von Fehlern bereits im Build ausschließt.

2. ts-json-schema-generator: Schema direkt aus Interfaces ableiten

ts-json-schema-generator analysiert die TypeScript-AST eines Projekts und erzeugt aus einem benannten Interface oder Typalias ein vollständiges JSON Schema, inklusive verschachtelter Typen, Unions, Enums und JSDoc-Kommentaren, die als description-Feld im Schema landen. Der Vorteil gegenüber manuell geschriebenen Interfaces plus separatem Schema: Es gibt nur eine Definition, das Schema ist immer ein reines Ableitungsergebnis.

Die Bibliothek lässt sich sowohl per CLI als auch programmatisch über die Node-API einbinden, was sich für Build-Pipelines eignet, in denen das Schema bei jedem Build automatisch neu generiert und beispielsweise als statische Datei für eine API-Dokumentation abgelegt wird.


// types/order.ts
/** Eine Bestellung im System. */
export interface Order {
  /** Eindeutige Bestell-ID, UUID v4 */
  id: string;
  /** Gesamtbetrag in Cent, immer positiv */
  totalCents: number;
  status: "pending" | "shipped" | "cancelled";
  items: OrderItem[];
}

export interface OrderItem {
  sku: string;
  quantity: number;
}

3. Generierung per CLI in die Build-Pipeline einbinden

Der CLI-Aufruf braucht den Pfad zur Typdatei, den Namen des Root-Typs und optional einen tsconfig-Pfad für Pfad-Aliase oder strikte Compiler-Optionen. Das erzeugte Schema lässt sich direkt in ein schemas/-Verzeichnis schreiben und dort von Validierungsbibliotheken wie AJV konsumieren.

In CI-Pipelines lohnt sich ein zusätzlicher Diff-Check: Das Schema wird generiert und mit der eingecheckten Version verglichen, ein Unterschied bricht den Build ab. So wird sichergestellt, dass niemand einen Typ ändert, ohne das generierte Schema mit einzuchecken, was Reviewer sofort sehen.


# Schema für den Order-Typ generieren
npx ts-json-schema-generator \
  --path types/order.ts \
  --type Order \
  --tsconfig tsconfig.json \
  --out schemas/order.schema.json

# In CI: Drift zwischen Code und eingechecktem Schema erkennen
npx ts-json-schema-generator --path types/order.ts --type Order \
  | diff - schemas/order.schema.json || \
  (echo "Schema ist veraltet, bitte neu generieren" && exit 1)

4. Der umgekehrte Weg: zod-Schema als Ausgangspunkt

Ein alternativer Ansatz dreht die Reihenfolge um: Statt aus TypeScript-Typen ein Schema abzuleiten, wird zuerst ein zod-Schema definiert, aus dem sowohl der TypeScript-Typ per z.infer als auch das JSON Schema per zod-to-json-schema abgeleitet werden. Dieser Weg hat den Vorteil, dass das zod-Schema gleichzeitig für Laufzeitvalidierung genutzt werden kann, was bei reiner AST-basierter Generierung fehlt.

Für neue Projekte, die ohnehin Laufzeitvalidierung brauchen, ist der zod-First-Ansatz meist die pragmatischere Wahl, weil er Typ, Validierung und Schema aus einer einzigen Definition erzeugt. Für bestehende Codebasen mit etablierten Interfaces ist ts-json-schema-generator oft der Weg mit weniger Umbauaufwand.


import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const OrderSchema = z.object({
  id: z.string().uuid(),
  totalCents: z.number().int().positive(),
  status: z.enum(["pending", "shipped", "cancelled"]),
  items: z.array(
    z.object({ sku: z.string(), quantity: z.number().int().positive() })
  ),
});

type Order = z.infer<typeof OrderSchema>;

const jsonSchema = zodToJsonSchema(OrderSchema, "Order");
// jsonSchema ist ein vollständiges JSON-Schema-Dokument

5. Grenzfälle: Mapped Types, Generics und Discriminated Unions

Nicht jeder TypeScript-Typ lässt sich verlustfrei in JSON Schema übersetzen. Mapped Types wie Record<string, T> werden in der Regel zu additionalProperties-Definitionen, was semantisch ähnlich, aber nicht identisch ist. Generics erfordern, dass der Generator entweder eine konkrete Instanziierung des generischen Typs bekommt oder das Generic explizit aufgelöst wird, bevor generiert werden kann.

Discriminated Unions mit einem gemeinsamen Literalfeld wie type: "a" | "b" lassen sich gut als oneOf mit passenden const-Werten abbilden, sowohl ts-json-schema-generator als auch zod-to-json-schema unterstützen dieses Muster zuverlässig, solange die Discriminator-Werte eindeutige String-Literale sind.


type PaymentEvent =
  | { type: "charge"; amountCents: number }
  | { type: "refund"; amountCents: number; reason: string };

// Wird zu einem oneOf mit const-Discriminator im JSON Schema:
// { "oneOf": [ { "properties": { "type": { "const": "charge" }, ... } }, ... ] }

6. Generiertes Schema mit AJV zur Laufzeit validieren

Das generierte JSON Schema entfaltet seinen Wert erst, wenn es zur Laufzeit tatsächlich verwendet wird, etwa um eingehende Request-Bodies zu validieren, bevor sie in typisierten Code weitergereicht werden. AJV kompiliert das Schema zu einer schnellen Validierungsfunktion und liefert bei Fehlern eine strukturierte Liste der verletzten Regeln.

Wichtig ist, AJV mit strict: true zu betreiben und unbekannte Zusatzfelder in Requests konsequent abzulehnen, sonst schleichen sich stillschweigend Felder ein, die im Typ gar nicht vorgesehen sind, aber trotzdem unbeanstandet durch die Validierung rutschen.


import Ajv from "ajv";
import orderSchema from "../schemas/order.schema.json";

const ajv = new Ajv({ strict: true, allErrors: true });
const validateOrder = ajv.compile(orderSchema);

function parseOrder(raw: unknown): Order {
  if (!validateOrder(raw)) {
    throw new Error(ajv.errorsText(validateOrder.errors));
  }
  return raw as Order;
}

7. Generierte Schemas in OpenAPI-Dokumente einbinden

Weil OpenAPI 3.1 eine vollständige Teilmenge von JSON Schema für Schema-Objekte nutzt, lassen sich generierte Schemas direkt unter components.schemas in eine OpenAPI-Spezifikation einbetten, ohne manuelle Übersetzung. Das bedeutet, dass eine automatisch generierte API-Dokumentation immer exakt den tatsächlichen TypeScript-Typen entspricht, ohne dass jemand die Doku separat pflegt.

Für ältere OpenAPI-Versionen wie 3.0, die nicht vollständig JSON-Schema-kompatibel sind, braucht es einen zusätzlichen Übersetzungsschritt, der beispielsweise const-Schlüssel in enum mit einem Element umwandelt, das viele Tools in diesem Ökosystem inzwischen automatisch mitliefern.

8. Empfohlener CI-Workflow für generierte Schemas

Ein robuster Workflow generiert das Schema als Teil des Build-Skripts, checkt die generierte Datei mit ins Repository ein und lässt einen CI-Schritt prüfen, ob die eingecheckte Version noch dem aktuellen Typ entspricht. Das kombiniert die Nachvollziehbarkeit einer versionierten Datei mit der Garantie, dass sie nie veraltet.

Alternativ generieren manche Teams das Schema komplett zur Build-Zeit ohne es einzuchecken, was Repository-Rauschen vermeidet, aber Code-Reviewern die Möglichkeit nimmt, Änderungen am öffentlichen API-Vertrag direkt im Pull-Request-Diff zu sehen, ein Kompromiss, den jedes Team für sich abwägen muss.

9. Wann sich automatische Schema-Generierung lohnt

Für interne Typen, die nie das Node-Prozess-Grenze verlassen, ist ein generiertes JSON Schema meist überflüssiger Aufwand. Sobald ein Typ aber als API-Vertrag mit externen Konsumenten, als Validierungsregel für eingehende Requests oder als Grundlage einer OpenAPI-Dokumentation dient, zahlt sich die automatische Ableitung schnell aus, weil sie Drift zwischen Typ und Schema strukturell unmöglich macht.

Die Wahl zwischen AST-basierter Generierung und zod-First-Ansatz hängt vor allem davon ab, ob bereits Laufzeitvalidierung im Projekt existiert: Wo sie schon da ist, ist zod meist der natürlichere Ausgangspunkt, wo reine Typdefinitionen dominieren, ist ts-json-schema-generator der Weg mit dem geringsten Umbau.

In beiden Fällen lohnt es sich, die Generierung so früh wie möglich in den Entwicklungsworkflow einzubauen, statt sie erst nachträglich einzuführen, wenn bereits mehrere API-Verträge manuell und inkonsistent gepflegt werden. Je früher ein Team diese Automatisierung etabliert, desto geringer der einmalige Migrationsaufwand und desto größer der langfristige Nutzen für alle Konsumenten der Schnittstelle.

Merkmal ts-json-schema-generator zod-to-json-schema Manuelles Schema
Ausgangspunkt bestehende TS-Typen/Interfaces zod-Schema-Definition separate JSON-Datei
Laufzeitvalidierung inklusive nein, nur Schema ja, direkt über zod nein
JSDoc-Kommentare übernommen ja, als description teilweise über .describe() manuell
Aufwand bei bestehendem Code gering mittel, Migration zu zod nötig hoch, dauerhafte Doppelpflege
Discriminated Unions als oneOf mit const als oneOf mit literal manuell nachgebaut

Mironsoft

TypeScript-Migration, Typsicherheit und Team-Onboarding

JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?

Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.

Migrations-Fahrplan

Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.

Strict-Mode-Einführung

tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.

Team-Onboarding

Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.

10. Zusammenfassung

JSON Schema Generierung

AST-Ansatz

ts-json-schema-generator leitet Schema aus bestehenden Interfaces ab

zod-Ansatz

Ein Schema erzeugt Typ, Validierung und JSON Schema zugleich

Laufzeit-Check

AJV kompiliert generiertes Schema zu einer schnellen Validierfunktion

CI-Sicherung

Diff-Check verhindert veraltete, eingecheckte Schema-Dateien

11. FAQ: JSON Schema Generierung

1Muss ich mein gesamtes Projekt auf zod umstellen, um JSON Schema zu generieren?
Nein, ts-json-schema-generator arbeitet direkt mit bestehenden TypeScript-Interfaces und Typaliasen, ganz ohne zod. Ein Umstieg auf zod lohnt sich nur, wenn ohnehin Laufzeitvalidierung gebraucht wird.
2Werden JSDoc-Kommentare im generierten Schema übernommen?
Bei ts-json-schema-generator ja, JSDoc-Beschreibungen über einem Feld landen automatisch im description-Attribut des generierten Schemas, was die Dokumentation direkt am Typ pflegt.
3Wie gehe ich mit generischen Typen um, die generiert werden sollen?
Der Generator braucht entweder eine konkrete Instanziierung des Generics, etwa Response, oder das Generic wird vorab in einen konkreten Typalias aufgelöst, bevor die Generierung läuft.
4Kann ich Discriminated Unions verlustfrei nach JSON Schema übersetzen?
Ja, solange der Discriminator ein eindeutiges String-Literal ist, wird die Union zuverlässig als oneOf mit passenden const-Werten pro Variante abgebildet.
5Ist generiertes JSON Schema mit OpenAPI 3.1 kompatibel?
Ja, OpenAPI 3.1 nutzt JSON Schema direkt als Schema-Format, generierte Schemas lassen sich ohne Übersetzung unter components.schemas einbetten.
6Was passiert bei älteren OpenAPI-Versionen wie 3.0?
Dort ist ein zusätzlicher Übersetzungsschritt nötig, weil 3.0 nicht vollständig JSON-Schema-kompatibel ist, etwa bei const-Schlüsseln, die in ein Einzelwert-enum umgewandelt werden müssen.
7Soll das generierte Schema ins Repository eingecheckt werden?
Meistens ja, mit einem CI-Check, der Drift zwischen Code und eingechecktem Schema erkennt, weil Reviewer so Änderungen am API-Vertrag direkt im Pull-Request-Diff sehen.
8Wie schnell ist AJV im Vergleich zu manueller Validierung?
AJV kompiliert Schemas zu optimierten JavaScript-Funktionen und gehört zu den schnellsten Validierungsbibliotheken im Node-Ökosystem, deutlich schneller als generische, interpretierte Validierungsansätze.
9Kann ich mehrere Typen in einem einzigen Schema-Durchlauf generieren?
Ja, sowohl ts-json-schema-generator als auch zod-to-json-schema unterstützen mehrere Root-Typen, entweder als separate Dateien oder als ein Schema mit mehreren definitions-Einträgen.
10Lohnt sich die Umstellung für ein kleines internes Tool?
Selten, für ein kleines Tool ohne externe API-Konsumenten reicht meist ein einfaches TypeScript-Interface ohne generiertes Schema, der Mehraufwand zahlt sich erst bei echten API-Verträgen aus.