JSON Toolkit

JSON Schema এরর যখন অর্থহীন লাগে: Ajv এরর লোকেশন ও মেসেজ গভর্নেন্স

সংক্ষেপে: ভ্যালিডেটর মেশিন এরর দেয়; ব্যবহারকারী জানতে চায় কোন ইনপুট বক্স ভুল। Ajv errors অ্যারে, instancePath বনাম schemaPath, গ্রুপিং ও লেখার ধরন।

উন্নত~10 মিনিট পড়াপ্রকাশ 2026-10-31আপডেট 2026-10-31
Ajv ভ্যালিডেশন এরর লোকেশন ও মেসেজ গভর্নেন্সের চিত্র

মূল কথা: ব্যর্থ ডেটা লোকেট করতে আগে instancePath পড়ুন, তারপর যে নিয়ম ট্রিগার হয়েছে তা লোকেট করতে schemaPath; required ও additionalProperties-এ মেসেজ বাছার আগে params থেকে প্রকৃত কি-নাম নিন।

ভ্যালিডেটরের কাজ কোন নিয়ম ব্যর্থ হয়েছে বলা। একে "এই ইনপুট বক্স ভুল" বানানো আপনার কাজ।

১. errors অ্যারে আসলে কী দেয়

Ajv ভ্যালিডেশন ব্যর্থতায় throw করে না; ফল validate.errors-এ রাখে। ডিফল্ট কনফিগ কেবল প্রথম এরর রাখে, আর allErrors পূর্ণ তালিকা উৎপন্ন করে। একটি এরর অবজেক্টে পাঁচটি কাজের ফিল্ড, আর সেগুলো বোঝা পরের সবকিছুর পূর্বশর্ত:

ফিল্ড অর্থ উদাহরণ
instancePath ব্যর্থ মানের JSON Pointer /email
schemaPath স্কিমায় ব্যর্থ কি-ওয়ার্ডের অবস্থান #/properties/email/format
keyword লঙ্ঘিত বাধ্যবাধকতার নাম format
params সেই বাধ্যবাধকতার মেশিন-পাঠযোগ্য প্যারামিটার { "format": "email" }
message সংস্করণভেদে বদলানো ইংরেজি গদ্য must match format "email"

একটি বাস্তব এরর এরকম:

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

লক্ষ্য করুন message মানুষ-readable গদ্য, স্থিতিশীল ইন্টারফেস নয়। Ajv সংস্করণভেদে শব্দ বদলাতে পারে, আর এটি localized পণ্যে কাজে আসে না। কখনো রেগুলার এক্সপ্রেশন দিয়ে এটি পার্স করবেন না, আর কখনো ক্লায়েন্টকে হুবহু ফেরত দেবেন না।

২. instancePath ও schemaPath ভিন্ন প্রশ্নের উত্তর দেয়

instancePath বলে ডেটার কোন অংশ ব্যর্থ; schemaPath বলে কোন নিয়ম ট্রিগার হয়েছে। এরা দুই কোঅর্ডিনেট সিস্টেম, আর প্রতিসম নয়:

  • instancePath RFC 6901 JSON Pointer। খালি স্ট্রিং মানে রুট; /items/0/price মানে প্রথম অ্যারে এলিমেন্টের price ফিল্ড।
  • schemaPath # দিয়ে শুরু হয় আর স্কিমা ডকুমেন্টের ভেতরের কি-ওয়ার্ডে ইশারা করে। $ref জুড়ে এটি রেফারেন্সড স্কিমার ভেতরে ইশারা করে, তাই কোন ফাইল থেকে এসেছে জানতে $id দরকার।
  • / বা ~ ধারণকারী কি যথাক্রমে ~1 ও ~0-এ এস্কেপ হয়। প্রদর্শনের আগে উল্টে দিন, নাহলে UI বিভ্রান্তিকর চিহ্ন দেখাবে।

