JSON Toolkit

Mengapa JSON Schema Dijana Automatik Terus Menolak Data Sebenar

Inti pati: Schema terjana sering menyekat data yang sah. Ketahui mengapa contoh yang lebih kaya menghasilkan Schema yang lebih teruk, kemudian aliran kerja: pensampelan berbilang rekod, penyempurnaan semantik, penentuan lokasi ralat.

Pertengahan~14 min bacaanDiterbitkan 2026-10-30Dikemas kini 2026-10-30
Kulit yang menggambarkan konflik antara draf JSON-ke-Schema dan data sebenar

Intinya: satukan dahulu sempadan required/optional dengan tangan — suapkan 5–20 rekod pelbagai bentuk dengan "semua wajib" dimatikan — kemudian jana Schema dan tambah format, enum, dan additionalProperties secara manual; satu contoh penuh medan akan sentiasa terlalu ketat.

Penjana hanya boleh melihat satu rekod yang anda tampal. Ia membaca "rekod ini mempunyai e-mel" sebagai "setiap rekod mesti mempunyai e-mel", jadi semakin lengkap contoh anda, semakin banyak ia menuntut.

1. Penjana melakukan pemetaan struktur, bukan pemahaman perniagaan

Berikan sekeping JSON kepada penjana dan ia melakukan tepat satu perkara: menelusuri struktur nilai dan menterjemah setiap nod menjadi kata kunci Schema. Keseluruhan jadual pemetaan adalah tetap:

Nilai dalam contoh Jenis yang disimpulkan Nota
"hello" {"type": "string"} Tidak membezakan e-mel, tarikh, UUID
42 {"type": "number"} Integer tidak disimpulkan sebagai integer
3.14 {"type": "number"} Sama seperti di atas
true {"type": "boolean"} —
null {"type": "null"} Mengenal hanya null, tidak pernah "nullable"
[1, "a"] {"type": "array", "items": {"type": ["number", "string"]}} Jenis elemen digabungkan
{"a": 1} {"type": "object", "properties": {...}, "required": [...]} required ialah titik penting di sini

Semua masalah terdapat pada baris terakhir. required tidak diperoleh daripada jenis; ia diperoleh daripada "kunci ini wujud dalam contoh ini". Apabila contoh ialah satu rekod, pernyataan itu bersamaan dengan "setiap kunci adalah wajib".

Dan data yang dipulangkan oleh API sebenar hampir selalu mempunyai separuh medannya pilihan. Di situlah konflik bermula.

2. Lima rekod berbentuk sama, lima kadar penerimaan

Mulakan dengan pengukuran. Sesiapa yang melihat lima rekod di bawah akan berkata "ini bentuk yang sama":

