{ }
GET
REST API · OpenAPI · RFC 9457 · Business-Errors · Error-Modeling
Error-Payloads und Business-Errors
in OpenAPI richtig modellieren

Fehlermodellierung ist der vernachlässigte Teil des API-Designs. RFC 9457 Problem Details, domänenspezifische Fehlercodes und polymorphe Fehlerschemas machen den Unterschied zwischen einer API, bei der Integratoren raten müssen, was ein Fehler bedeutet, und einer API, bei der Fehler selbsterklärend und maschinell verarbeitbar sind.

20 Min. Lesezeit RFC 9457 · Problem Details · Business-Errors · Validierungsfehler · OpenAPI REST · Symfony · PHP 8.4

1. Warum Fehlermodellierung so oft scheitert

In den meisten REST-APIs wird Fehlermodellierung als Nachgedanke behandelt. Die Happy-Path-Responses sind sorgfältig modelliert, in OpenAPI dokumentiert und mit Beispielen versehen. Die Fehler-Responses sind dagegen ein Mix aus zufällig gewählten HTTP-Statuscodes, unterschiedlichen Payload-Strukturen je nach Endpunkt, und Fehlermeldungen, die offensichtlich für Entwickler im eigenen Team geschrieben wurden, nicht für externe Integratoren. Das Ergebnis: Integratoren können Error-Handling nicht zuverlässig implementieren, weil sie nicht wissen, welche Fehlerstruktur sie erwarten sollen – und ob die Struktur für einen 400er am Endpunkt A dieselbe ist wie am Endpunkt B.

Das konkrete Problem: Eine API gibt bei einem Validierungsfehler {"error": "invalid input"} zurück, bei einem Business-Fehler {"message": "Insufficient stock", "code": 1042}, und bei einem Server-Fehler nichts außer HTTP 500. Ein Client, der korrekt auf alle drei Fehlertypen reagieren soll, muss drei verschiedene Response-Parser implementieren – und hofft, dass es keine weiteren Formate gibt. Die Lösung ist kein kompliziertes Schema-System, sondern ein konsequent angewendeter Standard: RFC 9457 Problem Details for HTTP APIs mit sinnvollen Erweiterungen für domänenspezifische Fehlercodes.

2. RFC 9457 Problem Details: Der Standard für HTTP-Fehler

RFC 9457 (ehemals RFC 7807) definiert einen standardisierten JSON-Response-Body für HTTP-Fehler. Das Problem-Details-Objekt hat fünf definierte Felder: type (URI, die den Fehlertyp identifiziert), title (kurze, menschenlesbare Zusammenfassung), status (HTTP-Statuscode), detail (ausführliche, menschenlesbare Beschreibung des konkreten Fehlers), und instance (URI, die auf die konkrete Probleminstanz verweist, z.B. eine Support-Ticket-ID). Der Content-Type-Header für Problem-Details-Responses ist application/problem+json.

Was RFC 9457 so praktisch macht: Das Objekt ist erweiterbar. Man kann eigene Felder hinzufügen, ohne den Standard zu verletzen. Validierungsfehler können ein violations-Array ergänzen. Business-Errors können einen errorCode und retryable-Flag hinzufügen. Die type-URI muss nicht erreichbar sein (sie dient als eindeutiger Bezeichner), aber es ist eine gute Praxis, sie auf eine Dokumentationsseite zu verlinken, die den Fehlertyp detailliert beschreibt. So kann ein Client-Entwickler, der auf einen unbekannten Fehlertyp trifft, direkt die Dokumentation aufrufen.

// RFC 9457 Problem Details — Basis-Struktur
// Content-Type: application/problem+json

// Einfacher Business-Error (HTTP 409 Conflict)
{
  "type": "https://mironsoft.de/errors/insufficient-stock",
  "title": "Nicht ausreichend Lagerbestand",
  "status": 409,
  "detail": "Artikel PRD-042 hat nur noch 3 Einheiten auf Lager, angefragt wurden 10.",
  "instance": "/api/v2/orders/ORD-2026-0042",
  "errorCode": "INSUFFICIENT_STOCK",
  "sku": "PRD-042",
  "available": 3,
  "requested": 10,
  "retryable": false
}

