JSON Toolkit

Znovupoužití JSON Schema: $ref, $defs a kompozice allOf/oneOf

Shrnutí: $ref řeší znovupoužití, allOf a oneOf řeší kompozici, a právě jejich kombinování je místo, kde se věci rozbíjejí. Pokrývá $id, $defs, podmínky, rekurzi, bundling a ladění.

Středně pokročilý~11 min čteníPublikováno 2026-10-31Aktualizováno 2026-10-31
Diagram znovupoužití a kompozice JSON Schema pomocí $ref, $defs a allOf/oneOf

Stručně: nejprve extrahujte sdílené struktury do $defs s $ref, poté sáhněte po allOf/oneOf; když větve mohou odpovídat současně, použijte anyOf nebo const tag a oneOf si nechte pro případy, které skutečně vyžadují přesně jednu shodu.

Kopírovat a vkládat ušetří deset minut a stojí vás tichou vidličku o tři měsíce později, kdy si nikdo nepamatuje, která kopie je správná.

1. Tři vrstvy znovupoužití – zvolte jednu, než začnete rozdělovat soubory

První instinkt, jakmile pole začnou přibývat, je nacpat každé omezení do jediného souboru. Je to nejrychlejší cest k nasazení, ale po několika stovkách řádků se každá změna pole musí zrcadlit na třech místech. Vynechte jedno a dostanete „lokálně projde, v produkci vrátí 500“. Znovupoužití znamená napsat fakt jednou a odkazovat na něj všude ostatní.

JSON Schema nabízí tři vrstvy, každou s jiným rozsahem:

Vrstva Nosič Rozsah Typické použití
Podstruktura $defs + $ref Jeden soubor schématu Adresa, peníze, stránkování, tvary chyb
Soubor Samostatné schéma + $id Několik souborů v jedné službě Entity uživatele, objednávky, produktu
Knihovna kontraktů Repozitář schémat + zabalený artefakt Více služeb, více jazyků Komponenty OpenAPI, publikované kontrakty

Pro většinu projektů první vrstva stačí. Rozdělování do souborů se vyplatí jen tehdy, když dvě služby skutečně sdílejí stejný kontrakt, nebo když je schéma publikovaný artefakt konzumovaný jiným jazykem. Příliš brzké rozdělení činí $id rozlišení a relativní cesty hůř laditelné, a ta cena obvykle převyšuje úsporu z vyhnutí se duplikaci.

Jednoduché pravidlo palce: pokud se struktura objeví na dvou místech, jedna kopie je snesitelná; když se objeví třetí, povyšte ji na $defs; teprve když ji musí číst jiný jazyk nebo repozitář, zaslouží si samostatný soubor s vlastním $id.

2. $id stanoví identitu, $ref provádí skok

$id není komentář. Deklaruje základní URI schématu. Hodnota $ref je URI reference: rozliší se proti aktuálnímu základnímu URI na absolutní URI a poté fragment za # lokalizuje uzel. Pokud je základní URI špatné, každá relativní reference ukazuje jinam, přesto chyba obvykle jen řekne „can't resolve reference“, aniž by pojmenovala špatný hostitel.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "address": {
      "$anchor": "address",
      "type": "object",
      "required": ["city"],
      "properties": {
        "city": { "type": "string", "minLength": 1 },
        "detail": { "type": "string" }
      }
    }
  },
  "type": "object",
  "required": ["shipTo"],
  "properties": {
    "shipTo": { "$ref": "#/$defs/address" },
    "billTo": { "$ref": "#address" }
  }
}

shipTo používá JSON Pointer, zatímco billTo používá anchor; oba přistanou na stejném uzelu. Tabulka níže mapuje každou formu na její rozlišení:

Forma Ukazuje na Draft Poznámky
# Kořen aktuálního schématu Všechny Běžné pro rekurzi
#/$defs/address Uzel pod $defs 2019-09 a novější Nejjasnější; preferované
#address Pojmenovaný anchor $anchor v 2020-12 Draft-07 psal $id: "#address"
common.json Soubor relativní k základnímu URI Všechny Musí být registrován nebo zabalen
https://example.com/x.json Absolutní URI Všechny V produkci se vyhněte stahování za běhu

