JSON Toolkit

Ketika Error JSON Schema Tidak Masuk Akal: Pelokasian dan Tata Kelola Pesan Error Ajv

Intinya: Validator mengeluarkan error mesin; pengguna ingin tahu kotak input mana yang salah. Mencakup array errors Ajv, instancePath versus schemaPath, pengelompokan, dan perumusan kata.

Lanjutan±10 menit bacaDiterbitkan 2026-10-31Diperbarui 2026-10-31
Diagram pelokasian error validasi Ajv dan tata kelola pesan

Intinya: baca instancePath terlebih dahulu untuk melokasikan data yang gagal, lalu schemaPath untuk melokasikan aturan yang terpicu; untuk required dan additionalProperties, ambil nama key sebenarnya dari params sebelum memilih pesan.

Tugas validator adalah menyatakan aturan mana yang gagal. Menerjemahkannya menjadi "kotak input ini salah" adalah tugas Anda.

1. Apa yang sebenarnya diberikan array errors kepada Anda

Ajv tidak melempar exception saat validasi gagal; ia meninggalkan hasilnya di validate.errors. Konfigurasi default hanya menyimpan error pertama, dan allErrors adalah yang menghasilkan daftar lengkap. Satu objek error memiliki lima field yang penting, dan memahaminya adalah prasyarat untuk semua hal di hilir:

Field Arti Contoh
instancePath JSON Pointer ke nilai yang gagal /email
schemaPath Lokasi kata kunci yang gagal di dalam schema #/properties/email/format
keyword Nama batasan yang dilanggar format
params Parameter batasan tersebut yang dapat dibaca mesin { "format": "email" }
message Prosa bahasa Inggris yang berubah antar versi must match format "email"

Error nyata terlihat seperti ini:

[
  {
    "instancePath": "/email",
    "schemaPath": "#/properties/email/format",
    "keyword": "format",
    "params": { "format": "email" },
    "message": "must match format \"email\""
  }
]

Perhatikan bahwa message adalah prosa yang ditulis untuk manusia, bukan antarmuka yang stabil. Ajv dapat mengubah kata-katanya antar versi, dan ia tidak dapat melayani produk yang terlokalisasi. Jangan pernah mengurainya dengan ekspresi reguler, dan jangan pernah mengembalikannya apa adanya ke klien.

2. instancePath dan schemaPath menjawab pertanyaan berbeda

instancePath menyatakan bagian data mana yang gagal; schemaPath menyatakan aturan mana yang terpicu. Mereka adalah dua sistem koordinat, dan tidak simetris:

  • instancePath adalah JSON Pointer RFC 6901. String kosong berarti akar; /items/0/price berarti field price dari elemen array pertama.
  • schemaPath dimulai dengan # dan menunjuk kata kunci di dalam dokumen schema. Melewati $ref, ia menunjuk ke dalam schema yang dirujuk, sehingga Anda memerlukan $id untuk mengetahui dari file mana asalnya.
  • Key yang mengandung / atau ~ di-escape menjadi ~1 dan ~0 secara berurutan. Batalkan itu sebelum ditampilkan, atau UI akan menampilkan simbol yang membingungkan.

Misalnya, instancePath: "/email" bersama dengan schemaPath: "#/properties/email/format" berarti field email pada objek akar tidak lolos pemeriksaan format.

Jebakan paling umum adalah required dan additionalProperties: instancePath mereka menunjuk objek induk, bukan key yang bermasalah.

Kata kunci instancePath menunjuk ke Di mana key sebenarnya berada
required Objek induk (string kosong di akar) params.missingProperty
additionalProperties Objek induk params.additionalProperty
type Nilai yang gagal itu sendiri —
enum Nilai yang gagal itu sendiri params.allowedValues

Saat memetakan error ke field form, tangani dua kasus ini terlebih dahulu. Jika tidak, "email hilang" menjadi "error objek akar", dan pengguna tidak tahu di mana harus mengetik.

