JSON Toolkit

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.

Pertengahan~11 min bacaanDiterbitkan 2026-10-31Dikemas kini 2026-10-31
Gambar rajah penggunaan semula dan komposisi JSON Schema dengan $ref, $defs, dan allOf/oneOf

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 $defs tidak membawa semantik. address, Address, dan addr semuanya sah, jadi sepakati satu gaya.
  • Sebelum 2019-09 kata kunci itu ialah definitions. Kedua-duanya boleh disasarkan oleh $ref, tetapi mod ketat mungkin melaporkan definitions sebagai kata kunci tidak diketahui.
  • $defs boleh menyimpan mana-mana Schema, termasuk serpihan allOf atau subschema kendiri dengan $id sendiri.
  • Jangan layan $defs sebagai 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:

  1. Entri properties di dalam if tidak gagal apabila kunci tiada. Gandingkannya dengan required untuk menyatakan "kind wujud dan sama dengan image".
  2. if itu sendiri tidak pernah menghasilkan ralat pengesahan. Kegagalannya hanya mengalihkan kepada else, jadi ia tidak boleh menyekat data buruk sendiri.
  3. then atau else bersendirian adalah lengai. Mereka hanya terpakai bersebelahan if.

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 dalam oneOf. $ref tidak 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.