Skip to content

Validation (Joi) — Basics

Runtime schema description + data validator for JavaScript. Originally extracted from hapi ecosystem, now maintained as hapijs/joi. Notes target Joi 17.x.

  • Boundary check — never trust HTTP input.
  • Normalize — trim whitespace, lowercase email, coerce "5"5.
  • Document — schemas double as a typed contract; .describe() reflects into OpenAPI/admin UIs.
  • Centralize errors — single 400-formatter instead of ifs scattered through handlers.

Joi schemas are immutable, fluent builders: every method call returns a new schema, so chaining .required().default(...) is safe across modules.

Root types: Joi.object(), Joi.string(), Joi.number(), Joi.array(), Joi.boolean(), Joi.date(), Joi.alternatives(), Joi.any().

Composition:

MethodEffect
.keys({...})Extend allowed keys
.append({...})Sugar for .keys()
.concat(other)Merge two same-typed schemas — rules of both apply
.extract(path)Pull a sub-schema out (e.g. for partial-update endpoints)
.label('x')Rewrite error label from default key path
.strip()Remove the key from validated output (write-only fields like password)
.allow(null)Permit literal null without changing presence
ModifierMeaning
.required()Must be present (not undefined)
.optional()May be undefined (default)
.forbidden()Must be absent — fails if present
.allow(null)Adds null to valid values list
.empty('')Treat empty string as if absent (triggers default/optional)

They compose: Joi.string().empty('').optional().allow(null) is the common “tolerant” form.

CallBehavior
schema.validate(value, opts)Sync; returns { value, error }
schema.validateAsync(value, opts)Async (required if .external()); resolves value or throws
Joi.attempt(value, schema)Throws on failure (good for startup config)
Joi.assert(value, schema)Same but discards value
OptionEffect
abortEarly: falseCollect all errors (forms want this)
stripUnknown: trueRemove unknown keys from output
allowUnknown: trueLet unknown keys pass through (only for headers)
convert: true (default)Coerce types ("5"5, "true"true)
context: { … }Values available via Joi.ref('$key')
  • .allow(null).optional(). Often need both.
  • .default(obj) shares reference. Use .default(() => ({...})).
  • Joi.object() with no .keys() allows any child key. Joi.object({}) allows none.
  • .custom() is sync. Returning a Promise is treated as truthy value. Use .external() for async.
  • .external() runs only after schema passes AND only via validateAsync().
  • Joi is not a security sanitizer. No XSS strip, no SQL escape. Layer DOMPurify + parameterized queries.
  • In Express 5, req.query is a getter. Mutating it throws. Store on req.validated or Object.defineProperty.