{ }
GET
OpenAPI · Symfony · Nelmio · Schema-Design
OpenAPI Beispiele und Schemas in Symfony konsistent pflegen
ohne Drift zwischen Docs und Implementierung

Dokumentation, die vom Code abweicht, ist schlimmer als gar keine Dokumentation. Wer OpenAPI-Schemas, Validierungsregeln und Beispielwerte in Symfony nicht strukturiert pflegt, baut technische Schulden auf, die sich bei jedem Breaking Change entladen. Dieser Artikel zeigt, wie man Schemas, Beispiele und Validierung dauerhaft synchron hält.

15 Min. Lesezeit Nelmio API Doc · PHP-Attribute · Schema-Vererbung · Contract-Tests Symfony 6.x · 7.x · OpenAPI 3.1

1. Das Drift-Problem: Wenn Docs und Code auseinanderlaufen

Schema-Drift ist das häufigste Problem in API-Projekten, die über mehrere Monate oder Teams wachsen. Ein Entwickler fügt einem Endpunkt ein neues Pflichtfeld hinzu, aktualisiert den Symfony-Validator, vergisst aber das OpenAPI-Schema. Ein anderer Entwickler verlässt sich auf die Dokumentation, baut einen Client, der das neue Feld nicht sendet – und erhält erst zur Laufzeit einen Validierungsfehler. Die Ursache ist strukturell: Wenn Schemas und Code in verschiedenen Artefakten leben, divergieren sie unter Entwicklungsdruck unweigerlich.

Die Lösung liegt nicht in Disziplin, sondern in Werkzeugen, die Divergenz unmöglich machen oder zumindest sofort sichtbar machen. In Symfony gibt es dafür drei Hebel: Erstens Schemas direkt am PHP-Code definieren, sodass eine Codeänderung die Dokumentation automatisch mitändert. Zweitens Validierungsconstraints aus denselben Quellen ableiten wie Schema-Definitionen. Drittens automatisierte Contract-Tests, die bei jedem Push prüfen, ob der tatsächliche API-Response dem deklarierten Schema entspricht. Dieser Artikel durchläuft alle drei Ansätze mit konkreten Symfony-Beispielen.

2. Nelmio API Doc als Grundlage in Symfony

Das Bundle nelmio/api-doc-bundle ist der Standard für OpenAPI-Dokumentation in Symfony-Projekten. Es liest PHP-Attribute, Doctrine-Entities, Symfony-Formulare und PHPDoc-Annotationen aus und generiert daraus eine vollständige openapi.json zur Laufzeit. Das Entscheidende: Die generierte Spezifikation ist niemals älter als der Code, weil sie bei jedem Request neu berechnet wird – es gibt keine separate Datei, die manuell aktuell gehalten werden müsste.

Die Installation und Grundkonfiguration ist minimal. Nach composer require nelmio/api-doc-bundle legt man in config/packages/nelmio_api_doc.yaml fest, welche Route-Prefixe dokumentiert werden sollen, welche Authentifizierungsmechanismen existieren und welche globalen Schema-Definitionen eingebunden werden. Die Route /api/doc.json liefert die maschinenlesbare Spezifikation, /api/doc die Swagger-UI. Für CI-Pipelines exportiert man die Spezifikation einmalig per bin/console nelmio:apidoc:dump --format=json > openapi.json und versioniert das Ergebnis neben dem Code.


# Install Nelmio API Doc Bundle
composer require nelmio/api-doc-bundle

# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
  documentation:
    info:
      title: "Mironsoft API"
      description: "REST API für Mironsoft-Dienste"
      version: "1.0.0"
    servers:
      - url: "https://api.mironsoft.de/v1"
        description: "Production"
      - url: "http://localhost:8080/v1"
        description: "Development"
    components:
      securitySchemes:
        bearerAuth:
          type: http
          scheme: bearer
          bearerFormat: JWT
    security:
      - bearerAuth: []
  areas:
    path_patterns:
      - ^/api(?!/doc$)

# Export spec for CI
bin/console nelmio:apidoc:dump --format=json > openapi.json

Ein häufig übersehenes Feature von Nelmio ist der Area-Mechanismus. Über areas lassen sich mehrere getrennte API-Dokumentationen erzeugen – eine öffentliche für externe Konsumenten und eine interne für Admin-Endpunkte. Jede Area hat eigene Authentifizierungsschemata, eigene Server-URLs und kann über separate Routen erreichbar sein. Das verhindert, dass interne Endpunkte mit sensiblen Parametern in der öffentlichen Dokumentation auftauchen.

