JSON Schema: od základů k produkci
Shrnutí: Klíčových slov je mnoho, ale denně se používá jen tucet. Zvládněte `type`/`properties`/`required`/`$ref`, dobře ošetřete chybové zprávy a pokryjete osmdesát procent skutečných případů.

Stručně: nejprve zafixujte $schema na 2020-12, poté extrahujte sdílené struktury pomocí $defs + $ref; oneOf použijte jen tehdy, když musí odpovídat přesně jedna větve, a všude jinde preferujte diskriminátory nebo anyOf.
Schéma není přílohou dokumentace; je to aktivum, které lze vykonat, testovat a generovat.
1. Nejprve si ujasněte verzi
| Verze | Hodnota $schema |
Stav | Poznámky |
|---|---|---|---|
| draft-04 | http://json-schema.org/draft-04/schema# |
Zastaralý | Legacy projekty a některé nástroje OpenAPI 2.0 |
| draft-07 | http://json-schema.org/draft-07/schema# |
Široce používaný | De facto základ OpenAPI 3.0 |
| 2019-09 | https://json-schema.org/draft/2019-09/schema |
Dostupný | Zavedl $defs a anotační slovník |
| 2020-12 | https://json-schema.org/draft/2020-12/schema |
Aktuálně doporučený | Zavedl prefixItems, změnil sémantiku items |
Pro nové projekty použijte 2020-12. Pozor, v 2020-12 omezuje items jen „zbývající prvky“ – deklarace ve stylu n-tic se přesunuly do prefixItems.
To je nejčastější past při migraci z draft-07.
2. Minimální použitelné schéma
{
"$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
}
Toto schéma pokrývá čtyři nejběžnější starosti: typová omezení, povinná pole, limity rozsahu a omezení prvků pole.
3. Mapa klíčových slov
3.1 Obecné
| Klíčové slovo | Účel | Pozor |
|---|---|---|
type |
Typ; může být pole | "type": ["string", "null"] znamená nullable |
enum |
Výčtové hodnoty | Hodnoty musí být jedinečné |
const |
Omezení na jedinou hodnotu | Vhodné pro pevné hodnoty, například číslo verze |
$ref |
Odkazuje na jiné schéma | Odkazuje na celé schéma a neslučuje se souvisejícími klíčovými slovy na odkazujícím objektu |
3.2 Čísla
multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum.
Pozor, v draft-04 byl exclusiveMaximum boolean (používaný spolu s maximum); samostatným číselným klíčovým slovem se stal až od draft-07 –
a to způsobuje, že mnoho „schémat zkopírovaných z dokumentace“ selhává ve striktním režimu.
3.3 Řetězce
minLength, maxLength, pattern (regulární výraz ECMA-262) a format (anotace).
minLength počítá kódové body Unicode, nikoli jednotky UTF-16 ani bajty. Validace pomocí JavaScriptového string.length s ním nebude souhlasit.
3.4 Pole
| Klíčové slovo | Účel |
|---|---|
prefixItems |
Omezení ve stylu n-tic v 2020-12, párování prvků jeden po druhém podle indexu |
items |
Jednotné omezení zbývajících prvků (2020-12) |
contains + minContains/maxContains |
Nejméně/nejvíce kolik odpovídajících prvků musí být přítomno |
uniqueItems |
Prvky se musí všechny lišit (hluboké porovnání; vyhněte se u velkých polí) |
minItems / maxItems |
Omezení délky |
3.5 Objekty
properties, required, patternProperties, additionalProperties, propertyNames,
minProperties, maxProperties, dependentRequired, dependentSchemas.
Tři častá nedorozumění:
requiredvyžaduje jen existenci klíče:{"name": ""}je platné. Pro odmítnutí prázdného řetězce musíte přidatminLength: 1;additionalProperties: falsezvětšuje problémy s kompatibilitou: jedno pole navíc od klienta selže celý payload, takže vývoj API nutí každého volajícího k synchronnímu upgradu;propertiesneomezuje nedeklarované klíče – ve výchozím nastavení jsou povoleny.
3.6 Logická kompozice
{
"oneOf": [
{ "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
{ "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
]
}
| Klíčové slovo | Sémantika | Typické selhání |
|---|---|---|
allOf |
Vše musí být splněno | Více větví ukládá protichůdná omezení na stejné pole |
anyOf |
Alespoň jedna musí být splněna | Chybovou zprávu je těžké vystopovat ke konkrétní větvi |
oneOf |
Přesně jedna musí být splněna | Data odpovídají dvěma větvím a selžou, i když „zjevně vypadají správně“ |
not |
Nesmí být splněno | Používané zřídka a nejméně přívětivé chybové zprávy |
oneOf je klíčové slovo nejsnáze produkující „platná data, která selžou validací“. Pokud se omezení větví překrývají (například obě povolují nadbytečná pole),
jediný záznam odpovídá několika větvím a oneOf rovnou selže. V takovém případě přejděte k anyOf, nebo použijte diskriminační pole s const uvnitř větví.
{
"type": "object",
"required": ["kind"],
"properties": { "kind": { "enum": ["text", "image"] } },
"if": { "properties": { "kind": { "const": "image" } } },
"then": { "required": ["url", "width", "height"] },
"else": { "required": ["content"] }
}
Diskriminační styl je čitelnější než oneOf a dává lepší chyby.
4. Rozdělování a znovupoužití
{
"$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" }
}
}
$defsje forma zavedená v 2019-09; draft-07 používádefinitions;- Reference napříč soubory používají relativní
$id, ale v produkci se nespoléhejte na stahování vzdálených schémat za běhu – zabalte je jako statické soubory; - Nelze přidat omezení vedle reference (chování
$refse souvisejícími klíčovými slovy závisí na verzi). Když potřebujete kompozici, použijteallOf.
5. Validace za běhu s 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)}`);
}
Klíčové body:
- Volejte
compilejen jednou, na úrovni modulu; rekompilace při každém požadavku mění validaci za 10 ms na stovky milisekund; strict: truevčas odhalí překlepy v klíčových slovech (napříkladminlengthmístominLength), takže se vyplatí ho zapnout;- Jen
ajv-formatszpůsobí, žeformat: "email"aformat: "date-time"skutečně účinkují; - 2020-12 vyžaduje vstupní bod
ajv/dist/2020– nepoužívejte výchozí export.
6. Jak navrhovat chybové zprávy
Validátor vám dá „které klíčové slovo selhalo“; uživatel chce „které vstupní pole je špatné“. Vyplatí se přidat mapovací vrstvu:
const FIELD_LABEL: Record<string, string> = {
'/name': 'Name',
'/age': 'Age',
'/tags': 'Tags',
};
/**
* Převede chyby Ajv na zprávy připravené k zobrazení.
* @param errors Seznam chyb Ajv
* @returns Pole zobrazitelných zpráv
*/
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})`;
});
}
Na straně API držte strojově čitelnou strukturu, aby frontend mohl rozhodnout, jak ji prezentovat:
{
"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. Výkon a pasti
- Náklad validace roste se složitostí schématu a velikostí dat; více větví
oneOfzpůsobuje kombinatorickou explozi, což se u tisíců záznamů velmi znatelné; uniqueItemsprovádí hluboké porovnání O(n²) přes pole; u velkých polí deduplikujte raději v obchodní vrstvě;- Vyhněte se katastrofálním backtracking vzorům v
pattern(například(a+)+$); to je klasický vstup pro ReDoS; - Když se
additionalProperties: falsezkombinuje sallOf, pole deklarovaná v podschemě posoudí rodič jako „additional“, takže je musíte sloučit do jediného schématu, nebo použítunevaluatedProperties.
8. Kontrolní seznam
- [ ] Zafixujte 2020-12 a deklarujte
$schemana začátku souboru; - [ ] Extrahujte sdílené struktury pomocí
$defs+$ref, místo kopírování a vkládání; - [ ] Preferujte diskriminátory před
oneOf; - [ ] Znovu používejte výsledek
compilevalidátoru a zapněte striktní režim; - [ ] Jasně zdokumentujte, zda je
formatvynucen; - [ ] Chybové odpovědi poskytují cestu, klíčové slovo a zprávu pro uživatele;
- [ ] Dejte schéma do CI: spouštějte případy z JSON Schema Test Suite jako regresní testy.
Časté dotazy
Jak vybrat návrh JSON Schema?
Pro nové projekty použijte 2020-12 a deklarujte $schema na začátku souboru. Pokud schéma jde do OpenAPI 3.1, musí být také 2020-12, protože 3.1 tuto verzi přímo zahrnuje; draft-07 ponechte jen pro projekty stále svázané se staršími nástrojovými řetězci. Oba se chovají odlišně u items a souvisejících klíčových slov $ref, takže je nemíchejte v jedné sadě schémat.
Proč additionalProperties: false zvětšuje problémy s kompatibilitou?
Považuje každý nedeklarovaný klíč za chybu, takže jedno pole navíc od klienta selže celý payload. Každý volající se pak musí upgradovat synchronně, kdykoli se API vyvíjí, nebo jsou staří klienti odmítnuti. Konfigurační schémata by měla nadbytečná pole explicitně uzavřít, zatímco veřejná API musí zvážit náklady na kompatibilitu.
Mám použít oneOf, nebo anyOf?
oneOf použijte jen tehdy, když data musí odpovídat přesně jedné větvi. Pokud omezení větví mohou platit současně, jeden záznam odpovídá několika větvím a oneOf rovnou selže; přejděte k anyOf, nebo do každé větve přidejte diskriminátor const.