// Validierungsfehler (HTTP 422 Unprocessable Entity)
{
  "type": "https://mironsoft.de/errors/validation-failed",
  "title": "Validierungsfehler",
  "status": 422,
  "detail": "Die Anfrage enthält 2 Validierungsfehler.",
  "violations": [
    {
      "field": "customerEmail",
      "message": "Muss eine gültige E-Mail-Adresse sein.",
      "rejectedValue": "not-an-email"
    },
    {
      "field": "items[0].quantity",
      "message": "Muss größer als 0 sein.",
      "rejectedValue": 0
    }
  ]
}

3. Business-Errors: Domänenspezifische Fehlercodes richtig designen

Business-Errors sind Fehler, die keine technischen Probleme beschreiben, sondern Verletzungen von Domänenregeln. "Insufficient stock", "Account suspended", "Order already shipped" sind Business-Errors – sie passieren, wenn eine fachlich gültige Anfrage an einer Geschäftsregel scheitert. Diese Fehler unterscheiden sich fundamental von Validierungsfehlern (falsch formatierte Eingaben) und technischen Fehlern (Datenbankausfall). Sie brauchen einen eigenen HTTP-Statuscode (typischerweise 409 Conflict oder 422 Unprocessable Entity) und domänenspezifische Informationen im Response-Body, damit der Client sinnvoll reagieren kann.

Die Wahl der Fehlercode-Benennung ist wichtig für Integratoren. Codes in SCREAMING_SNAKE_CASE (INSUFFICIENT_STOCK, ACCOUNT_SUSPENDED) sind lesbar und sortierbar. Numerische Codes (1042) sind kürzer, aber ohne Lookup nicht verständlich. URIs (https://mironsoft.de/errors/insufficient-stock) sind selbstdokumentierend und können auf eine Dokumentationsseite verlinken. RFC 9457 empfiehlt URIs für den type-Field – man kann zusätzlich einen menschenlesbaren Code in einem eigenen Feld ergänzen. Integratoren, die auf einem Error-Code-String matchen, haben robusteren Code als solche, die auf HTTP-Statuscodes oder Fehlermeldungs-Strings matchen, weil Statuscodes mehrdeutig sein können und Meldungen sich mit Lokalisierung ändern.

4. Validierungsfehler: Feldinformationen und mehrere Violations

Validierungsfehler sind die häufigste Fehlerart in REST-APIs. Was Integratoren brauchen, ist nicht "Validierungsfehler" als Fehlermeldung, sondern präzise Information darüber, welches Feld welchen Wert mit welchem Problem erhalten hat. Das ist besonders wichtig für Formulare und Bulk-Operationen: Der Client muss dem Benutzer oder dem eigenen System mitteilen können, welche konkreten Felder korrigiert werden müssen.

Das empfohlene Muster: Ein violations-Array als RFC-9457-Erweiterung, das pro Eintrag field (JSONPath-Notation für verschachtelte Felder), message (menschenlesbare Fehlerbeschreibung), code (maschinell verarbeitbarer Fehlercode) und optional rejectedValue enthält. In Symfony implementiert man das mit einem eigenen ConstraintViolationNormalizer, der Symfony-Validierungsfehler in dieses Format überführt. Wichtig: Alle Validierungsfehler eines Requests auf einmal zurückgeben, nicht nur den ersten. Ein Client, der seinen Request dreimal korrigieren muss, weil die API immer nur einen Fehler gleichzeitig meldet, ist ineffizient und frustrierend.

<?php
// Symfony ExceptionListener: Business-Errors zu RFC 9457 Problem Details konvertieren
declare(strict_types=1);

namespace App\EventSubscriber;

use App\Exception\BusinessException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Validator\ConstraintViolationListInterface;

/**
 * Converts domain exceptions and validation errors to RFC 9457 Problem Details.
 */
final class ApiExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [KernelEvents::EXCEPTION => ['onKernelException', 10]];
    }

    public function onKernelException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();

        if ($exception instanceof BusinessException) {
            $event->setResponse($this->buildBusinessErrorResponse($exception));
            return;
        }
    }

    private function buildBusinessErrorResponse(BusinessException $ex): JsonResponse
    {
        $payload = [
            'type'      => 'https://mironsoft.de/errors/' . $ex->getErrorSlug(),
            'title'     => $ex->getTitle(),
            'status'    => $ex->getStatusCode(),
            'detail'    => $ex->getMessage(),
            'errorCode' => $ex->getErrorCode(),
            'retryable' => $ex->isRetryable(),
        ];

        // Merge domain-specific context fields (e.g. sku, available, requested)
        $payload = array_merge($payload, $ex->getContext());

        return new JsonResponse($payload, $ex->getStatusCode(), [
            'Content-Type' => 'application/problem+json',
        ]);
    }

    /**
     * Converts Symfony ConstraintViolationList to RFC 9457 + violations extension.
     */
    public function buildValidationErrorResponse(
        ConstraintViolationListInterface $violations,
    ): JsonResponse {
        $violationList = [];

        foreach ($violations as $violation) {
            $violationList[] = [
                'field'         => $violation->getPropertyPath(),
                'message'       => $violation->getMessage(),
                'code'          => $violation->getCode(),
                'rejectedValue' => $violation->getInvalidValue(),
            ];
        }

        return new JsonResponse([
            'type'       => 'https://mironsoft.de/errors/validation-failed',
            'title'      => 'Validierungsfehler',
            'status'     => 422,
            'detail'     => sprintf('Die Anfrage enthält %d Validierungsfehler.', count($violationList)),
            'violations' => $violationList,
        ], 422, ['Content-Type' => 'application/problem+json']);
    }
}

