JSON Toolkit

Proč automaticky vygenerovaná JSON Schema stále odmítá skutečná data

Shrnutí: Vygenerovaná schémata často blokují platná data. Zjistěte, proč bohatší vzorky dávají horší schémata, a poté pracovní postup: vzorkování více záznamů, doplnění sémantiky, přesné určení chyb.

Středně pokročilý~14 min čteníPublikováno 2026-10-30Aktualizováno 2026-10-30
Titulní obrázek článku ilustrující konflikty mezi návrhem JSON-do-Schema a skutečnými daty

Stručně: nejprve ručně slaďte hranici required/optional – předejte 5–20 záznamů různých tvarů s vypnutým „all required“ – poté schéma vygenerujte a ručně doplňte format, enum a additionalProperties; jediný vzorek se všemi poli bude vždy příliš přísný.

Generátor vidí jen jediný záznam, který jste vložili. Čte „tento záznam má e-mail“ jako „každý záznam musí mít e-mail“, takže čím úplnější vzorky, tím více vyžaduje.

1. Generátor dělá strukturální mapování, nikoli porozumění obchodu

Předejte kus JSON generátoru a udělá přesně jednu věc: projde strukturu hodnot a přeloží každý uzel do klíčových slov schématu. Celá mapovací tabulka je pevná:

Hodnota ve vzorku Odvozený typ Poznámky
"hello" {"type": "string"} Nerozlišuje e-mail, datum, UUID
42 {"type": "number"} Celé číslo se neodvodí jako integer
3.14 {"type": "number"} Stejné jako výše
true {"type": "boolean"} —
null {"type": "null"} Rozpozná jen null, nikdy „nullable“
[1, "a"] {"type": "array", "items": {"type": ["number", "string"]}} Typy prvků se sjednotí
{"a": 1} {"type": "object", "properties": {...}, "required": [...]} Zde je jádro problému required

Všechny potíže jsou v posledním řádku. required se neodvozuje z typů; odvozuje se z „tento klíč existuje v tomto vzorku“. Když je vzorek jediný záznam, toto tvrzení je ekvivalentní „každý klíč je povinný“.

A data vrácená skutečnými API mají téměř vždy polovinu polí volitelných. Zde začíná konflikt.

2. Pět záznamů stejného tvaru, pět míry přijetí

Začněte měřením. Každý, kdo se podívá na pět záznamů níže, by řekl „mají stejný tvar“:

[
  {
    "id": 1,
    "name": "Ada",
    "email": "ada@example.com",
    "tags": ["a"],
    "score": 10
  },
  { "id": 2, "name": "Bob", "tags": [], "score": 7 },
  {
    "id": 3,
    "name": "Cy",
    "email": "cy@example.com",
    "tags": ["a", "b"],
    "score": 8.5
  },
  { "id": 4, "name": "Dee", "email": null, "tags": ["c"], "score": 9 },
  {
    "id": 5,
    "name": "Eve",
    "email": "eve@example.com",
    "tags": ["d"],
    "score": 6,
    "admin": true
  }
]

email se objevuje ve 3 z 5 záznamů, admin jen jednou a záznam 2 má prázdné pole tags.

Nyní vygenerujte schéma z každého záznamu zvlášť a poté každý výsledek použijte k validaci všech pěti záznamů (validátor: ajv 8):

Vygenerováno z Počet klíčů Vygenerované required Přijme z 5
Záznam 1 5 id, name, email, tags, score 3 záznamy
Záznam 2 4 id, name, tags, score 5 záznamů
Záznam 3 5 id, name, email, tags, score 3 záznamy
Záznam 4 5 id, name, email, tags, score 1 záznam
Záznam 5 6 id, name, email, tags, score, admin 1 záznam

Žádné z pěti schémat není správné, ale jediné, které přijímá všechna data, pochází ze záznamu 2 – záznamu, kterému chybí email. Vítězí z prostého důvodu: klíče, které se nikdy neobjeví, se nikdy nezapíšou do required.

Záznam 4 je ještě instruktivnější: jeho hodnota email je null, takže generátor natvrdo zapíše {"type": "null"} a ostatní čtyři záznamy s řetězcovými e-maily jsou odmítnuty.

3. Proč je „vložit nejúplnější záznam“ nejhorší volba

