JSON Toolkit

JSON Schema: Dari Dasar hingga Produksi

Intinya: Kata kuncinya banyak, tetapi hanya selusin yang digunakan setiap hari. Kuasai type/properties/required/$ref, tangani pesan error dengan baik, dan Anda mencakup delapan puluh persen kasus nyata.

Menengah±14 menit bacaDiterbitkan 2026-10-30Diperbarui 2026-10-30
Sampul untuk JSON Schema: Dari Dasar hingga Produksi

Intinya: kunci dulu $schema ke 2020-12, lalu ekstrak struktur bersama dengan $defs + $ref; gunakan oneOf hanya ketika tepat satu branch harus cocok, dan utamakan discriminator atau anyOf di tempat lain.

Schema bukan lampiran dokumentasi; ia adalah aset yang dapat dieksekusi, diuji, dan dihasilkan.

1. Luruskan dulu versinya

Versi Nilai $schema Status Catatan
draft-04 http://json-schema.org/draft-04/schema# Usang Proyek warisan dan beberapa tooling OpenAPI 2.0
draft-07 http://json-schema.org/draft-07/schema# Banyak digunakan Dasar de facto OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema Tersedia Memperkenalkan $defs dan kosakata anotasi
2020-12 https://json-schema.org/draft/2020-12/schema Saat ini direkomendasikan Memperkenalkan prefixItems, mengubah semantik items

Gunakan 2020-12 untuk proyek baru. Perhatikan bahwa pada 2020-12 items hanya membatasi "elemen tersisa" — deklarasi bergaya tuple pindah ke prefixItems. Itu adalah jebakan paling umum saat memigrasi dari draft-07.

2. Schema minimal yang dapat 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 mencakup empat kekhawatiran paling umum: batasan tipe, field wajib, batas rentang, dan batasan elemen array.

3. Peta kata kunci

3.1 Umum

Kata kunci Tujuan Waspadai
type Tipe; dapat berupa array "type": ["string", "null"] berarti nullable
enum Nilai terenumerasi Nilai harus unik
const Batasan nilai tunggal Baik untuk nilai tetap seperti nomor versi
$ref Merujuk schema lain Merujuk seluruh schema dan tidak bergabung dengan kata kunci saudara pada objek yang merujuk

3.2 Angka

multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum. Perhatikan bahwa pada draft-04 exclusiveMaximum adalah boolean (digunakan bersama maximum); baru sejak draft-07 ia menjadi kata kunci numerik mandiri — yang membuat banyak "schema yang disalin dari dokumen lama" gagal di bawah mode ketat.

3.3 String

minLength, maxLength, pattern (ekspresi reguler ECMA-262), dan format (anotasi).

minLength menghitung code point Unicode, bukan unit UTF-16 dan bukan byte. Memvalidasi dengan string.length JavaScript akan tidak sesuai dengannya.

3.4 Array

Kata kunci Tujuan
prefixItems Batasan bergaya tuple pada 2020-12, mencocokkan item satu per satu berdasarkan indeks
items Batasan seragam pada elemen tersisa (2020-12)
contains + minContains/maxContains Sedikitnya/paling banyak berapa banyak item yang cocok harus ada
uniqueItems Elemen harus semua berbeda (perbandingan dalam; hindari pada array besar)
minItems / maxItems Batasan panjang

3.5 Objek

properties, required, patternProperties, additionalProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas.

Tiga kesalahpahaman frekuensi tinggi:

  1. required hanya mengharuskan key ada: {"name": ""} valid. Untuk menolak string kosong Anda harus menambahkan minLength: 1;
  2. additionalProperties: false memperbesar masalah kompatibilitas: satu field tambahan dari klien menggagalkan seluruh payload, sehingga mengevolusikan API memaksa setiap pemanggil untuk meningkatkan secara serempak;
  3. properties tidak membatasi key yang tidak dideklarasikan — secara default mereka diizinkan.

3.6 Komposisi logika

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
Kata kunci Semantik Kegagalan khas
allOf Semua harus terpenuhi Beberapa branch memberlakukan batasan yang saling bertentangan pada field yang sama
anyOf Setidaknya satu harus terpenuhi Pesan error sulit dilacak ke branch tertentu
oneOf Tepat satu harus terpenuhi Data cocok dengan dua branch dan gagal, meskipun "terlihat jelas benar"
not Tidak boleh terpenuhi Jarang digunakan, dan pesan errornya paling tidak ramah

oneOf adalah kata kunci yang paling mungkin menghasilkan "data valid yang gagal validasi". Jika batasan branch tumpang tindih (misalnya, keduanya mengizinkan field tambahan), satu record cocok dengan beberapa branch dan oneOf langsung gagal. Dalam kasus itu, beralih ke anyOf, atau gunakan field discriminator dengan const di dalam branch.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

Gaya discriminator lebih mudah dibaca daripada oneOf, dan menghasilkan error yang lebih baik.

4. Pemisahan dan penggunaan ulang