5. Polymorphe Fehlerschemas mit oneOf und discriminator

Eine REST-API hat typischerweise mehrere Fehlertypen mit unterschiedlicher Struktur: Validierungsfehler mit violations-Array, Business-Errors mit domänenspezifischen Feldern, technische Fehler ohne zusätzliche Felder. Wie modelliert man das in OpenAPI, wenn verschiedene Endpunkte verschiedene Fehlerstrukturen zurückgeben können? Die Antwort in OpenAPI 3.1: polymorphe Schemas mit oneOf und discriminator.

Das Basis-Schema ProblemDetail enthält die gemeinsamen RFC-9457-Felder (type, title, status, detail). Davon abgeleitete Schemas (ValidationError, BusinessError, InsufficientStockError) verwenden allOf mit dem Basis-Schema und ergänzen ihre spezifischen Felder. Im Endpunkt-Response-Schema verweist man mit oneOf auf alle möglichen Fehlertypen und definiert einen discriminator auf dem type-Feld, damit OpenAPI-Tools und Code-Generatoren die richtige Implementierung für jeden Fehlertyp auswählen können. Das ermöglicht typsichere Client-Generierung aus dem OpenAPI-Schema.

# OpenAPI 3.1: Polymorphe Fehlerschemas mit oneOf und discriminator
components:
  schemas:
    ProblemDetail:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          example: "https://mironsoft.de/errors/insufficient-stock"
        title:
          type: string
          example: "Nicht ausreichend Lagerbestand"
        status:
          type: integer
          example: 409
        detail:
          type: string
        instance:
          type: string
          format: uri

    ValidationError:
      allOf:
        - $ref: '#/components/schemas/ProblemDetail'
        - type: object
          properties:
            violations:
              type: array
              items:
                type: object
                required: [field, message]
                properties:
                  field:   { type: string }
                  message: { type: string }
                  code:    { type: string }
                  rejectedValue: {}

    InsufficientStockError:
      allOf:
        - $ref: '#/components/schemas/ProblemDetail'
        - type: object
          properties:
            errorCode: { type: string, example: INSUFFICIENT_STOCK }
            sku:       { type: string }
            available: { type: integer }
            requested: { type: integer }
            retryable: { type: boolean, example: false }

  responses:
    OrderCreateErrors:
      description: Fehler beim Erstellen einer Bestellung
      content:
        application/problem+json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/ValidationError'
              - $ref: '#/components/schemas/InsufficientStockError'
            discriminator:
              propertyName: type
              mapping:
                "https://mironsoft.de/errors/validation-failed": '#/components/schemas/ValidationError'
                "https://mironsoft.de/errors/insufficient-stock": '#/components/schemas/InsufficientStockError'

6. Implementierung in Symfony: ExceptionListener und Normalizer