Intuice je „vlož vzorek s nejvíce poli a schéma bude kompletní“. Měření ukazují přesný opak – a třetí strana provedla rozsáhlejší kontrolovaný test.

absurdtools použil 3000 náhodných „datových rodin“, 8 záznamů na rodinu, s každým volitelným polem přítomným zhruba v polovině případů, a měřil, kolik dat schéma vygenerované ze vzorků s různým počtem klíčů dokáže přijmout:

Počet klíčů vzorku Podíl dat rodiny přijatých
2 100,0 %
3 56,8 %
4 34,8 %
5 23,4 %
6 17,7 %
7 14,9 %

Závěr není „mírný pokles“, ale úplný obrat: minimální vzorek produkoval nejvolnější schéma v 95 % rodin, zatímco nejúplnější vzorek to dokázal jen v 0,1 % případů.

Důvod spočívá v sémantice required, která znamená „všechno“; volitelnost lze pozorovat jen nepřítomností. Vložením záznamu se všemi poli řeknete generátoru „ani jeden z těchto klíčů nesmí být vynechán“; vložením záznamu s chybějícím polem mu předáte důkaz, že „tento klíč může chybět“.

Pamatujte si to v jedné větě: hodnota vzorku není úplnost, ale které nepřítomnosti pokrývá.

Kolik záznamů stačí

Stejná dávka experimentů také měřila rychlost konvergence: hromaděním záznamů jeden po druhém podle pravidla „klíč zůstane required jen tehdy, pokud ho má každý dosavadní záznam“, je medián počtu záznamů, kdy se required sblíží se skutečnou povinnou množinou, 3; 6 záznamů pokryje asi devadesát procent rodin a 12 pokryje všechny.

Takže vzorkování 5–10 záznamů denně je nejlepší poměr úsilí a přínosu; vynucovat dvacet nebo více dává velmi málo navíc.

Existuje však předpoklad: tyto záznamy musí pocházet ze skutečně různých situací. Pět záznamů zkopírovaných ze stejné šablony přestane poskytovat jakoukoli novou informaci už od druhého – jejich množiny polí jsou identické a required se nemůže zúžit ani o jeden klíč.

4. Oprava: předejte několik záznamů najednou a nechte mluvit nepřítomnosti

Protože vzorkování jediného záznamu je nutně příliš přísné, správný přístup je nechat generátor vidět více záznamů najednou a sblížit required pravidlem „required jen tehdy, pokud ho má každý záznam“.

Generátor na tomto webu přijímá pole přímo a má přepínač „all required“. Vypněte ho a rozhoduje podle počtu výskytů: jen klíče nesené každým záznamem se zapíšou do required.

Stále těch pět záznamů – tentokrát je spojte do pole a odvodte společně, poté vezměte items jako kontrakt prvku:

Předešlé záznamy Odvozené required Validace všech 5
1 záznam id, name, email, tags, score 3 projdou
2 záznamy id, name, tags, score 4 projdou
3 záznamy id, name, tags, score 4 projdou
4 záznamy id, name, tags, score 5 projde
5 záznamů id, name, tags, score 5 projde

required se sblíží hned po přidání čtvrtého záznamu a dál se nemění. email a admin jsou správně umístěny v properties, ale ne v required. A protože e-mail záznamu 4 je null, odvozování z více záznamů ho zkombinuje do {"type": ["string", "null"]}, takže projdou řetězce i null – což vzorkování jediného záznamu nikdy neumí.

Je to stejný řád jako zjištění absurdtools, že „required konverguje po mediánu 3 záznamů“.

Odvozování z více záznamů má ovšem své slepé skvrny: pokud je email v těchto záznamech náhodou null v každém (řekněme, že všechny pocházejí z účtů bez vyplněného profilu), výsledek odvození se stane {"type": "null"} a řetězcové e-maily jsou stále odmítnuty. Podobně, pokud je každá hodnota tags prázdné pole, typ prvku nelze odvodit vůbec.

Takže vzorkování má ještě jednu tvrdou podmínku: každá forma každého pole se musí objevit alespoň jednou. Přineste jeden řetězec, jedno číslo, jeden null a jedno prázdné pole; nenechte žádnou formu úplně chybět.

