Files
cereale/src/errors.ts
T
Claude 666762a146 ✨ feat: add field-name mapping, access control and transform options
A library whose headline feature is "JSON mapping" could not map a name: there
was no way to read {"first_name": ...} into firstName, no way to keep a password
out of the response, and no way to parse a payload without validating it.

Name mapping
- @JsonProperty(name) renames a property in both directions
- @JsonAlias(...names) accepts extra names on input only, so a field can be
  renamed without breaking older clients
- naming strategies (snake_case, kebab-case, SCREAMING_SNAKE_CASE, PascalCase,
  camelCase, or your own function) for properties with no explicit name.
  Acronyms split where a reader expects: parseHTTPResponse -> parse_http_response

Access control
- @JsonIgnore()    excluded both ways
- @JsonWriteOnly() accepted from input, never echoed back (passwords)
- @JsonReadOnly()  serialized, never settable by a client (server-owned ids)

Blocked names are dropped explicitly rather than falling through to the unknown
key path, which would otherwise have copied a rejected id straight back on under
the default policy.

Transform options, per call or globally via configure()
- validate: false to map without validating, for lenient parsing
- unknownKeys: 'allow' | 'strip' | 'error'
- namingStrategy

Error ergonomics — the nested ValidationError tree was hard to turn into an HTTP
400 body. flattenErrors() yields {"items[0].qty": ["qty must be at least 1"]},
plus formatErrors() and collectErrorMessages(). Adds validateOrReject().

All defaults preserve existing behaviour; the 68 prior tests pass unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-03 23:46:18 +00:00

58 lines
1.7 KiB
TypeScript

import type { ValidationError } from './utils.js';
/**
* Flattens the nested {@link ValidationError} tree into a flat map of dotted paths to
* messages — the shape you actually want when turning a failure into an HTTP 400 body.
*
* ```ts
* flattenErrors(errors);
* // {
* // "name": ["name must be a string"],
* // "items[0].qty": ["qty must be at least 1"]
* // }
* ```
*/
export function flattenErrors(errors: ValidationError[]): Record<string, string[]> {
const flat: Record<string, string[]> = {};
const walk = (nodes: ValidationError[], prefix: string) => {
for (const node of nodes) {
// Array indices read as `items[0]`, named properties as `order.total`.
const path = node.property.startsWith('[')
? `${prefix}${node.property}`
: prefix ? `${prefix}.${node.property}` : node.property;
const messages = Object.values(node.constraints);
if (messages.length > 0) {
(flat[path] ??= []).push(...messages);
}
if (node.children?.length) {
walk(node.children, path);
}
}
};
walk(errors, '');
return flat;
}
/**
* Renders the error tree as human-readable lines, one per failed rule.
*
* Intended for logs and CLI output; use {@link flattenErrors} when the destination is JSON.
*/
export function formatErrors(errors: ValidationError[]): string {
const flat = flattenErrors(errors);
return Object.entries(flat)
.flatMap(([path, messages]) => messages.map(message => `${path}: ${message}`))
.join('\n');
}
/**
* Collects every message in the tree, discarding paths.
*/
export function collectErrorMessages(errors: ValidationError[]): string[] {
return Object.values(flattenErrors(errors)).flat();
}