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.

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ě:
- 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);
- Seskládejte je do JSON pole a vložte do generátoru;
- Vypněte „all required“ a vyberte draft 2020-12;
- Výsledkem je
type: arrayplusitems; vezměte částitemsjako kontrakt pro jeden záznam; - 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:
- 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;
- Odvodte: vložte je společně jako pole do generátoru, vypněte „all required“ a vezměte
items; - Doplňte sémantiku: přidejte
format,enum,minimum/maximum,minItemsaadditionalPropertiesjednu položku po druhé; - Sebekontrola: původní vzorky musí projít 100%, jinak se vraťte ke kroku 1 a přidejte vzorky;
- 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;
- 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.