Konkrétně:

  1. Vytáhněte 5–20 odpovědí z logů, mocků nebo skutečných požadavků a určitě zahrňte abnormální řádky (ten s prázdnými poli, ten s null, ten s prázdným polem);
  2. Seskládejte je do JSON pole a vložte do generátoru;
  3. Vypněte „all required“ a vyberte draft 2020-12;
  4. Výsledkem je type: array plus items; vezměte část items jako kontrakt pro jeden záznam;
  5. Ihned validujte původní vzorky tímto schématem – 100 % musí projít. Pokud selže byť jeden, vaše vzorkování nestačí; vraťte se a přidejte záznamy.

Krok 5 je nejlevnější sebekontrola. Pokud generátor nedokáže pojmout ani data, která jste mu předali, vaše metoda vzorkování je špatná a nemá smysl pokračovat.

5. Čtyři věci, které generátor nemůže vyplnit – musíte je napsat ručně

I po předání více záznamů následující stále nelze odvodit a musí se doplnit ručně:

Chybějící část Naměřené chování Důsledek Jak opravit
Formát řetězce "a@b.com" produkuje jen {"type": "string"} Validace "not an email" projde Přidejte "format": "email" a zapněte asserty formátu
Rozsahy a výčty Nevidí, že score je omezen na 0–100, ani že status má jen tři hodnoty Záporná čísla, obrovské hodnoty i nesmyslné výčtové hodnoty projdou Přidejte minimum / maximum / enum
Politika neznámých polí additionalProperties se negeneruje Překlep v klíči je tiše přijat, což je nebezpečnější než chyba Přidejte "additionalProperties": false pro konfigurační soubory
Sémantika nullability a prázdných kolekcí Jediný vzorek obsahující null se uzamkne na {"type": "null"}; prázdné pole produkuje "items": {} Platné hodnoty jsou odmítnuty, nebo prvky pole zcela neomezené Změňte na ["string", "null"] a prázdným polím dejte skutečné prvky

Třetí řádek si zaslouží zvláštní zmínku: výchozí hodnota additionalProperties je „povolit“. To znamená, že konfigurační soubor, který napíše timeout jako timeuot, projde validací a poté za běhu tiše nic neudělá. Pro konfigurační schémata je to položka, kterou musíte explicitně vypnout.

format má také past: od draft 2019-09 patří format do anotačního slovníku a ve výchozím nastavení jen anotuje, místo aby vynucoval. Pokud jste přidali format: "email" a neplatné hodnoty stále projdou, v osmi z deseti případů validátor nezapnul validateFormats nebo odpovídající plugin formátu – není to chyba schématu.

Poznámka k volbě draftu, když už jsme u toho: pokud schéma jde do OpenAPI 3.1, musí být draft 2020-12, protože 3.1 tuto verzi přímo zahrnuje; starší nástrojové řetězce (ekosystém Swagger 2.0 a některé implementace $ref) stále těží na draft-07. Oba se chovají odlišně u souvisejících klíčových slov $ref a u klíčových slov souvisejících s poli, takže je nemíchejte v jedné sadě schémat.

6. U nezdokumentovaných API třetích stran zpětně odvodte kontrakt ze vzorků odpovědí

Integrace callbacku třetí strany, práce s partnerem, který nepíše dokumentaci, nebo údržba deset let starého legacy systému – to vše znamená „data jsou v ruce, specifikace ne“. Zde je generátor nejen pohodlím; je nejrychlejší cestou ke kontraktu.

Metoda je stejná jako výše, se dvěma povinnostmi navíc:

Za prvé: vzorkujte napříč časem a napříč stavy. Pole třetích stran se často mění s obchodním stavem – propagace přidá discount, vrácená objednávka přidá refundAt a chybová odpověď nemá data vůbec. Zachycení jediného snapshotu znamená vidět jen jeden stav.

Předejte spolu několik úspěchů, několik selhání a několik prázdných výsledků: error a data se často objevují vzájemně výlučně a odvozování z více záznamů dá obojí do properties, zatímco mimo required je nechá – přesně odpovídá realitě.

Za druhé: uložte schéma do repozitáře a přiložte denní kontrolu. Validujte proti němu dnešní skutečné odpovědi a o polích, která tiše mizí nebo mění typ, se dozvíte dřív než vaši uživatelé.

// Spusťte jednou denně: alert na drift kontraktu
const drifted = records.filter((record) => !validate(record));