উদাহরণ, instancePath: "/email" ও schemaPath: "#/properties/email/format" মানে রুট অবজেক্টের email ফিল্ড format চেক পাস করেনি।

সবচেয়ে সাধারণ ফাঁদ required ও additionalProperties: তাদের instancePath প্যারেন্ট অবজেক্টে ইশারা করে, দোষী কি-তে নয়।

কি-ওয়ার্ড instancePath কোথায় ইশারা করে প্রকৃত কি কোথায়
required প্যারেন্ট অবজেক্ট (রুটে খালি স্ট্রিং) params.missingProperty
additionalProperties প্যারেন্ট অবজেক্ট params.additionalProperty
type ব্যর্থ মান নিজেই —
enum ব্যর্থ মান নিজেই params.allowedValues

এররকে ফর্ম ফিল্ডে ম্যাপ করার সময় এই দুই ক্ষেত্র আগে সামলান। নাহলে "email নেই" হয়ে যায় "রুট অবজেক্ট এরর", আর ব্যবহারকারীর বোঝার উপায় থাকে না কোথায় টাইপ করবে।

৩. allErrors ও verbose: সম্পূর্ণতা বনাম খরচ

ডিফল্ট allErrors: false Ajv-কে প্রথম ব্যর্থতায় থামায়। কমান্ড-লাইন টুলের জন্য ঠিক, ফর্মের জন্য বিপর্যয়: ব্যবহারকারী একটি ফিল্ড ঠিক করে, জমা দেয়, পরের এরর দেখে, আর ফর্ম পাস হতে তিনবার পুনরাবৃত্তি করে।

সেটিং আচরণ সুবিধা খরচ
allErrors: false কেবল প্রথম এরর দ্রুততম, কম মেমরি বারবার জমা
allErrors: true প্রতিটি এরর জমা একবারে সব সমস্যা জটিল স্কিমায় এরর-সংখ্যা বিস্ফোরিত
verbose: true প্রতিটি এররে schema ও data জুড়ে সার্ভার-পক্ষে নির্ণয় সহজ আকার অনেক বাড়ে; কখনো প্রকাশ করবেন না
messages: false message ফিল্ড বাদ ছোট পেললোড ইংরেজি ডিবাগ সূত্র হারায়

সুপারিশকৃত বিভাজন: সার্ভার ভ্যালিডেশন ফর্মের জন্য allErrors: true চালু করুন ও পূর্ণ অ্যারে অভ্যন্তরীণ লগে রাখুন, আর পাবলিক রেসপন্স কেবল হোয়াইটলিস্ট ফিল্ড বহন করুক। verbose কেবল তদন্তের সময় চালু করুন; এটি প্রতিটি এররে মিলে যাওয়া স্কিমা খণ্ড ও ব্যবহারকারীর ডেটা এম্বেড করে, যা মেমরি খায় ও লগে সংবেদনশীল মান ফাঁস করতে পারে।

এররের আয়তন আরেক সাধারণ সমস্যা। ব্যর্থ anyOf শাখা কয়েকটি শিশু এরর প্লাস একটি সারসংক্ষেপ এরর দেয়, আর বহু-শাখার oneOf আরও খারাপ। অ্যারে যেমন আছে দেখালে ব্যবহারকারী এক ফিল্ডের জন্য পাঁচটি প্রায়-ডুপ্লিকেট মেসেজ দেখে। রেন্ডারের আগেই গ্রুপিং করতে হবে।

৪. এররকে ফিল্ড অনুযায়ী গ্রুপ করুন

গ্রুপিং তিন কাজ করে: পাথকে ফিল্ড-নামে ফেরানো, required ও additionalProperties-এর অফসেট ঠিক করা, আর একই ফিল্ডের একাধিক এররকে সবচেয়ে কার্যকর একটি মেসেজে মিলানো।

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

