JSON Toolkit

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.

Pertengahan~14 min bacaanDiterbitkan 2026-10-30Dikemas kini 2026-10-30
Kulit untuk JSON Schema: Daripada Asas kepada Pengeluaran

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:

  1. required hanya memerlukan kunci wujud: {"name": ""} adalah sah. Untuk menolak rentetan kosong anda mesti menambah minLength: 1;
  2. additionalProperties: false membesar-besarkan masalah keserasian: satu medan tambahan daripada klien menggagalkan keseluruhan muatan, jadi evolusi API memaksa setiap pemanggil menaik taraf secara serentak;
  3. properties tidak 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" }
  }
}
  • $defs ialah bentuk yang diperkenalkan pada 2019-09; draft-07 menggunakan definitions;
  • Rujukan merentas fail menggunakan $id relatif, 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 $ref dengan kata kunci adik-beradik berbeza mengikut versi). Apabila anda memerlukan komposisi, gunakan allOf.

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 compile sekali sahaja, pada tahap atas modul; mengkompil semula pada setiap permintaan menukar pengesahan 10 ms menjadi ratusan milisaat;
  • strict: true mendedahkan kata kunci yang salah eja lebih awal (contohnya minlength dan bukannya minLength), jadi ia wajar didayakan;
  • Hanya ajv-formats menjadikan format: "email" dan format: "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 oneOf menyebabkan letupan kombinatorial, yang menjadi sangat ketara dengan ribuan rekod;
  • uniqueItems melakukan 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: false digabungkan dengan allOf, medan yang diisytiharkan dalam subschema dinilai "tambahan" oleh induk, jadi anda perlu menggabungkan mereka ke dalam satu Schema atau menggunakan unevaluatedProperties.

8. Senarai Semak

  • [ ] Tetapkan 2020-12 dan isytiharkan $schema di bahagian atas fail;
  • [ ] Ekstrak struktur kongsi dengan $defs + $ref dan bukannya menyalin-tampal;
  • [ ] Utamakan pembezalayan berbanding oneOf;
  • [ ] Guna semula hasil compile pengesah dan dayakan mod ketat;
  • [ ] Dokumentasikan dengan jelas sama ada format dikuatkuasakan;
  • [ ] 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.

Rujukan

Artikel berkaitan

Penyegerakan Dua Hala Antara Schema dan Jenis TypeScript

Jenis dan Schema masing-masing meliputi satu segmen: satu pada masa kompil, satu pada masa jalan. Persoalannya bukan mana yang perlu dipilih, tetapi cara mengekalkan kedua-duanya selaras secara automatik.

Pertengahan~11 min bacaanDikemas kini 2026-10-30

Perkara Penting untuk Mereka Bentuk Kontrak Data JSON API

Enam perkara sangat sukar diubah selepas dihantar: sampul respons, format kesalahan, bentuk penomboran halaman, perwakilan ID dan masa, semantik null berbanding ketiadaan, dan evolusi versi.

Pertengahan~13 min bacaanDikemas kini 2026-10-30

Tanpa additionalProperties, pengesahan skema tidak mengesahkan apa-apa

Pengesahan yang lulus tidak bermakna data bersih. Pydantic secara lalai mengabaikan medan asing, Zod mengalihkannya, Ajv membiarkannya — parameter halusinasi dan kunci tersilap lolos tanpa bunyi. Artikel ini menunjukkan tiga bentuk penerimaan senyap dan memberikan resipi lengkap: menutup objek, mengawal paksaan jenis, menggenggam setiap peringkat bersarang.

Intermediate~8 min bacaanDikemas kini 2026-11-02