ValidationErrors views
ValidationErrors has two structured views (aggregate(), report()) plus a raw items array. Obtain the collection from entities.validate() or from InstantiationError.errors.
| Surface | Returns | Best for |
|---|---|---|
.items | readonly ValidationErrorType[] | Raw access - path, keyword, message, params |
aggregate() | { count, paths, keywords } | Structured logs, metric labels |
report() | ProblemDetailsType | HTTP 422 response bodies (RFC 7807) |
All examples use the bookstore domain.
Usage examples
Common shapes for projecting errs.items into the format a caller wants.
Path-prefixed message strings
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
const errs = bookstoreEntities.validate(ReviewSchema.$id, {
'body': 'Short',
'rating': 6
});
if (!errs.ok) {
const messages = errs.items.map((err) => {
return `${err.path || 'root'}: ${err.message}`;
});
console.assert(messages.length > 0, 'Should have error messages');
console.assert(
messages.some((m) => {
return m.includes('must be');
}),
'Should have validation error messages'
);
for (const msg of messages) {
console.log(msg);
}
}
Group by path
import type { ValidationErrorType } from '../../../src/types/index.js';
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
const errs = bookstoreEntities.validate(ReviewSchema.$id, {
'body': 'Too',
'rating': 6
});
if (!errs.ok) {
const grouped: Partial<Record<string, ValidationErrorType[]>> = {};
for (const err of errs) {
(grouped[err.path || '_root'] ??= []).push(err);
}
console.assert(Object.keys(grouped).length > 0, 'Should have grouped errors');
console.assert(
grouped['/rating'] !== undefined || grouped['/body'] !== undefined,
'Should have field-level errors'
);
for (const [
path,
items
] of Object.entries(grouped)) {
console.log(`${path}: [${items?.map((entry) => {
return entry.message;
}).join(', ')}]`);
}
}
Field vs form errors
import type { ValidationErrorType } from '../../../src/types/index.js';
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
const errs = bookstoreEntities.validate(ReviewSchema.$id, {
'body': 'Too',
'rating': 6
});
if (!errs.ok) {
const fieldErrors: ValidationErrorType[] = [];
const formErrors: ValidationErrorType[] = [];
for (const err of errs) {
if (err.path) {
fieldErrors.push(err);
} else {
formErrors.push(err);
}
}
console.assert(
fieldErrors.length + formErrors.length === errs.items.length,
'All errors should be classified'
);
console.log('field errors:', fieldErrors.length);
console.log('form errors:', formErrors.length);
for (const err of fieldErrors) {
console.log(` field path=${err.path} message=${err.message}`);
}
for (const err of formErrors) {
console.log(` form message=${err.message}`);
}
}
ValidationErrors.aggregate
Declaration. Returns { count: number; paths: string[]; keywords: string[] } - a compact rollup with the total error count, deduplicated sorted paths (in access form: items[0].quantity not /items/0/quantity), and deduplicated sorted keyword names. No per-instance params values.
Use this when logging validation failures as structured data or recording metric labels. Because paths and keywords are deduplicated and sorted with no unbounded params values, the output has bounded cardinality - safe to use as a metric label value without risk of cardinality explosion.
Don't use this when you need JSON Pointer paths (use errs.items.map(e => e.path)) or individual messages (iterate errs.items). Don't use it for user-facing error display.
Return type
// aggregate() return type
{
count: number; // total errors (NOT deduplicated)
paths: string[]; // deduplicated, sorted, access form (items[0].qty)
keywords: string[]; // deduplicated, sorted
}Examples
Example 1: Structured log
/**
* ValidationErrors — format recipe
* Demonstrates: grouping errors by JSON Pointer path — cookbook recipe for the removed format() method.
* Use this when you need to map errors to specific form fields.
*/
import type { ValidationErrorType } from '../../../src/types/index.js';
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
const errs = bookstoreEntities.validate(ReviewSchema.$id, {
'body': 'short',
'bookIsbn': '9780140449136',
'customerId': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
'id': 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'postedAt': '2026-01-15T10:30:00Z',
'rating': 6
});
// Recipe: group by path (equivalent to removed format())
const grouped: Record<string, ValidationErrorType[]> = {};
for (const err of errs) {
const key = err.path || '_root';
(grouped[key] ??= []).push(err);
}
console.assert(typeof grouped === 'object');
console.assert(Array.isArray(grouped['/rating']));
console.assert(Array.isArray(grouped['/body']));
console.assert((grouped['/rating'] ?? []).every((err) => {
return typeof err.message === 'string';
}));
for (const [
path,
items
] of Object.entries(grouped)) {
console.log(`${path}: [${items.map((entry) => {
return entry.message;
}).join(', ')}]`);
}
Example 2: Metric recording
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
const errs = bookstoreEntities.validate(ReviewSchema.$id, {
'body': 'Too',
'rating': 6
});
if (!errs.ok) {
const rollup = errs.aggregate();
// paths and keywords are bounded sets - safe as metric labels
console.assert(rollup.count > 0, 'Should have errors to count');
console.assert(rollup.paths.length > 0, 'Should have error paths');
console.assert(rollup.keywords.length > 0, 'Should have error keywords');
console.log('aggregate.count:', rollup.count);
console.log('aggregate.paths:', rollup.paths);
console.log('aggregate.keywords:', rollup.keywords);
}
Example 3: JSON Pointer paths (use items, not aggregate)
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
const errs = bookstoreEntities.validate(ReviewSchema.$id, {
'body': 'Too',
'rating': 6
});
if (!errs.ok) {
// aggregate().paths is access form - use items for JSON Pointer
const jsonPointerPaths = errs.items.map((err) => {
return err.path;
});
console.assert(
jsonPointerPaths.length > 0,
'Should have JSON Pointer paths'
);
console.assert(
jsonPointerPaths.every((path) => {
return path.startsWith('/') || path === '';
}),
'Paths should be in JSON Pointer format'
);
console.log('JSON Pointer paths:', jsonPointerPaths);
}
Comparison
errs.aggregate()
// { count: number; paths: string[]; keywords: string[] }
// Deduplicated, sorted, access form paths, no unbounded params values// Manual derivation:
const issues = result.error.issues;
const count = issues.length;
const paths = [...new Set(issues.map(i => i.path.join('.')))].sort();
const keywords = [...new Set(issues.map(i => i.code))].sort();import * as v from 'valibot';
// Limitation: no aggregate() built in - derive from result.issues:
const result = v.safeParse(schema, data);
const issues = result.success ? [] : result.issues;
const count = issues.length;
const paths = [...new Set(issues.map(i => i.path?.map(p => p.key).join('.') ?? ''))].sort();
const keywords = [...new Set(issues.map(i => i.expected ?? i.type))].sort();import { isLeft } from 'fp-ts/Either';
// Limitation: no aggregate() built in - derive from result.left:
const result = codec.decode(data);
const errors = isLeft(result) ? result.left : [];
const count = errors.length;
const paths = [...new Set(errors.map(err =>
err.context.map(node => node.key).filter(Boolean).join('.'),
))].sort();
const keywords = [...new Set(errors.map(err => err.context.at(-1)?.type.name ?? ''))].sort();// Manual derivation from Value.Errors iterator:
const errs = [...Value.Errors(schema, value)];
const count = errs.length;
const paths = [...new Set(errs.map(e => e.path))].sort();
const keywords = [...new Set(errs.map(e => e.type))].sort();// Manual derivation from ajv.errors array:
const errs = ajv.validate(schema, data) ? [] : ajv.errors ?? [];
const count = errs.length;
const paths = [...new Set(errs.map(e => e.instancePath))].sort();
const keywords = [...new Set(errs.map(e => e.keyword))].sort();errors = exc.errors()
count = len(errors)
paths = sorted(set('.'.join(str(p) for p in e['loc']) for e in errors))
keywords = sorted(set(e['type'] for e in errors))// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.Related
report- when you need the full RFC 7807 payload for HTTP responses- Use
.itemsto access full error objects with JSON Pointer paths
ValidationErrors.report
Declaration. Returns a ProblemDetailsType object conforming to RFC 7807 Problem Details. Default values: type: 'https://json-tology.dev/problems/validation', title: 'Validation failed', status: 422. Accepts partial overrides for instance, status, title, and type. The errors array in the payload mirrors errs.items with path, keyword, message, and params on each entry. path values in the payload are JSON Pointer format.
Use this when returning HTTP 422 Unprocessable Entity responses from an API. Set Content-Type: application/problem+json. Pass instance: req.url to include the request path in the problem details.
Don't use this when you need internal logging - the errors array can grow large; use aggregate for metrics.
Return type
// ProblemDetailsType
{
type: string; // problem type URI
title: string; // human-readable title
status: number; // HTTP status code (default 422)
detail: string; // "N validation error(s)"
instance?: string; // optional request URI
errors: Array<{
path: string; // JSON Pointer (/items/0/quantity)
keyword: string; // JSON Schema keyword
message: string; // human-readable message
params: Record<string, unknown>;
}>;
}Examples
Example 1: Express/Fastify/Hono request handler
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
// Simulated request handler
function handleReviewSubmission(req: { 'body': unknown;
'url': string }) {
const errs = bookstoreEntities.validate(ReviewSchema, req.body);
if (!errs.ok) {
return {
'body': errs.report({ 'instance': req.url }),
'contentType': 'application/problem+json',
'status': 422
};
}
const review = bookstoreEntities.instantiate(ReviewSchema, req.body);
return {
'body': review,
'status': 201
};
}
// Test with invalid data
const response = handleReviewSubmission({
'body': {
'body': 'Short',
'rating': 6
},
'url': '/reviews'
});
console.assert(response.status === 422, 'Invalid review should return 422');
console.assert(
response.contentType === 'application/problem+json',
'Response should be problem+json'
);
if ('body' in response && typeof response.body === 'object') {
const problem = response.body as Record<string, unknown>;
console.assert(problem.status === 422, 'Problem should have 422 status');
console.assert(Array.isArray(problem.errors), 'Problem should have errors array');
console.log('status:', response.status);
console.log('Content-Type:', response.contentType);
console.log('problem body:', JSON.stringify(problem, null, 2));
}
Example 2: Override defaults
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
const errs = bookstoreEntities.validate(ReviewSchema.$id, {
'body': 'Too',
'rating': 6
});
if (!errs.ok) {
const problem = errs.report({
'status': 400,
'title': 'Review submission failed',
'type': 'https://api.bookstore.example/problems/validation'
});
console.assert(
problem.type === 'https://api.bookstore.example/problems/validation',
'Custom type should be set'
);
console.assert(
problem.title === 'Review submission failed',
'Custom title should be set'
);
console.assert(
problem.status === 400,
'Custom status should be set'
);
console.log('type:', problem.type);
console.log('title:', problem.title);
console.log('status:', problem.status);
console.log('detail:', problem.detail);
}
Bad examples - what NOT to do
Anti-pattern: Constructing RFC 7807 manually
import {
bookstoreEntities, ReviewSchema
} from '../bookstore/index.js';
const errs = bookstoreEntities.validate(ReviewSchema.$id, {
'body': 'Too',
'rating': 6
});
if (!errs.ok) {
// Anti-pattern: Constructing RFC 7807 manually
// Don't do this - roll-your-own is fragile and inconsistent
const _problemWrong = {
'errors': errs.items.map((err) => {
return {
'error': err.message,
'field': err.path
};
}),
'status': 422,
'type': 'validation-error'
};
// The manual shape above is intentionally unused — it exists only to
// contrast with the report() call below. void marks it as deliberate.
void _problemWrong;
// Correct approach: use report() for RFC 7807 compliance
const problem = errs.report({ 'instance': '/reviews' });
console.assert(
problem.type === 'https://json-tology.dev/problems/validation',
'Should use standard problem type'
);
console.assert(
problem.status === 422,
'Should have correct status'
);
console.assert(
Array.isArray(problem.errors) && problem.errors.length > 0,
'Should have errors array with standard structure'
);
console.log('report() type:', problem.type);
console.log('report() status:', problem.status);
console.log('report() errors count:', (problem.errors as unknown[]).length);
}
Comparison
errs.report({ instance: '/reviews' })
// ProblemDetailsType - RFC 7807 compliant, ready to send as 422 body// Manual RFC 7807 construction - not built in:
const problem = {
type: 'https://example.com/problems/validation',
status: 422,
detail: `${result.error.issues.length} validation errors`,
errors: result.error.issues.map(i => ({ path: i.path.join('/'), message: i.message })),
};import * as v from 'valibot';
// Limitation: no report() built in - manual RFC 7807 construction:
const result = v.safeParse(schema, data);
const issues = result.success ? [] : result.issues;
const problem = {
type: 'https://example.com/problems/validation',
status: 422,
detail: `${issues.length} validation errors`,
errors: issues.map(i => ({
path: `/${i.path?.map(p => p.key).join('/') ?? ''}`,
keyword: i.expected ?? i.type,
message: i.message,
params: {},
})),
};import { isLeft } from 'fp-ts/Either';
import { PathReporter } from 'io-ts/PathReporter';
// Limitation: no report() built in - manual RFC 7807 construction:
const result = codec.decode(data);
const errors = isLeft(result) ? result.left : [];
const messages = isLeft(result) ? PathReporter.report(result) : [];
const problem = {
type: 'https://example.com/problems/validation',
status: 422,
detail: `${errors.length} validation errors`,
errors: errors.map((err, idx) => ({
path: `/${err.context.map(node => node.key).filter(Boolean).join('/')}`,
keyword: err.context.at(-1)?.type.name ?? '',
message: messages[idx] ?? '',
params: {},
})),
};// Not built in - manual construction required.
const errs = [...Value.Errors(schema, value)];
const problem = {
type: 'https://example.com/problems/validation',
status: 422,
errors: errs.map(e => ({ path: e.path, message: e.message, keyword: e.type, params: {} })),
};// Not built in - manual construction required.
const problem = {
type: 'https://example.com/problems/validation',
status: 422,
errors: (ajv.errors ?? []).map(e => ({
path: e.instancePath, keyword: e.keyword, message: e.message ?? '', params: e.params
})),
};# FastAPI handles this automatically:
# @app.post('/reviews') async def create(body: Review): ...
# Pydantic validation failure → FastAPI returns 422 with detail list.// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.Related
aggregate- compact rollup for logging/metrics (not for HTTP responses)
See also
entities.validate()- how to obtain theValidationErrorscollectionentities.instantiate()-InstantiationError.errorscarries the same collection- Bookstore domain - schema definitions used in examples