3. PHP-Attribute statt YAML: Schemas direkt am Code

Die OpenApi\Attributes-Klassen aus dem Paket zircote/swagger-php ermöglichen es, OpenAPI-Schemas direkt als PHP-Attribute an Controller-Methoden, DTOs und Model-Klassen zu schreiben. Nelmio liest diese Attribute automatisch aus. Der entscheidende Vorteil gegenüber separaten YAML-Dateien: Wenn ein Entwickler die Methode umbenennt, Parameter hinzufügt oder den Response-Typ ändert, sieht er die zugehörige Dokumentation direkt daneben – und die Wahrscheinlichkeit steigt, dass er sie mit aktualisiert.

Das Attribute-System unterstützt den vollen OpenAPI-3.1-Umfang: #[OA\RequestBody], #[OA\Response], #[OA\Parameter], #[OA\Property] für Schema-Felder, #[OA\Schema] für wiederverwendbare Definitionen. In DTOs annotiert man jede Property direkt, was Typ, Format, Beschreibung und Beispielwerte zusammen mit dem PHP-Typdeklaration hält. Wenn der PHP-Typ von string auf int geändert wird, fällt der Widerspruch zum #[OA\Property(type: "string")] sofort auf.


// src/Dto/ProductCreateDto.php
<?php
declare(strict_types=1);

namespace App\Dto;

use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;

#[OA\Schema(
    schema: "ProductCreateRequest",
    required: ["name", "price", "categoryId"],
    description: "Payload to create a new product"
)]
final class ProductCreateDto
{
    public function __construct(
        #[OA\Property(description: "Product name", example: "Wireless Headphones", maxLength: 255)]
        #[Assert\NotBlank]
        #[Assert\Length(max: 255)]
        public readonly string $name,

        #[OA\Property(description: "Price in Euro cents", example: 4999, minimum: 1)]
        #[Assert\Positive]
        public readonly int $price,

        #[OA\Property(description: "UUID of the parent category", format: "uuid")]
        #[Assert\Uuid]
        public readonly string $categoryId,

        #[OA\Property(description: "Optional product description", nullable: true)]
        #[Assert\Length(max: 2000)]
        public readonly ?string $description = null,
    ) {}
}

4. Schema-Vererbung: allOf, oneOf und discriminator

Reale APIs haben selten flache, homogene Schemas. Bestellungen können verschiedene Zahlungsarten haben, Benachrichtigungen können E-Mail, SMS oder Push sein, Produkte können physische oder digitale Varianten sein. OpenAPI 3.x bietet dafür allOf, oneOf und anyOf mit optionalem discriminator. In Symfony-Projekten bildet man diese Polymorphie durch abstrakte Basis-DTOs und konkrete Ableitungen ab, die jeweils ihr eigenes #[OA\Schema]-Attribut tragen.

Der discriminator teilt OpenAPI-Clients mit, anhand welches Felds der konkrete Subtyp unterschieden wird. Das Schema der Basisklasse deklariert das Diskriminatorfeld als required, die Kindklassen erweitern per allOf. Nelmio kann diese Hierarchie automatisch ableiten, wenn man die Vererbung durch PHP-Interfaces oder abstrakte Klassen ausdrückt und die Attribute korrekt setzt. Ohne discriminator müssen Code-Generatoren raten, welchen Subtyp sie instanziieren sollen – was zu schwer debugbaren Fehlern in generierten Clients führt.

5. Reusable Examples und example-Felder richtig einsetzen

OpenAPI unterscheidet zwischen example (ein einzelner Wert direkt am Schema oder Parameter) und examples (eine benannte Map von Beispielobjekten in der Components-Sektion). Die examples-Map ist für Situationen gedacht, in denen ein Endpunkt mehrere repräsentative Szenarien hat: einen Erfolgsfall, einen Validierungsfehler, einen Berechtigungsfehler. Jedes Beispiel hat einen summary-Text und einen value. Swagger-UI und Redoc zeigen diese Beispiele als auswählbare Varianten an, was die Dokumentation für API-Konsumenten erheblich nützlicher macht.

