{ }
GET
REST API · Symfony · RFC 7807 · Validierung
Validation Errors in Symfony APIs
als Problem Details ausgeben

Validierungsfehler, die als rohe HTML-Seite oder als unstrukturiertes JSON-Array ankommen, kosten Frontend-Teams Zeit. RFC 7807 Problem Details definieren ein maschinenlesbares Format, das Symfony mit wenig Aufwand aus der ConstraintViolationList erzeugt – konsistent, dokumentierbar und ohne Framework-Lock-in.

12 Min. Lesezeit RFC 7807 · application/problem+json · ConstraintViolation · API Platform Symfony 6.x / 7.x · PHP 8.2+

1. Das Problem mit unstrukturierten Validierungsfehlern

Eine REST API, die bei einem fehlerhaften Request einfach einen HTTP 400 mit leerem Body oder einem generischen "Validation failed"-String zurückgibt, zwingt jeden Client-Entwickler dazu, selbst herauszufinden, welches Feld konkret falsch ist. Noch schlimmer: Symfony gibt standardmäßig bei nicht behandelten Exceptions eine HTML-Fehlerseite zurück, wenn der Accept-Header nicht korrekt gesetzt ist. Das ergibt Frontend-Tickets wie "API gibt HTML zurück", die eigentlich Backend-Konfigurationsfehler sind.

Das Grundproblem ist fehlende Standardisierung. Jede API erfindet ihr eigenes Fehlerformat: mal ein Array unter dem Key errors, mal ein flaches Objekt mit Feldnamen als Keys, mal ein einzelner message-String. RFC 7807 Problem Details for HTTP APIs schließt diese Lücke: Das Format ist maschinenlesbar, erweiterbar und wird von Bibliotheken in allen gängigen Sprachen unterstützt. Symfony und API Platform implementieren es bereits – man muss nur die richtigen Stellschrauben kennen.

2. RFC 7807 Problem Details – was der Standard vorschreibt

RFC 7807 (inzwischen durch RFC 9457 präzisiert) definiert ein JSON-Format mit dem Content-Type application/problem+json. Das Pflichtminimum ist ein Objekt mit optionalen Standardfeldern: type (URI, die die Fehlerklasse beschreibt), title (menschenlesbare Kurzbeschreibung), status (HTTP-Statuscode als Zahl), detail (konkrete Fehlerbeschreibung für diesen Request) und instance (URI des konkreten Request-Kontexts). Jede dieser Eigenschaften ist optional, aber type und status sind in der Praxis Pflicht.

Für Validierungsfehler erlaubt der Standard eigene Erweiterungsfelder. Das typische Muster in der Symfony-Praxis: ein zusätzliches Feld violations als Array, das jede einzelne ConstraintViolation mit propertyPath, message und optionalem code beschreibt. Diese Struktur ist direkt maschinell auswertbar: Ein React-Formular kann die Violation-Liste iterieren und jeden Fehler dem richtigen Eingabefeld zuordnen, ohne reguläre Ausdrücke auf Fehlertexten auszuführen.


{
  "type": "https://mironsoft.de/errors/validation-error",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The request contains 3 invalid fields.",
  "instance": "/api/orders/create",
  "violations": [
    {
      "propertyPath": "email",
      "message": "This value is not a valid email address.",
      "code": "bd79c0ab-ddba-46cc-a703-a7a4b08de310"
    },
    {
      "propertyPath": "items[0].quantity",
      "message": "This value should be greater than 0.",
      "code": "ea4e51d1-3342-48bd-8f9e-67a7f287f7ee"
    },
    {
      "propertyPath": "shippingAddress.postalCode",
      "message": "This value is not a valid postal code.",
      "code": null
    }
  ]
}

3. Symfony Validator und ConstraintViolationList

Symfony's Validator-Komponente gibt bei fehlgeschlagener Validierung eine ConstraintViolationList zurück. Jedes Element der Liste ist ein ConstraintViolationInterface mit getPropertyPath(), getMessage() und getCode(). Die Aufgabe ist es, diese Liste in das Problem-Details-Format zu übersetzen. Der einfachste Weg in einem Controller: Validierung auslösen, bei Fehlern eine eigene Exception werfen, und diese Exception in einem Kernel-Listener abfangen und als JsonResponse mit dem richtigen Content-Type ausgeben.

