JSON Toolkit

JSON Schema: od podstaw do produkcji

Podsumowanie: Słów kluczowych jest wiele, ale codziennie używa się tylko kilkunastu. Opanuj type/properties/required/$ref, dobrze obsłuż komunikaty błędów i pokryjesz osiemdziesiąt procent realnych przypadków.

Średniozaawansowany~14 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Okładka artykułu JSON Schema: od podstaw do produkcji

Najważniejsze: najpierw przypnij $schema do 2020-12, a następnie wyciągnij wspólne struktury przez $defs + $ref; oneOf używaj tylko wtedy, gdy dokładnie jedna gałąź musi pasować, a wszędzie indziej preferuj dyskryminatory lub anyOf.

Schemat nie jest aneksem do dokumentacji; to zasób, który można wykonać, przetestować i wygenerować.

1. Najpierw uporządkuj wersję

Wersja Wartość $schema Status Uwagi
draft-04 http://json-schema.org/draft-04/schema# Przestarzała Odziedziczone projekty i część narzędzi OpenAPI 2.0
draft-07 http://json-schema.org/draft-07/schema# Szeroko używana De facto podstawa OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema Dostępna Wprowadziła $defs i słownik adnotacji
2020-12 https://json-schema.org/draft/2020-12/schema Obecnie zalecana Wprowadziła prefixItems, zmieniła semantykę items

Dla nowych projektów używaj 2020-12. Zauważ, że w 2020-12 items ogranicza tylko „pozostałe elementy” — deklaracje w stylu krotki przeniosły się do prefixItems. To najczęstsza pułapka przy migracji z draft-07.

2. Minimalny użyteczny schemat

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/user.json",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 64 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
  },
  "additionalProperties": false
}

Ten schemat pokrywa cztery najczęstsze zagadnienia: ograniczenia typów, pola wymagane, limity zakresu i ograniczenia elementów tablicy.

3. Mapa słów kluczowych

3.1 Ogólne

Słowo kluczowe Cel Na co uważać
type Typ; może być tablicą "type": ["string", "null"] znaczy nullable
enum Wartości wyliczeniowe Wartości muszą być unikatowe
const Ograniczenie do jednej wartości Dobre do stałych wartości, takich jak numer wersji
$ref Odwołanie do innego schematu Odnosi się do całego schematu i nie łączy się z sąsiednimi słowami kluczowymi obiektu odwołującego

3.2 Liczby

multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum. Zauważ, że w draft-04 exclusiveMaximum było wartością logiczną (używaną razem z maximum); dopiero od draft-07 stało się samodzielnym słowem kluczowym numerycznym — co sprawia, że wiele „schematów skopiowanych ze starych dokumentów” zawodzi w trybie ścisłym.

3.3 Łańcuchy

minLength, maxLength, pattern (wyrażenie regularne ECMA-262) i format (adnotacja).

minLength liczy punkty kodowe Unicode, a nie jednostki UTF-16 i nie bajty. Walidacja przez string.length w JavaScript nie zgodzi się z nim.

3.4 Tablice

Słowo kluczowe Cel
prefixItems Ograniczenie w stylu krotki z 2020-12, dopasowujące elementy po indeksie
items Jednolite ograniczenie pozostałych elementów (2020-12)
contains + minContains/maxContains Przynajmniej/co najwyżej ile pasujących elementów musi wystąpić
uniqueItems Elementy muszą się wszystkie różnić (głębokie porównanie; unikaj na dużych tablicach)
minItems / maxItems Ograniczenia długości

3.5 Obiekty

properties, required, patternProperties, additionalProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas.

Trzy częste błędne przekonania:

  1. required wymaga tylko istnienia klucza: {"name": ""} jest poprawne. Aby odrzucić pusty łańcuch, musisz dodać minLength: 1;
  2. additionalProperties: false powiększa problemy zgodności: jedno dodatkowe pole od klienta zawodzi cały ładunek, więc ewolucja API wymusza aktualizację każdego wywołującego w tym samym tempie;
  3. properties nie ogranicza niezgłoszonych kluczy — domyślnie są dozwolone.

