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);
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);
}
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}`);
}
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');
}
}
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);
Comparison
const errs = jt.validate(CustomerSchema.$id, data);
// ValidationErrors - .ok, .length, iterable, .items, .aggregate(), .report()
// does not throw, does not coerceconst 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 ZodErrorimport * 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.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.import { TypeCompiler } from '@sinclair/typebox/compiler';
const C = TypeCompiler.Compile(CustomerSchema);
const errors = [...C.Errors(data)].map(e => `${e.path}: ${e.message}`);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}`);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()]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}`);
}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}`) ?? [];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')
: [];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}`)
: [];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.Related
JsonTology.is- boolean type guard, no error dataJsonTology.instantiate- validate + apply defaults + return typed valueJsonTology.subschemaAt- validate against a sub-schema by JSON PointerValidationErrors- the structured collection shape returned byvalidate
See also
- Error views -
aggregate,report, iteration overValidationErrors - Bookstore domain - schema definitions used in examples