{
  "$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 adalah bentuk yang diperkenalkan pada 2019-09; draft-07 menggunakan definitions;
  • Referensi lintas file menggunakan $id relatif, tetapi jangan mengandalkan pengambilan schema remote saat runtime di produksi — bundel mereka sebagai file statis;
  • Anda tidak dapat menambahkan batasan di samping referensi (perilaku $ref dengan kata kunci saudara berbeda antar versi). Ketika Anda memerlukan komposisi, gunakan allOf.

5. Validasi runtime 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)}`);
}

Poin penting:

  • Panggil compile hanya sekali, di level atas modul; mengompilasi ulang pada setiap request mengubah validasi 10 ms menjadi ratusan milidetik;
  • strict: true memunculkan kata kunci yang salah ketik lebih awal (misalnya minlength alih-alih minLength), jadi ia layak diaktifkan;
  • Hanya ajv-formats yang membuat format: "email" dan format: "date-time" benar-benar berlaku;
  • 2020-12 memerlukan entry point ajv/dist/2020 — jangan gunakan ekspor default.

6. Cara merancang pesan error

Apa yang diberikan validator kepada Anda adalah "kata kunci mana yang gagal"; apa yang diinginkan pengguna adalah "kotak input mana yang salah". Layak menambahkan lapisan pemetaan:

const FIELD_LABEL: Record<string, string> = {
  '/name': 'Name',
  '/age': 'Age',
  '/tags': 'Tags',
};

/**
 * Mengubah error Ajv menjadi pesan siap tampil.
 * @param errors Daftar error Ajv
 * @returns Array pesan yang akan ditampilkan
 */
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 sisi API, pertahankan struktur yang dapat dibaca mesin agar front end dapat memutuskan cara menyajikannya:

{
  "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. Performa dan jebakan

  • Biaya validasi tumbuh dengan kompleksitas schema dan ukuran data; beberapa branch oneOf menyebabkan ledakan kombinatorial, yang menjadi sangat terasa dengan ribuan record;
  • uniqueItems melakukan perbandingan dalam O(n²) pada array; untuk array besar, deduplikasi di lapisan bisnis sebagai gantinya;
  • Hindari pola backtracking katastrofik dalam pattern (seperti (a+)+$); ini adalah pintu masuk klasik untuk ReDoS;
  • Ketika additionalProperties: false digabungkan dengan allOf, field yang dideklarasikan dalam subschema dinilai "tambahan" oleh induk, sehingga Anda perlu menggabungkannya menjadi satu schema atau menggunakan unevaluatedProperties.

8. Daftar Periksa

  • [ ] Kunci 2020-12 dan deklarasikan $schema di atas file;
  • [ ] Ekstrak struktur bersama dengan $defs + $ref alih-alih menyalin-tempel;
  • [ ] Utamakan discriminator daripada oneOf;
  • [ ] Gunakan kembali hasil compile validator dan aktifkan mode ketat;
  • [ ] Dokumentasikan dengan jelas apakah format ditegakkan;
  • [ ] Respons error menyediakan path, kata kunci, dan pesan untuk pengguna;
  • [ ] Letakkan schema di CI: jalankan kasus JSON Schema Test Suite sebagai pengujian regresi.

FAQ

Bagaimana saya memilih draft JSON Schema?

Gunakan 2020-12 untuk proyek baru dan deklarasikan $schema di atas file. Jika schema masuk ke OpenAPI 3.1, ia juga harus 2020-12, karena 3.1 menyematkan versi itu secara langsung; pertahankan draft-07 hanya untuk proyek yang masih terikat pada toolchain lama. Keduanya berperilaku berbeda untuk items dan untuk kata kunci saudara $ref, jadi jangan mencampurnya dalam satu set schema.

Mengapa additionalProperties: false memperbesar masalah kompatibilitas?

Ia memperlakukan setiap key yang tidak dideklarasikan sebagai error, sehingga satu field tambahan dari klien menggagalkan seluruh payload. Setiap pemanggil kemudian harus meningkatkan secara serempak setiap kali API berevolusi, atau klien lama ditolak. Schema bergaya konfigurasi harus menutup field tambahan secara eksplisit, sementara API publik perlu menimbang biaya kompatibilitas.

Haruskah saya menggunakan oneOf atau anyOf?

Gunakan oneOf hanya ketika data harus cocok dengan tepat satu branch. Jika batasan branch dapat berlaku secara bersamaan, satu record cocok dengan beberapa branch dan oneOf langsung gagal; beralih ke anyOf, atau tambahkan discriminator const di dalam setiap branch.

Referensi

Artikel terkait

Sinkronisasi Dua Arah antara Schema dan Tipe TypeScript

Tipe dan schema masing-masing mencakup satu segmen: satu saat kompilasi, satu saat runtime. Pertanyaannya bukan mana yang dipilih, tetapi bagaimana menjaga keduanya tetap sinkron secara otomatis.

Menengah±11 menit bacaDiperbarui 2026-10-30

Poin Penting dalam Mendesain Kontrak Data JSON API

Enam hal sangat sulit diubah setelah rilis: envelope respons, format error, bentuk pagination, representasi ID dan waktu, semantik null versus tidak ada, serta evolusi versi.

Menengah±13 menit bacaDiperbarui 2026-10-30

Tanpa additionalProperties, validasi skema tidak memvalidasi apa pun

Validasi yang lulus bukan berarti data bersih. Pydantic secara bawaan mengabaikan kolom tak dikenal, Zod menghapusnya, Ajv membiarkannya — parameter halusinasi dan kunci salah ketik lolos tanpa suara. Artikel ini menunjukkan tiga bentuk penerimaan diam dan memberikan resep lengkap: menutup objek, mengontrol koersi, mengencangkan setiap tingkat sarang.

Intermediate±8 menit bacaDiperbarui 2026-11-02