{ }
type
GraphQL · Input Validation · Custom Scalars · Fehlertypen
GraphQL Input Validation
sauber modellieren

Validierung in GraphQL ist kein einzelner Mechanismus, sondern ein Zusammenspiel aus Schema-Typen, Custom Scalars, Resolver-Logik und expliziten Fehlertypen. Wer nur eine Ebene nutzt, hat eine lückenhafte Validierung. Dieser Artikel zeigt, wie alle Ebenen zusammenspielen und welche Fehler dabei typischerweise entstehen.

16 Min. Lesezeit Input-Typen · Custom Scalars · UserErrors · Non-Null GraphQL · Magento · Schema Design

1. Die drei Validierungsebenen in GraphQL

GraphQL-Validierung findet auf drei voneinander unabhängigen Ebenen statt. Die erste Ebene ist die Schema-Validation: Der GraphQL-Server prüft vor jeder Execution, ob die Query syntaktisch korrekt ist und ob alle angeforderten Felder und Typen im Schema existieren. Diese Ebene lehnt Requests mit falschem Typ oder fehlendem Pflichtfeld automatisch ab – noch bevor ein Resolver aufgerufen wird. Die zweite Ebene sind Custom Scalars: Spezialisierte Scalar-Typen können eigene Serialisierungs- und Validierungslogik mitbringen. Ein Email-Scalar akzeptiert nur Strings in E-Mail-Format, ein PositiveInt-Scalar nur positive Ganzzahlen.

Die dritte Ebene ist die Resolver-seitige Validierung: Geschäftsregeln, die das Schema allein nicht ausdrücken kann, werden im Service-Layer validiert und als explizite Fehlertypen im Response zurückgegeben. Dazu gehören: Prüfen ob eine E-Mail-Adresse bereits vergeben ist, ob eine Menge die verfügbare Lagermenge überschreitet oder ob ein Rabattcode gültig und nicht abgelaufen ist. Diese drei Ebenen ergänzen sich – nur wenn alle drei bewusst geplant sind, entsteht eine vollständige Validierungsstrategie.

2. Input-Typen: die Basis sauberer Validierung

In GraphQL werden Eingabedaten für Mutations über input-Typen übergeben. Diese sind separat von normalen Object-Typen und dürfen keine Felder enthalten, die auf anderen Output-Typen referenzieren. Das erzwingt eine saubere Trennung zwischen Eingabedaten und Ausgabedaten. Ein Input-Typ definiert explizit, welche Felder eine Mutation erwartet, welche Typen sie haben und ob sie Pflichtfelder sind. Das Schema selbst übernimmt damit die erste Validierungsschicht.

Häufiger Fehler: zu breite Input-Typen, die für mehrere Mutations geteilt werden. Ein Input-Typ wie AddressInput der für Liefer- und Rechnungsadresse bei der Bestellung und für die Adressverwaltung im Kundenprofil verwendet wird, wächst mit unterschiedlichen Anforderungen und wird schwer wartbar. Besser ist, für jeden Kontext einen eigenen, schmalen Input-Typ zu definieren: PlaceOrderAddressInput mit genau den Feldern, die beim Checkout benötigt werden. Refactoring eines Input-Typs, der nur intern von einer Mutation verwendet wird, ist kein Breaking Change. Refactoring eines geteilten Input-Typs dagegen sehr wohl.


# Well-modeled input types: specific per use-case, not generic
# Each input type has exactly the fields needed for its context

input RegisterCustomerInput {
  firstname: String!
  lastname: String!
  email: String!         # validated as Email scalar server-side
  password: String!      # validated for strength in resolver
  dateOfBirth: String    # optional, ISO date string
  acceptsMarketing: Boolean! = false
}

input UpdateCustomerProfileInput {
  firstname: String
  lastname: String
  dateOfBirth: String
  # email change is a separate mutation with re-verification
  # password change is a separate mutation with current password check
}

input ChangeCustomerEmailInput {
  newEmail: String!
  currentPassword: String!  # re-authentication required
}

input ChangeCustomerPasswordInput {
  currentPassword: String!
  newPassword: String!
}

# Wrong pattern to avoid — one generic input for everything
# input CustomerInput {
#   firstname: String
#   lastname: String
#   email: String
#   password: String
#   currentPassword: String  # context-dependent, confusing
# }

3. Non-Null und Required: Pflichtfelder im Schema

Das Ausrufezeichen ! in GraphQL markiert ein Feld oder einen Typ als Non-Null – es darf kein null zurückgegeben oder übergeben werden. Für Input-Felder bedeutet !, dass das Feld in der Client-Anfrage vorhanden sein muss. Fehlt ein Non-Null-Feld, schlägt die Schema-Validation fehl, bevor ein Resolver ausgeführt wird. Das ist die effektivste Form der Pflichtfeld-Validierung: sie ist automatisch, benötigt keinen Resolver-Code und liefert eine klare Fehlermeldung.