/**
 * Group Ajv errors by field.
 * @param errors Error array produced by Ajv
 * @returns Map keyed by field path, each value listing its errors
 */
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;
}

অগ্রাধিকার ক্রমে মিলান: আগে required (অনুপস্থিত), তারপর type, তারপর enum/const, তারপর format/pattern, তারপর দৈর্ঘ্য ও রেঞ্জ। প্রতি ফিল্ডে কেবল সর্বোচ্চ-অগ্রাধিকার মেসেজ দেখান আর বাকি লগে রাখুন। কারণ সরল: টাইপ ভুল হলে দৈর্ঘ্যের মেসেজ শব্দ, আর টাইপ ঠিক করাই একমাত্র বুদ্ধিমান পরবর্তী পদক্ষেপ।

oneOf ও anyOf-এর সারসংক্ষেপ এরর সাধারণত কোনো কার্যকর তথ্য বহন করে না। রেন্ডারে সেগুলো এড়িয়ে কেবল নির্দিষ্ট ফিল্ডে ইশারা করা শাখা এরর রাখুন।

৫. কাস্টম মেসেজ: প্লাগইন পথ ও টেমপ্লেট পথ

Ajv localized লেখা দেয় না, তাই দুই বাস্তব বিকল্প।

বিকল্প এক: ajv-errors প্লাগইন। এটি errorMessage কি-ওয়ার্ড যোগ করে যাতে মেসেজ বাধ্যবাধকতার পাশেই থাকে:

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

সুবিধা হলো লেখা ফিল্ডের সঙ্গে যায় আর স্কিমা একইসঙ্গে ডকুমেন্টেশন হয়। অসুবিধা হলো টেক্সট এক ভাষায় জমে যায়, তাই localization মানে কয়েকটি স্কিমা রক্ষণাবেক্ষণ। প্লাগইন allErrors: true সেট থাকলেই সব মেসেজ প্রতিস্থাপন করে; ফ্ল্যাগ ভুলে গেলে কাস্টম ও ডিফল্ট ইংরেজির মিশ্রণ পাবেন।

বিকল্প দুই: কি-ওয়ার্ড→টেমপ্লেট টেবিল। ভ্যালিডেটর কেবল keyword ও params দেয়; অ্যাপ্লিকেশন লেখা উৎপন্ন করে:

কি-ওয়ার্ড টেমপ্লেট প্রাপ্য প্যারামিটার
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 (do not echo it)

বিকল্প দুই একাধিক ভাষা ও সামঞ্জস্যপূর্ণ স্বর দরকার এমন পণ্যে মানায়। এটি ভ্যালিডেশনকে উপস্থাপন থেকে আলাদা করে, অনুবাদ স্কিমার বাইরে রাখে, আর ফ্রন্টএন্ডকে একই টেমপ্লেট পুনরায় ব্যবহার করতে দেয়।

৬. লোকালাইজেশন: মেসেজ ভ্যালিডেটরের নয়

লেখা ভাষা প্যাকে সরানো মেসেজ গভর্নেন্সের সূচনা। ভাষা প্যাক টেমপ্লেট ও কি রাখে, কখনো ফিল্ড-নাম নয়; ফিল্ড লেবেল ফর্ম কনফিগ বা API চুক্তি থেকে আসে, আর রেন্ডার সময় দুইটা মিলিত হয়।

/** 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',
};

/**
 * Build one user-facing validation message.
 * @param keyword Name of the failed constraint
 * @param params Constraint parameters
 * @returns Localized message text
 */
function buildMessage(keyword: string, params: Record<string, any>): string {
  const template = VALIDATION_TEMPLATES[keyword];
  return template ? template(params) : 'The value is invalid';
}

