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.

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:
requiredwymaga tylko istnienia klucza:{"name": ""}jest poprawne. Aby odrzucić pusty łańcuch, musisz dodaćminLength: 1;additionalProperties: falsepowię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;propertiesnie 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" }
}
}
$defsto forma wprowadzona w 2019-09; draft-07 używadefinitions;- 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
$refz sąsiednimi słowami kluczowymi zależy od wersji). Gdy potrzebujesz kompozycji, użyjallOf.
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
compiletylko raz, na poziomie modułu; rekompilacja przy każdym żądaniu zamienia walidację 10 ms w setki milisekund; strict: trueujawnia błędnie zapisane słowa kluczowe wcześnie (na przykładminlengthzamiastminLength), więc warto je włączyć;- Tylko
ajv-formatssprawia, żeformat: "email"iformat: "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
oneOfpowoduje eksplozję kombinatoryczną, bardzo widoczną przy tysiącach rekordów; uniqueItemswykonuje 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ę zallOf, 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
$schemana górze pliku; - [ ] Wyciągaj wspólne struktury przez
$defs+$ref, zamiast kopiować i wklejać; - [ ] Preferuj dyskryminatory zamiast
oneOf; - [ ] Używaj ponownie wyniku
compilewalidatora i włącz tryb ścisły; - [ ] Dokumentuj jasno, czy
formatjest 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.