Ein häufiger Fehler beim Schema-Design: alles nullable lassen, weil GraphQL standardmäßig nullable ist. Das verschiebt die Validierung vollständig in den Resolver und macht das Schema informationsärmer. Der Client weiß nicht, welche Felder wirklich benötigt werden. Ein guter Ansatz: Felder, die für eine Operation immer benötigt werden, als Non-Null markieren. Optionale Felder, die weggelassen werden können, bleiben nullable. Das Schema kommuniziert damit explizit, welche Daten der Server garantiert erwartet.

4. Custom Scalars: domänenspezifische Typen

GraphQL bietet fünf eingebaute Scalars: String, Int, Float, Boolean und ID. Für domänenspezifische Einschränkungen reichen diese oft nicht aus. Custom Scalars erlauben die Definition eigener Typen mit Serialisierungs- und Validierungslogik. Ein Email-Scalar prüft das E-Mail-Format, ein URL-Scalar die URL-Syntax, ein PositiveInt-Scalar lehnt negative Zahlen und null ab. Diese Validierung findet vor dem Resolver statt und liefert automatisch typsichere Fehlermeldungen.

Bibliotheken wie graphql-scalars (Node.js) oder webonyx/graphql-php stellen fertige Custom-Scalar-Implementierungen bereit. In Magento können Custom Scalars im eigenen GraphQL-Modul registriert werden. Wichtig: Custom Scalars verbessern die Schematypisierung, ersetzen aber keine fachliche Resolver-Validierung. Ein Email-Scalar prüft das Format, nicht die Existenz der Adresse. Ein PositiveInt-Scalar prüft den Wert, nicht ob die Menge in den Lagerbestand passt. Beide Ebenen sind notwendig.


# Custom scalars for domain-specific validation — validated before resolver execution
scalar Email      # valid email format: user@domain.tld
scalar URL        # valid URL with scheme
scalar PositiveInt # integer > 0
scalar ISODate    # ISO 8601 date string: 2026-05-09

# Using custom scalars in input types
input CreateProductReviewInput {
  sku: String!
  nickname: String!       # 2-50 characters (validated in resolver)
  summary: String!        # 5-255 characters (validated in resolver)
  text: String!           # 20-5000 characters (validated in resolver)
  ratings: [RatingInput!]!
}

input RatingInput {
  id: String!
  value_id: String!
}

# Result type with UserErrors for domain validation failures
type CreateProductReviewResult {
  review: ProductReview
  userErrors: [ValidationError!]!
}

type ValidationError {
  field: String!
  code: ValidationErrorCode!
  message: String!
}

enum ValidationErrorCode {
  REQUIRED
  TOO_SHORT
  TOO_LONG
  INVALID_FORMAT
  ALREADY_REVIEWED
  PRODUCT_NOT_FOUND
}

5. Fehlertypen: UserErrors vs. Top-Level-Errors

GraphQL-Fehler können auf zwei Weisen zurückgegeben werden. Top-Level-Errors erscheinen im errors-Array neben data und sind für Transport- und Systemfehler vorgesehen: Netzwerkfehler, Authentication-Fehler, ungültige Queries. Sie haben keinen stabilen Code, mit dem der Client programmatisch umgehen kann. UserErrors sind fachliche Validierungsfehler, die als expliziter Typ im Rückgabewert einer Mutation modelliert werden. Sie erscheinen im data-Teil des Response und haben einen strukturierten Code und eine Meldung.

Das wichtigste Design-Prinzip: Fachliche Fehler niemals als Top-Level-Errors zurückgeben. Ein Passwort, das die Anforderungen nicht erfüllt, ist kein System- sondern ein User-Fehler. Er gehört in ein userErrors-Array im Result-Typ. Top-Level-Errors werden vom Apollo Client automatisch in eine Error-State umgewandelt und landen oft nur im Fehlerlog. UserErrors im Result-Typ werden von der Komponente ausgelesen und dem Nutzer angezeigt. Das macht den Unterschied zwischen einer Mutation, die bei Validierungsfehlern sinnvoll reagiert, und einer, die kommentarlos scheitert.

6. Resolver-seitige Validierung: was das Schema nicht kann

Schema-Validierung und Custom Scalars prüfen Typ und Format. Fachliche Geschäftsregeln – ob ein Benutzername bereits vergeben ist, ob eine Quantity die Lagermenge überschreitet, ob ein Gutscheincode für den aktuellen Warenkorb gilt – müssen im Service-Layer validiert werden. Der Resolver delegiert diese Prüfungen an einen Validation-Service, der alle relevanten Geschäftsregeln kennt und strukturierte Fehler zurückgibt. Der Resolver mappt diese auf das UserErrors-Format und gibt sie im Result-Typ zurück.

