JSON Toolkit

Gdy błędy JSON Schema nie mają sensu: lokalizacja błędów Ajv i zarządzanie komunikatami

Podsumowanie: Walidator emituje błędy maszynowe; użytkownicy chcą wiedzieć, które pole wejściowe jest złe. Omawia tablicę errors Ajv, instancePath vs schemaPath, grupowanie i treść komunikatów.

Zaawansowany~10 min czytaniaOpublikowano 2026-10-31Zaktualizowano 2026-10-31
Diagram lokalizacji błędów walidacji Ajv i zarządzania komunikatami

Najważniejsze: najpierw czytaj instancePath, aby zlokalizować zawodzące dane, potem schemaPath, aby zlokalizować regułę, która zadziałała; dla required i additionalProperties wyciągnij prawdziwą nazwę klucza z params, zanim wybierzesz komunikat.

Zadaniem walidatora jest powiedzieć, która reguła zawiodła. Przetłumaczenie tego na „to pole wejściowe jest złe” należy do ciebie.

1. Co naprawdę daje tablica errors

Ajv nie rzuca przy porażce walidacji; zostawia wynik w validate.errors. Domyślna konfiguracja trzyma tylko pierwszy błąd, a allErrors produkuje pełną listę. Pojedynczy obiekt błędu ma pięć istotnych pól, a ich zrozumienie jest warunkiem wstępnym dla wszystkiego dalej:

Pole Znaczenie Przykład
instancePath JSON Pointer do zawodzącej wartości /email
schemaPath Położenie zawodzącego słowa kluczowego w schemacie #/properties/email/format
keyword Nazwa naruszonego ograniczenia format
params Czytelne maszynowo parametry tego ograniczenia { "format": "email" }
message Angielska prosa zmieniająca się między wersjami must match format "email"

Prawdziwy błąd wygląda tak:

[
  {
    "instancePath": "/email",
    "schemaPath": "#/properties/email/format",
    "keyword": "format",
    "params": { "format": "email" },
    "message": "must match format \"email\""
  }
]

Zauważ, że message to prosa napisana dla ludzi, a nie stabilny interfejs. Ajv może ją przeformułować między wersjami i nie może służyć produktowi zlokalizowanemu. Nigdy nie parsuj jej wyrażeniem regularnym i nigdy nie zwracaj jej dosłownie do klienta.

2. instancePath i schemaPath odpowiadają na różne pytania

instancePath mówi, który fragment danych zawiódł; schemaPath mówi, która reguła zadziałała. To dwa układy współrzędnych i nie są symetryczne:

  • instancePath to JSON Pointer RFC 6901. Pusty łańcuch oznacza korzeń; /items/0/price oznacza pole ceny pierwszego elementu tablicy.
  • schemaPath zaczyna się od # i wskazuje słowo kluczowe wewnątrz dokumentu schematu. Przez $ref wskazuje wewnątrz przywoływanego schematu, więc potrzebujesz $id, by wiedzieć, z którego pliku pochodzi.
  • Klucz zawierający / lub ~ jest escape'owany odpowiednio do ~1 i ~0. Odwróć to przed wyświetleniem, inaczej UI pokaże zagadkowe symbole.

Na przykład instancePath: "/email" razem z schemaPath: "#/properties/email/format" znaczy, że pole email na obiekcie głównym nie przeszło sprawdzenia formatu.

Najczęstszą pułapką są required i additionalProperties: ich instancePath wskazuje obiekt nadrzędny, a nie naruszający klucz.

Słowo kluczowe instancePath wskazuje Gdzie żyje prawdziwy klucz
required Obiekt nadrzędny (pusty łańcuch w korzeniu) params.missingProperty
additionalProperties Obiekt nadrzędny params.additionalProperty
type Samą zawodzącą wartość —
enum Samą zawodzącą wartość params.allowedValues

Mapując błędy na pola formularza, najpierw obsłuż te dwa przypadki. Inaczej „brakuje emaila” staje się „błąd obiektu głównego”, a użytkownik nie ma pojęcia, gdzie wpisać.

3. allErrors i verbose: kompletność a koszt

Domyślne allErrors: false sprawia, że Ajv zatrzymuje się na pierwszej porażce. To jest w porządku dla narzędzia wiersza poleceń i katastrofalne dla formularza: użytkownik poprawia jedno pole, wysyła, widzi kolejny błąd i powtarza trzy razy, zanim formularz przejdzie.

Ustawienie Zachowanie Korzyść Koszt
allErrors: false Tylko pierwszy błąd Najszybsze, najmniejsza pamięć Powtarzane wysyłania
allErrors: true Zbiera każdy błąd Wszystkie problemy w jednym przebiegu Liczba błędów eksploduje na złożonych schematach
verbose: true Dołącza schema i data do każdego błędu Łatwiejsza diagnostyka po stronie serwera Znacznie zwiększony rozmiar; nigdy tego nie ujawniaj
messages: false Usuwa pole message Mniejsze ładunki Tracisz angielską wskazówkę debugowania

