JSON Toolkit

Pengujian Kontrak JSON Schema: Las Aturan Validasi ke Dalam CI

Intinya: Skema tidak berakhir saat ditulis — di situlah degradasi dimulai. Satu salah ketik diam-diam melonggarkan validasi, satu refaktoring memecahkan semua fixture. Panduan ini membedah susunan pengujian kontrak yang bisa dilaksanakan: fixture positif, negatif dan snapshot, lint skema itu sendiri, kompilasi sekali dengan penggunaan ulang, dan empat gerbang CI yang mengubah pecahnya kontrak menjadi build merah.

Menengah±8 menit bacaDiterbitkan 2026-11-05Diperbarui 2026-11-05
Diagram pengujian kontrak JSON Schema dan gerbang validasi CI

Kesimpulan: skema adalah kode, dan kode membutuhkan pengujian. Fixture positif membuktikan ia meloloskan yang semestinya; negatif membuktikan ia menahan yang wajib; snapshot mengawasi perubahan tak disengaja. Terakhir ajv compile dan snapshot dengan --ci berjaga malam dalam pipeline Anda.

Pada saat aturan validasi pecah, tak ada yang melempar galat: pelebaran penerimaan tak disadari siapa pun, pengencangan penolakan ditanggung seluruh tim. Skema bukan berkas konfigurasi — ia kode yang tak ada yang mengujinya.

1. Skema pecah — siapa tahu lebih dulu

Mari mulai dengan jebakan yang benar-benar kami injak. Deklarasikan skema menurut draf 2020-12 dan kompilasikan dengan paket bawaan Ajv: pengecualian langsung meloncat:

Error: no schema with key or ref "https://json-schema.org/draft/2020-12/schema"

Titik masuk bawaan Ajv 8 hanya kenal draft-07; untuk 2020-12 perlu impor dari ajv/dist/2020. Jebakan lain: pada mode strict, menulis format: "email" tanpa memasang ajv-formats juga menjatuhkan kompilasi:

Error: unknown format "email" ignored in schema at path "#/properties/email"

Melihat galat-galat ini secara lokal pada kompilasi pertama itu baik. Masalahnya: banyak proyek menunda kompilasi sampai request tiba, dan skema pecah menunggu korban pertama. Lebih licik lagi skema setengah pecah: required tersasar ditulis sebagai string alih-alih larik membuang schema is invalid: data/required must be array saat kompilasi — tetapi bila langkah itu tak pernah berjalan di CI, ia tergeletak di produksi.

Jadi gerbang pertama tidak memvalidasi data: ia memvalidasi skema itu sendiri. Satu perintah:

node scripts/lint-schemas.mjs
// scripts/lint-schemas.mjs
import { readFileSync, readdirSync } from 'node:fs';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true, strict: true });
addFormats(ajv);
for (const file of readdirSync('schemas')) {
  const schema = JSON.parse(readFileSync(`schemas/${file}`, 'utf8'));
  ajv.compile(schema);
  console.log(`ok ${file}`);
}

Mengapa bukan npx ajv compile: paket ajv v8 tidak membawa CLI, npx ajv gagal dengan could not determine executable to run; CLI ada di paket terpisah ajv-cli (5.0.0 dalam pengukuran kami). Tetapi ajv-cli 5 hanya mendukung draft-07 dan draft-2019-09 — diarahkan ke skema 2020-12 pada bagian 1, ia melempar no schema with key or ref "https://json-schema.org/draft/2020-12/schema" yang sama. Karena itu proyek 2020-12 menjalankan lint lewat Ajv2020 yang sama dengan artikel; npx ajv-cli compile -s '**/*.schema.json' --strict hanya cocok untuk proyek draft-07.

Jalankan seperti linter: salah ketik, kata kunci usang, dan format tak terdaftar muncul sudah di tahap PR. Itulah kemiripan pertama menguji skema dengan menguji kode biasa: dahulu pastikan ia bisa dikompilasi.

2. Trio: positif, negatif, snapshot

Kerangka pengujian skema punya tiga keping.

Fixture positif: sampel dipetik dari respons nyata, yang harus lolos. Ambil dari staging, bukan dari ingatan — positif tulisan tangan hanya menutup happy path dan lupa null, string kosong, dan batas berisi-hanya-field-wajib.

Fixture negatif: untuk setiap batasan skema, sekurang-kurangnya satu input pelanggar. Untuk minimum: 0 pasangan -1 (ditolak) dan 0 (lolos); untuk enum nilai di luar daftar; untuk additionalProperties: false field berlebih. Negatif lebih berat daripada positif: skema yang diam-diam meloloskan data jelek adalah bug diam yang mencemari sampai basis data.

Konvensi penamaan yang menjadikan nama berkas asersi, bisa disalin dari sumber terbuka:

schemas/order.schema.json
fixtures/order/valid-minimal.json        # harus lolos
fixtures/order/valid-full.json           # harus lolos
fixtures/order/invalid-type.json         # harus ditolak
fixtures/order/invalid-extra-field.json  # harus ditolak

