Skip to content

ValidationErrors Runtime

Validation modes: Validation modes reference

Declaration. validate() returns ValidationErrors (not string[]). See validate() for the method reference. This page covers the ValidationErrors collection shape and usage patterns.

The ValidationErrors collection is also carried on InstantiationError.errors and CoercionError.errors, so the same patterns apply when you catch those exceptions.

Use this when you need programmatic access to the structured error list - paths, keywords, params - without wanting an exception. This is the right collection for API validation where you collect errors, then decide what to do with them (return a 422, log, display in a form). The collection is iterable with for...of.

Don't use this when you only need a boolean (use is). Don't use it when you want the coerced typed value on success (use instantiate).

Public surface

MemberTypePurpose
itemsreadonly ValidationErrorType[]Raw error list with JSON Pointer paths
lengthnumberNumber of errors
okbooleantrue when length === 0
aggregate()AggregateViewType{ count, paths, keywords } rollup for logs and metrics
report(overrides?)ProblemDetailsTypeRFC 7807 Problem Details payload
[Symbol.iterator]()Iterator<ValidationErrorType>Enables for...of

Each ValidationErrorType carries:

ts
type ValidationErrorType = {
  path:    string;                 // JSON Pointer path
  keyword: string;                 // e.g. 'required', 'type', 'jt:invariant'
  message: string;                 // human-readable
  params:  Record<string, unknown> // keyword-specific params
};

Examples

Example 1: Check validity, iterate errors

/**
 * ValidationErrors — Example 1: Check validity, iterate errors
 * Demonstrates: .ok, .length, for...of iteration, .path, .keyword, .message, .params
 *
 * An Order with a negative total and an empty items array violates
 * `exclusiveMinimum` and `minItems` — surfaces at least two structured errors.
 */

import {
  aboxFixtures, bookstoreEntities, OrderSchema
} from '../bookstore/index.js';

const errs = bookstoreEntities.validate(OrderSchema.$id, {
  'customerId': aboxFixtures.customer.customerId,
  'orderId': aboxFixtures.order.orderId,
  // minItems: 1 violated
  'orderLines': [],
  'orderTotal': {
    // exclusiveMinimum: 0 violated
    'amount': -5,
    'currency': 'EUR'
  },
  'placedAt': '2026-01-15T10:30:00Z',
  'shippingAddress': aboxFixtures.order.shippingAddress
});

console.assert(!errs.ok);
console.assert(errs.length >= 2);

console.log('ok:', errs.ok, ', error count:', errs.length);

for (const err of errs) {
  console.assert(typeof err.path === 'string');
  console.assert(typeof err.keyword === 'string');
  console.assert(typeof err.message === 'string');
  console.assert(typeof err.params === 'object');
  console.log(`  path="${err.path}" keyword="${err.keyword}" message="${err.message}"`);
}
Output
Press Execute to run this example against the real library.

Example 2: Valid data returns empty collection

/**
 * ValidationErrors — Example 2: Valid data returns empty collection
 * Demonstrates: .ok === true, .length === 0 on valid input
 *
 * Michael Ende's "Die unendliche Geschichte" — Thienemann Verlag, 1979.
 * All required fields present and valid; collection is empty.
 */

import {
  aboxFixtures, BookSchema, bookstoreEntities
} from '../bookstore/index.js';

const errs = bookstoreEntities.validate(BookSchema.$id, {
  'authors': ['Michael Ende'],
  'isbn': aboxFixtures.rareBook.isbn,
  'price': aboxFixtures.rareBook.price,
  'printStatus': 'outOfPrint',
  'title': aboxFixtures.rareBook.title
});

console.assert(errs.ok);
console.assert(errs.length === 0);

console.log('ok:', errs.ok, ', errors:', errs.length);
Output
Press Execute to run this example against the real library.

Example 3: Combine with the structured views

See Error views for full documentation of each view.

/**
 * ValidationErrors — Example 3: Structured views on a failed review
 * Demonstrates: .items map, .aggregate(), .report()
 *
 * A review with a rating above the maximum and too-short body
 * triggers multiple validation errors.
 */