Zalecany podział: włącz allErrors: true dla walidacji serwerowej i trzymaj pełną tablicę w logach wewnętrznych, a publiczna odpowiedź niesie tylko białej listy pól. Włączaj verbose tylko podczas śledztwa; osadza dopasowany fragment schematu i dane użytkownika w każdym błędzie, co kosztuje pamięć i może wyciec wrażliwe wartości do logów.

Wolumen błędów to inny częsty problem. Zawiedziona gałąź anyOf emituje kilka błędów podrzędnych plus jeden błąd podsumowujący, a oneOf z wieloma gałęziami jest gorszy. Pokaż tablicę tak, jak jest, a użytkownicy zobaczą pięć niemal identycznych komunikatów dla jednego pola. Grupowanie musi nastąpić przed renderowaniem.

4. Grupuj błędy po polach

Grupowanie robi trzy rzeczy: zamienia ścieżkę z powrotem w nazwę pola, koryguje przesunięcie required i additionalProperties oraz scala wiele błędów na tym samym polu w jeden najbardziej wykonalny komunikat.

/** 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;
}

Scalaj z porządkiem priorytetów: najpierw required (brak), potem type, potem enum/const, potem format/pattern, a na końcu długość i zakres. Pokazuj tylko komunikat o najwyższym priorytecie per pole, a resztę trzymaj w logach. Powód jest prosty: gdy typ jest zły, komunikat o długości jest szumem, a poprawa typu jest jedynym sensownym następnym krokiem.

Błędy podsumowujące dla oneOf i anyOf zwykle nie niosą wykonalnej informacji. Pomiń je podczas renderowania i zachowaj tylko błędy gałęzi wskazujące konkretne pole.

5. Niestandardowe komunikaty: droga wtyczki i droga szablonu

Ajv nie dostarcza zlokalizowanej treści, więc istnieją dwie praktyczne opcje.

Opcja pierwsza: wtyczka ajv-errors. Dodaje słowo kluczowe errorMessage, aby komunikaty żyły obok ograniczeń:

{
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "errorMessage": {
        "format": "Please enter a valid email address",
        "type": "Email must be a string"
      }
    }
  }
}

Zaletą jest to, że treść podróżuje z polem, a schemat służy też jako dokumentacja. Wadą jest to, że tekst jest zamrożony w jednym języku, więc lokalizacja oznacza utrzymywanie kilku schematów. Wtyczka też zastępuje wszystkie komunikaty tylko wtedy, gdy ustawiono allErrors: true; zapomnij tej flagi i dostajesz mieszankę niestandardowych i domyślnych angielskich.

Opcja druga: tabela słowo-kluczowe → szablon. Walidator dostarcza tylko keyword i params; aplikacja produkuje treść:

Słowo kluczowe Szablon Dostępne parametry
required Brak wymaganego pola: {field} missingProperty
type Oczekiwany typ: {type} type
minLength Wpisz co najmniej {limit} znaków limit
maximum Nie może przekroczyć {limit} limit
format Niepoprawny format: {format} format
enum Musi być jedną z dozwolonych opcji allowedValues
pattern Wartość nie pasuje do oczekiwanego formatu pattern (nie powtarzaj go)

Opcja druga pasuje do produktów wymagających wielu języków i spójnego głosu. Rozdziela walidację od prezentacji, trzyma tłumaczenia poza schematem i pozwala frontendowi używać tych samych szablonów.

6. Lokalizacja: komunikaty nie należą do walidatora

Przeniesienie treści do pakietów językowych to punkt wyjścia zarządzania komunikatami. Pakiet językowy przechowuje szablony i klucze, nigdy nazwy pól; etykiety pól pochodzą z konfiguracji formularza lub kontraktu API, a oba łączą się w czasie renderowania.

/** 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';
}

Trzy zasady mają tu znaczenie:

  1. Nie sklejaj zdań. „Format emaila jest niepoprawny” i jego chiński odpowiednik nie dzielą szyku wyrazów, a składanie łańcuchów prędzej czy później da nonsens w jednym z nich.
  2. Oddeleguj liczby mnogie i klasyfikatory do biblioteki i18n. Nigdy nie wpisuj na sztywno „znaków” w szablonie; „1 znak” i „3 znaki” się różnią.
  3. Tłumacz też etykiety pól. params.missingProperty to email, a UI powinno mówić „Adres e-mail”. Trzymaj to mapowanie w konfiguracji formularza, a nie w pakiecie językowym.

7. Nie zwracaj klientom wewnętrznej struktury

Zwracanie schemaPath, params i message razem jest wygodne w rozwoju i wyciekiem informacji w produkcji. allowedValues ujawnia wewnętrzne wyliczenia, pattern ujawnia regex walidacji, a schemaPath ujawnia układ plików i nazewnictwo pól — w niektórych przypadkach wręczając atakującemu mapę słabych punktów.

Publiczna odpowiedź trzyma dokładnie trzy rzeczy: ścieżkę pola (lub nazwę pola formularza), stabilny kod błędu i tekst dla użytkownika. Używaj krótkich stabilnych kodów zamiast surowego keyword, aby zachować miejsce na zmianę walidatora w przyszłości.

{
  "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" }
  ]
}

Logi serwera powinny trzymać odwrotność: pełną tablicę błędów, id żądania i wersję schematu, aby śledztwo mogło odpowiedzieć „która reguła to zablokowała”. Unikaj zrzucania całych treści żądań do logów; trzymaj zawodzące ścieżki i słowa kluczowe.

8. Prezentacja we frontendzie: od podświetlenia pola do znaczników edytora

Błędy w końcu lądują w UI, a prezentacja zależy od kontekstu:

Kontekst Prezentacja Kluczowe działanie
Formularz Czerwony tekst pod polem, fokus na pierwszym błędzie Mapuj ścieżki na nazwy formularza; zachowaj indeksy tablic
Import zbiorczej tabeli Numer wiersza, nazwa kolumny, podsumowanie, eksportowalne Zamień /items/3/price na wiersz 4, kolumnę ceny
Edytor JSON Znaczniki inline i panel problemów Użyj parsera do mapowania JSON Pointer na wiersz i kolumnę
Narzędzie debugowania API Strukturalna lista błędów z zachowaniem schemaPath Skierowane do dewelopera, więc więcej szczegółów jest akceptowalne

Trzy uwagi implementacyjne:

  • Pokazuj tylko pierwszy błąd per pole, zwijając resztę w „2 więcej problemów”, aby formularz nie tonął w czerwieni.
  • Zachowaj indeksy tablic do celowania, ale usuń je z etykiet. „Wiersz 4, cena” czyta się lepiej niż /items/3/price.
  • Scenariusze edytora wymagają mapowania pozycji. Zwykłe wyszukiwanie łańcucha jest zawodne, gdy nazwa pola się powtarza; parser powinien dostarczać przesunięcia dla każdego węzła, które potem konwertujesz na wiersz i kolumnę.

Jeśli JSON pochodzi od użytkownika wpisującego, a nie z formularza, najpierw potwierdź poprawność składniową dokumentu. Błędy składni występują podczas parsowania i nigdy nie docierają do walidacji schematem. Parsowanie i formatowanie JSON podaje dokładny wiersz i kolumnę, co jest znacznie szybsze niż ściganie błędów schematu. O tym, jak powstają zbyt ścisłe schematy, gdy są wnioskowane z próbek, przeczytasz w Dlaczego automatycznie wygenerowany JSON Schema odrzuca prawdziwe dane; o konfiguracji walidatora i systemie słów kluczowych w JSON Schema: od podstaw do produkcji.

9. Często zadawane pytania

Dlaczego dostaję tylko jeden błąd?

allErrors jest domyślnie false, więc Ajv zwraca przy pierwszej porażce. Włącz to jawnie dla walidacji formularza; inaczej użytkownicy muszą wysyłać wielokrotnie, by odkryć każdy problem.

Dlaczego ścieżka przy błędzie required jest pusta?

instancePath dla required wskazuje obiekt nadrzędny, który w korzeniu jest pustym łańcuchem; brakująca nazwa klucza siedzi w params.missingProperty. Połącz oba, aby dostać pełną ścieżkę. Pokazanie samego instancePath daje bezużyteczny „błąd obiektu głównego”.

Czy mogę pokazywać message bezpośrednio użytkownikom?

Nie zaleca się. To angielska prosa, której treść zmienia się między wersjami, i nie ma możliwości lokalizacji. Generuj tekst z keyword i params, a message zachowaj do logów.

Dlaczego mój niestandardowy komunikat błędu jest ignorowany?

Najpierw sprawdź, czy allErrors: true jest włączone; ajv-errors potrafi zastąpić wszystkie komunikaty tylko z tą flagą. Potem potwierdź, że errorMessage siedzi na tym samym poziomie co zawodzące słowo kluczowe — jeden poziom obok i nie zostanie rozpoznane.

Które pola są bezpieczne do zwracania w produkcji?

Ścieżka pola, stabilny kod błędu i tekst dla użytkownika. schemaPath, params i message trzymaj w logach serwera: to wspiera debugowanie bez ujawniania wewnętrznych wyliczeń, regexów czy układu plików.