[
  {
    "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 muncul dalam 3 daripada 5 rekod, admin muncul hanya sekali, dan rekod 2 mempunyai tatasusunan tags kosong.

Sekarang jana Schema daripada setiap rekod secara individu, kemudian gunakan setiap hasil untuk mengesahkan kelima-lima rekod (pengesah: ajv 8):

Dijana daripada Bilangan kunci required terjana Menerima daripada 5
Rekod 1 5 id, name, email, tags, score 3 rekod
Rekod 2 4 id, name, tags, score 5 rekod
Rekod 3 5 id, name, email, tags, score 3 rekod
Rekod 4 5 id, name, email, tags, score 1 rekod
Rekod 5 6 id, name, email, tags, score, admin 1 rekod

Tiada satu pun daripada lima Schema itu yang betul, tetapi satu-satunya yang menerima semua data datang daripada rekod 2 — rekod yang tiada email. Ia menang atas sebab mudah: kunci yang tidak pernah muncul tidak pernah ditulis ke dalam required.

Rekod 4 lebih memberi pengajaran: nilai email-nya ialah null, jadi penjana mengekod keras {"type": "null"}, dan empat rekod lain dengan e-mel rentetan semuanya ditolak.

3. Mengapa "tampal rekod paling lengkap" ialah pilihan terburuk

Intuisi ialah "tampal contoh dengan medan terbanyak dan Schema akan lengkap". Pengukuran menunjukkan sebaliknya — dan pihak ketiga menjalankan ujian terkawal yang lebih besar.

absurdtools menggunakan 3000 "keluarga data" rawak, 8 rekod setiap keluarga, dengan setiap medan pilihan hadir kira-kira separuh masa, dan mengukur berapa banyak data yang boleh diterima oleh Schema yang dijana daripada contoh dengan bilangan kunci berbeza:

Bilangan kunci contoh Bahagian data keluarga yang diterima
2 100.0%
3 56.8%
4 34.8%
5 23.4%
6 17.7%
7 14.9%

Kesimpulannya bukan "penurunan sedikit" tetapi pembalikan sepenuhnya: contoh minimum menghasilkan Schema paling longgar dalam 95% keluarga, sedangkan contoh paling lengkap hanya berjaya 0.1% masa.

Sebabnya terletak pada semantik required, yang bermaksud "semua"; keoptionalan hanya boleh diperhatikan melalui ketiadaan. Menampal rekod dengan setiap medan memberitahu penjana "tidak satu pun daripada kunci ini boleh ditinggalkan"; menampal rekod dengan medan yang tiada memberikannya bukti bahawa "kunci ini mungkin tiada".

Ingat dalam satu ayat: nilai sesuatu contoh bukanlah kelengkapan, tetapi ketiadaan yang diliputinya.

Berapa banyak rekod yang mencukupi

Kumpulan eksperimen yang sama juga mengukur kelajuan penumpuan: mengumpul rekod satu demi satu di bawah peraturan "kunci kekal required hanya jika setiap rekod setakat ini memilikinya", bilangan median rekod untuk required menumpu kepada set required sebenar ialah 3; 6 rekod meliputi kira-kira sembilan puluh peratus keluarga, dan 12 meliputi kesemuanya.

Jadi pensampelan 5–10 rekod sehari-hari ialah nilai terbaik untuk usaha; memaksa dua puluh atau lebih memberikan tambahan yang sangat sedikit.

Tetapi ada prasyarat: rekod-rekod ini mesti datang daripada situasi yang benar-benar berbeza. Lima rekod yang disalin daripada templat yang sama berhenti memberikan maklumat baharu daripada yang kedua dan seterusnya — set medan mereka sama, dan required tidak boleh menyempit walaupun satu kunci.

4. Pembaikan: suapkan beberapa rekod sekali gus dan biarkan ketiadaan bercakap

Memandangkan pensampelan satu rekod semestinya terlalu ketat, pendekatan yang betul ialah membiarkan penjana melihat berbilang rekod sekali gus dan menumpukan required dengan peraturan "wajib hanya jika setiap rekod memilikinya".

Penjana di laman ini menerima tatasusunan secara langsung dan mempunyai togol "semua wajib". Matikannya dan ia memutuskan mengikut kiraan kemunculan: hanya kunci yang dibawa oleh setiap rekod ditulis ke dalam required.

Masih kelima-lima rekod itu — kali ini gabungkan mereka menjadi tatasusunan dan simpulkan bersama, kemudian ambil items sebagai kontrak elemen:

Rekod yang disuapkan required yang disimpulkan Pengesahan kelima-lima
1 rekod id, name, email, tags, score 3 lulus
2 rekod id, name, tags, score 4 lulus
3 rekod id, name, tags, score 4 lulus
4 rekod id, name, tags, score 5 lulus
5 rekod id, name, tags, score 5 lulus

required menumpu sebaik sahaja rekod keempat ditambah, dan tidak berubah selepasnya. email dan admin diletakkan dengan betul dalam properties tetapi tidak dalam required. Dan kerana e-mel rekod 4 ialah null, inferens berbilang rekod menggabungkannya menjadi {"type": ["string", "null"]}, jadi kedua-dua rentetan dan null lulus — sesuatu yang pensampelan satu rekod tidak pernah boleh lakukan.

Ini ialah urutan magnitud yang sama dengan penemuan absurdtools bahawa "required menumpu selepas median 3 rekod".

Inferens berbilang rekod mempunyai titik buta sendiri, walaupun: jika email dalam rekod ini kebetulan null dalam setiap satu (katakan semuanya datang daripada akaun yang tiada profil diisi), hasil inferens menjadi {"type": "null"}, dan e-mel rentetan masih ditolak. Begitu juga, jika setiap nilai tags ialah tatasusunan kosong, jenis elemen tidak dapat disimpulkan sama sekali.

Jadi pensampelan mempunyai satu lagi keperluan keras: setiap bentuk setiap medan mesti muncul sekurang-kurangnya sekali. Bawa satu rentetan, satu nombor, satu null, dan satu tatasusunan kosong; jangan biarkan mana-mana bentuk benar-benar tiada.

Secara konkrit:

  1. Tarik 5–20 respons daripada log, olok-olok, atau permintaan sebenar, dan pastikan termasuk baris abnormal (yang dengan medan kosong, yang dengan null, yang dengan tatasusunan kosong);
  2. Himpunkan mereka menjadi tatasusunan JSON dan tampal ke dalam penjana;
  3. Matikan "semua wajib" dan pilih draft 2020-12;
  4. Hasilnya ialah type: array ditambah items; ambil bahagian items sebagai kontrak untuk satu rekod;
  5. Segera sahkan contoh asal dengan Schema ini — 100% mesti lulus. Jika walaupun satu gagal, pensampelan anda tidak mencukupi; kembali dan tambah rekod.

Langkah 5 ialah semakan kendiri paling murah. Jika penjana tidak dapat menampung data yang anda suapkan, kaedah pensampelan anda salah, dan tiada gunanya meneruskan.

5. Empat perkara yang tidak dapat diisi oleh penjana — anda mesti menulisnya sendiri

Walaupun selepas menyuapkan berbilang rekod, perkara berikut masih tidak dapat disimpulkan dan mesti ditambah secara manual:

Bahagian yang hilang Tingkah laku yang diukur Akibat Cara membaiki
Format rentetan "a@b.com" menghasilkan hanya {"type": "string"} Mengesahkan "not an email" lulus Tambah "format": "email" dan dayakan penegasan format
Julat dan enum Tidak dapat melihat bahawa score terhad kepada 0–100, mahupun bahawa status hanya mempunyai tiga nilai Nombor negatif, nilai besar, dan nilai enum sampah semuanya lulus Tambah minimum / maximum / enum
Dasar medan tidak diketahui additionalProperties tidak dijana Kunci yang salah eja diterima secara senyap, yang lebih berbahaya daripada ralat Tambah "additionalProperties": false untuk fail gaya konfigurasi
Semantik kebolehnolan dan koleksi kosong Satu contoh yang mengandungi null terkunci kepada {"type": "null"}; tatasusunan kosong menghasilkan "items": {} Nilai sah ditolak, atau elemen tatasusunan tidak dikawal sepenuhnya Tukar kepada ["string", "null"], dan berikan tatasusunan kosong elemen sebenar

Baris ketiga wajar disebut secara berasingan: nilai lalai additionalProperties ialah "benarkan". Itu bermaksud fail konfigurasi yang mengeja timeout sebagai timeuot lulus pengesahan dan kemudian secara senyap tidak melakukan apa-apa pada masa jalan. Untuk Schema gaya konfigurasi, ini ialah item yang mesti anda matikan secara eksplisit.

format juga mempunyai perangkap: sejak draft 2019-09, format tergolong dalam perbendaharaan kata anotasi, dan secara lalai ia hanya membuat anotasi dan bukannya menguatkuasakan. Jika anda menambah format: "email" dan nilai tidak sah masih lulus, dalam lapan daripada sepuluh kes pengesah belum mendayakan validateFormats atau pemalam format yang berkaitan — bukan Schema itu yang salah.

Nota tentang pilihan draft sementara kita di sini: jika Schema akan masuk ke OpenAPI 3.1, ia mesti draft 2020-12, kerana 3.1 membenamkan versi itu secara langsung; rantai alat lama (ekosistem Swagger 2.0 dan sesetengah pelaksanaan $ref) masih berpusat pada draft-07. Kedua-duanya berkelakuan berbeza untuk kata kunci adik-beradik $ref dan untuk kata kunci berkaitan tatasusunan, jadi jangan campurkan mereka dalam satu set Schema.

6. Untuk API pihak ketiga tanpa dokumentasi, kejuruteraan balik kontrak daripada contoh respons

Mengintegrasikan panggilan balik pihak ketiga, bekerja dengan rakan kongsi yang tidak menulis dokumentasi, atau menyelenggara sistem lama berusia sepuluh tahun — semuanya bermaksud "data ada di tangan, spesifikasi tiada". Di sini penjana bukan sekadar kemudahan; ia ialah laluan terpantas kepada kontrak.

Kaedahnya sama seperti di atas, dengan dua tambahan wajib:

Satu: sampel merentas masa dan merentas keadaan. Medan pihak ketiga sering berubah mengikut keadaan perniagaan — promosi menambah discount, pesanan bayaran balik menambah refundAt, dan respons ralat tidak mempunyai data sama sekali. Mengambil satu petikan bermaksud melihat hanya satu keadaan.

Suapkan beberapa kejayaan, beberapa kegagalan, dan beberapa hasil kosong bersama-sama: error dan data sering muncul secara saling eksklusif, dan inferens berbilang rekod meletakkan kedua-duanya dalam properties sambil meninggalkan mereka di luar required — tepat sepadan dengan realiti.

Dua: simpan Schema dalam repositori dan lampirkan semakan harian. Sahkan respons sebenar hari ini terhadapnya, dan anda akan mengetahui tentang medan yang hilang secara senyap atau berubah jenis sebelum pengguna anda mengetahuinya.

// Run once a day: contract-drift alert
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,
  );
}

