JSON Toolkit

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ů.

Středně pokročilý~14 min čteníPublikováno 2026-10-30Aktualizováno 2026-10-30
Titulní obrázek článku JSON Schema: od základů k produkci

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í:

  1. required vyžaduje jen existenci klíče: {"name": ""} je platné. Pro odmítnutí prázdného řetězce musíte přidat minLength: 1;
  2. additionalProperties: false zvě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;
  3. properties neomezuje 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" }
  }
}
  • $defs je 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í $ref se souvisejícími klíčovými slovy závisí na verzi). Když potřebujete kompozici, použijte allOf.

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 compile jen jednou, na úrovni modulu; rekompilace při každém požadavku mění validaci za 10 ms na stovky milisekund;
  • strict: true včas odhalí překlepy v klíčových slovech (například minlength místo minLength), takže se vyplatí ho zapnout;
  • Jen ajv-formats způsobí, že format: "email" a format: "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í oneOf způsobuje kombinatorickou explozi, což se u tisíců záznamů velmi znatelné;
  • uniqueItems prová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: false zkombinuje s allOf, pole deklarovaná v podschemě posoudí rodič jako „additional“, takže je musíte sloučit do jediného schématu, nebo použít unevaluatedProperties.

8. Kontrolní seznam

  • [ ] Zafixujte 2020-12 a deklarujte $schema na 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 compile validátoru a zapněte striktní režim;
  • [ ] Jasně zdokumentujte, zda je format vynucen;
  • [ ] 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.

Zdroje

Související články

Obousměrná synchronizace schématu a typů TypeScript

Typy a schémata každé pokrývá jeden segment: jedno při kompilaci, druhé za běhu. Otázka není, které zvolit, ale jak udržet obě kopie automaticky v souladu.

Středně pokročilý~11 min čteníAktualizováno 2026-10-30

Klíčové body návrhu datových kontraktů JSON API

Šest věcí se po nasazení mění velmi těžko: obálky odpovědí, formáty chyb, tvar stránkování, reprezentace ID a času, sémantika null versus chybějící pole a vývoj verzí.

Středně pokročilý~13 min čteníAktualizováno 2026-10-30

Bez additionalProperties validace schématu nic nevaliduje

Úspěšná validace neznamená čistá data. Pydantic standardně ignoruje neznámá pole, Zod je odstraní, Ajv je pustí — vymyšlené parametry a překlepy projdou potichu. Článek ukazuje tři formy tiché acceptance a dává kompletní recepty: zavřít objekty, zkontrolovat koerci, dotáhnout každou úroveň zanoření.

Intermediate~8 min čteníAktualizováno 2026-11-02