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.
Inhaltsverzeichnis
- 1. Das Problem mit unstrukturierten Validierungsfehlern
- 2. RFC 7807 Problem Details – was der Standard vorschreibt
- 3. Symfony Validator und ConstraintViolationList
- 4. Custom Exception Listener für Problem Details
- 5. API Platform: Problem Details out of the box
- 6. Problem Details in OpenAPI dokumentieren
- 7. Vergleich: Fehlerformate im Überblick
- 8. Zusammenfassung
- 9. FAQ
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?
application/problem+json. Felder: type, title, status, detail, instance – plus eigene Erweiterungen wie violations.2Warum 422 statt 400 für Validierungsfehler?
3Wie erzwinge ich JSON statt HTML in Symfony?
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?
use_symfony_listeners: true ist das Standard. Anpassungen über Dekorieren des ValidationExceptionNormalizer.5Wie dokumentiere ich Problem Details in OpenAPI?
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?
validators.de.yaml-Katalog. Im Normalizer TranslatorInterface injizieren und Messages mit aktuellem Locale übersetzen.7Kann ich eigene Felder hinzufügen?
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?
WebTestCase: Response-Status 422 prüfen, Content-Type auf application/problem+json testen, violations-Array auf korrekte propertyPath-Werte assertieren.