3. allErrors dan verbose: kelengkapan versus biaya

Default allErrors: false membuat Ajv berhenti pada kegagalan pertama. Itu baik untuk alat command-line dan bencana untuk form: pengguna memperbaiki satu field, mengirim, melihat error berikutnya, dan mengulanginya tiga kali sebelum form lolos.

Pengaturan Perilaku Manfaat Biaya
allErrors: false Hanya error pertama Tercepat, memori terkecil Pengiriman berulang
allErrors: true Mengumpulkan setiap error Semua masalah dalam satu lintasan Jumlah error meledak pada schema kompleks
verbose: true Melampirkan schema dan data ke setiap error Diagnosa sisi server lebih mudah Ukuran sangat bertambah; jangan pernah mengeksposnya
messages: false Membuang field message Payload lebih kecil Kehilangan petunjuk debugging bahasa Inggris

Pembagian yang disarankan: aktifkan allErrors: true untuk validasi server dan simpan array lengkap di log internal, sementara respons publik hanya membawa field yang di-whitelist. Aktifkan verbose hanya saat menyelidiki; ia menyematkan fragmen schema yang cocok dan data pengguna ke setiap error, yang memakan memori dan dapat membocorkan nilai sensitif ke log.

Volume error adalah masalah umum lainnya. Branch anyOf yang gagal memancarkan beberapa error anak plus satu error ringkasan, dan oneOf dengan banyak branch lebih buruk lagi. Tampilkan array apa adanya dan pengguna melihat lima pesan hampir duplikat untuk satu field. Pengelompokan harus dilakukan sebelum rendering.

4. Kelompokkan error berdasarkan field

Pengelompokan melakukan tiga hal: mengubah path kembali menjadi nama field, memperbaiki offset required dan additionalProperties, dan menggabungkan beberapa error pada field yang sama menjadi satu pesan yang paling dapat ditindaklanjuti.

/** One validation error ready for display */
interface FieldError {
  field: string;
  keyword: string;
  params: Record<string, unknown>;
}

/**
 * Mengelompokkan error Ajv berdasarkan field.
 * @param errors Array error yang dihasilkan Ajv
 * @returns Map dengan kunci path field, setiap nilai mencantumkan errornya
 */
function groupByField(errors: Array<Record<string, any>>): Record<string, FieldError[]> {
  const grouped: Record<string, FieldError[]> = {};

  for (const errorItem of errors) {
    const keyword: string = errorItem.keyword;
    let field: string = errorItem.instancePath;

    if (keyword === 'required') {
      const parentPath: string = errorItem.instancePath;
      field = `${parentPath}/${errorItem.params.missingProperty}`;
    } else if (keyword === 'additionalProperties') {
      field = `${errorItem.instancePath}/${errorItem.params.additionalProperty}`;
    }

    const normalizedField = field === '' ? '(root)' : field;
    (grouped[normalizedField] ??= []).push({
      field: normalizedField,
      keyword,
      params: errorItem.params ?? {},
    });
  }

  return grouped;
}

Gabungkan dengan urutan prioritas: required (hilang) lebih dulu, lalu type, lalu enum/const, lalu format/pattern, lalu panjang dan rentang. Tampilkan hanya pesan berprioritas tertinggi per field dan simpan sisanya di log. Alasannya lugas: ketika tipe salah, pesan panjang adalah kebisingan, dan memperbaiki tipe adalah satu-satunya langkah berikutnya yang masuk akal.

Error ringkasan untuk oneOf dan anyOf biasanya tidak membawa informasi yang dapat ditindaklanjuti. Lewati mereka saat rendering dan pertahankan hanya error branch yang menunjuk field konkret.

5. Pesan kustom: jalur plugin dan jalur template

Ajv tidak menyertakan perumusan kata terlokalisasi, jadi ada dua opsi praktis.