In Symfony-Projekten lagert man häufig verwendete Beispiele in die Components-Sektion aus und referenziert sie mit $ref: '#/components/examples/ProductCreated'. Das verhindert Duplikate und stellt sicher, dass ein geänderter Beispielwert überall aktualisiert wird. Besonders wichtig: Beispielwerte müssen dem Schema entsprechen. Ein Beispiel, das ein Pflichtfeld weglässt oder einen falschen Typ verwendet, wird von manchen Validierungstools als Fehler markiert. Die Kombination aus Schema-Validierung und Beispiel-Konsistenz ist ein häufig unterschätzter Qualitätsindikator für API-Dokumentation.


# openapi/examples.yaml — Reusable examples in components section
components:
  examples:
    ProductCreated:
      summary: "Successful product creation"
      value:
        id: "01906c3f-a8b2-7e4d-9f1a-3c2d4e5f6a7b"
        name: "Wireless Headphones"
        price: 4999
        categoryId: "01906c3f-1111-7e4d-9f1a-aabbccddeeff"
        description: "Over-ear with active noise cancellation"
        createdAt: "2026-05-09T14:30:00Z"

    ValidationError:
      summary: "Validation failed — missing required field"
      value:
        type: "https://mironsoft.de/errors/validation"
        title: "Validation Failed"
        status: 422
        violations:
          - field: "name"
            message: "This value should not be blank."
          - field: "price"
            message: "This value should be positive."

  schemas:
    ProblemDetail:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        violations:
          type: array
          items:
            $ref: '#/components/schemas/ConstraintViolation'

6. Symfony Validator und OpenAPI-Constraints synchron halten

Der häufigste Drift-Punkt in Symfony-APIs liegt zwischen Validierungsconstraints und OpenAPI-Schema-Eigenschaften. Ein Feld wird im Symfony-Validator als #[Assert\Length(max: 255)] markiert, im OpenAPI-Schema aber ohne maxLength: 255 deklariert. Clients, die die Dokumentation lesen, haben keine Möglichkeit zu wissen, dass es ein Längenlimit gibt – bis sie beim Senden langer Werte einen 422-Fehler erhalten. Das ist schlechtes API-Design, das durch Disziplin allein nicht dauerhaft verhindert werden kann.

Die sauberste Lösung ist eine gemeinsame Datenquelle: Die DTO-Properties tragen sowohl Symfony-Validation-Attribute als auch OpenAPI-Property-Attribute, die dieselben Constraint-Werte enthalten. Durch eine einfache PHPUnit-Extension lässt sich automatisch prüfen, ob alle #[Assert\Length(max: X)]-Constraints einen entsprechenden maxLength: X im zugehörigen OA-Property-Attribut haben. Dasselbe gilt für #[Assert\Range] und minimum/maximum, für #[Assert\NotBlank] und das required-Array im Schema sowie für Enum-Constraints und den enum-Wert im Schema.

7. Components-Sektion: Schemas zentral verwalten

Die components/schemas-Sektion einer OpenAPI-Spezifikation ist das Äquivalent eines Typsystems. Schemas, die an mehreren Stellen verwendet werden – Pagination-Wrapper, Error-Response, Timestamps, Money-Objekte – gehören in die Components-Sektion und werden über $ref referenziert, niemals inline dupliziert. Ein inline dupliziertes Schema, das an zehn Stellen vorkommt, muss bei einer Änderung an zehn Stellen aktualisiert werden – und wird es in der Praxis nicht, weil der Entwickler drei davon vergisst.

In Symfony nutzt man dafür dedizierte Schema-Klassen, die ausschließlich als OpenAPI-Typdefinitionen dienen und nie instanziiert werden – reine Dokumentationsobjekte. Alternativ annotiert man bestehende Value Objects und DTOs mit #[OA\Schema(schema: "MoneyValue")]. Nelmio sammelt alle so markierten Klassen und fügt sie automatisch in die Components-Sektion ein. Die Referenzierung erfolgt dann durch Typdeklaration in anderen Schemas: Nelmio erkennt, dass ein Property den Typ MoneyValue hat, und generiert den entsprechenden $ref.

8. Contract-Tests: Schemas automatisch gegen Implementierung prüfen

Der letzte Schutzmechanismus gegen Schema-Drift sind automatisierte Contract-Tests. Das Paket league/openapi-psr7-validator oder spectator ermöglicht es, PHPUnit-Tests zu schreiben, die einen echten HTTP-Request gegen die Symfony-Applikation senden und den Response automatisch gegen das deklarierte OpenAPI-Schema validieren. Wenn ein Response ein Pflichtfeld weglässt, einen falschen Typ zurückgibt oder eine nicht deklarierte Property enthält, schlägt der Test fehl – ohne dass der Testautor selbst eine Assertion schreiben muss.