3.6 Kompozycja logiczna

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
Słowo kluczowe Semantyka Typowa porażka
allOf Wszystkie muszą być spełnione Wiele gałęzi narzuca sprzeczne ograniczenia na to samo pole
anyOf Co najmniej jedna musi być spełniona Komunikat błędu trudno powiązać z konkretną gałęzią
oneOf Dokładnie jedna musi być spełniona Dane pasują do dwóch gałęzi i zawodzą, mimo że „wyglądają oczywiście poprawnie”
not Nie może być spełnione Rzadko używane i najmniej przyjazne komunikaty błędów

oneOf to słowo kluczowe najbardziej podatne na produkcję „poprawnych danych, które zawodzą walidację”. Jeśli ograniczenia gałęzi się nakładają (na przykład obie pozwalają na dodatkowe pola), pojedynczy rekord pasuje do kilku gałęzi i oneOf zawodzi od razu. W takim przypadku przejdź na anyOf albo użyj pola dyskryminatora z const wewnątrz gałęzi.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

Styl z dyskryminatorem jest bardziej czytelny niż oneOf i daje lepsze błędy.

4. Dzielenie i ponowne użycie

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "address": {
      "type": "object",
      "required": ["city"],
      "properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
    }
  },
  "type": "object",
  "required": ["total", "shipTo"],
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "shipTo": { "$ref": "#/$defs/address" }
  }
}
  • $defs to forma wprowadzona w 2019-09; draft-07 używa definitions;
  • Odwołania między plikami używają względnych $id, ale nie polegaj na pobieraniu zdalnych schematów w czasie wykonania w produkcji — zamiast tego dołącz je jako pliki statyczne;
  • Nie możesz dodać ograniczeń obok odwołania (zachowanie $ref z sąsiednimi słowami kluczowymi zależy od wersji). Gdy potrzebujesz kompozycji, użyj allOf.

5. Walidacja w czasie wykonania z Ajv

import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({
  allErrors: true,
  strict: true,
  allowUnionTypes: true,
});
addFormats(ajv);

const validateOrder = ajv.compile(orderSchema);

export function assertOrder(input: unknown): void {
  if (validateOrder(input)) {
    return;
  }
  const detail = (validateOrder.errors ?? []).map((errorItem) => ({
    path: errorItem.instancePath || '/',
    keyword: errorItem.keyword,
    message: errorItem.message,
    params: errorItem.params,
  }));
  throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}

Kluczowe punkty:

  • Wywołaj compile tylko raz, na poziomie modułu; rekompilacja przy każdym żądaniu zamienia walidację 10 ms w setki milisekund;
  • strict: true ujawnia błędnie zapisane słowa kluczowe wcześnie (na przykład minlength zamiast minLength), więc warto je włączyć;
  • Tylko ajv-formats sprawia, że format: "email" i format: "date-time" faktycznie działają;
  • 2020-12 wymaga punktu wejścia ajv/dist/2020 — nie używaj domyślnego eksportu.

6. Jak projektować komunikaty błędów

Walidator daje ci „które słowo kluczowe zawiodło”; użytkownik chce wiedzieć „które pole formularza jest złe”. Warto dodać warstwę mapowania:

const FIELD_LABEL: Record<string, string> = {
  '/name': 'Name',
  '/age': 'Age',
  '/tags': 'Tags',
};

/**
 * Convert Ajv errors into display-ready messages.
 * @param errors The list of Ajv errors
 * @returns An array of display messages
 */
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
  return errors.map((errorItem) => {
    const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
    if (errorItem.keyword === 'required') {
      const missing = (errorItem.params as { missingProperty: string }).missingProperty;
      return `${label} is missing the required field ${missing}`;
    }
    if (errorItem.keyword === 'minLength') {
      return `${label} is too short`;
    }
    return `${label} failed validation (${errorItem.keyword})`;
  });
}

Po stronie API utrzymuj strukturę czytelną maszynowo, aby frontend mógł zdecydować, jak ją przedstawić:

{
  "type": "https://bbdrug.com/problems/validation",
  "title": "Request data validation failed",
  "status": 422,
  "errors": [
    { "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
  ]
}

7. Wydajność i pułapki

  • Koszt walidacji rośnie ze złożonością schematu i rozmiarem danych; wiele gałęzi oneOf powoduje eksplozję kombinatoryczną, bardzo widoczną przy tysiącach rekordów;
  • uniqueItems wykonuje głębokie porównanie O(n²) na tablicy; przy dużych tablicach deduplikuj w warstwie biznesowej;
  • Unikaj wzorców katastrofalnego backtrackingu w pattern (takich jak (a+)+$); to klasyczny punkt wejścia dla ReDoS;
  • Gdy additionalProperties: false łączy się z allOf, pola zadeklarowane w podschemat są oceniane przez rodzica jako „dodatkowe”, więc musisz je scalić w jeden schemat albo użyć unevaluatedProperties.

8. Lista kontrolna

  • [ ] Przypnij 2020-12 i zadeklaruj $schema na górze pliku;
  • [ ] Wyciągaj wspólne struktury przez $defs + $ref, zamiast kopiować i wklejać;
  • [ ] Preferuj dyskryminatory zamiast oneOf;
  • [ ] Używaj ponownie wyniku compile walidatora i włącz tryb ścisły;
  • [ ] Dokumentuj jasno, czy format jest wymuszany;
  • [ ] Odpowiedzi błędów zawierają ścieżkę, słowo kluczowe i komunikat dla użytkownika;
  • [ ] Umieść schemat w CI: uruchamiaj przypadki JSON Schema Test Suite jako testy regresji.

FAQ

Jak wybrać draft JSON Schema?

Dla nowych projektów używaj 2020-12 i zadeklaruj $schema na górze pliku. Jeśli schemat trafia do OpenAPI 3.1, musi też być 2020-12, bo 3.1 osadza tę wersję bezpośrednio; draft-07 trzymaj tylko dla projektów wciąż związanych ze starszymi łańcuchami narzędzi. Wersje zachowują się różnie dla items i sąsiednich słów kluczowych $ref, więc nie mieszaj ich w jednym zestawie schematów.

Dlaczego additionalProperties: false powiększa problemy zgodności?

Traktuje każdy niezgłoszony klucz jako błąd, więc jedno dodatkowe pole od klienta zawodzi cały ładunek. Wtedy każdy wywołujący musi aktualizować się w tym samym tempie, gdy API ewoluuje, inaczej starsi klienci są odrzucani. Schematy w stylu konfiguracji powinny jawnie zamykać dodatkowe pola, a publiczne API muszą rozważyć koszt zgodności.

Czy używać oneOf czy anyOf?

oneOf używaj tylko wtedy, gdy dane muszą pasować do dokładnie jednej gałęzi. Jeśli ograniczenia gałęzi mogą obowiązywać jednocześnie, jeden rekord pasuje do kilku gałęzi i oneOf zawodzi od razu; przejdź na anyOf albo dodaj dyskryminator const w każdej gałęzi.

Źródła

Powiązane artykuły

Dwukierunkowa synchronizacja schematu i typów TypeScript

Typy i schematy pokrywają po jednym segmencie: jeden w czasie kompilacji, drugi w czasie wykonania. Pytanie nie brzmi, który wybrać, ale jak automatycznie utrzymać je w synchronizacji.

Średniozaawansowany~11 min czytaniaZaktualizowano 2026-10-30

Kluczowe punkty projektowania kontraktów danych API JSON

Sześć rzeczy bardzo trudno zmienić po wdrożeniu: koperta odpowiedzi, format błędów, kształt paginacji, reprezentacja identyfikatorów i czasu, semantyka null a brak pola oraz ewolucja wersji.

Średniozaawansowany~13 min czytaniaZaktualizowano 2026-10-30

Bez additionalProperties walidacja schematu nic nie sprawdza

Zdana walidacja nie oznacza czystych danych. Pydantic domyślnie ignoruje nieznane pola, Zod je usuwa, Ajv przepuszcza — zmyślone parametry i literówki przechodzą po cichu. Artykuł pokazuje trzy formy cichej akceptacji i daje pełne przepisy: zamykać obiekty, kontrolować koercję, zaciskać każdy poziom zagnieżdżenia.

Intermediate~8 min czytaniaZaktualizowano 2026-11-02