Opsi satu: plugin ajv-errors. Ia menambahkan kata kunci errorMessage sehingga pesan berada di samping batasan:

{
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "errorMessage": {
        "format": "Please enter a valid email address",
        "type": "Email must be a string"
      }
    }
  }
}

Keunggulannya adalah perumusan kata menyertai field dan schema sekaligus berfungsi sebagai dokumentasi. Kelemahannya adalah teks terkunci dalam satu bahasa, sehingga lokalisasi berarti memelihara beberapa schema. Plugin juga hanya mengganti setiap pesan ketika allErrors: true diatur; lupakan flag itu dan Anda mendapatkan campuran pesan kustom dan bahasa Inggris default.

Opsi dua: tabel kata kunci-ke-template. Validator hanya menyediakan keyword dan params; aplikasi menghasilkan perumusan kata:

Kata kunci Template Parameter yang tersedia
required Missing required field: {field} missingProperty
type Expected type: {type} type
minLength Enter at least {limit} characters limit
maximum Must not exceed {limit} limit
format Invalid format: {format} format
enum Must be one of the allowed options allowedValues
pattern Value does not match the expected format pattern (jangan menggemakannya)

Opsi dua cocok untuk produk yang membutuhkan banyak bahasa dan suara yang konsisten. Ia memisahkan validasi dari presentasi, menjaga terjemahan keluar dari schema, dan memungkinkan frontend menggunakan kembali template yang sama.

6. Lokalisasi: pesan bukan milik validator

Memindahkan perumusan kata ke paket bahasa adalah titik awal tata kelola pesan. Paket bahasa menyimpan template dan key, tidak pernah nama field; label field berasal dari konfigurasi form atau kontrak API, dan keduanya digabungkan saat rendering.

/** Validation message templates keyed by Ajv keyword */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
  required: (params) => `Missing required field: ${params.missingProperty}`,
  type: (params) => `Expected type: ${params.type}`,
  minLength: (params) => `Enter at least ${params.limit} characters`,
  maximum: (params) => `Must not exceed ${params.limit}`,
  format: () => 'Invalid format',
};

/**
 * Membangun satu pesan validasi untuk pengguna.
 * @param keyword Nama batasan yang gagal
 * @param params Parameter batasan
 * @returns Teks pesan yang terlokalisasi
 */
function buildMessage(keyword: string, params: Record<string, any>): string {
  const template = VALIDATION_TEMPLATES[keyword];
  return template ? template(params) : 'The value is invalid';
}

Tiga aturan penting di sini:

  1. Jangan menggabungkan kalimat. "Email format is invalid" dan padanan bahasa Indonesianya tidak berbagi urutan kata, dan perakitan string akhirnya akan menghasilkan omong kosong di salah satunya.
  2. Serahkan bentuk jamak dan kata pengukur kepada library i18n. Jangan pernah menulis "characters" secara hardcode dalam template; 1 character dan 3 characters berbeda.
  3. Terjemahkan juga label field. params.missingProperty adalah email, sementara UI seharusnya menampilkan "Alamat email". Simpan pemetaan itu di konfigurasi form, bukan di paket bahasa.

7. Jangan mengembalikan struktur internal ke klien

Mengembalikan schemaPath, params, dan message bersama-sama nyaman selama pengembangan dan kebocoran informasi di produksi. allowedValues mengungkap enum internal, pattern mengungkap regex validasi, dan schemaPath mengungkap tata letak file serta penamaan field — dalam beberapa kasus menyerahkan peta titik lemah Anda kepada penyerang.

Respons publik menyimpan tepat tiga hal: path field (atau nama field form), kode error yang stabil, dan teks untuk pengguna. Gunakan kode pendek yang stabil alih-alih keyword mentah agar Anda mempertahankan ruang untuk mengganti validator nanti.