Nilai amalan ini bukan "menjimatkan usaha menulisnya dengan tangan", tetapi menukar persetujuan lisan menjadi amaran yang boleh dilaksanakan. Pihak ketiga tidak pernah memberitahu anda lebih awal bahawa ia mengubah sesuatu medan, tetapi perubahan itu pasti akan muncul dalam log anda.

Satu sempadan yang perlu dilukis dengan jelas: Schema hasil kejuruteraan balik menerangkan apa yang pihak lain pulangkan sekarang, bukan apa yang ia janjikan untuk pulangkan. Yang dahulu untuk pemantauan; hanya yang kedua tergolong dalam kontrak atau dokumentasi API.

7. Selepas selesai, buat ralat pengesahan menunjuk kembali kepada medan tertentu

Sebaik sahaja Schema boleh digunakan, kebimbangan seterusnya ialah ralat yang mudah dibaca. Objek ralat ajv mempunyai lima medan, dan empat yang pertama ialah yang berguna:

Medan Maksud Contoh (id: 0 melanggar minimum: 1)
instancePath JSON Pointer kepada nilai yang melanggar dalam data "/id"
schemaPath Lokasi peraturan pencetus dalam Schema "#/properties/id/minimum"
keyword Nama kekangan yang gagal "minimum"
params Butiran kekangan itu yang boleh dibaca mesin { "comparison": ">=", "limit": 1 }
message Prosa bahasa Inggeris yang berubah antara versi, jangan huraikannya "must be >= 1"

