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.

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:
requiredhanya mengharuskan key ada:{"name": ""}valid. Untuk menolak string kosong Anda harus menambahkanminLength: 1;additionalProperties: falsememperbesar masalah kompatibilitas: satu field tambahan dari klien menggagalkan seluruh payload, sehingga mengevolusikan API memaksa setiap pemanggil untuk meningkatkan secara serempak;propertiestidak 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" }
}
}
$defsadalah bentuk yang diperkenalkan pada 2019-09; draft-07 menggunakandefinitions;- Referensi lintas file menggunakan
$idrelatif, tetapi jangan mengandalkan pengambilan schema remote saat runtime di produksi — bundel mereka sebagai file statis; - Anda tidak dapat menambahkan batasan di samping referensi (perilaku
$refdengan kata kunci saudara berbeda antar versi). Ketika Anda memerlukan komposisi, gunakanallOf.
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
compilehanya sekali, di level atas modul; mengompilasi ulang pada setiap request mengubah validasi 10 ms menjadi ratusan milidetik; strict: truememunculkan kata kunci yang salah ketik lebih awal (misalnyaminlengthalih-alihminLength), jadi ia layak diaktifkan;- Hanya
ajv-formatsyang membuatformat: "email"danformat: "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
oneOfmenyebabkan ledakan kombinatorial, yang menjadi sangat terasa dengan ribuan record; uniqueItemsmelakukan 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: falsedigabungkan denganallOf, field yang dideklarasikan dalam subschema dinilai "tambahan" oleh induk, sehingga Anda perlu menggabungkannya menjadi satu schema atau menggunakanunevaluatedProperties.
8. Daftar Periksa
- [ ] Kunci 2020-12 dan deklarasikan
$schemadi atas file; - [ ] Ekstrak struktur bersama dengan
$defs+$refalih-alih menyalin-tempel; - [ ] Utamakan discriminator daripada
oneOf; - [ ] Gunakan kembali hasil
compilevalidator dan aktifkan mode ketat; - [ ] Dokumentasikan dengan jelas apakah
formatditegakkan; - [ ] 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.