node:test bawaan Node sudah cukup:

import { test } from 'node:test';
import { readFileSync, readdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validate = ajv.compile(
  JSON.parse(readFileSync('schemas/order.schema.json', 'utf8'))
);

for (const f of readdirSync('fixtures/order')) {
  const data = JSON.parse(readFileSync(`fixtures/order/${f}`, 'utf8'));
  if (f.startsWith('valid-')) {
    test(`${f} seharusnya lolos`, () => { assert.ok(validate(data), JSON.stringify(validate.errors)); });
  } else {
    test(`${f} seharusnya ditolak`, () => { assert.ok(!validate(data)); });
  }
}

Snapshot: serialkan validate.errors ke berkas dan bandingkan. Seseorang tak sengaja mengubah batasan — snapshot seketika memerah. Dalam Jest/Vitest satu baris: expect(errors).toMatchSnapshot(); dalam CI selalu beri --ci: snapshot baru saat itu gagal dan bukan diciptakan diam-diam, dan hanyutan tak diberi kelonggaran.

3. Kumpulkan semua galat dalam satu lintasan

Pengaturan yang kerap terlupakan saat menguji negatif: secara bawaan validator berhenti pada galat pertama. Data pecah di tiga tempat, Anda melihat satu, membetulkan, menjalankan lagi — dan seterusnya. Dengan allErrors: true (Ajv) atau iter_errors (Python), semuanya terlihat sekaligus:

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(bad), key=lambda e: list(e.absolute_path))
for e in errors:
    print(list(e.absolute_path), e.validator, e.message)

Python juga menyimpan fakta melawan naluri: galat yang dilaporkan validate() bukan "yang pertama" melainkan pilihan best_match. Dalam pengukuran kami, dengan tiga pelanggaran (tipe id salah, role di luar daftar, field extra berlebih), hanya additionalProperties yang dilaporkan — galat tipe id tetap tersembunyi. Dalam CI selalu gunakan iter_errors untuk gambaran penuh.

Dan jebakan yang masih lebih mudah: format bukan asersi secara bawaan. Draf 2020-12 mengklasifikasikan format sebagai anotasi: jsonschema Python tidak memvalidasinya tanpa format_checker diserahkan — dalam uji kami "not-an-email" lolos bebas. Ajv menuntut pemasangan dan pendaftaran ajv-formats. Bila format itu penting (surel, tanggal, uuid), harus ada fixture negatif yang membuktikan konfigurasi validator Anda benar-benar memeriksanya.

4. Kompilasi sekali — jangan anggap kompiler sebagai bahan sekali pakai

Pengukuran lokal (Node v22.13.1, Ajv 8.17, skema bersarang tiga aras yang sama, data pelanggar yang sama):

Pendekatan Waktu untuk 10.000 validasi
Setiap kali new Ajv() lalu compile 42233 ms
Instans tingkat modul + compile sekali 6.9 ms

Faktor 6000. Validator di dalam request handler yang menciptakan Ajv baru pada tiap panggilan adalah kecelakaan performa paling umum. Catat juga tanggapan salah yang meluas: "ajv.validate(schema, data) mengompilasi ulang setiap kali". Diukur: 100.000 panggilan habis 8.2 ms — sebanding 11.2 ms fungsi sekali-kompilasi, karena Ajv menyimpan cache skema internal. Penyakit sesungguhnya bukan cache miss, melainkan instans tak digunakan ulang. Dalam Python pun sama: Draft202012Validator yang dipakai ulang sekitar 5000 kali lebih cepat daripada memanggil validate() tiap kali (2000 iterasi: 0.3 ms berbanding 1566 ms), sebab yang itu setiap panggilan memeriksa skema itu sendiri.

5. Las gerbang ke dalam CI: empat pintu

Tersusun dari murah ke mahal: jika yang depan jatuh, yang belakang tak perlu dijalankan:

Gerbang Bentuk perintah Menangkap apa Biaya
Lint skema node scripts/lint-schemas.mjs Galat sintaks, kata kunci usang Milidetik
Validasi fixture Rangkaian uji di bagian 2 Tak sinkron fixture/skema, batasan tertukar keliru Milidetik
Diff perubahan memecah json-schema-diff-validator lawan main required dibuang, enum dipersempit, pattern diperketat Detik
Validasi respons nyata Uji smoke ke staging dengan asersi .toMatchSchema() Hanyutan endpoint nyata Detik

Konfigurasi minimum GitHub Actions:

- run: npm ci
- run: npm run lint:schemas        # Ajv2020 lint
- run: npm run test:schema         # trio fixture
- run: npx jest --ci --testPathPattern=schema   # penjaga hanyutan snapshot

Gerbang perubahan memecah layak ditekankan tersendiri: alat seperti json-schema-diff-validator menandai merah "field required dihapus" dan "nilai enum hilang" sudah pada saat peninjauan — tepat dua perubahan yang API-diff biasa suka menilai "tidak memecah". Menghapus field respons tak berbahaya bagi konsumen toleran dan mematikan bagi konsumen dengan switch menyeluruh.