Ab Symfony 6.3 gibt es den #[MapRequestPayload]-Attribute, der automatisch deserialisiert und validiert. Ist die Validierung dort fehlgeschlagen, wirft Symfony intern eine HttpException, die man abfangen und ummappen kann. In älteren Versionen übernimmt der validator-Service die Validierung manuell, und der Controller entscheidet selbst, ob eine Exception geworfen wird.


<?php
// src/Exception/ValidationException.php
declare(strict_types=1);

namespace App\Exception;

use Symfony\Component\Validator\ConstraintViolationListInterface;

/**
 * Exception thrown when request payload validation fails.
 * Carries the violation list for structured error serialization.
 */
final class ValidationException extends \RuntimeException
{
    public function __construct(
        private readonly ConstraintViolationListInterface $violations,
        string $message = 'Validation Failed',
        int $code = 422,
    ) {
        parent::__construct($message, $code);
    }

    public function getViolations(): ConstraintViolationListInterface
    {
        return $this->violations;
    }
}

// src/Controller/OrderController.php
declare(strict_types=1);

namespace App\Controller;

use App\Dto\CreateOrderDto;
use App\Exception\ValidationException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Serializer\SerializerInterface;

final class OrderController extends AbstractController
{
    public function __construct(
        private readonly ValidatorInterface $validator,
        private readonly SerializerInterface $serializer,
    ) {}

    #[Route('/api/orders', methods: ['POST'])]
    public function create(Request $request): JsonResponse
    {
        /** @var CreateOrderDto $dto */
        $dto = $this->serializer->deserialize(
            $request->getContent(),
            CreateOrderDto::class,
            'json'
        );

        $violations = $this->validator->validate($dto);
        if (count($violations) > 0) {
            throw new ValidationException($violations);
        }

        // ... process order
        return $this->json(['id' => 'new-order-id'], 201);
    }
}

4. Custom Exception Listener für Problem Details

Der zentrale Baustein für konsistente Problem-Details-Antworten ist ein Kernel-Event-Listener auf das kernel.exception-Event. Dort wird die Exception abgefangen, geprüft ob es sich um eine ValidationException handelt, und die Response wird als application/problem+json zurückgegeben. Dieser Listener ist die einzige Stelle im gesamten Projekt, die weiß, wie Validierungsfehler serialisiert werden – kein Controller muss das selbst machen.

Wichtig: Der Listener muss mit einem höheren Priority-Wert als Symfonys eingebauter ExceptionListener registriert werden, damit er zuerst ausgeführt wird. Für andere Exception-Typen wie AccessDeniedException oder NotFoundHttpException kann der Listener dieselbe Problem-Details-Struktur zurückgeben, nur ohne das violations-Feld.


<?php
// src/EventListener/ProblemDetailsExceptionListener.php
declare(strict_types=1);

namespace App\EventListener;

use App\Exception\ValidationException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\Validator\ConstraintViolationInterface;

/**
 * Converts application exceptions into RFC 7807 Problem Details responses.
 * Registered with high priority to run before Symfony's default exception handling.
 */
final class ProblemDetailsExceptionListener
{
    private const PROBLEM_CONTENT_TYPE = 'application/problem+json';

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

        if (!$exception instanceof ValidationException) {
            return;
        }

        $violations = [];
        /** @var ConstraintViolationInterface $violation */
        foreach ($exception->getViolations() as $violation) {
            $violations[] = [
                'propertyPath' => $violation->getPropertyPath(),
                'message'      => $violation->getMessage(),
                'code'         => $violation->getCode(),
            ];
        }

        $body = [
            'type'       => 'https://mironsoft.de/errors/validation-error',
            'title'      => 'Validation Failed',
            'status'     => Response::HTTP_UNPROCESSABLE_ENTITY,
            'detail'     => sprintf(
                'The request contains %d invalid field(s).',
                count($violations)
            ),
            'instance'   => $event->getRequest()->getRequestUri(),
            'violations' => $violations,
        ];

        $response = new JsonResponse($body, Response::HTTP_UNPROCESSABLE_ENTITY);
        $response->headers->set('Content-Type', self::PROBLEM_CONTENT_TYPE);
        $event->setResponse($response);
    }
}

5. API Platform: Problem Details out of the box

