Skip to content

Error Format Standardization

Every schema library has its own error structure. KickJS normalizes all validation errors into a single format regardless of which library produced them.

Unified SchemaIssue

ts
interface SchemaIssue {
  path: string[] // ["address", "zip"] — dotted path segments
  message: string // "Must be at least 5 characters"
  code: string // "min_length", "required", "invalid_type"
  expected?: string // "string", ">=18"
  received?: string // "undefined", "12"
}

HTTP Error Response (422)

json
{
  "status": 422,
  "message": "Validation failed",
  "errors": [
    {
      "field": "email",
      "message": "Invalid email address",
      "code": "email"
    },
    {
      "field": "age",
      "message": "Must be at least 18",
      "code": "min",
      "expected": ">=18",
      "received": "12"
    },
    {
      "field": "address.zip",
      "message": "Required",
      "code": "required"
    }
  ]
}

The field value is path.join('.') — a dotted string for nested fields.

Library Error Comparison

Zod

ts
// ZodError.issues[]
{
  code: "too_small",
  minimum: 18,
  type: "number",
  inclusive: true,
  exact: false,
  message: "Number must be greater than or equal to 18",
  path: ["age"]
}
// → SchemaIssue
{
  path: ["age"],
  message: "Number must be greater than or equal to 18",
  code: "too_small",
  expected: ">=18",
  received: undefined
}

Valibot

ts
// Valibot issue
{
  kind: "validation",
  type: "min_value",
  input: 12,
  expected: ">=18",
  received: "12",
  message: "Must be at least 18",
  path: [{ type: "object", origin: "value", input: {...}, key: "age", value: 12 }]
}
// → SchemaIssue
{
  path: ["age"],
  message: "Must be at least 18",
  code: "min_value",
  expected: ">=18",
  received: "12"
}

Yup

ts
// Yup ValidationError.inner[]
{
  path: "age",
  message: "age must be greater than or equal to 18",
  type: "min",
  params: { min: 18 }
}
// → SchemaIssue
{
  path: ["age"],
  message: "age must be greater than or equal to 18",
  code: "min",
  expected: ">=18"
}

Joi

ts
// Joi error.details[]
{
  message: "\"age\" must be greater than or equal to 18",
  path: ["age"],
  type: "number.min",
  context: { limit: 18, value: 12, label: "age", key: "age" }
}
// → SchemaIssue
{
  path: ["age"],
  message: "\"age\" must be greater than or equal to 18",
  code: "number.min",
  expected: ">=18",
  received: "12"
}

Standard Schema

ts
// Standard Schema FailureResult.issues[]
{
  message: "Must be at least 18",
  path: ["age"]
}
// → SchemaIssue
{
  path: ["age"],
  message: "Must be at least 18",
  code: "validation"   // Standard Schema has no code field
}

Custom Error Formatter

Override the default 422 format globally:

ts
import { bootstrap } from '@forinda/kickjs'

bootstrap({
  validation: {
    formatError(issues: SchemaIssue[], ctx: RequestContext) {
      // RFC 9457 Problem Details
      return {
        type: 'https://api.example.com/errors/validation',
        title: 'Validation Error',
        status: 422,
        detail: `${issues.length} field(s) failed validation`,
        violations: issues.map((i) => ({
          property: i.path.join('.'),
          constraint: i.code,
          message: i.message,
        })),
      }
    },
  },
})

Per-Route Error Handling

ts
@Post('/', {
  body: createUserSchema,
  onValidationError(issues, ctx) {
    ctx.status(400).json({
      ok: false,
      fields: Object.fromEntries(issues.map((i) => [i.path.join('.'), i.message])),
    })
  },
})
async create(ctx: RequestContext) { /* ... */ }

Error Codes Reference

Common normalized codes across libraries:

CodeMeaningZodValibotYupJoi
requiredMissing required fieldinvalid_typenon_optionalrequiredany.required
invalid_typeWrong typeinvalid_type* (type name)typeError*.base
minBelow minimumtoo_smallmin_value/min_lengthmin*.min
maxAbove maximumtoo_bigmax_value/max_lengthmax*.max
patternRegex mismatchinvalid_stringregexmatchesstring.pattern.base
emailInvalid emailinvalid_stringemailemailstring.email
enumNot in allowed valuesinvalid_enum_valueenum_oneOfany.only
customCustom validationcustomcustomtestany.custom