এখানে তিন নিয়ম গুরুত্বপূর্ণ:

  1. বাক্য জোড়া লাগাবেন না। "Email format is invalid" ও তার বাংলা সমতুল্যের শব্দক্রম এক নয়, আর স্ট্রিং জোড়া লাগিয়ে শেষে কোনোটিতে অর্থহীন বাক্য তৈরি হবে।
  2. বহুবচন ও পরিমাপক i18n লাইব্রেরিতে ছাড়ুন। টেমপ্লেটে "characters" হার্ডকোড করবেন না; 1 character ও 3 characters আলাদা।
  3. ফিল্ড লেবেলও অনুবাদ করুন। params.missingProperty হলো email, অথচ UI-তে "ইমেইল ঠিকানা" বলা উচিত। সেই ম্যাপিং ফর্ম কনফিগে রাখুন, ভাষা প্যাকে নয়।

৭. ক্লায়েন্টকে অভ্যন্তরীণ কাঠামো ফেরত দেবেন না

schemaPath, params ও message একসঙ্গে ফেরত দেওয়া ডেভেলপমেন্টে সুবিধাজনক আর প্রোডাকশনে তথ্য ফাঁস। allowedValues অভ্যন্তরীণ এনাম প্রকাশ করে, pattern ভ্যালিডেশন রেগুলার এক্সপ্রেশন প্রকাশ করে, আর schemaPath ফাইল বিন্যাস ও ফিল্ড নামকরণ প্রকাশ করে — কিছু ক্ষেত্রে আক্রমণকারীকে দুর্বলতার ম্যাপ ধরিয়ে দেয়।

পাবলিক রেসপন্সে ঠিক তিনটি জিনিস থাকুক: ফিল্ড পাথ (বা ফর্ম ফিল্ড-নাম), স্থিতিশীল এরর কোড, আর ব্যবহারকারী-মুখী টেক্সট। কাঁচা keyword-এর বদলে ছোট স্থিতিশীল কোড ব্যবহার করুন যাতে পরে ভ্যালিডেটর বদলানোর জায়গা থাকে।

{
  "type": "about:blank",
  "title": "রিকোয়েস্ট ভ্যালিডেশনে ব্যর্থ",
  "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" }
  ]
}

সার্ভার লগে এর উল্টো থাকুক: পূর্ণ এরর অ্যারে, রিকোয়েস্ট id ও স্কিমা সংস্করণ, যাতে তদন্ত "কোন নিয়ম এটি আটকেছে" উত্তর দিতে পারে। পুরো রিকোয়েস্ট বডি লগে ঢালবেন না; ব্যর্থ পাথ ও কি-ওয়ার্ড রাখুন।

৮. ফ্রন্টএন্ড উপস্থাপন: ফিল্ড হাইলাইট থেকে এডিটর মার্কার

এরর শেষে UI-তে পৌঁছে, আর উপস্থাপন প্রসঙ্গের উপর নির্ভর করে:

প্রসঙ্গ উপস্থাপন মূল ক্রিয়া
ফর্ম ফিল্ডের নিচে লাল টেক্সট, প্রথম এররে ফোকাস পাথকে ফর্ম-নামে ম্যাপ করুন; অ্যারে ইনডেক্স রাখুন
বাল্ক টেবিল ইমপোর্ট সারি নম্বর, কলাম-নাম, সারসংক্ষেপ, এক্সপোর্টযোগ্য /items/3/price-কে সারি ৪, price কলাম বানান
JSON এডিটর ইনলাইন মার্কার ও সমস্যা প্যানেল JSON Pointer-কে লাইন ও কলামে ম্যাপ করতে পার্সার ব্যবহার করুন
API ডিবাগ টুল schemaPath ধরে রাখা কাঠামোগত এরর তালিকা ডেভেলপার-মুখী, তাই বেশি বিস্তার গ্রহণযোগ্য