Satu asimetri paling mudah tersandung: apabila required gagal, instancePath menunjuk kepada objek induk (biasanya rentetan kosong, iaitu akar), bukan kepada medan yang tiada. Nama kunci yang tiada berada dalam params.missingProperty. Begitu juga, apabila additionalProperties gagal, nama kunci tambahan berada dalam params.additionalProperty.

Jadi apabila memetakan ralat kepada "medan → mesej", tangani dua kes khas ini dahulu:

import Ajv from "ajv";

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

/** Convert the ajv error array into { fieldPath: [message] } for form highlighting */
function groupByField(errors) {
  const grouped = {};

  for (const error of errors) {
    // For required and additionalProperties, the offending field lives in params, not in 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 bukan pilihan. Di bawah konfigurasi lalai ajv berhenti pada ralat pertama, jadi pengguna membaiki satu medan, menghantar semula, mendapat ralat seterusnya, dan memerlukan tiga perjalanan pergi balik hanya untuk mengisi satu borang. Jika anda juga menggunakan ajv-errors untuk mesej tersuai, tanpa bendera ini pemalam akan secara senyap menggantikan hanya sebahagian mesej.

8. Proses pelaksanaan yang boleh digunakan semula

Rentikan langkah-langkah di atas menjadi rutin tetap dan ikutinya setiap kali anda mengintegrasikan API baharu:

  1. Sampel: ambil 5–20 respons sebenar, dengan sengaja memilih yang mempunyai medan kosong, null, tatasusunan kosong, dan medan tambahan;
  2. Simpulkan: tampal mereka bersama sebagai tatasusunan ke dalam penjana, matikan "semua wajib", dan ambil items;
  3. Lengkapkan semantik: tambah format, enum, minimum/maximum, minItems, dan additionalProperties satu item demi satu;
  4. Semakan kendiri: contoh asal mesti lulus 100%, jika tidak kembali ke langkah 1 dan tambah sampel;
  5. Bukti negatif: sediakan input "sepatutnya gagal" untuk setiap kekangan dan sahkan ia benar-benar gagal — menguji hanya laluan yang lulus sama seperti tidak menguji;
  6. Komit: letakkan Schema dalam kawalan versi, simpan sampel yang gagal sebagai kes ujian, dan jalankan ujian dahulu setiap kali API mengubah medan.

Ujian negatif langkah 5 paling mudah dilangkau. Menambah kunci pada required tanpa pernah mengesahkan bahawa kunci yang tiada menimbulkan ralat bermaksud mempertaruhkan bahawa pengesah bersetuju dengan model mental anda.

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

describe("user schema", () => {
  it("accepts a complete record", () => {
    expect(validate({ id: 1, name: "Ada", tags: [], score: 10 })).toBe(true);
  });

  it("rejects a record without name", () => {
    expect(validate({ id: 2, tags: [], score: 7 })).toBe(false);
    // Also assert the reason, so the test doesn't pass because some other field failed
    expect(
      validate.errors?.some((e) => e.params?.missingProperty === "name"),
    ).toBe(true);
  });
});

Penegasan terakhir itu penting. Menegaskan hanya false boleh kekal hijau walaupun selepas Schema rosak — kegagalan itu mungkin datang daripada punca yang sama sekali berbeza pada masa itu.

9. FAQ

Bolehkah Schema yang dihasilkan penjana terus ke pengeluaran

Tidak. Ia hanya petikan struktur. Anda mesti sekurang-kurangnya menambah format, julat, enum, dan dasar medan tidak diketahui, kemudian jalankan kedua-dua contoh asal dan contoh negatif melaluinya. Penjana menyelesaikan "menaip properties dengan tangan membosankan", bukan "ia memutuskan peraturan perniagaan".

Untuk sistem kata kunci penulisan Schema, pemisahan $ref, dan kata kunci komposisi, lihat JSON Schema: Daripada Asas kepada Pengeluaran.

Mengapa rekod dengan medan terbanyak menolak lebih banyak data

Kerana required dikumpulkan daripada "apa yang muncul dalam contoh". Contoh penuh medan menukar setiap kunci pilihan menjadi wajib, sedangkan kebanyakan kunci itu tiada dalam data sebenar. Menyuapkan berbilang rekod, atau rekod yang sengaja meninggalkan medan, membawa anda lebih dekat kepada kebenaran daripada contoh paling lengkap.

Adakah integer akan disimpulkan sebagai integer

Penjana di laman ini tidak melakukan itu: kedua-dua integer dan nombor perpuluhan dikeluarkan sebagai number, jadi Schema yang dijana daripada {"price": 5} menerima 5.99. Itu mengelakkan kemalangan seperti "contoh kebetulan integer, jadi pengeluaran menolak harga dengan perpuluhan". Sesetengah alat pihak ketiga memang menyimpulkan integer daripada nilai, jadi berhati-hati terutamanya dengan medan wang.

Untuk butiran tentang ketepatan nombor dan sempadan jenis, lihat Enam jenis data JSON dan perangkap ketepatan nombor.

Saya menambah format tetapi ia tidak berkesan

Sejak draft 2019-09, format adalah anotasi sahaja secara lalai. Sama ada dayakan penegasan format secara eksplisit dalam pengesah (ajv memerlukan ajv-formats), atau gunakan pattern untuk kekangan keras. Jangan anggap bahawa menambahnya akan menyekat apa-apa.

required melaporkan ralat, tetapi medan itu jelas ada dalam data

Lihat instancePath dahulu. Ralat required menunjuk kepada objek induk, yang bermaksud kunci yang tiada bukan pada tahap itu — punca biasa ialah tahap penyarangan yang salah (/profile/name tidak sama dengan name pada akar), perbezaan huruf besar (userId berbanding userID), atau penstriman huluan menggugurkan undefined sepenuhnya.

Sebelum menyiasat, sahkan bahawa dokumen itu sendiri ialah JSON yang sah: komen, koma mengekor, dan isu serupa gagal pada peringkat penghuraian, sebelum Schema mendapat peluang. Hurai & Format JSON menunjuk terus kepada baris yang melanggar.

Berapa banyak rekod yang perlu saya sampel, dan draft mana yang perlu saya gunakan?

Pensampelan 5–10 rekod sehari-hari memberikan nilai terbaik; required menumpu pada median 3 rekod, dan 12 rekod meliputi setiap keluarga. Setiap rekod mesti datang daripada situasi yang benar-benar berbeza, dan setiap bentuk setiap medan mesti muncul sekurang-kurangnya sekali. Kunci draft kepada 2020-12, kerana OpenAPI 3.1 hanya menerima versi itu.

Mengapa lalai additionalProperties yang permisif lebih berbahaya?

Lalai tidak mengeluarkan kata kunci sama sekali, jadi kunci yang salah eja diterima secara senyap, lulus pengesahan, dan kemudian gagal pada masa jalan — lebih sukar didiagnosis daripada ralat langsung. Schema gaya konfigurasi mesti menetapkan "additionalProperties": false secara eksplisit, sedangkan Schema API harus menimbang kos keserasian dahulu.

Patutkah saya menggunakan Schema hasil kejuruteraan balik sebagai pintu keras?

Tidak. Schema hasil kejuruteraan balik menerangkan apa yang pihak lain pulangkan sekarang, bukan apa yang ia janjikan untuk pulangkan, jadi ia tergolong dalam pemantauan dan amaran penyimpangan. Apa sahaja yang masuk ke dalam kontrak atau dokumentasi API mesti kontrak yang dijanjikan.