In der Praxis sieht ein Contract-Test aus wie ein normaler Symfony-WebTestCase: Kernel booten, Request senden, Response-Status prüfen. Der einzige Unterschied ist, dass der Response-Body zusätzlich durch den Schema-Validator geführt wird. Das Paket lädt dafür die generierte openapi.json aus dem Projekt, sucht den passenden Pfad und die passende Methode und validiert Body, Header und Status-Code. Dieser Ansatz findet Regressionsfehler, bevor sie in Produktion gelangen – und zwar automatisch, ohne dass jemand die Dokumentation manuell liest.


// tests/Api/ProductApiContractTest.php
<?php
declare(strict_types=1);

namespace App\Tests\Api;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;

final class ProductApiContractTest extends WebTestCase
{
    private static $validator;

    public static function setUpBeforeClass(): void
    {
        // Load the exported OpenAPI spec
        $specPath = __DIR__ . '/../../openapi.json';
        self::$validator = (new ValidatorBuilder())
            ->fromJsonFile($specPath)
            ->getResponseValidator();
    }

    public function testCreateProductResponseMatchesSchema(): void
    {
        $client = static::createClient();
        $client->request('POST', '/api/products', [], [], [
            'CONTENT_TYPE' => 'application/json',
            'HTTP_AUTHORIZATION' => 'Bearer ' . $this->getTestToken(),
        ], json_encode([
            'name' => 'Wireless Headphones',
            'price' => 4999,
            'categoryId' => '01906c3f-1111-7e4d-9f1a-aabbccddeeff',
        ]));

        $response = $client->getResponse();
        $this->assertSame(201, $response->getStatusCode());

        // Validate response body against OpenAPI schema — fails if schema drifts
        $psr7Response = $this->convertToPsr7($response);
        $operation = new \League\OpenAPIValidation\PSR7\OperationAddress('/api/products', 'post');
        self::$validator->validate($operation, $psr7Response); // throws on mismatch
    }
}

9. Ansätze im direkten Vergleich

Es gibt mehrere Strategien, OpenAPI-Schemas in Symfony zu pflegen. Die Wahl hat direkten Einfluss darauf, wie leicht Schema-Drift entsteht und wie schnell er erkannt wird.

Ansatz Drift-Risiko Wartungsaufwand Empfehlung
Manuelle YAML-Datei Sehr hoch Hoch (jede Änderung doppelt) Nur für sehr kleine APIs
PHP-Attribute + Nelmio Mittel Niedrig (neben dem Code) Empfohlen als Basis
Attribute + Contract-Tests Niedrig Niedrig + CI-Absicherung Empfohlen für Teams
API-Platform (auto-gen) Sehr niedrig Sehr niedrig Wenn API-Platform passt
Codegen aus YAML-First Niedrig (andere Richtung) Mittel (YAML als Source of Truth) Für API-First-Teams

Die Kombination aus PHP-Attributen direkt an DTOs und automatisierten Contract-Tests ist für die meisten Symfony-Teams der pragmatische Sweet Spot. Sie erfordert keine zusätzliche Infrastruktur, ist für jeden PHP-Entwickler lesbar und fängt Regressionsfehler automatisch ab. API-Platform ist die bessere Wahl, wenn die API stark an Doctrine-Entities gebunden ist und CRUD-Endpunkte dominieren.

Mironsoft

REST API Design, OpenAPI-Dokumentation und Symfony-Entwicklung

OpenAPI-Schemas, die nie mit dem Code driften?

Wir richten Nelmio API Doc, PHP-Attribute und Contract-Tests in eurem Symfony-Projekt ein und stellen sicher, dass eure OpenAPI-Spezifikation dauerhaft den echten API-Verhalten entspricht – automatisch geprüft bei jedem CI-Run.

Schema-Audit

Bestehende OpenAPI-Spezifikation auf Drift, fehlende Beispiele und Inkonsistenzen prüfen

Nelmio-Setup

PHP-Attribute, Components-Sektion und Area-Konfiguration für euer Projekt einrichten

Contract-Tests

Automatisierte Schema-Validierung in PHPUnit und CI-Pipeline integrieren

10. Zusammenfassung