if (drifted.length > 0) {
  console.warn(
    `${drifted.length} responses do not match the contract; first difference:`,
    validate.errors,
  );
}

Hodnota této praxe není „ušetřit úsilí ručního psaní“, ale změnit verbální dohodu ve vykonatelný alert. Třetí strana vám nikdy předem neřekne, že změnila pole, ale změna se určitě objeví ve vašich logech.

Jednu hranici je třeba jasně vytyčit: zpětně odvozené schéma popisuje, co protější strana vrací nyní, nikoli co slibuje vracet. První je pro monitoring; do kontraktu nebo dokumentace API patří jen druhé.

7. Po doplnění nechte validační chyby ukazovat na konkrétní pole

Jakmile je schéma použitelné, další starostí jsou čitelné chyby. Chybový objekt ajv má pět polí a první čtyři jsou ta užitečná:

Pole Význam Příklad (id: 0 porušující minimum: 1)
instancePath JSON Pointer na problematickou hodnotu v datech "/id"
schemaPath Umístění spouštějícího pravidla ve schématu "#/properties/id/minimum"
keyword Název selhavšího omezení "minimum"
params Strojově čitelné podrobnosti toho omezení { "comparison": ">=", "limit": 1 }
message Anglická próza, která se mezi verzemi mění, neparsujte ji "must be >= 1"

Jedna asymetrie je nejsnadnější zakopnutí: když selže required, instancePath ukazuje na rodičovský objekt (obvykle prázdný řetězec, tedy kořen), nikoli na chybějící pole. Název chybějícího klíče je v params.missingProperty. Podobně, když selže additionalProperties, název nadbytečného klíče je v params.additionalProperty.

Takže při mapování chyb na „pole → zpráva“ nejprve ošetřete tyto dva zvláštní případy:

import Ajv from "ajv";

const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);