API Platform implementiert RFC 7807 bereits in seiner Kernkomponente. Bei aktiviertem use_symfony_listeners: true (Standard seit API Platform 3) werden Validierungsfehler automatisch als application/problem+json ausgegeben, inklusive violations-Array mit propertyPath und message. Das Format ist kompatibel mit dem Hydra-Vocabulary und wird in der OpenAPI-Dokumentation automatisch referenziert.

Für angepasste Fehlermeldungen lässt sich der ValidationExceptionNormalizer dekorieren. Das eigene Service übernimmt die Normalisierung und kann z.B. übersetzte Meldungen aus dem Symfony Translator einfügen oder interne Constraint-Codes durch öffentlich dokumentierte Error-Codes ersetzen. Das Dekorieren ist gegenüber dem Überschreiben vorzuziehen, weil API-Platform-Updates den eingebauten Normalizer ändern können, ohne den eigenen Code zu brechen.


# config/packages/api_platform.yaml
api_platform:
    title: 'Mironsoft API'
    version: '1.0.0'
    formats:
        json: ['application/json']
        jsonld: ['application/ld+json']
        jsonproblem: ['application/problem+json']
    error_formats:
        jsonproblem: ['application/problem+json']
        jsonld: ['application/ld+json']
    use_symfony_listeners: true
    validator:
        # Map Symfony constraint groups to API Platform operation groups
        query_parameter_validation: true
    exception_to_status:
        # Map domain exceptions to HTTP status codes
        App\Exception\OrderConflictException: 409
        App\Exception\ResourceLockedException: 423

6. Problem Details in OpenAPI dokumentieren

Ein häufiger Fehler in OpenAPI-Dokumentationen ist das Auslassen des Fehlerformats in der Spezifikation. Wenn 422 Unprocessable Entity als Response aufgeführt ist, aber kein Schema für den Body angegeben wird, können Clients keinen typsicheren Code generieren. Das Problem-Details-Schema lässt sich in den components/schemas einmalig definieren und dann per $ref in allen Endpunkten referenzieren.

Besonders wichtig ist der Content-Type application/problem+json als Key des Response-Bodies in der OpenAPI-Spezifikation. Viele API-Definitionen deklarieren zwar ein Schema, aber als application/json – das ist technisch inkorrekt für Problem Details und führt dazu, dass Code-Generatoren den Content-Type falsch setzen.


# openapi/schemas/problem-details.yaml (eingebunden in openapi.yaml)
components:
  schemas:
    ProblemDetails:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          example: "https://mironsoft.de/errors/validation-error"
        title:
          type: string
          example: "Validation Failed"
        status:
          type: integer
          example: 422
        detail:
          type: string
          example: "The request contains 2 invalid fields."
        instance:
          type: string
          format: uri-reference
          example: "/api/orders/create"
        violations:
          type: array
          items:
            $ref: '#/components/schemas/ConstraintViolation'

    ConstraintViolation:
      type: object
      required: [propertyPath, message]
      properties:
        propertyPath:
          type: string
          example: "email"
        message:
          type: string
          example: "This value is not a valid email address."
        code:
          type: string
          nullable: true
          example: "bd79c0ab-ddba-46cc-a703-a7a4b08de310"

  # Usage in endpoint response:
  # responses:
  #   '422':
  #     description: Validation Failed
  #     content:
  #       application/problem+json:
  #         schema:
  #           $ref: '#/components/schemas/ProblemDetails'

7. Vergleich: Fehlerformate im Überblick

Es gibt mehrere verbreitete Ansätze für API-Fehlerformate. Die Wahl hat direkte Auswirkungen auf den Aufwand beim Client-Parsing, die Dokumentierbarkeit in OpenAPI und die Kompatibilität mit Tools wie Postman, Stoplight und API-Client-Generatoren.

Format Content-Type Standardisiert Felder pro Violation Empfehlung
RFC 7807 Problem Details application/problem+json Ja (IETF) propertyPath, message, code Bevorzugt
Symfony Standard (ohne Konfiguration) text/html Nein Nicht für APIs
Google API Design Guide application/json De-facto field, description, reason Akzeptabel
JSON:API Errors application/vnd.api+json Ja (JSON:API) source.pointer, title, detail, code Für JSON:API-Projekte
Eigenes Format application/json Nein Beliebig Vermeiden

Mironsoft

REST API Design, Symfony Backend-Entwicklung und OpenAPI-Dokumentation

Symfony APIs mit sauberer Fehlerbehandlung?

