JSON Toolkit

Ujian Kontrak JSON Schema: Kimpal Peraturan Pengesahan ke Dalam CI

Inti pati: Skema tidak berakhir dengan penulisan — di situlah kemerosotan bermula. Satu salah taip senyap mengendurkan pengesahan, satu pemfaktoran semula memecahkan semua fixture. Panduan ini menghuraikan susunan ujian kontrak yang boleh dilaksanakan: fixture positif, negatif dan snapshot, lint skema itu sendiri, pengumpulan sekali dengan penggunaan semula, dan empat gerbang CI yang menukar pecahan kontrak kepada binaan merah.

Pertengahan~8 min bacaanDiterbitkan 2026-11-05Dikemas kini 2026-11-05
Gambar rajah ujian kontrak JSON Schema dan gerbang pengesahan CI

Kesimpulan: skema ialah kod, dan kod memerlukan ujian. Fixture positif membuktikan ia meluluskan yang patut; negatif membuktikan ia menahan yang wajib; snapshot menjaga perubahan tidak sengaja. Akhirnya ajv compile dan snapshot dengan --ci berjaga malam dalam talian paip anda.

Pada saat peraturan pengesahan pecah, tiada apa melempar ralat: pelebaran penerimaan tidak disedari sesiapa, pengukuhan penolakan ditanggung seluruh pasukan. Skema bukan fail konfigurasi — ia kod yang tiada siapa mengujinya.

1. Skema pecah — siapa tahu lebih awal

Mari mulakan dengan perangkap yang benar-benar kami pijak. Isytiharkan skema mengikut draf 2020-12 dan kumpulkan dengan pakej lalai Ajv: pengecualian terus melonjak:

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

Titik masuk lalai Ajv 8 hanya kenal draft-07; untuk 2020-12 perlu import dari ajv/dist/2020. Perangkap lain: dalam mod strict, menulis format: "email" tanpa memasang ajv-formats juga meruntuhkan kompilasi:

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

Melihat ralat-ralat ini secara tempatan pada kompilasi pertama adalah baik. Masalahnya: banyak projek menangguhkan kompilasi sehingga ketibaan request, dan skema pecah menunggu mangsa pertama. Lebih licik lagi skema separuh pecah: required tersilap ditulis sebagai rentetan dan bukan tatasusunan membuang schema is invalid: data/required must be array semasa kompilasi — tetapi jika langkah itu tidak pernah berjalan dalam CI, ia terlantar dalam pengeluaran.

Jadi gerbang pertama tidak mengesahkan data: ia mengesahkan skema itu sendiri. Satu arahan:

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: pakej ajv v8 tidak membawa CLI, jadi npx ajv gagal dengan could not determine executable to run; CLI ada dalam pakej berasingan ajv-cli (5.0.0 dalam ukuran kami). Tetapi ajv-cli 5 hanya menyokong draft-07 dan draft-2019-09 — ditujukan pada skema 2020-12 di bahagian 1, ia membaling no schema with key or ref "https://json-schema.org/draft/2020-12/schema" yang sama. Maka projek 2020-12 menjalankan lint dengan Ajv2020 yang sama seperti artikel; npx ajv-cli compile -s '**/*.schema.json' --strict hanya sesuai untuk projek draft-07.

Jalankan seperti linter: salah taip, kata kinci lapuk dan format tidak berdaftar muncul sudah di peringkat PR. Itulah persamaan pertama menguji skema dengan menguji kod biasa: dahulu pastikan ia dapat dikompilasi.

2. Tiga sekawan: positif, negatif, snapshot

Kerangka ujian skema ada tiga keping.

Fixture positif: sampel dikutip daripada respons sebenar, yang patut lulus. Ambil dari staging, bukan dari ingatan — positif tulisan tangan hanya menutup happy path dan melupakan null, rentetan kosong dan sempatan medan wajib sahaja.

Fixture negatif: untuk setiap kekangan skema, sekurang-kurangnya satu input pelanggar. Untuk minimum: 0 pasangan -1 (penolakan) dan 0 (lulusan); untuk enum nilai di luar senarai; untuk additionalProperties: false medan berlebihan. Negatif lebih berat daripada positif: skema yang senyap meluluskan data jahat ialah pepijat senyap yang mencemar sehingga pangkalan data.

Konvensyen penamaan yang menjadikan nama fail penegasan, boleh disalin daripada sumber terbuka:

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

