Case-Study · OpenAPI 3.1 + openapi-typescript + custom Extractor + Ajv
Case-Study: JSON Schemas aus einer OpenAPI-3.1-Spec extrahieren
Bestehende OpenAPI-3.1-Spec mit 23 Endpoints. Build-Step extrahiert die Bodies als $defs in einzelne Schema-Files. Spart Tooling-Duplicates beim Frontend.
Schema · Draft 2020-12 (OpenAPI-3.1-default)
Payload-Beispiel
{
"id": 42,
"status": "open",
"title": "Bug: Login schlägt fehl bei Sonderzeichen",
"labels": ["bug", "auth"]
} Schema-Auszug
# openapi.yaml (Auszug)
components:
schemas:
Issue:
type: object
additionalProperties: false
required: [id, status, title]
properties:
id: { type: integer, minimum: 1 }
status: { enum: [open, in_progress, closed] }
title: { type: string, minLength: 1 }
labels: { type: array, items: { type: string } } Erkenntnisse aus dem Projekt
- ✓ OpenAPI 3.1 nutzt JSON Schema Draft 2020-12 1:1 - kein Subset mehr wie in 3.0
- ✓ openapi-typescript für Frontend-Types, eigene Extractor für reine Schemas
- ✓ Schemas mit $ref auf components/schemas funktionieren cross-tool
- ✓ YAML → JSON Konvertierung im Build-Step (yaml-Package)
- ✓ CI prüft: OpenAPI parse + Schema-Compile in einem Step
Setup: Bestehende API mit 23 Endpoints, dokumentiert als OpenAPI 3.1 in einer YAML-Datei. Schon vorhanden: Frontend-Types via openapi-typescript. Fehlend: stand-alone JSON Schemas, mit denen man auch im Backend-Worker (außerhalb des Express-Servers) Payloads validieren kann.
Warum extrahieren statt direkt parsen?
Ajv kann theoretisch auch direkt aus einem OpenAPI-Dokument die Schemas ziehen. Praktisch ist es aber bequemer, die components.schemas als separate .schema.json-Files zu haben - dann lassen sie sich auch in anderen Sprachen (Python-Worker, Java-Konsumer) ohne OpenAPI-Tooling laden.
Schritt 1: Build-Step zum Extrahieren
// build/extract-schemas.ts
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
import yaml from 'yaml';
const openapi = yaml.parse(readFileSync('openapi.yaml', 'utf8'));
const schemas = openapi.components?.schemas ?? {};
mkdirSync('schemas/components', { recursive: true });
for (const [name, schema] of Object.entries(schemas)) {
writeFileSync(
`schemas/components/${name}.schema.json`,
JSON.stringify(
{ $schema: 'https://json-schema.org/draft/2020-12/schema', ...schema },
null, 2
)
);
}
Schritt 2: $ref-Auflösung
OpenAPI nutzt $ref: "#/components/schemas/Address". Die extrahierten Files brauchen ein angepasstes $ref-Format:
function rewriteRefs(schema: any): any {
if (typeof schema !== 'object' || schema === null) return schema;
if ('$ref' in schema) {
schema.$ref = schema.$ref.replace(
/^#\\/components\\/schemas\\//,
'./'
).replace(/$/, '.schema.json');
}
for (const key in schema) {
schema[key] = rewriteRefs(schema[key]);
}
return schema;
}
Schritt 3: Ajv konfigurieren um Refs zu laden
const ajv = new Ajv({ loadSchema: async (uri) => {
const path = `schemas/components/${uri.replace('./', '')}`;
return JSON.parse(await fs.readFile(path, 'utf8'));
}});
const validate = await ajv.compileAsync(issueSchema);
Schritt 4: CI-Integration
# .gitlab-ci.yml
validate:openapi:
script:
- npx @redocly/cli lint openapi.yaml
- node build/extract-schemas.ts
- npx ajv-cli compile --strict=true -s 'schemas/**/*.schema.json'
Ergebnis
- Eine Source-of-Truth: openapi.yaml
- Drei Outputs: Frontend-Types (.d.ts), stand-alone Schemas (.json), Docs (Redoc-generated)
- Schema-Validation läuft auch in Python-Workern und Java-Konsumern
- Onboarding-Zeit für neue Sprachen halbiert - Schemas sind schon "passend gemacht"