In Symfony baut man die RFC-9457-Fehlerkonvertierung am besten als Event-Subscriber auf dem KernelEvents::EXCEPTION-Event. Für jede Domain-Exception wird eine eigene Exception-Klasse definiert, die von einer abstrakten BusinessException erbt und ihre spezifischen Felder (z.B. sku, available, requested für InsufficientStockException) als Constructor-Parameter hat. Der Subscriber wandelt die Exception in einen JsonResponse mit dem korrekten Content-Type: application/problem+json um.

Für Symfony-Validierungsfehler (Constraint Violations aus dem Validator-Component) implementiert man einen eigenen ConstraintViolationListNormalizer, der das RFC-9457-Violations-Format produziert. Dieser Normalizer wird über den kernel.normalizer-Tag oder direkt im Controller aufgerufen. Mit dem api_platform-Bundle ist vieles bereits vorkonfiguriert – aber die Anpassung auf ein eigenes Fehlerformat mit custom Business-Error-Typen und dem type-URI-Schema erfordert eigene Konfiguration, die nicht immer offensichtlich ist. Die Kernregel: Jede Domain-Exception braucht einen eindeutigen Slug, der in den type-URI eingeht und in der Dokumentation referenziert wird.

7. Fehlerschemas in OpenAPI vollständig dokumentieren

Fehler-Responses in OpenAPI zu dokumentieren bedeutet mehr als nur den Statuscode aufzulisten. Für jeden Fehler-Response müssen das Schema, ein konkretes Beispiel und eine Beschreibung angegeben werden, die erklärt, wann und warum dieser Fehler auftreten kann. Integratoren, die nur den Statuscode sehen, können kein robustes Error-Handling implementieren – sie müssen wissen, welche Felder im Error-Body vorhanden sind.

Für die Praxis empfiehlt sich folgendes Muster: Alle wiederverwendbaren Fehlerschemas als components/schemas definieren. Wiederverwendbare Fehler-Responses als components/responses definieren (z.B. UnauthorizedError, ValidationError). In jedem Endpunkt die spezifischen Fehler-Responses mit $ref referenzieren, und nur die endpunktspezifischen Business-Errors (wie InsufficientStockError) direkt am Endpunkt definieren. So entsteht kein Duplikat-Code im Schema, die Dokumentation ist konsistent, und Code-Generatoren produzieren typsichere Client-Klassen für jeden Fehlertyp.

8. Fehlermodell-Ansätze im Vergleich

Verschiedene Fehlermodellierungs-Ansätze haben unterschiedliche Konsequenzen für die Usability der API, die Entwicklungsgeschwindigkeit von Integratoren und die Wartbarkeit des Schemas.

Ansatz Beispiel Vorteile Nachteile
Kein Standard {"error": "…"} Einfach Inkonsistent, schwer zu parsen
Nur HTTP-Status 422 ohne Body Kein Schema nötig Keine Fehlerdetails für Integratoren
RFC 9457 Basis type, title, status Standard, tooling-kompatibel Braucht Erweiterungen für Business-Errors
RFC 9457 + Extensions +violations, +errorCode Vollständig, typsicher, dokumentierbar Mehr Implementierungsaufwand initial

Der initiale Mehraufwand für RFC-9457-konforme Fehlermodellierung amortisiert sich schnell: Integratoren brauchen weniger Support, Error-Handling-Code ist robuster und wartbarer, OpenAPI-Tools können typsichere Client-Bibliotheken generieren, und das Fehlerschema bleibt konsistent über alle Endpunkte. Teams, die von einem inkonsistenten Fehlerformat auf RFC 9457 umstellen, berichten regelmäßig, dass Integrations-Support-Anfragen zu Fehlerhandling nach der Umstellung signifikant zurückgehen.

9. Zusammenfassung

Fehlermodellierung in REST-APIs ist kein Nachgedanke – sie ist ein zentrales Qualitätsmerkmal, das direkt beeinflusst, wie robust Integratoren Error-Handling implementieren können. RFC 9457 Problem Details liefert das Fundament: type als eindeutiger URI, title als kurze Zusammenfassung, status als HTTP-Statuscode, detail als konkreter Fehlerkontext. Erweiterungen für Validierungsfehler (violations-Array) und Business-Errors (domänenspezifische Felder wie sku, available, retryable) machen das Modell vollständig.