Ein Anti-Pattern: Validierungslogik direkt im Resolver implementieren. Das macht Resolver schwer testbar, schwer wiederverwendbar und führt dazu, dass ähnliche Validierungen in verschiedenen Mutations redundant implementiert werden. Ein Validation-Service, der isoliert testbar ist, eine klare Schnittstelle hat und für verschiedene Mutations wiederverwendet werden kann, ist die wartbarere Alternative. In PHP-basierten Systemen wie Magento eignen sich Validator-Klassen, die über Dependency Injection in Services und Resolver eingebunden werden.

7. Vergleich: schlechte vs. gute Validierungsmodelle

Die Qualität des Validierungsmodells zeigt sich besonders an der Art, wie Fehler zurückgegeben werden und wie viel der Client mit ihnen anfangen kann. Ein schlechtes Validierungsmodell wirft Top-Level-Exceptions, gibt nur Boolean-Rückgaben oder erzwingt den Client, aus dem HTTP-Statuscode auf den Fehler zu schließen. Ein gutes Modell gibt strukturierte, codierte UserErrors zurück, die der Client direkt einem Formularfeld zuordnen kann.

Aspekt Problematisch Empfohlen Warum besser
Pflichtfelder Alle nullable, im Resolver prüfen Non-Null ! im Schema Automatisch, kein Resolver-Code nötig
E-Mail-Format email: String! + Regex im Resolver email: Email! (Custom Scalar) Validierung vor Resolver-Aufruf
Fachfehler Exception → Top-Level-Error UserErrors im Result-Typ Client kann Fehler Feldern zuordnen
Rückgabetyp mutation: Boolean! mutation: OperationResult! Daten und Fehler gemeinsam zurückgeben
Input-Typen Generischer geteilter Input Kontext-spezifischer Input Kein Breaking-Change-Risiko

Ein vollständiges Validierungsmodell kombiniert alle fünf Empfehlungen: Non-Null für Pflichtfelder, Custom Scalars für Formatprüfungen, kontext-spezifische Input-Typen für Mutations, strukturierte UserErrors im Result-Typ und Resolver, die an isoliert testbare Validation-Services delegieren. Kein einzelner dieser Punkte allein reicht aus – alle zusammen ergeben eine robuste Validierungsstrategie.

8. Input Validation im Magento-Kontext

Magento implementiert Validierung in seinem GraphQL-Layer auf mehreren Ebenen. Im Schema sind Input-Typen für alle Mutations definiert, zum Beispiel SetShippingAddressesOnCartInput oder AddProductsToCartInput. Non-Null-Felder markieren Pflichtfelder. Die eigentliche Geschäftsvalidierung – ob eine Adresse vollständig genug für eine Lieferung ist, ob ein Produkt in der gewünschten Menge verfügbar ist – findet in den zugehörigen Service-Klassen statt und wird als Fehler im UserErrors-Format zurückgegeben.

Wer eigene Mutations in Magento-Modulen baut, sollte sich an diesem Muster orientieren. Input-Typen in schema.graphqls definieren, Resolver-Klassen, die nur delegieren, und separate Validator-Klassen, die die Geschäftsregeln implementieren. Magento stellt dafür die Validator-Pattern aus dem Service-Contract-Bereich bereit. Fehler werden als GraphQlInputException oder GraphQlAuthorizationException geworfen, je nach Fehlertyp, und vom GraphQL-Layer automatisch in das richtige Format übersetzt.


# Complete mutation with multi-level validation
mutation PlaceOrder($input: PlaceOrderInput!) {
  placeOrder(input: $input) {
    order {
      order_number
      status
    }
    userErrors {
      code
      message
      field
    }
  }
}

# Example response with UserErrors (HTTP 200 — not an exception)
# {
#   "data": {
#     "placeOrder": {
#       "order": null,
#       "userErrors": [
#         {
#           "code": "OUT_OF_STOCK",
#           "message": "Produkt 'DEMO-001' ist nicht mehr in der gewünschten Menge verfügbar.",
#           "field": "cartItems[0].quantity"
#         },
#         {
#           "code": "INVALID_ADDRESS",
#           "message": "Postleitzahl ungültig für das gewählte Land.",
#           "field": "shippingAddress.postcode"
#         }
#       ]
#     }
#   }
# }

9. Validierungslogik testen

Validierungslogik ist gut testbar, wenn sie in isolierten Service-Klassen liegt. Für den Validation-Service kann man Unit-Tests schreiben, die verschiedene Input-Kombinationen testen: gültige Daten, fehlende Pflichtfelder, falsche Formate, verletzte Geschäftsregeln. Diese Tests laufen ohne HTTP-Layer und GraphQL-Execution – sie sind schnell und diagnostizieren Probleme präzise. Integration-Tests auf Resolver-Ebene können dann sicherstellen, dass Validation-Fehler korrekt ins UserErrors-Format übersetzt und im GraphQL-Response zurückgegeben werden.