6. Asersi objek galat, bukan hanya boolean

Uji negatif yang hanya mengasersi validate(data) === false tidak memadai. Untuk skema yang sama, pastikan ia menolak karena alasan yang benar: batasan asing bisa lolos kebetulan. Objek galat Ajv punya asimetri tersohor: ketika required gagal, instancePath menunjuk ke objek induk, dan nama field yang hilang bersembunyi di params.missingProperty. Diukur saat id tiada:

{ "instancePath": "", "schemaPath": "#/required",
  "keyword": "required", "params": { "missingProperty": "id" } }

Asersi ditulis "ada galat dengan keyword=required dan missingProperty=id", bukan kabur errors.length > 0. Pembongkaran penuh semantik field galat ada di Galat JSON Schema tak terbaca: lokasi galat Ajv dan perawatan pesan.

7. Jebakan sering — tabel cepat

Jebakan Akibat Obat
Meng uji hanya 200 happy path Hanyutan sampul galat luput perhatian Skema dan fixture juga untuk respons galat
Skema hasil generator tanpa telaah required disimpulkan dari satu contoh menolak data nyata; tim mematikan validasi Telaah manusia required/enum setelah generasi
Satu skema global untuk semua versi Perbedaan v1/v2 terhapus Versikan skema bersama endpoint
Payload penuh ke log saat gagal PII dan rahasia bocor ke log CI Tulis hanya jalur JSON Pointer dan cuplikan dipendekkan
Enum beku Penggantian nama di produk (canceled→cancelled) meruntuhkan validasi Perubahan enum lewat tinjauan kesesuaian
Versi validator tak dipinkan Laku berlainan antar mesin Pinkan versi validator dalam CI
Mengganti uji otorisasi 403 bisa cocok dengan skema galat dan logika peran tetap salah Skema jaga struktur; otorisasi diuji terpisah
Melupakan semantik format Alamat sampah lolos bebas Pasang ajv-formats / serahkan format_checker, tambah negatif

8. Daftar periksa penerapan

  1. Berkas skema ke kendali versi, node scripts/lint-schemas.mjs ke lint;
  2. Tiap skema sekurang-kurangnya 1 fixture positif dan 1 negatif per batasan; nama berkas adalah asersi;
  3. Objek galat ke snapshot; dalam CI beri --ci agar snapshot tak menghanyut sendiri;
  4. Instans validator dipakai ulang di tingkat modul — jangan new dalam handler;
  5. Diff perubahan memecah digantung pada PR; ubahan required/enum dengan tinjauan wajib;
  6. PR pertama sengaja membuktikan gerbang bisa jatuh: buang satu required, lihat merah, pulihkan.

Soal ketegasan validasi (kapan menutup additionalProperties) adalah topik lain — lihat additionalProperties dibiarkan terbuka: validasi skema tidak berguna. Artikel ini tentang menjaga aturan yang tertulis selalu bisa dipercaya. Untuk dasar, mulai dari JSON Schema dari dasar ke rekayasa.

FAQ

Apa hubungan validasi skema dengan pengujian kontrak ala Pact?

Itu pembagian kerja. Validasi skema menjawab "apakah dokumen ini benar secara struktur" — asersi satu arah. Pengujian kontrak dipacu konsumen (Pact) menjawab "apakah penyedia masih memenuhi kebutuhan spesifik konsumen X" dan menuntut verifikasi dua arah. Tim kecil mulai dengan gerbang skema, dan mengambil Pact saat jumlah layanan membesar.

Berapa banyak uji negatif cukup?

Satu negatif per batasan adalah lantai: sebanyak fixture negatif dengan jumlah batasan skema (required, type, minimum, enum, pattern, additionalProperties…), tambah pasangan nilai batas ditolak/diterima. Negatif kurang dari batasan berarti satu batasan tak berpenjaga.

Perubahan skema mana dihitung "memecah"?

Nilai dari sudut konsumen: membuang field respons, membuang atau mempersempit enum, memperketat pattern, menambah field wajib request — semuanya memecah; menambah field opsional respons biasanya aman — dengan syarat dekoder konsumen menoleransi kunci asing. Otomatiskan perbandingan dengan json-schema-diff-validator saat PR, dan zona abu-abu putuskan sendiri.

Menguji ke API nyata atau mock?

Keduanya, berlapis: uji fixture tak menyentuh jaringan, berjalan dalam milidetik, berjalan tiap PR; validasi nyata menghantam staging, memakan detik, milik pipeline smoke, dan dilompati skipif tanpa kredensial. Tim yang dalam CI hanya menjalankan API nyata terkikis oleh jaringan goyang dan batas laju, lalu pada akhirnya mematikan uji.

Referensi

Artikel terkait

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

JSON Schema: Dari Dasar hingga Produksi

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 bacaDiperbarui 2026-10-30