তিনটি বাস্তবায়ন নোট:

  • প্রতি ফিল্ডে কেবল প্রথম এরর দেখান, বাকিগুলো "আরও ২টি সমস্যা"-তে চেপে দিন, যাতে ফর্ম লালে ডুবে না যায়।
  • টার্গেটিংয়ে অ্যারে ইনডেক্স রাখুন কিন্তু লেবেলে বাদ দিন। "সারি ৪, price" /items/3/price-এর চেয়ে ভালো পড়া যায়।
  • এডিটর পরিস্থিতিতে পজিশন ম্যাপিং দরকার। ফিল্ড-নাম পুনরাবৃত্ত হলে সাধারণ স্ট্রিং খোঁজা অনির্ভরযোগ্য; পার্সার প্রতিটি নোডের অফসেট দিক, যা আপনি লাইন ও কলামে বদলাবেন।

JSON ব্যবহারকারীর টাইপ করা থেকে আসলে, ফর্মের বদলে, আগে নিশ্চিত করুন ডকুমেন্ট সিনট্যাক্টিকভাবে বৈধ। সিনট্যাক্স এরর পার্সিংয়ে ঘটে আর কখনো স্কিমা ভ্যালিডেশনে পৌঁছায় না। JSON পার্স ও ফরম্যাট ঠিক লাইন ও কলাম রিপোর্ট করে, যা স্কিমা এররের পিছনে দৌড়ানোর চেয়ে অনেক দ্রুত। নমুনা থেকে অনুমান করলে অতিরিক্ত কঠোর স্কিমা কীভাবে আসে জানতে দেখুন স্বয়ংক্রিয় JSON Schema কেন বাস্তব ডেটা বারবার প্রত্যাখ্যান করে; ভ্যালিডেটর কনফিগ ও কি-ওয়ার্ড ব্যবস্থার জন্য দেখুন JSON Schema: মূল বিষয় থেকে প্রোডাকশন।

৯. সাধারণ প্রশ্ন

আমি কেবল একটি এরর পাই কেন?

allErrors ডিফল্টে false, তাই Ajv প্রথম ব্যর্থতায় ফেরত দেয়। ফর্ম ভ্যালিডেশনের জন্য স্পষ্টভাবে চালু করুন; নাহলে ব্যবহারকারী প্রতিটি সমস্যা জানতে বারবার জমা দিতে বাধ্য।

required এররের পাথ খালি কেন?

required-এর instancePath প্যারেন্ট অবজেক্টে ইশারা করে, যা রুটে খালি স্ট্রিং; অনুপস্থিত কি-নাম থাকে params.missingProperty-তে। দুটো জোড়া লাগালে পূর্ণ পাথ পাবেন। কেবল instancePath দেখালে অকেজো "রুট অবজেক্ট এরর" ওঠে।

message কি সোজা ব্যবহারকারীকে দেখানো যায়?

সুপারিশ করা হয় না। এটি ইংরেজি গদ্য যার শব্দ সংস্করণভেদে বদলায়, আর localization ক্ষমতা নেই। এর বদলে keyword ও params থেকে টেক্সট উৎপন্ন করুন আর message লগের জন্য রাখুন।

আমার কাস্টম এরর মেসেজ উপেক্ষিত কেন?

আগে নিশ্চিত করুন allErrors: true চালু; সেই ফ্ল্যাগ থাকলেই ajv-errors সব মেসেজ প্রতিস্থাপন করতে পারে। তারপর নিশ্চিত করুন errorMessage ব্যর্থ কি-ওয়ার্ডের সমান স্তরে বসানো — এক স্তর সরে থাকলে তা চেনা হবে না।

প্রোডাকশনে কোন ফিল্ড ফেরত দেওয়া নিরাপদ?

ফিল্ড পাথ, স্থিতিশীল এরর কোড ও ব্যবহারকারী-মুখী টেক্সট। schemaPath, params ও message সার্ভার লগে রাখুন: তা ডিবাগিং সমর্থন করে অথচ অভ্যন্তরীণ এনাম, রেগুলার এক্সপ্রেশন বা ফাইল বিন্যাস প্রকাশ করে না।