Menggunakan Semula JSON Schema: $ref, $defs, dan Komposisi allOf/oneOf
Inti pati: $ref menyelesaikan penggunaan semula, allOf dan oneOf menyelesaikan komposisi, dan menimbun kedua-duanya ialah tempat sesuatu rosak. Merangkumi $id, $defs, bersyarat, rekursi, penggabungan dan penyahpepijatan.

Intinya: ekstrak struktur kongsi ke dalam $defs dengan $ref dahulu, kemudian gunakan allOf/oneOf; apabila cabang boleh sepadan serentak, gunakan anyOf atau tag const, dan simpan oneOf untuk kes yang benar-benar memerlukan tepat satu padanan.
Salin-tampal menjimatkan sepuluh minit dan merugikan anda dengan pencabangan senyap tiga bulan kemudian, apabila tiada siapa yang ingat salinan mana yang betul.
1. Tiga lapisan penggunaan semula — pilih satu sebelum anda memisahkan fail
Naluri pertama, apabila medan mula bertambah, ialah memasukkan setiap kekangan ke dalam satu fail. Ia ialah cara terpantas untuk menghantar, tetapi melepasi beberapa ratus baris setiap perubahan medan perlu dicerminkan di tiga tempat. Terlepas satu dan anda mendapat "lulus secara tempatan, memulangkan 500 dalam pengeluaran". Penggunaan semula bermaksud menulis satu fakta sekali dan merujuknya di tempat lain.
JSON Schema menawarkan tiga lapisan, setiap satu dengan skop berbeza:
| Lapisan | Pembawa | Skop | Penggunaan tipikal |
|---|---|---|---|
| Substruktur | $defs + $ref |
Satu fail Schema | Alamat, wang, penomboran halaman, bentuk ralat |
| Fail | Schema kendiri + $id |
Beberapa fail dalam satu perkhidmatan | Entiti pengguna, pesanan, produk |
| Pustaka kontrak | Repositori Schema + artifak bergabung | Berbilang perkhidmatan, berbilang bahasa | Komponen OpenAPI, kontrak yang diterbitkan |
Untuk kebanyakan projek lapisan pertama sudah cukup. Memisahkan kepada fail hanya berbaloi apabila dua perkhidmatan benar-benar berkongsi kontrak yang sama, atau apabila Schema ialah artifak yang diterbitkan dan digunakan oleh bahasa lain. Memisahkan terlalu awal menjadikan penyelesaian $id dan laluan relatif lebih sukar dinyahpepijat, dan kos itu biasanya melebihi apa yang anda jimat dengan mengelakkan pertindihan.
Peraturan praktikal yang mudah: jika sesuatu struktur muncul di dua tempat, satu salinan boleh diterima; apabila ia muncul di tempat ketiga, naikkan tarafnya kepada $defs; hanya apabila bahasa atau repositori lain mesti membacanya, ia wajar mendapat fail kendiri dengan $id sendiri.
2. $id menetapkan identiti, $ref melakukan lompatan
$id bukan komen. Ia mengisytiharkan URI asas Schema. Nilai $ref ialah rujukan URI: ia diselesaikan terhadap URI asas semasa menjadi URI mutlak, dan kemudian serpihan selepas # mencari nod. Jika URI asas salah, setiap rujukan relatif menunjuk ke tempat lain, namun ralat biasanya hanya menyatakan "can't resolve reference" tanpa menamakan hos yang salah.
{
"$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 menggunakan JSON Pointer manakala billTo menggunakan penanda; kedua-duanya mendarat pada nod yang sama. Jadual di bawah memetakan setiap bentuk kepada penyelesaiannya:
| Bentuk | Menunjuk kepada | Draf | Nota |
|---|---|---|---|
# |
Akar Schema semasa | Semua | Biasa untuk rekursi |
#/$defs/address |
Nod di bawah $defs |
2019-09 dan seterusnya | Paling jelas; diutamakan |
#address |
Penanda bernama | $anchor dalam 2020-12 |
Draft-07 menulis $id: "#address" |
common.json |
Fail relatif kepada URI asas | Semua | Mesti didaftarkan atau digabungkan |
https://example.com/x.json |
URI mutlak | Semua | Elakkan mengambil pada masa jalan dalam pengeluaran |
Satu perangkap halus: jika subschema di dalam $defs mengisytiharkan $id sendiri, ia menjadi URI asas baharu untuk segala-galanya di bawahnya. Tampal Schema yang disalin daripada fail lain dan $id yang tertinggal akan mengubah hala rujukan relatifnya kepada hos asal — dan pengesah hanya menyedarinya apabila ia benar-benar cuba menyelesaikannya.
3. $defs ialah bekas, bukan tapak pengesahan
$defs ialah peta biasa. Tiada apa-apa di dalamnya berkesan sehingga $ref menunjuk ke sana. Senarai required yang terletak di bahagian atas $defs tanpa rujukan masuk mengesahkan tepat tiada apa-apa, tidak kira betapa ketat ia kelihatan. Itu ialah spesifikasi berfungsi seperti yang dimaksudkan, bukan bug pengesah.
{
"$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" }
}
}
Beberapa konvensyen wajar ditetapkan dalam pasukan:
- Kunci di dalam
$defstidak membawa semantik.address,Address, danaddrsemuanya sah, jadi sepakati satu gaya. - Sebelum 2019-09 kata kunci itu ialah
definitions. Kedua-duanya boleh disasarkan oleh$ref, tetapi mod ketat mungkin melaporkandefinitionssebagai kata kunci tidak diketahui. $defsboleh menyimpan mana-mana Schema, termasuk serpihanallOfatau subschema kendiri dengan$idsendiri.- Jangan layan
$defssebagai pewarisan templat. Ia hanya menyimpan takrifan; makna masih datang daripada konteks yang merujuk.
4. allOf, anyOf, dan oneOf tidak boleh saling ditukar
Ketiga-tiganya melibatkan berbilang cabang, tetapi peraturan penerimaan mereka berbeza. Apabila data sepadan dua cabang, allOf dan anyOf lulus manakala oneOf gagal — sebab paling biasa orang menyimpulkan "Schema itu rosak".
| Kata kunci | Padanan minimum | Padanan maksimum | Data sepadan dua cabang | Kualiti ralat | Penggunaan tipikal |
|---|---|---|---|---|---|
allOf |
Semua | Tanpa had | Lulus | Baik, satu kekangan pada satu masa | Gabungkan serpihan kekangan |
anyOf |
1 | Tanpa had | Lulus | Lemah, cabang bertimbun | "E-mel atau telefon, salah satu" |
oneOf |
1 | 1 | Gagal | Lemah | Kesatuan terpencil |
not |
0 | 0 | Gagal | Terburuk | Mengecualikan beberapa nilai; jarang digunakan |
{
"allOf": [
{ "$ref": "#/$defs/auditable" },
{ "$ref": "#/$defs/softDeletable" },
{ "required": ["email"] }
],
"unevaluatedProperties": false
}
allOf tidak menggabungkan takrifan objek; ia hanya memerlukan setiap cabang berlaku. Setiap cabang dinilai secara berasingan, jadi additionalProperties: false di dalam satu cabang hanya mengetahui kunci yang diisytiharkan cabang itu dan akan menandakan medan yang diisytiharkan cabang lain sebagai tambahan. Dalam 2020-12 pembaikannya ialah meletakkan dasar sifat tidak diketahui di tahap atas dan menggunakan unevaluatedProperties: false, yang dinilai sekali selepas semua cabang.
Gunakan anyOf untuk "beberapa bentuk sah", bukan untuk "beberapa bentuk saling eksklusif". Jika dua cabang boleh sepadan serentak, oneOf menghasilkan must match exactly one schema in oneOf yang terkenal tidak membantu.
5. if/then/else dan pembezalayan: jadikan syarat boleh diputuskan
Kekangan bersyarat cenderung menghasilkan ralat yang lebih baik daripada oneOf kerana laluan keputusan adalah eksplisit: if lulus, then terpakai; jika tidak else terpakai.
{
"type": "object",
"required": ["kind"],
"properties": { "kind": { "enum": ["text", "image"] } },
"if": {
"properties": { "kind": { "const": "image" } },
"required": ["kind"]
},
"then": { "required": ["url", "width", "height"] },
"else": { "required": ["content"] }
}
Tiga butiran menentukan sama ada ia berkelakuan seperti yang dimaksudkan:
- Entri
propertiesdi dalamiftidak gagal apabila kunci tiada. Gandingkannya denganrequireduntuk menyatakan "kind wujud dan sama dengan image". ifitu sendiri tidak pernah menghasilkan ralat pengesahan. Kegagalannya hanya mengalihkan kepadaelse, jadi ia tidak boleh menyekat data buruk sendiri.thenatauelsebersendirian adalah lengai. Mereka hanya terpakai bersebelahanif.
Apabila cabang kesatuan bertambah, rantai bersyarat menjadi sarang yang tidak boleh diselenggara. Corak yang lebih kukuh ialah mengunci medan tag di dalam setiap cabang supaya cabang saling eksklusif secara binaan:
{
"oneOf": [
{
"properties": { "kind": { "const": "card" } },
"required": ["kind", "cardNumber"]
},
{
"properties": { "kind": { "const": "account" } },
"required": ["kind", "accountId"]
}
]
}
discriminator ialah sambungan OpenAPI 3.x, bukan kata kunci JSON Schema. Ajv menyokongnya apabila pilihan discriminator: true didayakan, tetapi pemetaannya berada dalam mapping dan tidak boleh dipindahkan merentas rantai alat. Jika Schema mesti berfungsi dalam beberapa pengesah, utamakan corak tag const, yang tidak bergantung pada sebarang sambungan.
6. Rujukan rekursif: pokok, senarai berpaut, dan fail yang merujuk satu sama lain
Struktur rekursif tidak memerlukan sintaks khas — $ref kembali kepada dirinya sudah cukup. Pengesah tidak mengembangkan rujukan secara tak terhingga pada masa kompil; mereka membuat rekursi mengikut kedalaman sebenar data, jadi beberapa dozen baris menerangkan pokok pada sebarang ketinggian.
{
"$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"
}
Tiga sempadan wajar diselesaikan lebih awal:
- Kedalaman data: Schema tidak mengenakan had, tetapi pengesahan dan penstriman kedua-duanya menggunakan timbunan berkadar dengan kedalaman. API awam harus menambah had perniagaan, katakan lapan tahap, atau muatan yang sengaja mendalam boleh melimpahi timbunan.
- Rujukan bersama: A merujuk B dan B merujuk A adalah sah, tetapi kedua-dua Schema mesti didaftarkan dahulu; jika tidak susunan penyelesaian menentukan hasilnya.
- Cabang boleh null: jika medan rekursif mungkin
null, tulis"type": ["object", "null"]atau bungkusnya dalamoneOf.$reftidak mewarisi kebolehnolan.
7. Rujukan merentas fail dan penggabungan: jangan sekali-kali mengambil Schema dalam pengeluaran
Rujukan merentas fail sering "berfungsi begitu sahaja" dalam pembangunan kerana pengesah mengambil sasaran mengikut $id melalui rangkaian. Tukar dasar rangkaian pada masa penggunaan dan keseluruhan rantaian pengesahan rosak. Peraturan pengeluaran mudah: setiap Schema yang dirujuk mesti didaftarkan dalam proses atau digabungkan lebih awal.
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 });
// Register the referenced file first; the key must equal its $id, or relative refs won't resolve
ajv.addSchema(addressSchema, 'https://bbdrug.com/schemas/address.json');
// Compile the entry schema once, at module scope
export const validateOrder = ajv.compile(orderSchema);
Jika order.json mempunyai $id: "https://bbdrug.com/schemas/order.json", "$ref": "address.json"-nya diselesaikan kepada https://bbdrug.com/schemas/address.json, yang sepadan dengan kunci yang didaftarkan. Satu aksara yang berbeza menghasilkan can't resolve reference.
Tiga pendekatan praktikal dibandingkan seperti ini:
| Pendekatan | Cara | Kekuatan | Kos |
|---|---|---|---|
| Daftar pada masa jalan | addSchema per fail |
Tiada langkah binaan, fail kekal bebas | Susunan pendaftaran dan kunci adalah manual |
| Gabung pada masa binaan | Gabung dengan penghurai rujukan menjadi satu fail | Kebergantungan masa jalan sifar | Artifak lebih besar, diff bising |
| Pemuatan tak segerak | loadSchema + compileAsync |
Mengendalikan Schema yang sering berubah | Kebimbangan rangkaian dan cache; elakkan pada laluan kritikal |
Untuk projek frontend, menggabungkan pada masa binaan lebih selamat. Pada pelayan, pendaftaran sudah cukup, dengan syarat ujian membuktikan setiap rujukan diselesaikan. Sama ada cara, jalankan compile dalam CI supaya ralat rujukan muncul sebelum gabungan dan bukannya pada permintaan pengeluaran pertama.
8. Susunan penyahpepijatan: selesaikan, kompil, kemudian periksa cabang
Hampir setiap masalah $ref boleh dicari dalam tiga peringkat, dan susunannya penting:
| Gejala | Punca kemungkinan | Pembaikan |
|---|---|---|
can't resolve reference ... from id ... |
Salah taip dalam $id, asas relatif salah, sasaran tidak didaftarkan |
Cetak $id dan URI mutlak yang diselesaikan, kemudian bandingkan dengan kunci yang didaftarkan |
schema with key or id already exists |
$id yang sama didaftarkan dua kali, atau compile dipanggil berulang kali |
Pindahkan pendaftaran ke inisialisasi modul dan jalankan sekali |
strict mode: unknown keyword: "definitions" |
Mencampurkan sintaks draft-07 | Namakan semula kepada $defs; jangan matikan mod ketat |
Data sepadan dua cabang oneOf |
Kekangan cabang bertindih, tiada medan tag | Tambah const pada setiap cabang, atau tukar kepada anyOf |
Kekangan di dalam $defs tidak berkesan |
Tiada apa-apa merujuknya | Semak laluan rujukan; $defs tidak mengesahkan apa-apa dengan sendirinya |
Semasa menyahpepijat, baca schemaPath sebelum message: ia menamakan cabang dan kata kunci yang gagal dan jauh lebih boleh dipercayai daripada prosa bahasa Inggeris. Simpan cache pengesah yang dikompil pada tahap modul dan bukannya mengkompil semula setiap permintaan. Untuk setiap perubahan Schema, liputi kedua-dua kes lulus dan gagal — menguji hanya laluan bahagia tidak menguji apa-apa. Untuk pemilihan pengesah dan reka bentuk mesej ralat, lihat JSON Schema: Daripada Asas kepada Pengeluaran; untuk menyambungkan Schema ke penjanaan jenis, lihat Penyegerakan Dua Hala Antara Schema dan Jenis TypeScript.
9. Soalan yang sering ditanya
Bolehkah $ref diletakkan bersebelahan type atau description?
2020-12 membenarkan $ref bersama kata kunci adik-beradik, dan adik-beradik itu dinilai secara normal. Dalam draft-07 adik-beradik diabaikan dan hanya $ref terpakai. Ini ialah perbezaan tingkah laku terbesar apabila Schema yang sama digunakan merentas draf, jadi sahkan draf mana yang disokong pengesah anda sebelum berhijrah.
Patutkah saya menggunakan $defs atau definitions?
Gunakan $defs untuk kerja baharu; ia datang dengan 2019-09. definitions bukan kata kunci dalam 2020-12. Penuding seperti $ref: "#/definitions/foo" masih boleh diselesaikan kerana ia JSON Pointer biasa, tetapi mod ketat akan melaporkan kata kunci tidak diketahui. Mencampurkan kedua-dua gaya meninggalkan periuk api yang hanya meletup dalam CI.
Mengapa oneOf menolak data yang kelihatan sah?
Kerana data memenuhi dua cabang serentak. oneOf menuntut tepat satu, dan cabang yang mengekang hanya sebahagian objek dipenuhi walaupun medan cabang lain tiada. Tambah tag const pada setiap cabang untuk menjadikan mereka saling eksklusif.
Adakah rujukan rekursif akan berulang selamanya?
Tidak. Pengesah tidak mengembangkan rujukan pada masa kompil; mereka membuat rekursi ke atas data sebenar. Risiko sebenar ialah data cukup mendalam untuk melimpahi timbunan dan susunan penyelesaian yang salah antara fail yang saling merujuk. Had kedalaman peringkat perniagaan pada medan rekursif lebih berkesan daripada mana-mana helah Schema.
Bolehkah pengeluaran bergantung pada $ref jauh?
Jangan. Rujukan jauh mengikat ketersediaan pengesahan kepada rangkaian: perubahan domain, sijil yang tamat tempoh, dan pengasingan rangkaian dalaman semuanya memutuskan rantaian. Gabungkan Schema yang dirujuk ke dalam artifak atau daftarkannya semasa permulaan jika anda mahukan tingkah laku yang boleh diramal.
Bagaimana saya memutuskan antara $defs dan fail berasingan?
Struktur yang muncul di dua tempat boleh disalin sekali; apabila ia muncul di tempat ketiga, naikkan tarafnya kepada $defs. Hanya apabila bahasa lain atau repositori lain mesti membacanya, ia wajar mendapat fail kendiri dengan $id sendiri.
Mengapa kekangan di dalam $defs tidak berkesan?
$defs hanyalah bekas, dan nod yang tiada $ref menunjuk kepadanya tidak pernah mengambil bahagian dalam pengesahan. Senarai required di bahagian atas $defs tanpa rujukan masuk masih membenarkan data lulus — itu ialah spesifikasi, bukan bug pengesah.
Patutkah saya meletakkan additionalProperties: false di dalam cabang allOf?
Tidak. Setiap cabang dinilai secara berasingan, jadi satu cabang menganggap medan yang diisytiharkan cabang lain sebagai tambahan. Pendekatan 2020-12 memindahkan dasar medan tidak diketahui ke tahap atas dan menggunakan unevaluatedProperties: false, dinilai sekali selepas semua cabang.