Für Custom Scalars empfiehlt sich eine eigene Test-Suite, die die Grenzen des Scalars prüft: gültige Werte, Grenzwerte und ungültige Werte. Ein PositiveInt-Scalar sollte 0, negative Zahlen und Nicht-Integer-Werte ablehnen. Schema-Level-Tests mit Tools wie graphql-tester oder einfachen HTTP-Requests können prüfen, ob das Schema Pflichtfeld-Verletzungen korrekt meldet. Eine vollständige Teststrategie für Validation deckt alle drei Ebenen ab – Schema, Scalar und Service-Layer – und stellt sicher, dass keine Validierungslücken entstehen.

10. Zusammenfassung

GraphQL Input Validation ist kein einzelnes Feature, sondern eine mehrschichtige Strategie. Non-Null-Felder im Schema validieren Pflichtfelder automatisch. Custom Scalars validieren Formate vor der Resolver-Ausführung. Isolierte Validation-Services implementieren Geschäftsregeln und werden durch Resolver aufgerufen. Strukturierte UserErrors im Result-Typ geben fachliche Fehler zurück, die der Client sinnvoll verarbeiten kann. Top-Level-Errors bleiben für System- und Transportfehler reserviert.

Im Magento-Kontext bedeutet das: Input-Typen in schema.graphqls sorgfältig designen, Validator-Klassen für Geschäftsregeln erstellen, Exceptions je nach Fehlertyp korrekt werfen, und das UserErrors-Muster für alle Custom-Mutations übernehmen. Wer diese Schichten sauber trennt, baut GraphQL-APIs, die robuster validieren, besser testbar sind und dem Client genug Information geben, um Fehler sinnvoll zu behandeln.

GraphQL Input Validation — Das Wichtigste auf einen Blick

Schema-Ebene

Non-Null für Pflichtfelder. Kontext-spezifische Input-Typen statt generische. Non-Null schlägt fehl, bevor ein Resolver aufgerufen wird.

Custom Scalars

Email, URL, PositiveInt für domänenspezifische Formatprüfungen. Validierung vor Resolver-Aufruf, ohne Resolver-Code.

UserErrors

Fachfehler im Result-Typ als strukturiertes Array mit Code, Meldung und Feldpfad. Niemals als Top-Level-Errors.

Service-Layer

Validation-Services für Geschäftsregeln: isoliert testbar, wiederverwendbar, unabhängig vom GraphQL-Layer.

11. FAQ: GraphQL Input Validation

1UserErrors vs. Top-Level-Errors?
Top-Level-Errors: System- und Transportfehler. UserErrors: fachliche Validierungsfehler im Result-Typ, die der Client programmatisch verarbeiten kann.
2Was sind Custom Scalars?
Benutzerdefinierte Typen mit eigener Validierungslogik: Email, URL, PositiveInt. Validierung findet vor dem Resolver-Aufruf statt.
3Warum kontext-spezifische Input-Typen?
Geteilte Input-Typen wachsen und werden schwer wartbar. Kontext-spezifische Typen können unabhängig weiterentwickelt werden ohne Breaking Changes.
4Was prüft das Schema automatisch?
Typen, Non-Null-Pflichtfelder, Enum-Werte und Feld-Existenz – alles vor der Resolver-Ausführung, ohne Resolver-Code.
5Was kann das Schema nicht validieren?
Eindeutigkeit, Verfügbarkeit und Geschäftsregeln. Diese Prüfungen gehören in isolierte Validation-Services im Service-Layer.
6Non-Null für alle Felder setzen?
Nur für Felder, die wirklich immer benötigt werden. Non-Null kommuniziert explizit die Erwartungen und spart Resolver-Code für Pflichtfeld-Checks.
7Wie teste ich Validierungslogik?
Unit-Tests für Validation-Services, Schema-Tests für Pflichtfeld-Verletzungen, Integration-Tests für den UserErrors-Pfad – alle drei Ebenen separat.
8Was ist das UserErrors-Muster?
Fachliche Fehler als explizites Array im Mutation-Rückgabetyp mit Code, Meldung und Feldpfad – nicht als Exception geworfen.
9Custom Scalars in Magento implementieren?
In schema.graphqls deklarieren, via di.xml mit einer Resolver-Klasse verbinden, die serialize/parseValue/parseLiteral implementiert.
10Wann als Exception werfen statt UserError?
Bei System-Fehlern (DB nicht erreichbar) und Sicherheits-Fehlern (keine Berechtigung). Fachliche Validierungsfehler gehören immer als UserErrors in den Result-Typ.