node:test terbina dalam 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} patut lulus`, () => { assert.ok(validate(data), JSON.stringify(validate.errors)); });
  } else {
    test(`${f} patut ditolak`, () => { assert.ok(!validate(data)); });
  }
}

Snapshot: serialkan validate.errors ke fail dan bandingkan. Seseorang tanpa sengaja mengubah kekangan — snapshot serta-merahnya. Dalam Jest/Vitest satu baris: expect(errors).toMatchSnapshot(); dalam CI sentiasa beri --ci: snapshot baharu ketika itu gagal dan bukan dibungkam secara senyap, dan hanyutan tidak diberi kelonggaran.

3. Kumpulkan semua ralat dalam satu laluan

Tetapan yang kerap dilupakan apabila menguji negatif: secara lalai pengesah berhenti pada ralat pertama. Data pecah di tiga tempat, anda nampak satu, baiki, jalankan semula — dan seterusnya. Dengan allErrors: true (Ajv) atau iter_errors (Python), semuanya nampak serentak:

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 menentang naluri: ralat yang dilaporkan validate() bukan "yang pertama" tetapi yang dipilih best_match. Dalam ukuran kami, dengan tiga pelanggaran (jenis id salah, role di luar senarai, medan extra berlebihan), hanya additionalProperties dilaporkan — ralat jenis id kekal tersembunyi. Dalam CI sentiasa gunakan iter_errors untuk gambaran penuh.

Dan perangkap yang masih lebih mudah: format bukan penegasan secara lalai. Draf 2020-12 mengelas format sebagai anotasi: jsonschema Python tidak mengesahkannya tanpa format_checker diserahkan — dalam ujian kami "not-an-email" lalu bebas. Ajv menuntut pemasangan dan pendaftaran ajv-formats. Jika format itu penting (e-mel, tarikh, uuid), mesti ada fixture negatif yang membuktikan konfigurasi pengesah anda benar-benar memeriksanya.

4. Kumpul sekali — jangan anggap pengkompil sebagai bahan pakai habis

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

Pendekatan Masa untuk 10,000 pengesahan
Setiap kali new Ajv() lalu compile 42233 ms
Instans peringkat modul + compile sekali 6.9 ms

Faktor 6000. Pengesah dalam handler request yang mencipta Ajv baharu pada setiap panggilan ialah kemalangan prestasi paling biasa. Catatkan juga tanggapan salah yang meluas: "ajv.validate(schema, data) mengumpul semula setiap kali". Diukur: 100,000 panggilan habis 8.2 ms — setanding 11.2 ms fungsi sekali-kumpul, kerana Ajv menyimpan cache skema dalaman. Penyakit sebenar bukan kegagalan cache, tetapi instans tidak diguna semula. Dalam Python pun sama: Draft202012Validator yang diguna semula kira-kira 5000 kali lebih pantas daripada memanggil validate() setiap kali (2000 lelaran: 0.3 ms berbanding 1566 ms), kerana yang itu setiap panggilan memeriksa skema itu sendiri.

5. Kimpal gerbang ke dalam CI: empat pintu

Tersusun dari murah ke mahal: jika yang hadapan gugur, yang belakang tidak perlu dijalankan:

Gerbang Bentuk arahan Menangkap apa Kos
Lint skema node scripts/lint-schemas.mjs Ralat sintaks, kata kunci lapuk Milisaat
Pengesahan fixture Set ujian di seksyen 2 Tak sepadan fixture/skema, kekangan tertukar tersilap Milisaat
Diff perubahan memecahkan json-schema-diff-validator melawan main required dibuang, enum dikecilkan, pattern diketatkan Saat
Pengesahan respons sebenar Ujian smoke ke staging dengan penegasan .toMatchSchema() Hanyutan endpoint sebenar Saat

Konfigurasi minimum GitHub Actions:

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

Gerbang perubahan memecahkan layak ditekankan berasingan: alat seperti json-schema-diff-validator menanda merah "medan required dibuang" dan "nilai enum hilang" sudah semasa semakan — tepat dua perubahan yang API-diff biasa sudi menilai "tidak memecahkan". Menyahut medan respons tidak berbahaya bagi pengguna bertoleransi dan mematikan bagi pengguna dengan switch menyeluruh.

6. Tegas pada objek ralat, bukan hanya boolean

Ujian negatif yang hanya menegaskan validate(data) === false tidak memadai. Untuk skema sama, pastikan ia menolak atas sebab yang betul: kekangan asing mungkin lulus secara kebetulan. Objek ralat Ajv mempunyai asimetri masyhur: apabila required gagal, instancePath menunjuk ke objek induk, dan nama medan hilang berselindung dalam params.missingProperty. Diukur semasa id tiada:

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

Penegasan ditulis "wujud ralat dengan keyword=required dan missingProperty=id", bukan kabur errors.length > 0. Penyahsulatan penuh semantik medan ralat ada di Ralat JSON Schema tak terbaca: lokasi ralat Ajv dan jagaan mesej.

7. Perangkap kerap — jadual pantas

Perangkap Natijah Ubat
Meng uji hanya 200 happy path Hanyutan sampul ralat tidak disedari Skema dan fixture juga bagi respons ralat
Skema janaan tanpa semakan required disimpulkan daripada satu sampel menolak data sebenar; pasukan mematikan pengesahan Semakan manusia required/enum selepas janaan
Satu skema sejagat untuk semua versi Beza v1/v2 terhapus Versikan skema bersama endpoint
Payload penuh ke log ketika gagal PII dan rahsia bocor ke log CI Tulis hanya laluan JSON Pointer dan keratan dipendekkan
Enum beku Penamaan semula produk (canceled→cancelled) meruntuhkan pengesahan Perubahan enum melalui semakan keserasian
Versi pengesah tidak dipinkam Laku berbeza antara mesin Pinkam versi pengesah dalam CI
Mengganti ujian kebenaran 403 boleh selari skema ralat dan logik peranan tetap salah Skema jaga struktur; kebenaran diuji berasingan
Melupakan semantik format Alamat sampah lalu bebas Pasang ajv-formats / serahkan format_checker, tambah negatif

8. Senarai semak pelaksanaan

  1. Fail skema ke kawalan versi, node scripts/lint-schemas.mjs ke lint;
  2. Bagi setiap skema sekurang-kurangnya 1 fixture positif dan 1 negatif bagi setiap kekangan; nama fail ialah penegasan;
  3. Objek ralat ke snapshot; dalam CI beri --ci agar snapshot tidak menghanyut sendiri;
  4. Instans pengesah diguna semula pada peringkat modul — jangan new dalam handler;
  5. Diff perubahan memecahkan digantung pada PR; ubahan required/enum dengan semakan wajib;
  6. PR pertama secara sengaja membuktikan gerbang boleh gugur: buang satu required, lihat merah, pulihkan.

Soal ketegasan pengesahan (bila menutup additionalProperties) ialah topik lain — lihat additionalProperties dibiarkan terbuka: pengesahan skema tidak berguna. Artikel ini tentang mengekalkan peraturan bertulis sentiasa boleh dipercayai. Untuk asas, mulakan dari JSON Schema dari asas ke kejuruteraan.

FAQ

Apa hubungan pengesahan skema dengan ujian kontrak ala Pact?

Itu pembahagian kerja. Pengesahan skema menjawab "adakah dokumen ini betul dari segi struktur" — penegasan satu hala. Ujian kontrak dipacu pengguna (Pact) menjawab "adakah pembekal masih memenuhi keperluan khusus pengguna X" dan menuntut semakan dua hala. Pasukan kecil mulakan dengan gerbang skema, dan Pakat diambil apabila jumlah perkhidmatan membesar.

Berapa banyak ujian negatif cukup?

Satu negatif setiap kekangan ialah paras lantai: sebanyak fixture negatif dengan bilangan kekangan skema (required, type, minimum, enum, pattern, additionalProperties…), tambah pasangan nilai sempadan ditolak/diterima. Negatif kurang daripada kekangan bermakna satu kekangan tiada penjagaan.

Perubahan skema manakah dikira "memecahkan"?

Nilai dari sudut pengguna: membuang medan respons, membuang atau mengecilkan enum, mengetatkan pattern, menambah medan wajib permintaan — semuanya memecahkan; menambah medan respons pilihan biasanya selamat — dengan syarat penyahkod pengguna menoleransi kunci asing. Automatikan perbandingan dengan json-schema-diff-validator semasa PR, dan zon kelabu putuskan sendiri.

Menguji ke API sebenar atau mock?

Kedua-duanya, berlapis: ujian fixture tidak menyentuh rangkaian, berjalan dalam milisaat dan bermula setiap PR; pengesahan sebenar menghentam staging, mengambil saat, milik pipeline smoke, dan dilangkau skipif tanpa kelayakan. Pasukan yang dalam CI hanya menjalankan API sebenar haus kerana rangkaian bergoyang dan had kadar, lalu akhirnya mematikan ujian.

Rujukan

Artikel berkaitan

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

JSON Schema: Daripada Asas kepada Pengeluaran

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 bacaanDikemas kini 2026-10-30