Wir implementieren RFC 7807 Problem Details in bestehenden Symfony-Projekten, dokumentieren das Schema in OpenAPI und schulen Ihr Team in konsistenter API-Fehlerbehandlung.

Audit

Analyse bestehender API-Fehlerformate und Identifikation von Inkonsistenzen

Implementierung

Problem Details Listener, Exception Mapping und OpenAPI-Schema-Integration

Tests

PHPUnit-Tests für alle Fehlerszenarien und Contract-Tests für API-Clients

8. Zusammenfassung

RFC 7807 Problem Details ist der einzige wirklich standardisierte Weg, Validierungsfehler in REST APIs zu kommunizieren. In Symfony setzt man das mit einer ValidationException, die die ConstraintViolationList trägt, und einem Kernel-Event-Listener um, der die Exception in eine application/problem+json-Response umwandelt. Wer API Platform einsetzt, bekommt das Grundformat kostenlos – muss aber trotzdem die OpenAPI-Dokumentation um das violations-Schema ergänzen, damit Code-Generatoren typsichere Client-Klassen erzeugen können.

Der wichtigste Grundsatz: Fehlerformate müssen zentral und konsistent sein. Jeder Endpunkt, der sein eigenes Fehlerformat hat, erhöht den Integrationsaufwand für jeden einzelnen Client. Ein einziger, gut dokumentierter Exception-Listener ist die wartbarste Lösung für das gesamte Projekt.

Problem Details in Symfony — Das Wichtigste auf einen Blick

Content-Type

application/problem+json ist Pflicht – nicht application/json. Tools und Clients erkennen das Format daran.

Violations-Array

Jede ConstraintViolation als Objekt mit propertyPath, message und code – maschinenlesbar für Frontend-Formulare.

Zentraler Listener

Einmal implementieren im kernel.exception-Listener – kein Controller darf Fehlerformate selbst definieren.

OpenAPI-Schema

ProblemDetails und ConstraintViolation als $ref-Komponenten – einmal definieren, überall referenzieren.

9. FAQ: Validation Errors als Problem Details in Symfony

1Was ist RFC 7807 Problem Details?
Ein IETF-Standard für maschinenlesbare JSON-Fehlerantworten. Content-Type: application/problem+json. Felder: type, title, status, detail, instance – plus eigene Erweiterungen wie violations.
2Warum 422 statt 400 für Validierungsfehler?
400 = syntaktisch ungültige Anfrage. 422 = syntaktisch korrekt, aber semantisch ungültig. Validierungsfehler sind semantische Fehler – 422 Unprocessable Entity ist die präzisere Antwort.
3Wie erzwinge ich JSON statt HTML in Symfony?
Im kernel.exception-Listener prüfen ob der Request ein API-Pfad ist und immer JSON zurückgeben – unabhängig vom Accept-Header. Alternativ framework.error_controller überschreiben.
4Gibt API Platform Problem Details automatisch zurück?
Ja, ab API Platform 3 mit use_symfony_listeners: true ist das Standard. Anpassungen über Dekorieren des ValidationExceptionNormalizer.
5Wie dokumentiere ich Problem Details in OpenAPI?
Schema in components/schemas definieren, per $ref in jeder 422-Response referenzieren. Content-Type muss application/problem+json sein, nicht application/json.
6Wie übersetze ich Constraint-Meldungen?
Symfony nutzt den validators.de.yaml-Katalog. Im Normalizer TranslatorInterface injizieren und Messages mit aktuellem Locale übersetzen.
7Kann ich eigene Felder hinzufügen?
Ja, RFC 7807 erlaubt beliebige Erweiterungsfelder. Sinnvoll: traceId, timestamp, documentationUrl.
8Unterschied type vs. instance in Problem Details?
type beschreibt die Fehlerklasse (stabile URI, gleich für alle Requests dieses Typs). instance beschreibt diesen konkreten Request-Kontext (z.B. der Request-Pfad).
9Wie teste ich Problem Details in PHPUnit?
Mit WebTestCase: Response-Status 422 prüfen, Content-Type auf application/problem+json testen, violations-Array auf korrekte propertyPath-Werte assertieren.
10Problem Details für alle Exception-Typen?
Ja – ein zentraler Listener mappt alle Exception-Typen: 404 für NotFound, 403 für AccessDenied, 409 für Konfliktfehler. Konsistenz über die gesamte API.