Jedna jemná past: pokud podschema uvnitř $defs deklaruje vlastní $id, stane se novým základním URI pro všechno pod ním. Vložte schéma zkopírované z jiného souboru a zbylé $id přesměruje jeho relativní reference na původního hostitele – a validátor si to všimne, až když je skutečně zkusí rozlišit.

3. $defs je kontejner, nikoli validační místo

$defs je obyčejná mapa. Nic uvnitř neúčinkuje, dokud na to neukazuje $ref. Seznam required sedící na začátku $defs bez příchozí reference nevaliduje vůbec nic, ať vypadá sebepřísnější. Je to specifikace fungující tak, jak má, nikoli chyba validátoru.

{
  "$defs": {
    "money": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]{2}$",
      "description": "A two-decimal money string that avoids floating-point drift"
    },
    "page": { "type": "integer", "minimum": 1 }
  },
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "page": { "$ref": "#/$defs/page" }
  }
}

Několik konvencí stojí za to v týmu zafixovat:

  • Klíče uvnitř $defs nenesou sémantiku. address, Address i addr jsou legální, takže se dohodněte na stylu.
  • Před 2019-09 bylo klíčové slovo definitions. Oba lze cílit přes $ref, ale striktní režim může definitions hlásit jako neznámé klíčové slovo.
  • $defs může obsahovat libovolné schéma, včetně fragmentu allOf nebo nezávislého podschematu s vlastním $id.
  • Nepovažujte $defs za dědičnost šablon. Jen ukládá definice; význam stále přichází z odkazujícího kontextu.

4. allOf, anyOf a oneOf nejsou vzájemně zastupitelné

Všechny tři zahrnují více větví, ale jejich pravidla přijetí se liší. Když data odpovídají dvěma větvím, allOf a anyOf projdou, zatímco oneOf selže – to je nejčastější důvod, proč lidé usoudí „schéma je rozbité“.

Klíčové slovo Minimální shody Maximální shody Data odpovídající dvěma větvím Kvalita chyb Typické použití
allOf Všechny Neomezené Projde Dobrá, jedno omezení naráz Kombinace fragmentů omezení
anyOf 1 Neomezené Projde Špatná, větve se kupí „E-mail nebo telefon, jedno z toho“
oneOf 1 1 Selže Špatná Diskriminované unie
not 0 0 Selže Nejhorší Vyloučení několika hodnot; používané zřídka
{
  "allOf": [
    { "$ref": "#/$defs/auditable" },
    { "$ref": "#/$defs/softDeletable" },
    { "required": ["email"] }
  ],
  "unevaluatedProperties": false
}

allOf neslučuje definice objektů; jen vyžaduje, aby každá větve platila. Každá větve se vyhodnocuje nezávisle, takže additionalProperties: false uvnitř jedné větve zná jen klíče, které ta větve deklarovala, a pole deklarovaná jinou větví označí jako nadbytečná. V 2020-12 je oprava umístit politiku neznámých vlastností na nejvyšší úrovni a použít unevaluatedProperties: false, které se vyhodnotí jednou po všech větvích.

anyOf použijte pro „několik legálních tvarů“, nikoli pro „několik vzájemně výlučných tvarů“. Pokud dvě větve mohou odpovídat současně, oneOf produkuje pověstnou nepříliš užitečnou zprávu must match exactly one schema in oneOf.

5. if/then/else a diskriminátor: udělejte podmínky rozhodnutelné

Podmíněná omezení obvykle produkují lepší chyby než oneOf, protože rozhodovací cesta je explicitní: if projde, then se uplatní; jinak se uplatní else.

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

Tři detaily rozhodují o tom, zda se chová zamýšleně:

  1. Položka properties uvnitř if neselže, když klíč chybí. Spárujte ji s required, abyste vyjádřili „kind existuje a rovná se image“.
  2. if samo nikdy neprodukuje validační chybu. Jeho selhání jen přesměruje na else, takže samo nemůže blokovat špatná data.
  3. Osamocené then nebo else je inertní. Uplatní se jen vedle if.

Jak větve unie rostou, podmíněný řetěz se stane neudržovatelným hnízdem. Robustnější vzor je uzamknout tag pole uvnitř každé větve, aby větve byly vzájemně výlučné už ze své konstrukce:

{
  "oneOf": [
    {
      "properties": { "kind": { "const": "card" } },
      "required": ["kind", "cardNumber"]
    },
    {
      "properties": { "kind": { "const": "account" } },
      "required": ["kind", "accountId"]
    }
  ]
}

discriminator je rozšíření OpenAPI 3.x, nikoli klíčové slovo JSON Schema. Ajv ho podporuje, když je zapnutá volba discriminator: true, ale jeho mapování žije v mapping a není přenosné mezi nástrojovými řetězci. Pokud schéma musí fungovat ve více validátorech, preferujte vzor s const tagem, který nezávisí na žádném rozšíření.

6. Rekurzivní reference: stromy, spojové seznamy a soubory, které odkazují na sebe

Rekurzivní struktury nepotřebují zvláštní syntaxi – stačí $ref zpět na sebe. Validátory nerozbalují reference nekonečně při kompilaci; rekurzují podle skutečné hloubky dat, takže několik desítek řádků popíše strom libovolné výšky.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/tree-node.json",
  "$defs": {
    "node": {
      "type": "object",
      "required": ["name"],
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "children": {
          "type": "array",
          "items": { "$ref": "#/$defs/node" }
        }
      }
    }
  },
  "$ref": "#/$defs/node"
}

Tři hranice stojí za to vyřešit předem:

  • Hloubka dat: schéma neukládá žádný limit, ale validace i serializace spotřebovávají zásobník úměrně hloubce. Veřejná API by měla přidat obchodní limit, řekněme osm úrovní, jinak zlovolně hluboký payload může přetéci zásobník.
  • Vzájemné reference: A odkazuje na B a B na A je legální, ale obě schémata musí být nejprve registrována; jinak o výsledku rozhodne pořadí rozlišení.
  • Nullable větve: pokud rekurzivní pole může být null, napište "type": ["object", "null"], nebo ho obalte do oneOf. $ref nullability nezdědí.

7. Reference napříč soubory a bundling: nikdy nestahujte schémata v produkci

Reference napříč soubory často „prostě fungují“ ve vývoji, protože validátor stáhne cílové schéma podle $id přes síť. Změňte síťovou politiku při nasazení a celý validační řetěz se rozbije. Pravidlo pro produkci je jednoduché: každé referencované schéma musí být registrováno v procesu nebo předem zabalené.

import Ajv2020 from 'ajv/dist/2020';
import addressSchema from './address.json' with { type: 'json' };
import orderSchema from './order.json' with { type: 'json' };

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

// Nejprve registrujte referencovaný soubor; klíč musí odpovídat jeho $id, jinak relativní refs nebudou rozlišeny
ajv.addSchema(addressSchema, 'https://bbdrug.com/schemas/address.json');

// Zkompilujte vstupní schéma jednou, na úrovni modulu
export const validateOrder = ajv.compile(orderSchema);

Pokud order.json má $id: "https://bbdrug.com/schemas/order.json", jeho "$ref": "address.json" se rozliší na https://bbdrug.com/schemas/address.json, což odpovídá registrovanému klíči. Jediný odlišný znak dává can't resolve reference.

Tři praktické přístupy se srovnávají takto:

Přístup Jak Přednost Cena
Registrace za běhu addSchema pro každý soubor Žádný build krok, soubory zůstávají nezávislé Pořadí registrace a klíče jsou ruční
Bundling při sestavení Sloučení pomocí ref parseru do jednoho souboru Žádné závislosti za běhu Větší artefakt, hlučné diffy
Asynchronní načítání loadSchema + compileAsync Zvládá často se měnící schémata Starosti o síť a cache; vyhněte se na kritických cestách

Pro frontendové projekty je bezpečnější bundling při sestavení. Na serveru stačí registrace, pokud test prokáže, že se každá reference rozliší. V obou případech spouštějte compile v CI, aby se chyby referencí objevily před sloučením, nikoli při prvním produkčním požadavku.

8. Pořadí ladění: rozlišit, zkompilovat, poté prozkoumat větve

Téměř každý problém s $ref lze lokalizovat ve třech fázích a pořadí záleží:

Příznak Pravděpodobná příčina Oprava
can't resolve reference ... from id ... Překlep v $id, špatný relativní základ, cíl není registrován Vypište $id a rozlišenou absolutní URI a srovnejte s registrovanými klíči
schema with key or id already exists Stejné $id registrováno dvakrát, nebo compile voláno opakovaně Přesuňte registraci do inicializace modulu a spusťte ji jednou
strict mode: unknown keyword: "definitions" Míchání syntaxe draft-07 Přejmenujte na $defs; nevypínejte striktní režim
Data odpovídají dvěma větvím oneOf Překrývající se omezení větví, žádné tag pole Přidejte const do každé větve, nebo přejděte k anyOf
Omezení uvnitř $defs nemají účinek Nic na ně neodkazuje Zkontrolujte cestu reference; $defs samo nevaliduje nic

Při ladění čtěte schemaPath dřív než message: pojmenovává větve a klíčové slovo, které selhalo, a je mnohem spolehlivější než anglická próza. Cachujte zkompilovaný validátor na úrovni modulu, místo abyste rekompilovali při každém požadavku. Pro každou změnu schématu pokryjte procházející i selhávající případ – testovat jen šťastnou cestu netestuje nic. Výběru validátoru a návrhu chybových zpráv se věnuje JSON Schema: od základů k produkci; zapojení schématu do generování typů Obousměrná synchronizace schématu a typů TypeScript.

9. Často kladené otázky

Může $ref žít vedle type nebo description?

2020-12 povoluje $ref vedle souvisejících klíčových slov a tato související klíčová slova se vyhodnocují normálně. V draft-07 jsou související klíčová slova ignorována a platí jen $ref. To je největší behaviorální rozdíl, když se stejné schéma používá napříč návrhy, takže před migrací potvrďte, který návrh váš validátor podporuje.

Mám použít $defs, nebo definitions?

Pro novou práci použijte $defs; přišlo s 2019-09. definitions není v 2020-12 klíčové slovo. Pointer jako $ref: "#/definitions/foo" se stále rozliší, protože je to obyčejný JSON Pointer, ale striktní režim ohlásí neznámé klíčové slovo. Míchání obou stylů zanechává minu, která exploduje až v CI.

Proč oneOf odmítá data, která vypadají platně?

Protože data splňují dvě větve najednou. oneOf vyžaduje přesně jednu a větve omezující jen část objektu je splněna, i když pole druhé větve chybí. Přidejte do každé větve const tag, aby byly vzájemně výlučné.

Budou rekurzivní reference opakovat donekonečna?

Ne. Validátory nerozbalují reference při kompilaci; rekurzují nad skutečnými daty. Skutečná rizika jsou data dost hluboká na přetečení zásobníku a špatné pořadí rozlišení mezi soubory, které na sebe odkazují. Obchodní limit hloubky u rekurzivních polí je účinnější než jakýkoli trik se schématem.

Může produkce spoléhat na vzdálené $ref?

Ne. Vzdálené reference svazují dostupnost validace se sítí: změny domén, vypršelé certifikáty i izolace interní sítě rozbijí řetěz. Pokud chcete předvídatelné chování, zabalte referencovaná schémata do artefaktu nebo je registrujte při startu.

Jak rozhodnout mezi $defs a samostatnými soubory?

Strukturu, která se objeví na dvou místech, lze kopírovat jednou; když se objeví na třetím místě, povyšte ji na $defs. Teprve když ji musí číst jiný jazyk nebo jiný repozitář, zaslouží si samostatný soubor s vlastním $id.

Proč omezení uvnitř $defs nemají účinek?

$defs je jen kontejner a uzly, na které neodkazuje žádný $ref, se validace nikdy neúčastní. Seznam required na začátku $defs bez příchozí reference stále nechá data projít – je to specifikace, nikoli chyba validátoru.

Mám dát additionalProperties: false do větví allOf?

Ne. Každá větve se vyhodnocuje nezávisle, takže jedna větve považuje pole deklarovaná jinou větví za nadbytečná. Přístup 2020-12 přesouvá politiku neznámých polí na nejvyšší úroveň a používá unevaluatedProperties: false, vyhodnocené jednou po všech větvích.