json-validator.de

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"