✨ 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
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { NamingStrategy } from './naming.js';
|
||||
|
||||
/**
|
||||
* How an incoming key that maps to no known property should be treated.
|
||||
*
|
||||
* - `allow` (default): copy it onto the instance untouched, preserving the previous behaviour.
|
||||
* - `strip`: drop it, so instances only ever carry declared properties.
|
||||
* - `error`: reject the payload with a {@link JsonMappingError}.
|
||||
*/
|
||||
export type UnknownKeyPolicy = 'allow' | 'strip' | 'error';
|
||||
|
||||
export interface TransformOptions {
|
||||
/**
|
||||
* Validate the result and throw {@link JsonValidationError} on failure.
|
||||
*
|
||||
* Defaults to `true`, matching the behaviour of every previous release. Set it to `false`
|
||||
* to map without validating — useful when you want to inspect a partially-valid payload,
|
||||
* or when validation happens elsewhere in your stack.
|
||||
*/
|
||||
validate?: boolean;
|
||||
|
||||
/**
|
||||
* Naming convention used on the JSON side for properties without an explicit
|
||||
* `@JsonProperty`. Defaults to `identity` (property names are used as-is).
|
||||
*/
|
||||
namingStrategy?: NamingStrategy;
|
||||
|
||||
/** What to do with incoming keys that match no declared property. Deserialization only. */
|
||||
unknownKeys?: UnknownKeyPolicy;
|
||||
}
|
||||
|
||||
/** Options that can be set once for the whole application via {@link configure}. */
|
||||
export type GlobalOptions = Pick<TransformOptions, 'namingStrategy' | 'unknownKeys' | 'validate'>;
|
||||
|
||||
const DEFAULTS: Required<GlobalOptions> = {
|
||||
namingStrategy: 'identity',
|
||||
unknownKeys: 'allow',
|
||||
validate: true,
|
||||
};
|
||||
|
||||
let globalOptions: Required<GlobalOptions> = { ...DEFAULTS };
|
||||
|
||||
/**
|
||||
* Sets library-wide defaults, so an application that consistently speaks `snake_case` does
|
||||
* not have to repeat itself at every call site.
|
||||
*
|
||||
* ```ts
|
||||
* configure({ namingStrategy: 'snake_case', unknownKeys: 'strip' });
|
||||
* ```
|
||||
*
|
||||
* Per-call options always take precedence over these.
|
||||
*/
|
||||
export function configure(options: GlobalOptions): void {
|
||||
globalOptions = { ...globalOptions, ...options };
|
||||
}
|
||||
|
||||
/** Returns the current library-wide defaults. */
|
||||
export function getConfig(): Required<GlobalOptions> {
|
||||
return { ...globalOptions };
|
||||
}
|
||||
|
||||
/** Restores the library-wide defaults to their original values. */
|
||||
export function resetConfig(): void {
|
||||
globalOptions = { ...DEFAULTS };
|
||||
}
|
||||
|
||||
/** Merges per-call options over the library-wide defaults. */
|
||||
export function resolveOptions(options?: TransformOptions): Required<GlobalOptions> {
|
||||
if (!options) return globalOptions;
|
||||
return {
|
||||
namingStrategy: options.namingStrategy ?? globalOptions.namingStrategy,
|
||||
unknownKeys: options.unknownKeys ?? globalOptions.unknownKeys,
|
||||
validate: options.validate ?? globalOptions.validate,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user