Skip to content

JsonTology.validate Runtime

Validation modes: Validation modes reference

Declaration. Validates data against a registered schema and returns a ValidationErrors collection. The collection is empty (.ok === true) when the data is valid. Does not mutate the input. Does not throw on validation failure.

Use this when you need programmatic access to the structured error list - paths, keywords, params - without wanting an exception. This is the right method 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).

Examples

Example 1: Basic valid and invalid cases

/**
 * validate — Example 1: Basic valid and invalid cases
 * Demonstrates: empty collection on success (.ok, .length), ValidationErrors on failure
 *
 * Uses the canonical Bastian Balthazar Bux customer fixture.
 */

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

// Valid input
const ok = bookstoreEntities.validate(CustomerSchema.$id, aboxFixtures.customer);

console.assert(ok.length === 0);
console.log('valid input: ok =', ok.ok, ', errors =', ok.length);

// Missing required fields — only email present.
const bad = bookstoreEntities.validate(CustomerSchema.$id, {
  'email': aboxFixtures.customer.email
  // id and name missing
});

console.assert(bad.length > 0);
console.assert(bad.items.some((err) => {
  return err.message.toLowerCase().includes('id') || err.path.toLowerCase().includes('id');
}));
console.log('missing fields: ok =', bad.ok, ', error count =', bad.length);
console.log('first error:', bad.items[0]?.path, '-', bad.items[0]?.message);
Output
Press Execute to run this example against the real library.

Example 2: Nested schema errors with JSON Pointer paths

OrderSchema contains items: [OrderLine] via $ref. Errors on nested fields include the full JSON Pointer path.

/**
 * validate — Example 2: Nested schema errors with JSON Pointer paths
 * Demonstrates: $ref resolution, paths like /total/amount and /items/0/quantity
 *
 * An Order with a negative total and a zero-quantity item — both nested under
 * $ref'd schemas — surfaces full JSON Pointer paths in the error items.
 */

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

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

const messages = errs.items.map((err) => {
  return `${err.path}: ${err.message}`;
});

console.assert(!errs.ok);
console.assert(messages.length >= 2);
// At least one error should reference a nested JSON Pointer path
console.assert(messages.some((msg) => {
  return msg.includes('/orderTotal') || msg.includes('/orderLines');
}));

console.log('error count:', messages.length);
for (const msg of messages) {
  console.log(' ', msg);
}
Output
Press Execute to run this example against the real library.

Example 3: Use as a lightweight form validator

Validate on blur before attempting a full instantiate.

/**
 * validate — Example 3: Use as a lightweight form validator
 * Demonstrates: validate on blur, return ValidationErrors to caller
 *
 * A review form validator validates before attempting a full instantiate.
 * Rating above maximum and short body surface as field-level errors.
 */

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

function validateReviewForm(formData: Record<string, unknown>): ValidationErrors {
  return bookstoreEntities.validate(ReviewSchema.$id, formData);
}

const fieldErrors = validateReviewForm({
  // 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(!fieldErrors.ok);
console.assert(fieldErrors.length >= 2);
// Errors reference specific field paths
console.assert(fieldErrors.items.some((err) => {
  return err.path.includes('rating') || err.path.includes('body');
}));

console.log('field error count:', fieldErrors.length);
for (const err of fieldErrors.items) {
  console.log(`  ${err.path}: ${err.message}`);
}
Output
Press Execute to run this example against the real library.

Bad examples - what NOT to do

Anti-pattern 1: Checking the return length and then re-instantiating

/**
 * validate — Anti-pattern 1: Check return length then re-instantiate
 * Demonstrates: double validation (bad) vs direct instantiate with catch (correct)
 *
 * Bastian Balthazar Bux — valid fixture used for the correct single-pass pattern.
 */

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

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

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

  void _customer;
}

// Correct approach: instantiate directly; validates + applies defaults in one pass
try {
  const customer = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);

  console.assert(customer.name === aboxFixtures.customer.name);
  console.log('single-pass instantiate:', customer.name);
} catch (error) {
  if (error instanceof InstantiationError) {
    console.assert(false, 'Should not throw for valid fixture');
  }
}
Output
Press Execute to run this example against the real library.

Anti-pattern 2: Re-parsing message strings to extract field paths

/**
 * validate — Anti-pattern 2: Re-parsing message strings to extract field paths
 * Demonstrates: fragile string parsing (bad) vs iterating structured errors (correct)
 *
 * An invalid customer body surfaces errors; the correct approach reads .path
 * directly from each ValidationErrorType rather than parsing formatted strings.
 */

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

