JSON Schema: Daripada Asas kepada Pengeluaran
Inti pati: Terdapat banyak kata kunci, tetapi hanya selusin digunakan setiap hari. Betulkan type/properties/required/$ref, kendalikan mesej ralat dengan baik, dan anda meliputi lapan puluh peratus kes sebenar.

Intinya: tetapkan $schema kepada 2020-12 dahulu, kemudian ekstrak struktur kongsi dengan $defs + $ref; gunakan oneOf hanya apabila tepat satu cabang mesti sepadan, dan utamakan pembezalayan atau anyOf di tempat lain.
Schema bukan lampiran kepada dokumentasi; ia ialah aset yang boleh dilaksanakan, diuji, dan dijana.
1. Betulkan versi dahulu
| Versi | Nilai $schema |
Status | Nota |
|---|---|---|---|
| draft-04 | http://json-schema.org/draft-04/schema# |
Lapuk | Projek lama dan sesetengah alat OpenAPI 2.0 |
| draft-07 | http://json-schema.org/draft-07/schema# |
Digunakan secara meluas | Asas de facto OpenAPI 3.0 |
| 2019-09 | https://json-schema.org/draft/2019-09/schema |
Tersedia | Memperkenalkan $defs dan perbendaharaan kata anotasi |
| 2020-12 | https://json-schema.org/draft/2020-12/schema |
Disyorkan pada masa ini | Memperkenalkan prefixItems, mengubah semantik items |
Gunakan 2020-12 untuk projek baharu. Perhatikan bahawa dalam 2020-12 items hanya mengekang "elemen yang tinggal" — pengisytiharan gaya tuple berpindah ke prefixItems.
Itu ialah perangkap paling biasa semasa berhijrah daripada draft-07.
2. Schema minimum yang boleh digunakan
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bbdrug.com/schemas/user.json",
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1, "maxLength": 64 },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
},
"additionalProperties": false
}
Schema ini merangkumi empat kebimbangan paling biasa: kekangan jenis, medan wajib, had julat, dan kekangan elemen tatasusunan.
3. Peta kata kunci
3.1 Umum
| Kata kunci | Tujuan | Perhatikan |
|---|---|---|
type |
Jenis; boleh jadi tatasusunan | "type": ["string", "null"] bermaksud boleh null |
enum |
Nilai terbilang | Nilai mesti unik |
const |
Kekangan nilai tunggal | Baik untuk nilai tetap seperti nombor versi |
$ref |
Merujuk Schema lain | Merujuk keseluruhan Schema dan tidak bergabung dengan kata kunci adik-beradik pada objek yang merujuk |
3.2 Nombor
multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum.
Perhatikan bahawa dalam draft-04 exclusiveMaximum ialah boolean (digunakan bersama maximum); hanya dari draft-07 dan seterusnya ia menjadi kata kunci berangka tersendiri —
yang menjadikan banyak "Schema yang disalin daripada dokumen lama" gagal di bawah mod ketat.
3.3 Rentetan
minLength, maxLength, pattern (ungkapan biasa ECMA-262), dan format (anotasi).
minLength mengira titik kod Unicode, bukan unit UTF-16 dan bukan bait. Mengesahkan dengan string.length JavaScript akan bercanggah dengannya.
3.4 Tatasusunan
| Kata kunci | Tujuan |
|---|---|
prefixItems |
Kekangan gaya tuple 2020-12, memadankan item satu demi satu mengikut indeks |
items |
Kekangan seragam pada elemen yang tinggal (2020-12) |
contains + minContains/maxContains |
Sekurang-kurangnya/paling banyak berapa banyak item sepadan mesti hadir |
uniqueItems |
Elemen mesti semua berbeza (perbandingan mendalam; elakkan pada tatasusunan besar) |
minItems / maxItems |
Kekangan panjang |
3.5 Objek
properties, required, patternProperties, additionalProperties, propertyNames,
minProperties, maxProperties, dependentRequired, dependentSchemas.
Tiga tanggapan salah berfrekuensi tinggi:
requiredhanya memerlukan kunci wujud:{"name": ""}adalah sah. Untuk menolak rentetan kosong anda mesti menambahminLength: 1;additionalProperties: falsemembesar-besarkan masalah keserasian: satu medan tambahan daripada klien menggagalkan keseluruhan muatan, jadi evolusi API memaksa setiap pemanggil menaik taraf secara serentak;propertiestidak menyekat kunci yang tidak diisytiharkan — secara lalai mereka dibenarkan.
3.6 Komposisi logik
{
"oneOf": [
{ "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
{ "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
]
}
| Kata kunci | Semantik | Kegagalan tipikal |
|---|---|---|
allOf |
Semua mesti dipenuhi | Berbilang cabang mengenakan kekangan bercanggah pada medan yang sama |
anyOf |
Sekurang-kurangnya satu mesti dipenuhi | Mesej ralat sukar dijejaki kepada cabang tertentu |
oneOf |
Tepat satu mesti dipenuhi | Data sepadan dua cabang dan gagal, walaupun ia "kelihatan jelas betul" |
not |
Tidak boleh dipenuhi | Jarang digunakan, dan mesej ralat paling tidak mesra |
oneOf ialah kata kunci yang paling mungkin menghasilkan "data sah yang gagal pengesahan". Jika kekangan cabang bertindih (contohnya, kedua-duanya membenarkan medan tambahan),
satu rekod sepadan beberapa cabang dan oneOf gagal secara langsung. Dalam kes itu, tukar kepada anyOf, atau gunakan medan pembezalayan dengan const di dalam cabang.
{
"type": "object",
"required": ["kind"],
"properties": { "kind": { "enum": ["text", "image"] } },
"if": { "properties": { "kind": { "const": "image" } } },
"then": { "required": ["url", "width", "height"] },
"else": { "required": ["content"] }
}
Gaya pembezalayan lebih mudah dibaca daripada oneOf, dan ia menghasilkan ralat yang lebih baik.
4. Pemisahan dan penggunaan semula
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bbdrug.com/schemas/order.json",
"$defs": {
"money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
"address": {
"type": "object",
"required": ["city"],
"properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
}
},
"type": "object",
"required": ["total", "shipTo"],
"properties": {
"total": { "$ref": "#/$defs/money" },
"shipTo": { "$ref": "#/$defs/address" }
}
}
$defsialah bentuk yang diperkenalkan pada 2019-09; draft-07 menggunakandefinitions;- Rujukan merentas fail menggunakan
$idrelatif, tetapi jangan bergantung pada mengambil Schema jauh pada masa jalan dalam pengeluaran — gabungkan mereka sebagai fail statik sebaliknya; - Anda tidak boleh menambah kekangan bersama rujukan (tingkah laku
$refdengan kata kunci adik-beradik berbeza mengikut versi). Apabila anda memerlukan komposisi, gunakanallOf.
5. Pengesahan masa jalan dengan Ajv
import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };
const ajv = new Ajv2020({
allErrors: true,
strict: true,
allowUnionTypes: true,
});
addFormats(ajv);
const validateOrder = ajv.compile(orderSchema);
export function assertOrder(input: unknown): void {
if (validateOrder(input)) {
return;
}
const detail = (validateOrder.errors ?? []).map((errorItem) => ({
path: errorItem.instancePath || '/',
keyword: errorItem.keyword,
message: errorItem.message,
params: errorItem.params,
}));
throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}
Perkara utama:
- Panggil
compilesekali sahaja, pada tahap atas modul; mengkompil semula pada setiap permintaan menukar pengesahan 10 ms menjadi ratusan milisaat; strict: truemendedahkan kata kunci yang salah eja lebih awal (contohnyaminlengthdan bukannyaminLength), jadi ia wajar didayakan;- Hanya
ajv-formatsmenjadikanformat: "email"danformat: "date-time"benar-benar berkesan; - 2020-12 memerlukan titik masuk
ajv/dist/2020— jangan gunakan eksport lalai.
6. Cara mereka bentuk mesej ralat
Apa yang diberikan pengesah kepada anda ialah "kata kunci mana yang gagal"; apa yang pengguna mahu ialah "kotak input mana yang salah". Wajar menambah lapisan pemetaan:
const FIELD_LABEL: Record<string, string> = {
'/name': 'Name',
'/age': 'Age',
'/tags': 'Tags',
};
/**
* Convert Ajv errors into display-ready messages.
* @param errors The list of Ajv errors
* @returns An array of display messages
*/
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
return errors.map((errorItem) => {
const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
if (errorItem.keyword === 'required') {
const missing = (errorItem.params as { missingProperty: string }).missingProperty;
return `${label} is missing the required field ${missing}`;
}
if (errorItem.keyword === 'minLength') {
return `${label} is too short`;
}
return `${label} failed validation (${errorItem.keyword})`;
});
}
Di bahagian API, kekalkan struktur yang boleh dibaca mesin supaya frontend dapat memutuskan cara memaparkannya:
{
"type": "https://bbdrug.com/problems/validation",
"title": "Request data validation failed",
"status": 422,
"errors": [
{ "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
]
}
7. Prestasi dan perangkap
- Kos pengesahan bertambah dengan kerumitan Schema dan saiz data; berbilang cabang
oneOfmenyebabkan letupan kombinatorial, yang menjadi sangat ketara dengan ribuan rekod; uniqueItemsmelakukan perbandingan mendalam O(n²) ke atas tatasusunan; untuk tatasusunan besar, deduplikasi di lapisan perniagaan sebaliknya;- Elakkan corak backtracking katastrofik dalam
pattern(seperti(a+)+$); ini ialah titik masuk klasik untuk ReDoS; - Apabila
additionalProperties: falsedigabungkan denganallOf, medan yang diisytiharkan dalam subschema dinilai "tambahan" oleh induk, jadi anda perlu menggabungkan mereka ke dalam satu Schema atau menggunakanunevaluatedProperties.
8. Senarai Semak
- [ ] Tetapkan 2020-12 dan isytiharkan
$schemadi bahagian atas fail; - [ ] Ekstrak struktur kongsi dengan
$defs+$refdan bukannya menyalin-tampal; - [ ] Utamakan pembezalayan berbanding
oneOf; - [ ] Guna semula hasil
compilepengesah dan dayakan mod ketat; - [ ] Dokumentasikan dengan jelas sama ada
formatdikuatkuasakan; - [ ] Respons ralat menyediakan laluan, kata kunci, dan mesej untuk pengguna;
- [ ] Letakkan Schema dalam CI: jalankan kes JSON Schema Test Suite sebagai ujian regresi.
FAQ
Bagaimana saya patut memilih draft JSON Schema?
Gunakan 2020-12 untuk projek baharu dan isytiharkan $schema di bahagian atas fail. Jika Schema masuk ke OpenAPI 3.1, ia juga mesti 2020-12, kerana 3.1 membenamkan versi itu secara langsung; kekalkan draft-07 hanya untuk projek yang masih terikat pada rantai alat lama. Kedua-duanya berkelakuan berbeza untuk items dan untuk kata kunci adik-beradik $ref, jadi jangan campurkan mereka dalam satu set Schema.
Mengapa additionalProperties: false membesar-besarkan masalah keserasian?
Ia menganggap setiap kunci yang tidak diisytiharkan sebagai ralat, jadi satu medan tambahan daripada klien menggagalkan keseluruhan muatan. Setiap pemanggil kemudian mesti menaik taraf secara serentak setiap kali API berkembang, atau klien lama ditolak. Schema gaya konfigurasi harus menutup medan tambahan secara eksplisit, sedangkan API awam perlu menimbang kos keserasian.
Patutkah saya menggunakan oneOf atau anyOf?
Gunakan oneOf hanya apabila data mesti sepadan tepat satu cabang. Jika kekangan cabang boleh berlaku serentak, satu rekod sepadan beberapa cabang dan oneOf gagal secara langsung; tukar kepada anyOf, atau tambah pembezalayan const di dalam setiap cabang.