Polymorphe Fehlerschemas mit oneOf und discriminator in OpenAPI ermöglichen typsichere Client-Generierung. Alle Validierungsfehler auf einmal zurückgeben statt iterativ. In Symfony konvertiert ein KernelEvents::EXCEPTION-Subscriber Domain-Exceptions in RFC-9457-konforme Responses mit korrektem Content-Type: application/problem+json. Jeder Business-Error-Typ erhält einen eindeutigen Slug, der in der type-URI und in der Dokumentation referenziert wird – so können Integratoren Fehlertypen stabil und ohne String-Matching auf Fehlermeldungen behandeln.

Error-Payloads und Business-Errors — Das Wichtigste auf einen Blick

RFC 9457 Problem Details

type (URI), title, status, detail als Basis. Content-Type: application/problem+json. Erweiterbar für domänenspezifische Felder. Standard für alle HTTP-Fehler.

Business-Errors

Eigene Exception-Klassen mit Slug, errorCode, retryable-Flag und Kontext-Feldern. HTTP 409 für Conflict, 422 für fachliche Validierung. URI als Fehler-Bezeichner.

Validierungsfehler

violations-Array mit field (JSONPath), message, code, rejectedValue. Alle Violations auf einmal zurückgeben. Symfony ConstraintViolationListNormalizer anpassen.

OpenAPI-Schema

Polymorphe Schemas mit oneOf + discriminator. ProblemDetail als Basis, spezifische Fehlertypen mit allOf. Alle Fehler-Responses vollständig mit Beispielen dokumentieren.

10. FAQ: Error-Payloads und Business-Errors in OpenAPI

1Was ist RFC 9457 Problem Details?
Standardisierter JSON-Response-Body für HTTP-Fehler mit type (URI), title, status, detail, instance. Content-Type: application/problem+json. Erweiterbar für domänenspezifische Felder.
2Business-Error vs. Validierungsfehler?
Validierungsfehler: Eingabe technisch falsch formatiert. Business-Error: Eingabe korrekt, scheitert an Domänenregel (Insufficient Stock, Account Suspended). Beide brauchen eigene Fehlerstrukturen.
3Welcher HTTP-Statuscode für Business-Errors?
409 Conflict für Zustands-Konflikte. 422 Unprocessable Entity für fachliche Validierung. 402 Payment Required für Zahlungsfehler. Statuscode muss fachlichen Sachverhalt widerspiegeln.
4Was ist oneOf mit discriminator?
oneOf erlaubt mehrere mögliche Schema-Varianten. discriminator gibt an, welches Feld (z.B. type) zur Unterscheidung genutzt wird. Ermöglicht typsichere Client-Generierung.
5Warum application/problem+json wichtig?
Signalisiert RFC-9457-Fehler-Dokument. Clients können diesen Content-Type erkennen und ihren Problem-Detail-Parser aktivieren statt normales JSON zu verarbeiten.
6Wie viele Validierungsfehler auf einmal zurückgeben?
Immer alle auf einmal. Iteratives Fehler-Reporting ist ineffizient und frustrierend. violations-Array im RFC-9457-Format ist genau dafür ausgelegt.
7RFC 9457 in Symfony implementieren?
EventSubscriber auf KernelEvents::EXCEPTION. Domain-Exceptions zu JsonResponse mit Content-Type application/problem+json. ConstraintViolationList über eigenen Normalizer in violations-Format.
8Muss die type-URI erreichbar sein?
Nein, dient als eindeutiger Bezeichner. Empfohlen: auf Dokumentationsseite verlinken, die Fehlertyp, Ursachen und empfohlene Reaktionen beschreibt.
9Was bedeutet retryable im Fehler-Payload?
Boolean-Flag: retryable: true bei temporären Fehlern (Rate-Limit). retryable: false bei permanenten Fehlern (Insufficient Stock, Invalid Input). Steuert automatisches Retry-Verhalten im Client.
10Fehlerschemas in OpenAPI vollständig dokumentieren?
ProblemDetail als Basis in components/schemas. Spezifische Typen mit allOf. Wiederverwendbare Responses in components/responses. Konkrete Beispiele. Alle Fehler-Responses je Endpunkt referenzieren.