{
  "type": "about:blank",
  "title": "Request validation failed",
  "status": 422,
  "errors": [
    { "field": "email", "code": "invalid_format", "message": "Please enter a valid email address" },
    { "field": "items[0].price", "code": "invalid_pattern", "message": "Amount must have exactly two decimals" }
  ]
}

Log server harus memuat sebaliknya: array error lengkap, id request, dan versi schema, sehingga investigasi dapat menjawab "aturan mana yang memblokir ini". Hindari membuang seluruh body request ke log; simpan path dan kata kunci yang gagal sebagai gantinya.

8. Presentasi frontend: dari penyorotan field ke penanda editor

Error akhirnya mendarat di UI, dan presentasinya bergantung pada konteks:

Konteks Presentasi Tindakan kunci
Form Teks merah di bawah field, fokuskan error pertama Petakan path ke nama form; pertahankan indeks array
Impor tabel massal Nomor baris, nama kolom, ringkasan, dapat diekspor Ubah /items/3/price menjadi baris 4, kolom price
Editor JSON Penanda inline dan panel masalah Gunakan parser untuk memetakan JSON Pointer ke baris dan kolom
Alat debugging API Daftar error terstruktur yang mempertahankan schemaPath Berorientasi pengembang, jadi lebih banyak detail dapat diterima

Tiga catatan implementasi:

  • Tampilkan hanya error pertama per field, runtuhkan sisanya menjadi "2 masalah lainnya", agar form tidak tenggelam dalam warna merah.
  • Pertahankan indeks array untuk penargetan tetapi buang dalam label. "Baris 4, price" lebih mudah dibaca daripada /items/3/price.
  • Skenario editor memerlukan pemetaan posisi. Pencarian string biasa tidak andal ketika nama field berulang; parser harus menyediakan offset untuk setiap node, yang kemudian Anda konversi ke baris dan kolom.

Jika JSON berasal dari pengguna yang mengetik alih-alih form, pastikan dulu dokumen valid secara sintaks. Error sintaks terjadi selama parsing dan tidak pernah mencapai validasi schema. JSON Parse & Format melaporkan baris dan kolom yang tepat, yang jauh lebih cepat daripada mengejar error schema. Untuk bagaimana schema yang terlalu ketat muncul ketika disimpulkan dari sampel, lihat Mengapa JSON Schema yang Dibuat Otomatis Terus Menolak Data Nyata; untuk konfigurasi validator dan sistem kata kunci, lihat JSON Schema: Dari Dasar hingga Produksi.

9. Pertanyaan yang sering diajukan

Mengapa saya hanya mendapatkan satu error?

allErrors bernilai false secara default, jadi Ajv kembali pada kegagalan pertama. Aktifkan secara eksplisit untuk validasi form; jika tidak, pengguna harus mengirim berulang kali untuk menemukan setiap masalah.

Mengapa path pada error required kosong?

instancePath untuk required menunjuk objek induk, yang merupakan string kosong di akar; nama key yang hilang berada di params.missingProperty. Gabungkan keduanya untuk mendapatkan path lengkap. Menampilkan instancePath saja menghasilkan "error objek akar" yang tidak berguna.

Dapatkah saya menampilkan message langsung kepada pengguna?

Tidak disarankan. Itu prosa bahasa Inggris yang perumusan katanya berubah antar versi, dan ia tidak memiliki kemampuan lokalisasi. Hasilkan teks dari keyword dan params sebagai gantinya dan sisakan message untuk log.

Mengapa pesan error kustom saya diabaikan?

Pertama pastikan allErrors: true diaktifkan; ajv-errors hanya dapat mengganti semua pesan dengan flag itu. Lalu pastikan errorMessage berada pada level yang sama dengan kata kunci yang gagal — meleset satu level dan ia tidak akan dikenali.

Field mana yang aman dikembalikan di produksi?

Path field, kode error yang stabil, dan teks untuk pengguna. Simpan schemaPath, params, dan message di log server: itu mendukung debugging tanpa mengekspos enum internal, regex, atau tata letak file.