const invalidBody = {
  'email': 'not-an-email',
  'id': 'x'
};

// Anti-pattern: parsing formatted strings is fragile
// Don't do this
const errsForAntipattern = bookstoreEntities.validate(CustomerSchema.$id, invalidBody);
const msg = errsForAntipattern.items[0]?.message ?? '';

// fragile string parsing
const _fragileExtract = msg.split(':')[0];

void _fragileExtract;

// Correct approach: iterate the structured ValidationErrorType objects
const structured = bookstoreEntities.validate(CustomerSchema.$id, invalidBody);

for (const err of structured) {
  console.assert(typeof err.path === 'string');
  console.assert(typeof err.keyword === 'string');
  console.assert(typeof err.message === 'string');
  console.log(`path="${err.path}" keyword="${err.keyword}" message="${err.message}"`);
}

console.assert(!structured.ok);
Output
Press Execute to run this example against the real library.

Comparison

ts
const errs = jt.validate(CustomerSchema.$id, data);
// ValidationErrors  - .ok, .length, iterable, .items, .aggregate(), .report()
// does not throw, does not coerce
ts
const result = CustomerSchema.safeParse(data);
if (!result.success) {
  const messages = result.error.issues.map(i => `${i.path.join('/')}: ${i.message}`);
}
// safeParse doesn't throw; parse() throws ZodError
ts
import * as v from 'valibot';
const result = v.safeParse(CustomerSchema, data);
if (!result.success) {
  const messages = result.issues.map(i =>
    `${i.path?.map(p => p.key).join('/') ?? ''}: ${i.message}`,
  );
}
// { success, output, issues } - parallels ValidationErrors but no .aggregate/.report views.
ts
import { isLeft } from 'fp-ts/Either';
import { PathReporter } from 'io-ts/PathReporter';
const result = CustomerCodec.decode(data); // Either<Errors, Customer>
const messages = isLeft(result) ? PathReporter.report(result) : [];
// Limitation: Errors are typed io-ts ValidationError nodes; PathReporter
// flattens them into strings but provides no .aggregate / .report / RFC 7807
// views.
ts
import { TypeCompiler } from '@sinclair/typebox/compiler';
const C = TypeCompiler.Compile(CustomerSchema);
const errors = [...C.Errors(data)].map(e => `${e.path}: ${e.message}`);
ts
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const ajv = new Ajv();
addFormats(ajv);
const valid = ajv.validate(customerSchema, data);
const messages = valid ? [] : ajv.errors!.map(e => `${e.instancePath}: ${e.message}`);
py
from pydantic import ValidationError

try:
    Customer(**data)
    messages = []
except ValidationError as e:
    messages = [f"{'/'.join(str(p) for p in err['loc'])}: {err['msg']}" for err in e.errors()]
ts
import * as yup from 'yup';

const Customer = yup.object({
  id: yup.string().uuid().required(),
  email: yup.string().email().required(),
  name: yup.string().required(),
});

try {
  Customer.validateSync(data, { abortEarly: false });
} catch (err) {
  const messages = (err as yup.ValidationError).inner.map(i => `${i.path}: ${i.message}`);
}
ts
import Joi from 'joi';

const Customer = Joi.object({
  id: Joi.string().uuid().required(),
  email: Joi.string().email().required(),
  name: Joi.string().required(),
});

const { error } = Customer.validate(data, { abortEarly: false });
const messages = error?.details.map(d => `${d.path.join('/')}: ${d.message}`) ?? [];
ts
import { Schema as S, Either } from 'effect';

const Customer = S.Struct({
  id: S.UUID,
  email: S.String.pipe(S.pattern(/^[^@]+@[^@]+$/)),
  name: S.String,
});

const result = S.decodeUnknownEither(Customer)(data);
const messages = Either.isLeft(result)
  ? S.TreeFormatter.formatErrorSync(result.left).split('\n')
  : [];
ts
import { type } from 'arktype';

const Customer = type({
  id: 'string.uuid',
  email: 'string.email',
  name: 'string',
});

const result = Customer(data);
const messages = result instanceof type.errors
  ? result.map(e => `${e.path.join('/')}: ${e.message}`)
  : [];
ts
import { Object as RtObject, String, Email, Uuid } from 'runtypes';

const Customer = RtObject({
  id: Uuid,
  email: Email,
  name: String,
});

const result = Customer.validate(data);
const messages = result.success ? [] : [`${result.code}: ${result.message}`];
// Limitation: Runtypes surfaces one error at a time; no all-errors mode.

See also

Released under the MIT License.