Wenn JSON-Schema-Fehler unverständlich sind: Ajv-Fehlerlokalisierung und Meldungsgovernance
Fazit: Ein Validator gibt Maschinenfehler aus; Nutzer wollen wissen, welches Eingabefeld falsch ist. Behandelt das Ajv-errors-Array, instancePath versus schemaPath, Gruppierung und Formulierung.

Kurz gesagt: Lesen Sie zuerst instancePath, um die scheiternden Daten zu lokalisieren, dann schemaPath, um die ausgelöste Regel zu lokalisieren; bei required und additionalProperties ziehen Sie den echten Schlüsselnamen aus params, bevor Sie die Meldung wählen.
Die Aufgabe des Validators ist, zu sagen, welche Regel gescheitert ist. Das in „dieses Eingabefeld ist falsch“ zu übersetzen, ist Ihre Aufgabe.
1. Was das errors-Array tatsächlich liefert
Ajv wirft bei einem Validierungsfehler nicht; es hinterlässt das Ergebnis in validate.errors. Die Standardkonfiguration behält nur den ersten Fehler, und allErrors ist es, was eine vollständige Liste erzeugt. Ein einzelnes Fehlerobjekt hat fünf Felder, die zählen, und sie zu verstehen ist die Voraussetzung für alles Weitere:
| Feld | Bedeutung | Beispiel |
|---|---|---|
instancePath |
JSON Pointer auf den scheiternden Wert | /email |
schemaPath |
Position des scheiternden Schlüsselworts im Schema | #/properties/email/format |
keyword |
Name der verletzten Beschränkung | format |
params |
Maschinenlesbare Parameter dieser Beschränkung | { "format": "email" } |
message |
Englische Prosa, die sich zwischen Versionen ändert | must match format "email" |
Ein echter Fehler sieht so aus:
[
{
"instancePath": "/email",
"schemaPath": "#/properties/email/format",
"keyword": "format",
"params": { "format": "email" },
"message": "must match format \"email\""
}
]
Beachten Sie, dass message für Menschen geschriebene Prosa ist, keine stabile Schnittstelle. Ajv formuliert sie zwischen Versionen womöglich um, und sie kann kein lokalisiertes Produkt bedienen. Niemals mit einem regulären Ausdruck parsen und niemals wörtlich an einen Client zurückgeben.
2. instancePath und schemaPath beantworten verschiedene Fragen
instancePath sagt, welcher Teil der Daten gescheitert ist; schemaPath sagt, welche Regel ausgelöst wurde. Es sind zwei Koordinatensysteme, und sie sind nicht symmetrisch:
instancePathist ein JSON Pointer nach RFC 6901. Ein leerer String bedeutet die Wurzel;/items/0/pricebedeutet das Preis-Feld des ersten Array-Elements.schemaPathbeginnt mit#und zeigt auf das Schlüsselwort innerhalb des Schemadokuments. Über einen$refhinweg zeigt es in das referenzierte Schema, Sie brauchen also die$id, um zu wissen, aus welcher Datei es stammt.- Ein Schlüssel mit
/oder~wird zu~1bzw.~0escaped. Machen Sie das vor der Anzeige rückgängig, sonst zeigt die UI rätselhafte Symbole.
Beispielsweise bedeutet instancePath: "/email" zusammen mit schemaPath: "#/properties/email/format", dass das E-Mail-Feld des Wurzelobjekts die Formatprüfung nicht bestanden hat.
Die häufigste Falle sind required und additionalProperties: Ihr instancePath zeigt auf das Elternobjekt, nicht auf den betreffenden Schlüssel.
| Schlüsselwort | instancePath zeigt auf |
Wo der echte Schlüssel lebt |
|---|---|---|
required |
Das Elternobjekt (leerer String an der Wurzel) | params.missingProperty |
additionalProperties |
Das Elternobjekt | params.additionalProperty |
type |
Den scheiternden Wert selbst | — |
enum |
Den scheiternden Wert selbst | params.allowedValues |
Behandeln Sie diese beiden Fälle zuerst, wenn Sie Fehler auf Formularfelder abbilden. Sonst wird aus „email fehlt“ ein „Fehler im Wurzelobjekt“, und der Nutzer weiß nicht, wo er tippen soll.
3. allErrors und verbose: Vollständigkeit versus Kosten
Der Standard allErrors: false lässt Ajv beim ersten Fehler stoppen. Das ist für ein Kommandozeilenwerkzeug in Ordnung und für ein Formular verheerend: Der Nutzer korrigiert ein Feld, sendet ab, sieht den nächsten Fehler und wiederholt das dreimal, bis das Formular besteht.
| Einstellung | Verhalten | Nutzen | Kosten |
|---|---|---|---|
allErrors: false |
Nur der erste Fehler | Am schnellsten, kleinster Speicher | Wiederholte Übermittlungen |
allErrors: true |
Jeden Fehler sammeln | Alle Probleme in einem Durchlauf | Fehlerzahl explodiert bei komplexen Schemas |
verbose: true |
schema und data an jeden Fehler hängen |
Leichtere serverseitige Diagnose | Stark erhöhte Größe; niemals exponieren |
messages: false |
Das message-Feld weglassen |
Kleinere Payloads | Der englische Debug-Hinweis geht verloren |
Empfohlene Aufteilung: allErrors: true für die Servervalidierung aktivieren und das volle Array in internen Logs behalten, während die öffentliche Antwort nur Whitelist-Felder trägt. Schalten Sie verbose nur bei der Untersuchung ein; es bettet das passende Schemafragment und die Nutzerdaten in jeden Fehler ein, was Speicher kostet und sensible Werte in Logs leiten kann.
Fehlervolumen ist das andere häufige Problem. Ein gescheiterter anyOf-Zweig erzeugt mehrere Kindfehler plus einen Zusammenfassungsfehler, und oneOf mit vielen Zweigen ist schlimmer. Zeigen Sie das Array unverändert, und Nutzer sehen fünf fast identische Meldungen für ein Feld. Die Gruppierung muss vor dem Rendern geschehen.
4. Fehler nach Feld gruppieren
Gruppieren tut drei Dinge: den Pfad zurück in einen Feldnamen verwandeln, den Versatz von required und additionalProperties korrigieren und mehrere Fehler zum selben Feld zur einzigen umsetzbarsten Meldung zusammenführen.
/** One validation error ready for display */
interface FieldError {
field: string;
keyword: string;
params: Record<string, unknown>;
}
/**
* Group Ajv errors by field.
* @param errors Error array produced by Ajv
* @returns Map keyed by field path, each value listing its errors
*/
function groupByField(errors: Array<Record<string, any>>): Record<string, FieldError[]> {
const grouped: Record<string, FieldError[]> = {};
for (const errorItem of errors) {
const keyword: string = errorItem.keyword;
let field: string = errorItem.instancePath;
if (keyword === 'required') {
const parentPath: string = errorItem.instancePath;
field = `${parentPath}/${errorItem.params.missingProperty}`;
} else if (keyword === 'additionalProperties') {
field = `${errorItem.instancePath}/${errorItem.params.additionalProperty}`;
}
const normalizedField = field === '' ? '(root)' : field;
(grouped[normalizedField] ??= []).push({
field: normalizedField,
keyword,
params: errorItem.params ?? {},
});
}
return grouped;
}
Mit einer Prioritätsreihenfolge zusammenführen: zuerst required (fehlend), dann type, dann enum/const, dann format/pattern, dann Länge und Bereich. Zeigen Sie pro Feld nur die Meldung mit der höchsten Priorität und behalten Sie den Rest in Logs. Der Grund ist direkt: Ist der Typ falsch, ist eine Längenmeldung Rauschen, und den Typ zu korrigieren ist der einzig sinnvolle nächste Schritt.
Zusammenfassungsfehler für oneOf und anyOf tragen üblicherweise keine umsetzbare Information. Überspringen Sie sie beim Rendern und behalten Sie nur die Zweigfehler, die auf ein konkretes Feld zeigen.
5. Eigene Meldungen: der Plugin-Weg und der Template-Weg
Ajv bringt keine lokalisierte Formulierung mit, es gibt also zwei praktische Optionen.
Option eins: das ajv-errors-Plugin. Es fügt ein errorMessage-Schlüsselwort hinzu, sodass Meldungen neben den Beschränkungen leben:
{
"properties": {
"email": {
"type": "string",
"format": "email",
"errorMessage": {
"format": "Please enter a valid email address",
"type": "Email must be a string"
}
}
}
}
Der Vorteil ist, dass die Formulierung mit dem Feld reist und das Schema zugleich als Dokumentation dient. Der Nachteil ist, dass der Text auf eine Sprache festgelegt ist, Lokalisierung also bedeutet, mehrere Schemas zu pflegen. Das Plugin ersetzt außerdem nur dann jede Meldung, wenn allErrors: true gesetzt ist; vergessen Sie das Flag, und Sie bekommen eine Mischung aus eigenen und Standard-Englisch.
Option zwei: eine Keyword-zu-Template-Tabelle. Der Validator liefert nur keyword und params; die Anwendung erzeugt die Formulierung:
| Schlüsselwort | Template | Verfügbare Parameter |
|---|---|---|
required |
Pflichtfeld fehlt: {field} | missingProperty |
type |
Erwarteter Typ: {type} | type |
minLength |
Geben Sie mindestens {limit} Zeichen ein | limit |
maximum |
Darf {limit} nicht übersteigen | limit |
format |
Ungültiges Format: {format} | format |
enum |
Muss eine der erlaubten Optionen sein | allowedValues |
pattern |
Wert entspricht nicht dem erwarteten Format | pattern (nicht wiederholen) |
Option zwei passt zu Produkten, die mehrere Sprachen und eine konsistente Stimme brauchen. Sie trennt Validierung von Darstellung, hält Übersetzung aus dem Schema heraus und lässt das Frontend dieselben Templates wiederverwenden.
6. Lokalisierung: Meldungen gehören nicht zum Validator
Die Formulierung in Sprachpakete zu verschieben ist der Ausgangspunkt der Meldungsgovernance. Ein Sprachpaket speichert Templates und Schlüssel, nie Feldnamen; Feldbeschriftungen kommen aus der Formularkonfiguration oder dem API-Vertrag, und beide werden zur Renderzeit kombiniert.
/** Validation message templates keyed by Ajv keyword */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
required: (params) => `Missing required field: ${params.missingProperty}`,
type: (params) => `Expected type: ${params.type}`,
minLength: (params) => `Enter at least ${params.limit} characters`,
maximum: (params) => `Must not exceed ${params.limit}`,
format: () => 'Invalid format',
};
/**
* Build one user-facing validation message.
* @param keyword Name of the failed constraint
* @param params Constraint parameters
* @returns Localized message text
*/
function buildMessage(keyword: string, params: Record<string, any>): string {
const template = VALIDATION_TEMPLATES[keyword];
return template ? template(params) : 'The value is invalid';
}
Drei Regeln zählen hier:
- Keine Sätze aneinanderhängen. „Email format is invalid“ und seine Übersetzung teilen nicht dieselbe Wortstellung, und String-Zusammensetzung wird in einer davon irgendwann Unsinn erzeugen.
- Plurale und Zählwörter an die i18n-Bibliothek delegieren. Niemals „characters“ im Template fest verdrahten;
1 characterund3 charactersunterscheiden sich. - Auch Feldbeschriftungen übersetzen.
params.missingPropertyistemail, während die UI „E-Mail-Adresse“ sagen sollte. Behalten Sie diese Abbildung in der Formularkonfiguration, nicht im Sprachpaket.
7. Interne Struktur nicht an Clients zurückgeben
schemaPath, params und message gemeinsam zurückzugeben ist während der Entwicklung bequem und in der Produktion ein Informationsleck. allowedValues verrät interne Enums, pattern verrät Validierungs-Regexe, und schemaPath verrät Dateilayout und Feldbenennung – in manchen Fällen liefert es Angreifern eine Karte Ihrer Schwachstellen.
Die öffentliche Antwort behält genau drei Dinge: einen Feldpfad (oder Formularfeldnamen), einen stabilen Fehlercode und nutzerfreundlichen Text. Verwenden Sie kurze stabile Codes statt des rohen keyword, damit Sie sich Spielraum für einen späteren Validatorwechsel bewahren.
{
"type": "about:blank",
"title": "Request validation failed",
"status": 422,
"errors": [
{ "field": "email", "code": "invalid_format", "message": "Please enter a valid email address" },
{ "field": "items[0].price", "code": "invalid_pattern", "message": "Amount must have exactly two decimals" }
]
}
Server-Logs sollten das Gegenteil enthalten: das vollständige Fehlerarray, die Request-id und die Schema-Version, damit eine Untersuchung beantworten kann, „welche Regel das blockiert hat“. Vermeiden Sie es, ganze Request-Bodies in Logs zu schütten; behalten Sie stattdessen die scheiternden Pfade und Schlüsselwörter.
8. Frontend-Darstellung: von der Feldhervorhebung zu Editor-Markern
Fehler landen irgendwann in einer UI, und die Darstellung hängt vom Kontext ab:
| Kontext | Darstellung | Zentrale Aktion |
|---|---|---|
| Formular | Roter Text unter dem Feld, den ersten Fehler fokussieren | Pfade auf Formularnamen abbilden; Array-Indizes behalten |
| Massentabellenimport | Zeilennummer, Spaltenname, Zusammenfassung, exportierbar | /items/3/price in Zeile 4, Spalte Preis verwandeln |
| JSON-Editor | Inline-Marker und ein Probleme-Panel | Einen Parser verwenden, um JSON Pointer auf Zeile und Spalte abzubilden |
| API-Debugwerkzeug | Strukturierte Fehlerliste mit schemaPath |
Für Entwickler gedacht, mehr Detail ist akzeptabel |
Drei Umsetzungshinweise:
- Pro Feld nur den ersten Fehler zeigen, den Rest zu „2 weitere Probleme“ zusammenklappen, damit das Formular nicht in Rot ertrinkt.
- Array-Indizes für die Ansteuerung behalten, aber in Beschriftungen weglassen. „Zeile 4, Preis“ liest sich besser als
/items/3/price. - Editor-Szenarien brauchen Positionsabbildung. Eine reine Stringsuche ist unzuverlässig, wenn sich ein Feldname wiederholt; der Parser sollte Offsets für jeden Knoten liefern, die Sie dann in Zeile und Spalte umrechnen.
Kommt das JSON vom Tippen eines Nutzers statt aus einem Formular, bestätigen Sie zuerst, dass das Dokument syntaktisch gültig ist. Syntaxfehler passieren in der Parsing-Phase und erreichen die Schema-Validierung nie. JSON parsen und formatieren meldet die genaue Zeile und Spalte, was weit schneller ist, als Schemafehlern nachzujagen. Wie überstrenge Schemas entstehen, wenn sie aus Beispielen inferiert werden, siehe Warum automatisch generierte JSON Schemas echte Daten ständig ablehnen; zur Validator-Konfiguration und zum Schlüsselwortsystem siehe JSON Schema: von den Grundlagen bis zur Produktion.
9. Häufige Fragen
Warum bekomme ich nur einen Fehler?
allErrors ist standardmäßig false, Ajv gibt also beim ersten Scheitern zurück. Schalten Sie es für die Formularvalidierung ausdrücklich ein; sonst müssen Nutzer wiederholt absenden, um jedes Problem zu entdecken.
Warum ist der Pfad bei einem required-Fehler leer?
instancePath zeigt bei required auf das Elternobjekt, an der Wurzel also den leeren String; der Name des fehlenden Schlüssels steht in params.missingProperty. Verketten Sie beides, um den vollständigen Pfad zu erhalten. Allein instancePath zu zeigen ergibt das nutzlose „Fehler im Wurzelobjekt“.
Kann ich message direkt Nutzern zeigen?
Nicht empfehlenswert. Es ist englische Prosa, deren Formulierung sich zwischen Versionen ändert, und es hat keine Lokalisierungsfähigkeit. Erzeugen Sie Text aus keyword und params und reservieren Sie message für Logs.
Warum wird meine eigene Fehlermeldung ignoriert?
Prüfen Sie zuerst, ob allErrors: true aktiviert ist; ajv-errors kann nur mit diesem Flag alle Meldungen ersetzen. Bestätigen Sie dann, dass errorMessage auf derselben Ebene wie das scheiternde Schlüsselwort steht – eine Ebene daneben und es wird nicht erkannt.
Welche Felder können in der Produktion sicher zurückgegeben werden?
Feldpfad, stabiler Fehlercode und nutzerfreundlicher Text. Behalten Sie schemaPath, params und message in Server-Logs: Das unterstützt Debugging, ohne interne Enums, Regexe oder Dateilayout zu exponieren.