Konsistente OpenAPI-Dokumentation in Symfony ist kein Zufall, sondern das Ergebnis struktureller Entscheidungen. PHP-Attribute direkt an DTOs und Controllern halten Schema-Definition und Implementierung nah beieinander – nicht in separaten Dateien, die unter Entwicklungsdruck auseinanderlaufen. Nelmio API Doc generiert die Spezifikation aus dem Code, nicht umgekehrt, und eliminiert damit die manuelle Synchronisation als Fehlerquelle. Die Components-Sektion zentralisiert wiederverwendbare Schemas und verhindert Duplikate.

Contract-Tests sind der entscheidende letzte Schutzmechanismus: Sie prüfen automatisch, ob tatsächliche API-Responses dem deklarierten Schema entsprechen. Kein Entwickler muss mehr manuell prüfen, ob eine Schema-Änderung alle Endpunkte betrifft – der CI-Run erledigt das. In Kombination bilden diese vier Werkzeuge einen geschlossenen Kreislauf: Code ändert sich, Dokumentation wird automatisch aktualisiert, Contract-Tests verifizieren die Konsistenz, CI blockiert den Merge bei Drift.

OpenAPI in Symfony konsistent pflegen — Das Wichtigste auf einen Blick

Nelmio API Doc

Generiert OpenAPI aus PHP-Code – keine manuell gepflegte YAML-Datei, keine veraltete Dokumentation. Export per Console-Command für CI.

PHP-Attribute am DTO

Schema-Definition, Validierungsconstraints und Typdeklaration zusammen an einer Stelle – Drift wird strukturell erschwert.

Components-Sektion

Wiederverwendbare Schemas, Beispiele und Responses zentral verwalten und per $ref referenzieren – nie inline duplizieren.

Contract-Tests

Automatische Schema-Validierung echter API-Responses in PHPUnit – fängt Regressionsfehler bevor sie in Produktion gelangen.

11. FAQ: OpenAPI Schemas und Beispiele in Symfony

1Was ist Schema-Drift in OpenAPI?
Schema-Drift: Die OpenAPI-Spezifikation weicht vom tatsächlichen API-Verhalten ab. Ein Feld existiert im Code aber nicht im Schema, oder umgekehrt. Clients, die auf Basis der Dokumentation gebaut wurden, erhalten Fehler zur Laufzeit.
2Warum Nelmio statt manueller YAML?
Nelmio generiert die Spezifikation aus dem PHP-Code. Manuelle YAML-Dateien müssen bei jeder Code-Änderung manuell aktualisiert werden – unter Druck geschieht das regelmäßig nicht.
3Symfony-Validierung und Schema synchron halten?
Assert- und OA-Attribute direkt nebeneinander an denselben DTO-Properties setzen. Automatisierte Tests prüfen, ob Constraint-Werte übereinstimmen.
4example vs. examples in OpenAPI?
example ist ein einzelner Inline-Wert. examples ist eine benannte Map wiederverwendbarer Beispielobjekte in der Components-Sektion – in Swagger-UI als auswählbare Varianten dargestellt.
5allOf vs. oneOf – wann was?
allOf: Objekt erfüllt alle Schemas – klassische Vererbung. oneOf: Objekt erfüllt genau eines – polymorphe Typen mit gegenseitig ausschließenden Varianten.
6Was sind Contract-Tests?
Tests, die echte HTTP-Requests senden und den Response automatisch gegen das OpenAPI-Schema validieren. Fangen Schema-Drift ohne manuelle Assertions ab.
7OpenAPI-Spec für CI exportieren?
bin/console nelmio:apidoc:dump --format=json > openapi.json – Datei versionieren, in CI mit neu generierter Version vergleichen, bei unerwarteten Änderungen warnen.
8Schemas in Components statt inline?
Inline duplizierte Schemas müssen an jeder Stelle aktualisiert werden. $ref-Referenzen auf Components-Schemas wirken sich überall aus – eine Änderung, konsistente Dokumentation.
9Mehrere API-Bereiche mit Nelmio?
Ja – über den Area-Mechanismus. Jede Area hat eigene path_patterns, Security-Schemata und Swagger-UI-Route. Öffentliche und interne APIs sauber getrennt.
10Was macht der discriminator?
Teilt Clients mit, anhand welches Felds der konkrete Subtyp bestimmt wird. Code-Generatoren nutzen ihn für korrekte Deserialisierungslogik bei polymorphen Schemas.