/** Převede pole chyb ajv na { fieldPath: [message] } pro zvýraznění formuláře */
function groupByField(errors) {
  const grouped = {};

  for (const error of errors) {
    // U required a additionalProperties žije problematické pole v params, nikoli v instancePath
    const field =
      error.keyword === "required"
        ? error.params.missingProperty
        : error.keyword === "additionalProperties"
          ? error.params.additionalProperty
          : error.instancePath.replace(/^\//u, "").split("/").join(".") ||
            "(root)";

    (grouped[field] ??= []).push(error.message);
  }

  return grouped;
}

if (!validate(data)) console.log(groupByField(validate.errors));

allErrors: true není volitelné. Ve výchozí konfiguraci se ajv zastaví u první chyby, takže uživatel opraví jedno pole, odešle znovu, dostane další chybu a potřebuje tři kola jen k vyplnění jednoho formuláře. Pokud používáte také ajv-errors pro vlastní zprávy, bez tohoto přepínače plugin tiše nahradí jen některé zprávy.

8. Znovupoužitelný zaváděcí postup

Navlečte výše uvedené kroky do pevné rutiny a dodržujte ji při každé integraci nového API:

  1. Vzorkujte: vezměte 5–20 skutečných odpovědí a záměrně vyberte ty s prázdnými poli, null, prázdnými poli a nadbytečnými poli;
  2. Odvodte: vložte je společně jako pole do generátoru, vypněte „all required“ a vezměte items;
  3. Doplňte sémantiku: přidejte format, enum, minimum/maximum, minItems a additionalProperties jednu položku po druhé;
  4. Sebekontrola: původní vzorky musí projít 100%, jinak se vraťte ke kroku 1 a přidejte vzorky;
  5. Negativní důkaz: připravte pro každé omezení vstup, který „má selhat“, a potvrďte, že skutečně selže – testovat jen cestu úspěchu je stejné jako netestovat;
  6. Commit: dejte schéma do vercového systému, selhávající vzorky držte jako testovací případy a při každé změně pole v API spusťte testy první.

Negativní testování z kroku 5 se nejsnadněji přeskakuje. Přidat klíč do required, aniž byste kdy ověřili, že chybějící klíč chybuje, znamená sázet na to, že validátor souhlasí s vaším mentálním modelem.

import { describe, expect, it } from "vitest";

describe("user schema", () => {
  it("přijme kompletní záznam", () => {
    expect(validate({ id: 1, name: "Ada", tags: [], score: 10 })).toBe(true);
  });

  it("odmítne záznam bez name", () => {
    expect(validate({ id: 2, tags: [], score: 7 })).toBe(false);
    // Tvrďte také důvod, aby test neprošel kvůli selhání jiného pole
    expect(
      validate.errors?.some((e) => e.params?.missingProperty === "name"),
    ).toBe(true);
  });
});

Poslední assert záleží. Tvrzení jen false může zůstat zelené, i když se schéma už rozbilo – selhání může mezitím přijít z úplně jiné příčiny.

9. Časté dotazy

Může schéma vygenerované generátorem jít rovnou do produkce

Ne. Je to jen snapshot struktury. Musíte minimálně přidat formáty, rozsahy, výčty a politiku neznámých polí a poté jím prohnat původní vzorky i negativní vzorky. Generátor řeší „vypisovat properties ručně je otravné“, nikoli „rozhoduje o obchodních pravidlech“.

Systému klíčových slov při psaní schématu, rozdělování pomocí $ref a kompozičním klíčovým slovům se věnuje JSON Schema: od základů k produkci.

Proč záznam s nejvíce poli odmítá více dat

Protože required se sbírá z „co se objevilo ve vzorku“. Vzorek se všemi poli mění každý volitelný klíč v povinný, zatímco většina těch klíčů ve skutečných datech chybí. Předání více záznamů, nebo záznamu, který záměrně pole vynechává, vás dostane blíž pravdě než nejúplnější vzorek.

Odvodí se celé číslo jako integer

Generátor na tomto webu to nedělá: celá i desetinná čísla vypisuje jako number, takže schéma vygenerované z {"price": 5} přijme 5.99. Tím se vyhnete nehodám typu „vzorek náhodou byl celé číslo, takže produkce odmítá ceny s desetinnými místy“. Některé nástroje třetích stran integer odvozují, takže u peněžních polí buďte obzvlášť opatrní.

Podrobnosti o přesnosti čísel a hranicích typů najdete v Šest datových typů JSON a úskalí přesnosti čísel.

Přidal jsem format, ale nemá účinek

Od draft 2019-09 je format ve výchozím nastavení jen anotace. Buď v validátoru explicitně zapněte asserty formátu (ajv potřebuje ajv-formats), nebo použijte pattern pro tvrdé omezení. Nepředpokládejte, že přidání cokoli zablokuje.

required hlásí chybu, ale pole je v datech zjevně přítomné

Podívejte se nejprve na instancePath. Chyba required ukazuje na rodičovský objekt, což znamená, že chybějící klíč není na té úrovni – časté příčiny jsou špatná úroveň vnoření (/profile/name není totéž jako name v kořeni), rozdílná velikost písmen (userId versus userID), nebo upstream serializace úplně zahazuje undefined.

Než začnete vyšetřovat, potvrďte, že dokument sám je platný JSON: komentáře, koncové čárky a podobné problémy selhávají ve fázi parsování, dřív než se schéma vůbec dostane ke slovu. Parsování a formátování JSON ukáže přímo na problematický řádek.

Kolik záznamů mám vzorkovat a který draft použít?

Vzorkování 5–10 záznamů denně dává nejlepší poměr; required konverguje po mediánu 3 záznamů a 12 záznamů pokryje každou rodinu. Každý záznam musí pocházet ze skutečně jiné situace a každá forma každého pole se musí objevit alespoň jednou. Zafixujte draft na 2020-12, protože OpenAPI 3.1 přijímá jen tuto verzi.

Proč je benevolentní výchozí hodnota additionalProperties nebezpečnější?

Výchozí nastavení nevydává žádné klíčové slovo, takže překlepený klíč je tiše přijat, projde validací a poté selže za běhu – hůř diagnostikovatelné než přímá chyba. Konfigurační schémata musí explicitně nastavit "additionalProperties": false, zatímco API schémata by měla nejprve zvážit náklady na kompatibilitu.

Mám zpětně odvozené schéma použít jako tvrdou bránu?

Ne. Zpětně odvozené schéma popisuje, co protější strana vrací nyní, nikoli co slibuje vracet, takže patří do monitoringu a alertů na drift. Cokoli jde do kontraktu nebo dokumentace API, musí být slibovaný kontrakt.