import {
  aboxFixtures, bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';

const errs = bookstoreEntities.validate(ReviewSchema.$id, {
  // minLength: 10 violated
  'body': 'hi',
  'bookIsbn': aboxFixtures.rareBook.isbn,
  'customerId': aboxFixtures.customer.customerId,
  'postedAt': '2026-04-20T09:15:00Z',
  // maximum: 5 violated
  'rating': 6,
  'reviewId': aboxFixtures.review.reviewId
});

console.assert(!errs.ok);

// string[] — one message per error
const messages = errs.items.map((err) => {
  return `${err.path}: ${err.message}`;
});

console.assert(Array.isArray(messages));
console.assert(messages.length > 0);
console.log('field errors:', messages);

// Aggregate rollup for logs and metrics
const agg = errs.aggregate();

console.assert(typeof agg.count === 'number' && agg.count > 0);
console.assert(Array.isArray(agg.paths));
console.assert(Array.isArray(agg.keywords));
console.log('aggregate: count =', agg.count, ', paths =', agg.paths, ', keywords =', agg.keywords);

// RFC 7807 Problem Details payload
const problem = errs.report();

console.assert(typeof problem === 'object');
console.assert('status' in problem && (problem as { 'status': number }).status === 422);
console.log('report status:', (problem as { 'status': number }).status);
Output
Press Execute to run this example against the real library.

Bad examples - what NOT to do

Anti-pattern 1: Calling validate() and then instantiate() separately

/**
 * ValidationErrors — Anti-pattern 1: validate() then instantiate() separately
 * Demonstrates: the bad pattern (double validation) vs the correct catch pattern
 *
 * Bastian Balthazar Bux — valid customer used for the correct path.
 */

import { InstantiationError } from '../../../src/index.js';
import {
  aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';

// Anti-pattern: validate then instantiate — validation runs twice
// Don't do this
const errs = bookstoreEntities.validate(CustomerSchema, aboxFixtures.customer);

if (errs.ok) {
  // validates again — redundant
  const _customer = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);

  void _customer;
}

// Correct approach: instantiate directly; catch InstantiationError if invalid
try {
  const customer = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);

  console.assert(customer.name === aboxFixtures.customer.name);
  console.log('instantiate succeeded:', customer.name);
} catch (error) {
  if (error instanceof InstantiationError) {
    // same ValidationErrors on InstantiationError
    const problem = error.errors.report();

    console.assert(typeof problem === 'object');
  } else {
    throw error;
  }
}
Output
Press Execute to run this example against the real library.

Anti-pattern 2: Re-implementing a built-in view

/**
 * ValidationErrors — Anti-pattern 2: Re-implementing a built-in view
 * Demonstrates: manual grouping (bad) vs .aggregate() (correct)
 *
 * A review with invalid rating and short body produces multiple field errors.
 * Rolling your own accumulator loop duplicates what .aggregate() provides.
 */

import {
  aboxFixtures, bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';

const errs = bookstoreEntities.validate(ReviewSchema.$id, {
  'body': 'hi',
  'bookIsbn': aboxFixtures.rareBook.isbn,
  'customerId': aboxFixtures.customer.customerId,
  'postedAt': '2026-04-20T09:15:00Z',
  'rating': 6,
  'reviewId': aboxFixtures.review.reviewId
});

// Anti-pattern: rolling your own path-to-messages accumulator
// Don't do this
const manualGrouped: Record<string, string[]> = {};

for (const item of errs.items) {
  const key = item.path.length > 0 ? item.path : '_root';

  (manualGrouped[key] ??= []).push(item.message);
}

// Correct approach: use .aggregate() or .items directly
const agg = errs.aggregate();

console.assert(agg.count === errs.length);
console.assert(agg.paths.length <= errs.length);

// The aggregate paths cover the same fields as manual grouping.
// agg.paths strips the leading slash; manualGrouped keys retain it from item.path.
console.assert(
  agg.paths.every((path) => {
    const withSlash = `/${path}`;

    return (
      Object.keys(manualGrouped).includes(path)
      || Object.keys(manualGrouped).includes(withSlash)
      || path === '_root'
    );
  }),
  'aggregate paths should correspond to the manually grouped paths'
);

console.log('aggregate count:', agg.count, ', paths:', agg.paths);
Output
Press Execute to run this example against the real library.

Comparison

ts
const errs = bookstoreEntities.validate(OrderSchema.$id, data);
// ValidationErrors  - .ok, .length, iterable, .items, .aggregate(), .report()
ts
const result = OrderSchema.safeParse(data);
if (!result.success) {
  result.error.issues; // ZodIssue[]  - path (array), code, message per issue
  result.error.flatten(); // { fieldErrors, formErrors }  - Zod-native flatten
}
ts
import * as v from 'valibot';
const result = v.safeParse(OrderSchema, data);
if (!result.success) {
  result.issues; // Issue[] - .message, .path, .expected, .received per issue
  // Limitation: no built-in aggregate() or report() views; project manually.
  v.flatten(result.issues); // { root, nested } summary
}
ts
import { isLeft } from 'fp-ts/Either';
import { PathReporter } from 'io-ts/PathReporter';
const result = OrderCodec.decode(data);
if (isLeft(result)) {
  result.left;                       // ValidationError[] - context + value per node
  PathReporter.report(result);       // string[] - flattened messages
  // Limitation: no aggregate() or report() RFC 7807 views; project manually
  // from the ValidationError context array.
}
ts
import { Value } from '@sinclair/typebox/value';
const errors = [...Value.Errors(OrderSchema, data)];
// ValueError[]  - path, message, schema, value per error
// No built-in views
ts
ajv.validate(orderSchema, data);
const errors = ajv.errors ?? [];
// ErrorObject[]  - instancePath, keyword, message, params per error
// No built-in views
py
try:
    Order(**data)
except ValidationError as e:
    e.errors()          # list of dicts: loc, msg, type
    e.error_count()     # int
    e.json()            # JSON string of errors
    # No aggregate() or report() equivalent built in
ts
// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.

See also

  • Invariants - cross-field rules that produce ValidationErrorType items with keyword: 'jt:invariant'
  • Bookstore domain - schema definitions used in examples

Released under the MIT License.