bookish-potato-dto
Version:
A TypeScript DTO (Data Transfer Object) parsing and validation library. Define a schema once — get runtime validation and a fully inferred TypeScript type for free.
894 lines (860 loc) • 30 kB
JavaScript
/**
* Attaches the three modifier methods to any field descriptor.
* Single source of truth — no per-field repetition needed.
*/
function withModifiers(descriptor) {
const { options, ...rest } = descriptor;
return {
...descriptor,
optional: (() => ({
...rest,
options: { ...options, isOptional: true },
})),
nullable: (() => ({
...rest,
options: { ...options, isNullable: true },
})),
optionalAndNullable: (() => ({
...rest,
options: { ...options, isOptional: true, isNullable: true },
})),
};
}
// ---- Field builders ----
const field = {
/** A string field. Replaces @StringProperty. */
string(options) {
return withModifiers({ kind: 'string', options: options ?? {} });
},
/** A floating-point number field. Replaces @NumberProperty. */
number(options) {
return withModifiers({ kind: 'number', options: options ?? {} });
},
/** An integer field. Replaces @IntegerProperty. */
integer(options) {
return withModifiers({ kind: 'integer', options: options ?? {} });
},
/** A boolean field. Replaces @BooleanProperty. */
boolean(options) {
return withModifiers({ kind: 'boolean', options: options ?? {} });
},
/** An enum field. Replaces @EnumProperty. */
enum(enumObj, options) {
return withModifiers({ kind: 'enum', options: options ?? {}, enumObj });
},
/** A Date field. Replaces @DateProperty. */
date(options) {
return withModifiers({ kind: 'date', options: options ?? {} });
},
/** A regex-validated string field. Replaces @RegexProperty. */
regex(pattern, options) {
return withModifiers({ kind: 'regex', options: options ?? {}, pattern });
},
/** An array of primitives. Replaces @ArrayProperty. */
array(itemType, options) {
return withModifiers({ kind: 'array', options: options ?? {}, itemType });
},
/** An array of nested DTOs. Replaces @ArrayDtoProperty. */
arrayDto(dtoDefinition, options) {
return withModifiers({
kind: 'arrayDto',
options: (options ?? {}),
dtoDefinition,
});
},
/** A nested DTO field. Replaces @DtoProperty. */
dto(dtoDefinition, options) {
return withModifiers({
kind: 'dto',
options: (options ?? {}),
dtoDefinition,
});
},
/** A field with a fully custom parser. Replaces @CustomProperty. */
custom(options) {
return withModifiers({ kind: 'custom', options, parser: options.parser });
},
};
function generateUuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Creates a DtoDefinition from a schema map of field descriptors.
* The returned object's `.fields` property can be spread to compose or extend DTOs.
*
* @example
* const PersonDto = defineDto({
* name: field.string(),
* age: field.integer(),
* });
*
* // Extend via spread:
* const EmployeeDto = defineDto({
* ...PersonDto.fields,
* position: field.string(),
* });
*/
function defineDto(schema) {
return {
_uuid: generateUuid(),
fields: schema,
};
}
/**
* Generates an OpenAPI schema for a given DTO definition.
* It also returns all referenced DTOs to help build the 'components/schemas' section.
*/
function generateOpenApi(dto, options = {}) {
const refs = {};
const processedUuids = new Set();
function getName(d) {
return options.nameResolver?.(d) ?? d._uuid;
}
function transformDto(d) {
if (processedUuids.has(d._uuid)) {
return { $ref: `#/components/schemas/${getName(d)}` };
}
processedUuids.add(d._uuid);
const properties = {};
const required = [];
for (const [key, descriptor] of Object.entries(d.fields)) {
properties[key] = transformField(descriptor);
if (!descriptor.options.isOptional) {
required.push(key);
}
}
refs[getName(d)] = {
type: 'object',
properties,
...(required.length > 0 ? { required } : {}),
};
return { $ref: `#/components/schemas/${getName(d)}` };
}
function transformField(descriptor) {
const { kind, options: fieldOptions } = descriptor;
let inferred = {};
switch (kind) {
case 'string': {
const opt = fieldOptions;
inferred = {
type: 'string',
...(opt.minLength === undefined ? {} : { minLength: opt.minLength }),
...(opt.maxLength === undefined ? {} : { maxLength: opt.maxLength }),
};
break;
}
case 'number':
case 'integer': {
const opt = fieldOptions;
inferred = {
type: kind === 'integer' ? 'integer' : 'number',
...(opt.minValue === undefined ? {} : { minimum: opt.minValue }),
...(opt.maxValue === undefined ? {} : { maximum: opt.maxValue }),
};
break;
}
case 'boolean':
inferred = { type: 'boolean' };
break;
case 'date':
inferred = { type: 'string', format: 'date-time' };
break;
case 'regex': {
const d = descriptor;
inferred = { type: 'string', pattern: d.pattern.source };
break;
}
case 'enum': {
const d = descriptor;
inferred = {
type: typeof Object.values(d.enumObj)[0] === 'number'
? 'number'
: 'string',
enum: Object.values(d.enumObj),
};
break;
}
case 'array': {
const d = descriptor;
const opt = fieldOptions;
inferred = {
type: 'array',
items: mapPrimitiveToOpenApi(d.itemType, opt),
...(opt.minLength === undefined ? {} : { minItems: opt.minLength }),
...(opt.maxLength === undefined ? {} : { maxItems: opt.maxLength }),
};
break;
}
case 'dto':
return {
...transformDto(descriptor.dtoDefinition),
...fieldOptions.openApi,
};
case 'arrayDto': {
const d = descriptor;
inferred = {
type: 'array',
items: transformDto(d.dtoDefinition),
...(d.options.minLength === undefined
? {}
: { minItems: d.options.minLength }),
...(d.options.maxLength === undefined
? {}
: { maxItems: d.options.maxLength }),
};
break;
}
case 'custom':
inferred = { type: 'object', description: 'Custom parsed field' };
break;
}
// Common metadata
const common = {
...(fieldOptions.isNullable ? { nullable: true } : {}),
...(fieldOptions.defaultValue === undefined
? {}
: { default: fieldOptions.defaultValue }),
};
// Ranking: Manual Overrides > Common/Inferred
return {
...inferred,
...common,
...fieldOptions.openApi,
};
}
const rootSchema = transformDto(dto);
return {
schema: rootSchema,
refs,
};
}
function mapPrimitiveToOpenApi(kind, options) {
switch (kind) {
case 'string':
return {
type: 'string',
...(options.stringsLength?.minLength === undefined
? {}
: { minLength: options.stringsLength.minLength }),
...(options.stringsLength?.maxLength === undefined
? {}
: { maxLength: options.stringsLength.maxLength }),
};
case 'number':
return {
type: 'number',
...(options.numbersRange?.minValue === undefined
? {}
: { minimum: options.numbersRange.minValue }),
...(options.numbersRange?.maxValue === undefined
? {}
: { maximum: options.numbersRange.maxValue }),
};
case 'boolean':
return { type: 'boolean' };
default:
return {};
}
}
class ParsingError extends Error {
constructor(message) {
super(message);
this.name = "ParsingError";
}
}
class PropertyParsingError extends Error {
get propertyKey() {
return this.data.propertyKey;
}
constructor(data) {
super();
this.data = data;
this.message =
data.customMessage ||
`Property "${data.propertyKey}" parsing error: ${data.causedBy.message}`;
}
}
class PropertyParserAbstract {
constructor() {
this._uuid = generateUuid();
}
get uuid() {
return this._uuid;
}
}
class PropertyParserFloating extends PropertyParserAbstract {
parse(value) {
if (Number.isNaN(value)) {
throw new ParsingError(`The value is NaN`);
}
if (typeof value === 'number') {
return value;
}
throw new ParsingError(`Unsupported type ${typeof value}`);
}
}
class PropertyParserInteger extends PropertyParserAbstract {
parse(value) {
if (Number.isNaN(value)) {
throw new ParsingError(`The value is NaN`);
}
const errorMessage = `The value of "${value}" is not an Integer`;
if (typeof value === 'number') {
const _parsedValue = this.checkInteger(value);
if (!_parsedValue)
throw new ParsingError(errorMessage);
return _parsedValue;
}
throw new ParsingError(`Unsupported type ${typeof value}`);
}
checkInteger(value) {
return parseInt(value.toFixed(0)) === value ? value : null;
}
}
class PropertyParserString extends PropertyParserAbstract {
parse(value) {
if (typeof value !== 'string') {
throw new ParsingError(`Value is not a string!`);
}
return value;
}
}
/**
* Decorator class for the number property parser.
* Parses string to number before processing the value by the decorated class.
*/
class PropertyParserStringToNumberDecorator extends PropertyParserAbstract {
constructor(_propertyParser) {
super();
this._propertyParser = _propertyParser;
}
parse(value) {
if (typeof value === 'string') {
const _parsedValue = parseFloat(value.replace(',', '.'));
return this._propertyParser.parse(_parsedValue);
}
return this._propertyParser.parse(value);
}
}
class PropertyParserBoolean extends PropertyParserAbstract {
parse(value) {
if (typeof value === 'boolean') {
return value;
}
throw new ParsingError(`Unsupported type ${typeof value}`);
}
}
class PropertyParserStringToBooleanDecorator extends PropertyParserAbstract {
constructor(parser) {
super();
this.parser = parser;
}
parse(value) {
if (typeof value === 'string') {
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
}
return this.parser.parse(value);
}
}
class PropertyParserArray extends PropertyParserAbstract {
constructor(parser) {
super();
this.parser = parser;
}
parse(value) {
if (!Array.isArray(value)) {
throw new ParsingError(`Value is not an array! Value: ${value}`);
}
return value.map(item => this.parser.parse(item));
}
}
class PropertyParserEnum extends PropertyParserAbstract {
constructor(enumType) {
super();
this.enumType = enumType;
}
parse(value) {
if (typeof value !== 'string' && typeof value !== 'number') {
throw new ParsingError(`Value is not a string or number! Value: ${value}`);
}
for (const key in this.enumType) {
if (this.enumType[key] === value) {
return this.enumType[key];
}
}
throw new ParsingError(`Value is not in the enum! Value: ${value}`);
}
}
class PropertyParserStringRegexDecorator extends PropertyParserAbstract {
constructor(regex, parser) {
super();
this.regex = regex;
this.parser = parser;
}
parse(value) {
const parsedValue = this.parser.parse(value);
if (!this.regex.test(parsedValue)) {
throw new ParsingError(`Value "${parsedValue}" does not match regex ${this.regex}`);
}
return parsedValue;
}
}
class PropertyParserDate extends PropertyParserAbstract {
parse(value) {
if (value instanceof Date) {
return value;
}
if (typeof value !== 'string') {
throw new ParsingError(`Value is not a string! Cannot parse ${value} to Date!`);
}
const _parsedDate = new Date(value);
if (isNaN(_parsedDate.getTime()) ||
_parsedDate.toString() === 'Invalid Date') {
throw new ParsingError(`Value ${value} is not a Date!`);
}
return _parsedDate;
}
}
class PropertyParserRangeDecorator extends PropertyParserAbstract {
constructor(data) {
super();
this._range = data.range;
this._parser = data.parser;
this._errorMessage =
data.errorMessage ||
`Value is not in range ${this._range.min} - ${this._range.max}`;
}
parse(value) {
const parsedValue = this._parser.parse(value);
if (this.checkIfInRange(parsedValue, this._range.min, this._range.max)) {
return parsedValue;
}
throw new ParsingError(this._errorMessage);
}
_checkIfMoreThan(value, min) {
return min === undefined || this.checkIfMoreThan(value, min);
}
_checkIfLessThan(value, max) {
return max === undefined || this.checkIfLessThan(value, max);
}
checkIfInRange(value, min, max) {
return (this._checkIfMoreThan(value, min) && this._checkIfLessThan(value, max));
}
}
function defineErrorMessageNumberRange(range) {
let message = '';
if (range.min) {
message = `Number should be more than ${range.min}`;
}
if (range.max) {
message = `Number should be less than ${range.max}`;
}
if (range.min && range.max) {
message = `Number should be in range of ${range.min} - ${range.max}`;
}
return message;
}
function defineErrorMessageStringRange(range) {
let message = '';
if (range.min) {
message = `String length should be more than ${range.min}`;
}
if (range.max) {
message = `String length should be less than ${range.max}`;
}
if (range.min && range.max) {
message = `String length should be in range of ${range.min} - ${range.max}`;
}
return message;
}
function defineErrorMessageArrayRange(range) {
let message = '';
if (range.min) {
message = `Array length should be more than ${range.min}`;
}
if (range.max) {
message = `Array length should be less than ${range.max}`;
}
if (range.min && range.max) {
message = `Array length should be in range of ${range.min} - ${range.max}`;
}
return message;
}
class PropertyParserNumberRangeDecorator extends PropertyParserRangeDecorator {
constructor(data) {
super({
errorMessage: defineErrorMessageNumberRange(data.range),
...data,
});
}
checkIfMoreThan(value, min) {
return value >= min;
}
checkIfLessThan(value, max) {
return value <= max;
}
}
class PropertyParserStringRangeDecorator extends PropertyParserRangeDecorator {
constructor(data) {
super({
errorMessage: defineErrorMessageStringRange(data.range),
...data,
});
}
checkIfMoreThan(value, min) {
return value.length >= min;
}
checkIfLessThan(value, max) {
return value.length <= max;
}
}
class PropertyParserArrayRangeDecorator extends PropertyParserRangeDecorator {
constructor(data) {
super({
errorMessage: defineErrorMessageArrayRange(data.range),
...data,
});
}
checkIfMoreThan(value, min) {
return value.length >= min;
}
checkIfLessThan(value, max) {
return value.length <= max;
}
}
let _fn;
function getDtoParseFn() {
return _fn;
}
function setDtoParseFn(fn) {
_fn = fn;
}
class ParsersCache {
constructor() {
this.cache = new Map();
}
get(key, factory) {
if (!this.cache.has(key)) {
this.cache.set(key, factory());
}
return this.cache.get(key);
}
}
class PropertyParsers {
/**
* Returns a cached parser or creates a new one.
* Can be used to cache parsers by key.
* @param key - unique key for the parser.
* @param factory - factory function to create a new parser.
*/
static CACHE_PARSER(key, factory) {
return this.cache.get(key, factory);
}
static get STRING() {
return this.cache.get('string', () => new PropertyParserString());
}
static STRING_IN_RANGE(range) {
return this.cache.get(`stringInRange${JSON.stringify(range)}`, () => new PropertyParserStringRangeDecorator({
range,
parser: this.STRING,
}));
}
static STRING_REGEX(regex) {
return this.cache.get(`stringRegex${regex.toString()}`, () => new PropertyParserStringRegexDecorator(regex, this.STRING));
}
static get NUMBER() {
return this.cache.get('number', () => new PropertyParserFloating());
}
static NUMBER_IN_RANGE(range) {
return this.cache.get(`numberInRange${JSON.stringify(range)}`, () => new PropertyParserNumberRangeDecorator({
range,
parser: this.NUMBER,
}));
}
static get STRING_TO_NUMBER() {
return this.cache.get('stringToNumber', () => new PropertyParserStringToNumberDecorator(this.NUMBER));
}
static STRING_TO_NUMBER_IN_RANGE(range) {
return this.cache.get(`stringToNumberInRange${JSON.stringify(range)}`, () => new PropertyParserStringToNumberDecorator(this.NUMBER_IN_RANGE(range)));
}
static get INTEGER() {
return this.cache.get('integer', () => new PropertyParserInteger());
}
static INTEGER_IN_RANGE(range) {
return this.cache.get(`integerInRange${JSON.stringify(range)}`, () => new PropertyParserNumberRangeDecorator({
range,
parser: this.INTEGER,
}));
}
static get STRING_TO_INTEGER() {
return this.cache.get('stringToInteger', () => new PropertyParserStringToNumberDecorator(this.INTEGER));
}
static STRING_TO_INTEGER_IN_RANGE(range) {
return this.cache.get(`stringToIntegerInRange${JSON.stringify(range)}`, () => new PropertyParserStringToNumberDecorator(this.INTEGER_IN_RANGE(range)));
}
static get BOOLEAN() {
return this.cache.get('boolean', () => new PropertyParserBoolean());
}
static get STRING_TO_BOOLEAN() {
return this.cache.get('stringToBoolean', () => new PropertyParserStringToBooleanDecorator(this.BOOLEAN));
}
static get DATE() {
return this.cache.get('date', () => new PropertyParserDate());
}
static ARRAY(parser) {
return this.cache.get(`array:${parser.uuid ?? generateUuid()}`, () => new PropertyParserArray(parser));
}
static ARRAY_IN_RANGE(parser, range) {
return this.cache.get(`arrayInRange${parser.uuid}${JSON.stringify(range)}`, () => new PropertyParserArrayRangeDecorator({
range,
parser: this.ARRAY(parser),
}));
}
static ENUM(enumType) {
const key = JSON.stringify(enumType);
return this.cache.get(key, () => new PropertyParserEnum(enumType));
}
/**
* Returns a cached PropertyParser that delegates to schemaParseObject.
* Uses the DtoDefinition's stable _uuid as the cache key.
* Prefix avoids collision with PropertyParsers.ARRAY which also caches by parser.uuid.
*/
static DTO_DEFINITION(dto) {
const cacheKey = `dto-def:${dto._uuid}`;
return this.cache.get(cacheKey, () => ({
uuid: cacheKey,
parse: (value) => {
const fn = getDtoParseFn();
if (!fn) {
throw new Error('[bookish-potato-dto] schema parse function not yet registered. ' +
'Ensure "bookish-potato-dto" is imported before parsing nested DTOs.');
}
return fn(dto, value);
},
}));
}
}
PropertyParsers.cache = new ParsersCache();
/**
* Resolves a runtime PropertyParser from a FieldDescriptor.
* Reuses PropertyParsers cache for all types.
*/
function resolveParser(descriptor) {
switch (descriptor.kind) {
case 'string': {
const { minLength, maxLength } = descriptor.options;
return PropertyParsers.STRING_IN_RANGE({ min: minLength, max: maxLength });
}
case 'number': {
const { strictDataTypes, minValue, maxValue } = descriptor.options;
const range = { min: minValue, max: maxValue };
return (strictDataTypes
? PropertyParsers.NUMBER_IN_RANGE(range)
: PropertyParsers.STRING_TO_NUMBER_IN_RANGE(range));
}
case 'integer': {
const { strictDataTypes, minValue, maxValue } = descriptor.options;
const range = { min: minValue, max: maxValue };
return (strictDataTypes
? PropertyParsers.INTEGER_IN_RANGE(range)
: PropertyParsers.STRING_TO_INTEGER_IN_RANGE(range));
}
case 'boolean':
return (descriptor.options.strictDataTypes
? PropertyParsers.BOOLEAN
: PropertyParsers.STRING_TO_BOOLEAN);
case 'enum':
return PropertyParsers.ENUM(descriptor.enumObj);
case 'date':
return PropertyParsers.DATE;
case 'regex':
return PropertyParsers.STRING_REGEX(descriptor.pattern);
case 'array': {
const { strictDataTypes, minLength, maxLength, stringsLength, numbersRange } = descriptor.options;
const arrayRange = { min: minLength, max: maxLength };
switch (descriptor.itemType) {
case 'string':
return PropertyParsers.ARRAY_IN_RANGE(PropertyParsers.STRING_IN_RANGE({ min: stringsLength?.minLength, max: stringsLength?.maxLength }), arrayRange);
case 'number': {
const nr = { min: numbersRange?.minValue, max: numbersRange?.maxValue };
return PropertyParsers.ARRAY_IN_RANGE(strictDataTypes ? PropertyParsers.NUMBER_IN_RANGE(nr) : PropertyParsers.STRING_TO_NUMBER_IN_RANGE(nr), arrayRange);
}
case 'boolean':
return PropertyParsers.ARRAY_IN_RANGE(strictDataTypes ? PropertyParsers.BOOLEAN : PropertyParsers.STRING_TO_BOOLEAN, arrayRange);
default:
throw new Error('field.array: unsupported itemType');
}
}
case 'dto':
return PropertyParsers.DTO_DEFINITION(descriptor.dtoDefinition);
case 'arrayDto': {
const { minLength, maxLength } = descriptor.options;
const itemParser = PropertyParsers.DTO_DEFINITION(descriptor.dtoDefinition);
return PropertyParsers.ARRAY_IN_RANGE(itemParser, { min: minLength, max: maxLength });
}
case 'custom':
return descriptor.parser;
}
}
class ParseChainBase {
setNextChain(nextChain) {
this.nextChain = nextChain;
return nextChain;
}
parse(data) {
return this._parse(data, data => {
if (this.nextChain) {
return this.nextChain.parse(data);
}
return undefined;
});
}
}
class ParseChainDefaultValue extends ParseChainBase {
constructor() {
super();
}
_parse(data, next) {
const { propertyDataToParse, dto } = data;
const { key, defaultValue } = propertyDataToParse;
if (dto[key] === undefined) {
dto[key] = defaultValue;
}
return next(data);
}
}
class ParseChainOptionalValue extends ParseChainBase {
_parse(data, next) {
const { propertyDataToParse, dto } = data;
const { key, isOptional } = propertyDataToParse;
// skip this chain if the value is optional and not present
if (isOptional && dto[key] === undefined) {
return;
}
return next(data);
}
}
class ParseChainRequiredCheck extends ParseChainBase {
_parse(data, next) {
const { propertyDataToParse, dto } = data;
const { key } = propertyDataToParse;
// throw an error if the value is required and not present
if (dto[key] === undefined) {
return new PropertyParsingError({
propertyKey: key,
causedBy: new ParsingError(`Property is required!`),
});
}
return next(data);
}
}
class ParseChainParseValue extends ParseChainBase {
_parse(data, next) {
const { propertyDataToParse, dto } = data;
const { key, parser, useDefaultValueOnParseError, defaultValue, isNullable, } = propertyDataToParse;
if (dto[key] === null) {
if (isNullable) {
return next(data);
}
return new PropertyParsingError({
propertyKey: key,
causedBy: new Error(`Property cannot be null!`),
});
}
try {
dto[key] = parser.parse(dto[key]);
}
catch (error) {
const value = dto[key];
delete dto[key];
// Go to next chain if use default value on parse error is enabled
if (useDefaultValueOnParseError && defaultValue !== undefined) {
dto[key] = defaultValue;
return next(data);
}
return new PropertyParsingError({
propertyKey: key,
causedBy: error,
customMessage: data.propertyDataToParse.parsingErrorMessage?.(key, value, error),
});
}
return next(data);
}
}
/**
* This is facade for the chain of responsibility pattern.
* Defines the order of the chain and starts the chain.
*/
class ParseChainFacade {
constructor() {
this._chain = new ParseChainDefaultValue();
this._chain
.setNextChain(new ParseChainOptionalValue())
.setNextChain(new ParseChainRequiredCheck())
.setNextChain(new ParseChainParseValue());
}
parse(data) {
return this._chain.parse(data);
}
}
const parseChainFacade = new ParseChainFacade();
/**
* parseObject implementation for DtoDefinition.
* Reuses the existing chain-of-responsibility parse engine.
* Called by the public parseObject() overload in src/parser/parser.ts.
*/
function schemaParseObject(dto, toParse) {
if (!toParse || typeof toParse !== 'object' || Array.isArray(toParse)) {
throw new ParsingError('Provided value to parse is not a valid object');
}
const rawObj = toParse;
const parsedObject = {};
const parsingErrors = [];
for (const [fieldKey, descriptor] of Object.entries(dto.fields)) {
// Resolve mapFrom before the chain runs — the chain only uses the field key
const sourceKey = descriptor.options.mapFrom ?? fieldKey;
parsedObject[fieldKey] = rawObj[sourceKey];
const opts = descriptor.options;
const propertyDataToParse = {
key: fieldKey,
parser: resolveParser(descriptor),
defaultValue: opts.defaultValue,
isOptional: opts.isOptional,
isNullable: opts.isNullable,
useDefaultValueOnParseError: opts.useDefaultValueOnParseError,
mapFrom: opts.mapFrom,
parsingErrorMessage: opts.parsingErrorMessage,
};
const error = parseChainFacade.parse({
propertyDataToParse,
dto: parsedObject,
});
if (error) {
parsingErrors.push(error);
}
}
if (parsingErrors.length > 0) {
throw new ParsingError(parsingErrors.map(e => e.message).join('\n'));
}
return parsedObject;
}
// Self-register when this module first loads.
// Uses the dependency-free _dto-parse-registry to avoid circular import issues.
setDtoParseFn(schemaParseObject);
/**
* Parses a plain object using a schema-first DtoDefinition.
* Returns a fully typed plain object — no class instantiation.
*/
function parseObject(dto, toParse) {
return schemaParseObject(dto, toParse);
}
export { ParsingError, PropertyParsingError, defineDto, field, generateOpenApi, parseObject };