diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7387375..a581e16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,17 +2,19 @@ name: CI on: push: - branches: [ main ] + branches: [ main, develop ] pull_request: - branches: [ main ] + branches: [ main, develop ] jobs: - build: + verify: + name: Node ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v4 @@ -25,5 +27,15 @@ jobs: run: npm ci - name: Type Check run: npm run type-check + - name: Lint + run: npm run lint + - name: Test + run: npm run test:coverage + - name: Build + run: npm run build + - name: Verify published entry points load + run: | + node --input-type=module -e "import * as m from './dist/esm/index.js'; if (typeof m.toInstance !== 'function') throw new Error('ESM entry point broken');" + node --input-type=commonjs -e "const m = require('./dist/cjs/index.js'); if (typeof m.toInstance !== 'function') throw new Error('CJS entry point broken');" - name: Run Demo run: npm run demo diff --git a/.gitignore b/.gitignore index 9fe6ced..81b0bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Node modules and dependency files +# NOTE: package-lock.json is intentionally committed — CI installs with `npm ci`, +# which requires a lockfile to be present in the repository. /node_modules/ -/package-lock.json # Build outputs /dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..98065b4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,252 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2026-08-04 + +> The project stays on 0.x while nothing has been published: under semver that signals the +> API may still move, which is honest for software with no real-world users. A breaking +> change is therefore a minor bump, which is why this is 0.2.0 rather than 2.0.0. + +**Breaking.** Cereale moves to TC39 standard decorators, which is what makes validation rules +type-checked against the fields they are attached to. + +### The headline + +A rule that does not fit its field is now a compile error: + +```ts +class User { + @IsString() name!: string; // fine + @IsString() age!: number; // Type 'number' is not assignable to type 'string' +} +``` + +Legacy decorators receive `(target: any, key: string)` and lose the field's type entirely, so +this was impossible in v1. Standard decorators receive `ClassFieldDecoratorContext`, +which carries it. Checked rules include: + +- scalar rules against scalar fields (`@Min` on a string is rejected) +- `{ each: true }` against arrays (`@IsString({ each: true })` demands a `string[]`, and a bare + `@IsString()` on a `string[]` is rejected) +- `@JsonType(() => Address)` against the field's class +- `@JsonSerialize` / `@JsonDeserialize` against the field's type +- `@IsIn([...])` and `@IsEnum(E)` against the field's value type + +17 tests invoke the real compiler to assert these stay rejected. + +### Migration + +- Remove `"experimentalDecorators": true`; add `"ESNext.Decorators"` to `lib`. +- `registerDecorator({ target, propertyName, validator })` is replaced by + `defineRule(Class, 'field', constraint)`. +- Decorators cannot be applied to `abstract` fields. Declare the field concretely in the base. +- Field types may need tightening where a rule narrows them: `@IsIn(['a','b']) x!: string` + becomes `x!: 'a' | 'b'`. +- `@JsonPolymorphic` takes its base type explicitly to check subtypes: + `@JsonPolymorphic('type', [...])`. +- `@ValidateIf` takes the class as a type argument: `@ValidateIf(m => ...)`. + +Everything else — the engine, options, naming strategies, access control, error helpers, the +sync API — is unchanged. + +### Removed + +- `metadata-storage.ts` and its WeakMap singleton. Metadata now lives on `context.metadata`, + the language's own mechanism, which also removes the dual ESM/CJS double-singleton hazard. +- `registerDecorator`, replaced by `defineRule`. + +### Fixed + +- Inheritance merging is now structural rather than reconstructed: `context.metadata` inherits + through the prototype chain, so the subclass-shadowing defect fixed by hand in 0.1.0 cannot + reoccur by construction. Identical inherited rules are still collapsed so re-stating a rule + on an override does not double-report. + +### Toolchain + +Standard decorators are transformed by `tsc` and by esbuild; **oxc does not implement them +yet**. The library builds with `tsc` and consumers bundling with esbuild or Vite are fine, but +the test runner (Vitest 4, which uses oxc) needs an esbuild transform plugin — see +`vitest.config.ts`. Projects on an oxc-based toolchain should stay on 0.1.x for now. + +## [0.1.0] - 2026-08-05 + +### Added + +**Synchronous API.** `validateSync`, `validateOrRejectSync`, `toPlainSync`, `toJsonSync`, +`toInstanceSync`, `toInstanceArraySync`, `fromJsonSync` and `fromJsonArraySync`. Nothing on +the default path is genuinely asynchronous — only a user-supplied serializer, deserializer or +validator can be — so requiring `await` everywhere was a tax on the common case. + +The engines are now written synchronously, and anything a hook makes asynchronous is recorded +and reconciled once at the end. There is no second copy of the traversal logic to keep in +step, and the async entry points stop paying for a microtask per property. If a hook does +return a Promise, the `*Sync` call raises a `JsonMappingError` naming the async alternative +rather than silently returning a half-built object. + +`fromRequest` has no synchronous counterpart, because reading a request body is inherently +asynchronous. + +- `maxDepth` option (default 64) on every mapping function and on `configure()`. All three + engines recurse, so a hostile payload nested thousands of levels deep could exhaust the + call stack; it now raises a `JsonMappingError`. Cycles were already handled, but legitimate + deep nesting was not bounded. +- `validate(obj, options?)` accepts options, so `maxDepth` applies to standalone validation. +- `REDACTED` export, the placeholder substituted for withheld values. + +### Security + +- **Validation errors no longer carry the value of a property that is never serialized.** + A `@JsonWriteOnly` password that failed `@MinLength` put the rejected password into + `ValidationError.value`, and from there into any log that recorded the error. Values for + `@JsonWriteOnly` and `@JsonIgnore` properties are replaced with `REDACTED`; the property + name and the failure message are unchanged, so the error is still actionable. + +### Performance + +Profiling the validator showed roughly **half of all validation time** was spent re-deriving +answers that cannot change: `collectConstraints` (22%), `getOwnMetadata` (12%), +`getMetadataChain` (9%), `getProperties` (4%) and `getMetadata` (3%), plus 8% garbage +collection from the allocation churn. The constraint predicates themselves accounted for +under 1%. + +Decorator metadata is fixed once classes are declared, so the derived structures are now +memoized per prototype — the validation plan, the serialization plan, the deserialization +plan, and serializer/deserializer instances (previously constructed fresh for every property +of every object). `MetadataStorage` carries a version counter that invalidates every cache +if metadata is registered late, so `registerDecorator` after first use still works. + +Together with the synchronous core, measured on a customer record with a nested address and +orders, against `JSON.parse` + `JSON.stringify` (5.8 us) as a fixed reference point: + +| Operation | 0.1.0 | Now | Speedup | +| --- | --- | --- | --- | +| `validate` (50 orders) | 221.6 us | 17.8 us | 12.4x | +| `validate` (10 orders) | 47.8 us | 4.5 us | 10.6x | +| `toPlain` (50 orders) | 294.4 us | 36.0 us | 8.2x | +| `toInstance` (50 orders) | 255.1 us | 31.7 us | 8.0x | +| `toInstance` (10 orders) | 64.6 us | 8.2 us | 7.9x | +| `toInstance` (single) | 19.8 us | 5.2 us | 3.8x | + +### Changed + +- `each: true` failures now report which element failed — `"... (failed at index 3)"`. A bad + entry in a 200-item array previously produced a message that could not locate it. A message + function now receives the failing element as `args.value` rather than the whole array; + caller-supplied string messages are still reported verbatim. + +## [0.1.0] - 2026-08-03 + +The first release with a working test suite. Everything below the "Fixed" heading was +found by writing tests against the previous release; the suite has grown from 40 tests +that never executed to 136 that do. + +### Added + +**Field-name mapping.** A library whose headline feature is "JSON mapping" could not map +a name. It can now. + +- `@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 — applied to properties with no explicit name. + Acronyms split where a reader expects: `parseHTTPResponse` → `parse_http_response`. + +**Access control.** + +- `@JsonIgnore()` — excluded in both directions. +- `@JsonWriteOnly()` — accepted from input, never echoed back (passwords). +- `@JsonReadOnly()` — serialized, never settable by a client (server-owned ids). + +**Transform options**, per call or globally via `configure()`. + +- `validate: false` maps without validating, for lenient parsing. +- `unknownKeys: 'allow' | 'strip' | 'error'` decides what happens to undeclared keys. +- `namingStrategy` selects the JSON naming convention. + +**Error ergonomics.** Turning the nested `ValidationError` tree into an HTTP 400 body used +to be the caller's problem. + +- `flattenErrors(errors)` → `{ "items[0].qty": ["qty must be at least 1"] }` +- `formatErrors(errors)` → one human-readable line per failure +- `collectErrorMessages(errors)` → just the messages +- `validateOrReject(obj)` throws instead of returning an array you might forget to check + +**30 validation decorators.** `@Equals`, `@NotEquals`, `@IsEmpty`, `@IsEnum`, `@IsInstance`, +`@Length`, `@IsAlpha`, `@IsAlphanumeric`, `@IsNumberString`, `@IsLowercase`, `@IsUppercase`, +`@Contains`, `@NotContains`, `@StartsWith`, `@EndsWith`, `@IsUUID`, `@IsJSON`, +`@IsDateString`, `@IsSemVer`, `@IsHexColor`, `@IsIP`, `@IsDivisibleBy`, `@IsPort`, +`@IsLatitude`, `@IsLongitude`, `@IsBigInt`, `@MinDate`, `@MaxDate`, `@ArrayUnique`, +`@ArrayContains`, `@ArrayNotContains`. + +**Conditional validation.** `@ValidateIf(o => ...)` makes a property's rules depend on the +rest of the object; `@Allow()` declares a property that needs no rules of its own. + +**Correctly typed array entry points.** `toInstanceArray()` and `fromJsonArray()`. +`toInstance`/`fromJson` accept arrays at runtime but type the result as `T`, so callers had +to cast to reach the elements. + +**`@JsonPolymorphic` options.** `{ onUnknown: 'error' }` and `{ fallback: SomeClass }`. + +**`JsonMappingError`** — raised when a value cannot be mapped at all, as distinct from +mapping fine and failing validation. + +### Fixed + +- **Inheritance silently discarded base-class rules.** A subclass re-decorating an inherited + property registered its constraints against its own prototype, and the engine read only the + nearest set. Constraints now merge down the whole prototype chain, base first. The + library's own example was affected: `Media`'s `@IsString() title` had never been enforced + for `Book`. +- **A circular reference exhausted the heap.** `serialize()` recursed forever, taking 8 GB + and the process with it. It now raises a `JsonMappingError` naming the cause. Diamonds + still serialize; `validate()` skips back-edges. +- **`@Matches` with a `g` or `y` flag was stateful.** `RegExp.test` advances `lastIndex`, so + validating the same value twice gave different answers. Those flags are stripped. +- **An unmatched `@JsonPolymorphic` discriminator silently dropped the value.** The + single-object branch fell through without assigning; the property came back `undefined`. + The raw value is now preserved. +- **`@JsonSerialize` serializers ran on `null`/`undefined`**, crashing on any unset optional + property. They now only see real values. +- **`serialize()` crashed on null-prototype objects.** It read `obj.constructor.prototype`; + both engines now agree on `Object.getPrototypeOf`. +- **`__proto__`, `constructor` and `prototype` in untrusted JSON** were copied onto the + instance, detaching it from its own class. They are dropped. +- **Caller-supplied messages were mangled** by the `each element in ...` prefix, producing + sentences like "each element in tags must all be strings". +- **Two rules sharing a name overwrote each other**, so only one failure was ever reported. +- **`fromRequest` leaked a raw `SyntaxError`** for a non-JSON body; it now reports a + `JsonMappingError`. +- **`@ValidateNested({ each: true })`** was documented in the README but did not compile — + `ValidateNested()` accepted no arguments. It now does, and asserts the value is an array. + +### Changed + +- `toPlain`, `toJson`, `toInstance`, `fromJson`, `fromJsonArray`, `toInstanceArray` and + `fromRequest` accept an optional trailing options argument. All defaults preserve the + previous behaviour. +- `src/example.ts` is no longer published in `dist`. It called `runExample()` at import + time — an import side effect in a package declaring `"sideEffects": false`. +- Minimum supported Node is 20. + +### Infrastructure + +- **The test suite had never run.** Vitest 4 transpiles with oxc, which does not read + `experimentalDecorators` from a tsconfig that excludes the files it is transforming, so + every decorator-using suite failed to parse and was reported as "0 test" rather than as an + error. A `vitest.config.ts` enabling legacy decorators brought all 40 existing tests back + to life. +- Test files are now type-checked, which surfaced 17 strict-mode errors. +- CI runs lint, coverage tests, build and ESM/CJS entry-point smoke checks across Node + 20/22/24, and `npm ci` works because `package-lock.json` is committed. +- `npm run build:docs` regenerates the previously hand-maintained `docs/cereale.js`. + +## [0.0.1] + +Initial release: mapping and validation decorators, polymorphic types, custom +serializers/deserializers, and the `toJson` / `fromJson` / `toPlain` / `toInstance` API. diff --git a/README.md b/README.md index 8220fae..6666c63 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,58 @@ # Cereale -Cereale is a lightweight TypeScript library that provides Spring-like decorators for JSON mapping and validation. Built with ZERO external dependencies, it simplifies the process of converting between plain JSON and class instances with full validation support. +**Validated domain objects, not validated data.** + +Cereale maps JSON onto your own classes and gives you back real instances — with your methods, +your inheritance, your `instanceof` checks — and type-checks the validation rules against the +fields they are attached to. Zero runtime dependencies. + +```typescript +class User { + @JsonProperty('display_name') + @IsString() @MinLength(2) + displayName!: string; + + @IsInt() @Min(0) + age!: number; + + @IsString() + age2!: number; // ← compile error: Type 'number' is not assignable to type 'string' + + greet() { return `Hi ${this.displayName}`; } +} + +const user = fromJsonSync(User, body); // a real User +user.greet(); // your methods are still there +``` + +## Why not Zod? + +Zod is excellent, and if a plain validated object is what you want, use it. The difference is +what you get back: + +| | Zod | Cereale | +| --- | --- | --- | +| Result of parsing | an anonymous object matching a schema | an instance of **your class** | +| Methods, getters, inheritance | none — data only | preserved | +| Where the type comes from | inferred from the schema | your class declaration | +| Rules checked against the type | not applicable — schema *is* the type | **yes, at compile time** | +| Bidirectional mapping (renaming both ways) | not the focus | first-class | + +Cereale does not infer your type from a schema, so you still write the field type and the rule. +What it guarantees is that **the two cannot disagree** — `@IsInt() name!: string` does not +compile. That is the guarantee class-validator has never offered. + +Reach for Cereale when your domain model is already a class — a NestJS provider, a TypeORM +entity, anything with behaviour attached. Reach for Zod when you just want the data. ## Features -- **Spring-like Decorators:** Familiar `@JsonSerialize`, `@JsonDeserialize`, `@JsonType`, and `@JsonPolymorphic`. -- **Custom Serializers/Deserializers:** Easily handle complex types like Dates, BigInts, or custom objects. -- **Polymorphism Support:** Native handling of polymorphic types via discriminators. -- **Integrated Validation:** Automatically validates objects during serialization and deserialization. -- **Type Safety:** Fully written in TypeScript for excellent developer experience. -- **Zero Dependencies:** Extremely lightweight and fast. +- **Strongly typed decorators:** a rule that does not fit its field is a compile error. +- **Real instances:** nested objects, polymorphic subtypes and arrays all come back as classes. +- **Field-name mapping:** `@JsonProperty`, `@JsonAlias` and naming strategies, both directions. +- **Access control:** keep passwords out of responses and server-owned ids out of requests. +- **Sync and async:** every entry point has a synchronous twin. +- **Zero dependencies**, ESM + CJS, Node 20+. ## Installation @@ -17,110 +60,93 @@ Cereale is a lightweight TypeScript library that provides Spring-like decorators npm install cereale ``` -Make sure to enable `experimentalDecorators` and `emitDecoratorMetadata` in your `tsconfig.json`: +Cereale v2 uses **TC39 standard decorators**, so no `experimentalDecorators` flag: ```json { "compilerOptions": { - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "target": "ES2025" + "target": "ES2022", + "lib": ["ESNext", "ESNext.Decorators"] } } ``` +Requires TypeScript 5.2+ and Node 20+. `reflect-metadata` is not needed and +`emitDecoratorMetadata` is not read. + +> **Toolchain note.** Standard decorators are transformed by `tsc` and by esbuild (so Vite +> works). They are **not** yet transformed by oxc; if your toolchain uses it, decorator syntax +> will fail to parse. The 0.1.x line, which uses legacy decorators, remains available for +> those setups. + ## Quick Start -### 1. Define your Models - -Use decorators to define how your data should be transformed and validated. +### 1. Define your model ```typescript -import { - IsString, - IsInt, - Min, - IsDate, - ValidateNested, - JsonSerialize, - JsonDeserialize, - JsonPolymorphic, - JsonSerializer, - JsonDeserializer +import { + IsString, IsDate, IsInt, Min, ValidateNested, JsonType, + JsonProperty, JsonWriteOnly, JsonPolymorphic, } from 'cereale'; -// Custom Date Serializer -class DateSerializer implements JsonSerializer { - serialize(value: Date): string { - return value.toISOString().split('T')[0]; - } -} +class Address { + @IsString() street!: string; + @IsString() city!: string; -class DateDeserializer implements JsonDeserializer { - deserialize(value: string): Date { - return new Date(value); - } + format() { return `${this.street}, ${this.city}`; } } abstract class Media { - @IsString() - abstract type: string; - - @IsString() - title: string; + // Standard decorators cannot decorate an `abstract` member, so declare it concretely. + @IsString() type: string = ''; + @IsString() title: string = ''; } class Book extends Media { - type = 'book'; - - @IsString() - author: string; + @IsString() override type = 'book'; + @IsString() author!: string; - @JsonSerialize(DateSerializer) - @JsonDeserialize(DateDeserializer) + @JsonProperty('published_at') @IsDate() - publishedAt: Date; + publishedAt!: Date; } class Library { - @IsString() - name: string; + @IsString() name!: string; + + @ValidateNested() @JsonType(() => Address) + address!: Address; // the class must match the field @ValidateNested({ each: true }) - @JsonPolymorphic('type', [ - { value: Book, name: 'book' } - ]) - items: Media[]; + @JsonPolymorphic('type', [{ value: Book, name: 'book' }]) + items!: Media[]; } ``` -### 2. Map JSON with Validation +Constraints accumulate down an inheritance chain: `Book` is checked against `Media`'s rules as +well as its own, and re-stating a rule on an override does not report it twice. -Use standalone utility functions to handle the conversion process directly. +### 2. Map JSON, synchronously or not ```typescript -import { fromJson, toJson, JsonValidationError } from 'cereale'; +import { fromJsonSync, toJsonSync, JsonValidationError, flattenErrors } from 'cereale'; -async function main() { - const json = '{"name": "Central Library", "items": [{"type": "book", "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "publishedAt": "1925-04-10"}]}'; - - try { - // Deserialize JSON to Class Instance - const library = await fromJson(Library, json); - console.log(library.name); // "Central Library" - console.log(library.items[0] instanceof Book); // true - - // Serialize Class Instance back to JSON - const outputJson = await toJson(library); - console.log(outputJson); - } catch (error) { - if (error instanceof JsonValidationError) { - console.error("Validation failed:", error.errors); - } +try { + const library = fromJsonSync(Library, json); + library.address.format(); // your method, on a real Address + library.items[0] instanceof Book; // true + console.log(toJsonSync(library)); +} catch (error) { + if (error instanceof JsonValidationError) { + console.error(flattenErrors(error.errors)); + // { "items[0].title": ["title must be a string"] } } } ``` +Every function has an async form too (`fromJson`, `toJson`, …) for when a serializer, +deserializer or validator of yours returns a Promise. + ### 3. Modern Web Frameworks (Request Integration) Cereale is compatible with Fetch-based frameworks like Hono, Next.js, and Remix. Use the `fromRequest` async helper. @@ -135,20 +161,123 @@ app.post('/books', async (c) => { }); ``` +## Field-name Mapping + +JSON rarely uses the same names as your classes. + +```typescript +import { JsonProperty, JsonAlias } from 'cereale'; + +class User { + @JsonProperty('first_name') + firstName: string; // <-> {"first_name": "Ada"} + + @JsonProperty('surname') + @JsonAlias('last_name') // also accepted on input, never emitted + lastName: string; +} +``` + +Or convert every property at once with a naming strategy: + +```typescript +import { configure, toPlain } from 'cereale'; + +// once, for the whole application +configure({ namingStrategy: 'snake_case' }); + +// or per call +await toPlain(user, { namingStrategy: 'snake_case' }); +``` + +Built-in strategies: `identity` (default), `camelCase`, `PascalCase`, `snake_case`, +`SCREAMING_SNAKE_CASE`, `kebab-case`. You can also pass your own +`(propertyKey: string) => string`. An explicit `@JsonProperty` always wins. + +Acronyms split where a reader expects them to: `parseHTTPResponse` becomes +`parse_http_response`, not `parse_h_t_t_p_response`. + +## Access Control + +```typescript +import { JsonIgnore, JsonReadOnly, JsonWriteOnly } from 'cereale'; + +class Account { + @JsonReadOnly() // sent to clients, never settable by them + id: number; + + @IsString() + email: string; + + @JsonWriteOnly() // accepted from clients, never echoed back + @IsString() + password: string; + + @JsonIgnore() // never crosses the boundary in either direction + internalNotes: string; +} +``` + +## Options + +Every mapping function takes an optional trailing options argument, and `configure()` sets +defaults for the whole application. Per-call options win. + +| Option | Values | Default | Meaning | +| --- | --- | --- | --- | +| `validate` | `boolean` | `true` | Validate the result; throw `JsonValidationError` on failure. | +| `namingStrategy` | strategy name or function | `identity` | JSON naming convention for properties without `@JsonProperty`. | +| `unknownKeys` | `allow` \| `strip` \| `error` | `allow` | What to do with incoming keys matching no declared property. | +| `maxDepth` | `number` | `64` | Nesting depth before a `JsonMappingError` is raised, bounding hostile payloads. | + +```typescript +// lenient parse: build the instance, inspect the damage yourself +const draft = await fromJson(Order, body, { validate: false }); +const problems = flattenErrors(await validate(draft)); + +// strict intake: reject anything you did not declare +const order = await fromJson(Order, body, { unknownKeys: 'error' }); +``` + +## Synchronous API + +Nothing on the default path is genuinely asynchronous — only a serializer, deserializer or +validator you supply can be — so every mapping function has a synchronous twin. + +```typescript +import { fromJsonSync, toJsonSync, validateSync } from 'cereale'; + +const user = fromJsonSync(User, body); // no await +const errors = validateSync(user); +const payload = toJsonSync(user); +``` + +`validateSync`, `validateOrRejectSync`, `toPlainSync`, `toJsonSync`, `toInstanceSync`, +`toInstanceArraySync`, `fromJsonSync`, `fromJsonArraySync`. + +If one of your hooks does return a Promise, the synchronous call raises a `JsonMappingError` +naming the async function to use instead, rather than handing back a half-built object. +`fromRequest` has no synchronous form, since reading a request body is inherently async. + ## API Reference -### Decorators +### Mapping Decorators -- `@JsonSerialize(serializer: ClassConstructor)`: Specifies a custom serializer for a property. -- `@JsonDeserialize(deserializer: ClassConstructor)`: Specifies a custom deserializer for a property. -- `@JsonType(typeFunction: () => ClassConstructor)`: Explicitly sets the type for nested transformations. -- `@JsonPolymorphic(discriminator: string, subTypes: { value: ClassConstructor, name: string }[])`: Configures polymorphic transformation based on a discriminator field. +- `@JsonProperty(name: string)`: Renames the property in JSON, both directions. +- `@JsonAlias(...names: string[])`: Extra names accepted on input only. +- `@JsonIgnore()`: Excludes the property from mapping entirely. +- `@JsonReadOnly()`: Serialized, but never populated from incoming JSON. +- `@JsonWriteOnly()`: Populated from incoming JSON, but never serialized. +- `@JsonSerialize(serializer: ClassConstructor)`: Custom serializer for a property. Skipped when the value is `null`/`undefined`. +- `@JsonDeserialize(deserializer: ClassConstructor)`: Custom deserializer for a property. +- `@JsonType(typeFunction: () => ClassConstructor)`: Explicitly sets the type for nested transformations. Applies element-wise to arrays. +- `@JsonPolymorphic(discriminator, subTypes, options?)`: Polymorphic transformation based on a discriminator field. `options` accepts `{ onUnknown: 'keep' | 'error' }` (default `keep`, which preserves the raw value) and `{ fallback: ClassConstructor }`. -#### Validation Decorators +### Validation Decorators Most validation decorators accept an optional `ValidationOptions` object: - `each: boolean`: Apply validation to each element of an array. -- `message: string | ((args: ValidationArguments) => string)`: Custom error message. +- `message: string | ((args: ValidationArguments) => string)`: Custom error message, reported verbatim. | Decorator | Description | | --- | --- | @@ -156,46 +285,95 @@ Most validation decorators accept an optional `ValidationOptions` object: | `@IsNumber()` | Checks if value is a number (and not NaN). | | `@IsInt()` | Checks if value is an integer. | | `@IsBoolean()` | Checks if value is a boolean. | +| `@IsBigInt()` | Checks if value is a bigint. | | `@IsObject()` | Checks if value is an object (not null/array). | | `@IsDate()` | Checks if value is a valid Date object. | | `@IsDefined()` | Checks if value is not null or undefined. | | `@IsOptional()` | Skips other validations if value is null/undefined. | | `@IsNotEmpty()` | Checks if value is not null/undefined/empty string. | -| `@Min(value)` | Checks if number is >= value. | -| `@Max(value)` | Checks if number is <= value. | -| `@Positive()` | Checks if number is > 0. | -| `@Negative()` | Checks if number is < 0. | -| `@MinLength(len)` | Checks if string length is >= len. | -| `@MaxLength(len)` | Checks if string length is <= len. | +| `@IsEmpty()` | Checks if value is null/undefined/`''`/`[]`/`{}`. | +| `@Equals(value)` / `@NotEquals(value)` | Strict equality against a fixed value. | +| `@IsEnum(enumObject)` | Checks membership of a TypeScript enum. | +| `@IsInstance(Class)` | Checks `value instanceof Class`. | +| `@Min(value)` / `@Max(value)` | Numeric bounds. | +| `@Positive()` / `@Negative()` | Checks sign. | +| `@IsDivisibleBy(n)` | Checks `value % n === 0`. | +| `@IsPort()` | Integer in 0–65535, as number or numeric string. | +| `@IsLatitude()` / `@IsLongitude()` | Geographic bounds. | +| `@MinLength(len)` / `@MaxLength(len)` | String length bounds. | +| `@Length(min, max?)` | Both bounds in one rule. | +| `@IsAlpha()` / `@IsAlphanumeric()` | Character-class checks. | +| `@IsLowercase()` / `@IsUppercase()` | Case checks. | +| `@IsNumberString()` | String that parses as a finite number. | +| `@Contains(s)` / `@NotContains(s)` | Substring checks. | +| `@StartsWith(s)` / `@EndsWith(s)` | Affix checks. | | `@Email()` | Checks if string is a valid email. | | `@IsUrl()` | Checks if string is a valid URL. | -| `@Matches(regex)`| Checks if string matches a regular expression. | +| `@IsUUID(version?)` | Checks if string is a valid UUID. | +| `@IsIP(version?)` | Checks if string is a valid IPv4/IPv6 address. | +| `@IsJSON()` | Checks if string parses as JSON. | +| `@IsDateString()` | Checks if string is a parseable date. | +| `@IsSemVer()` | Checks if string is a semantic version. | +| `@IsHexColor()` | Checks `#rgb`, `#rrggbb`, `#rrggbbaa`. | +| `@Matches(regex)` | Checks if string matches a regular expression. | +| `@MinDate(d)` / `@MaxDate(d)` | Date bounds. Accepts `() => Date` for a moving bound. | | `@IsArray()` | Checks if value is an array. | -| `@ArrayNotEmpty()`| Checks if array is not empty. | -| `@ArrayMinSize(n)`| Checks if array has at least n elements. | -| `@ArrayMaxSize(n)`| Checks if array has at most n elements. | -| `@IsIn(values)` | Checks if value is in the allowed list. | -| `@IsNotIn(vals)` | Checks if value is NOT in the list. | -| `@ValidateNested()`| Recursively validates nested objects/arrays. | +| `@ArrayNotEmpty()` | Checks if array is not empty. | +| `@ArrayMinSize(n)` / `@ArrayMaxSize(n)` | Array size bounds. | +| `@ArrayUnique(keyFn?)` | Checks for duplicate elements. | +| `@ArrayContains(vals)` / `@ArrayNotContains(vals)` | Membership checks. | +| `@IsIn(values)` / `@IsNotIn(values)` | Allow/deny lists. | +| `@ValidateNested(options?)` | Recursively validates nested objects/arrays. | +| `@ValidateIf(o => boolean)` | Skips this property's rules when the condition is false. | +| `@Allow()` | Declares a property with no rules of its own. | +| `@Validate(validator, constraints?, options?)` | Applies a custom validator class or function. | + +Write your own with `registerDecorator({ name, target, propertyName, validator })`. ### Utilities -- `toJson(obj: any)`: Validates and serializes an instance to a JSON string (Returns `Promise`). -- `toPlain(obj: any)`: Validates and transforms an instance to a plain object (Returns `Promise`). -- `fromJson(clazz: ClassConstructor, json: string)`: Parses JSON and transforms it to a validated class instance (Returns `Promise`). -- `toInstance(clazz: ClassConstructor, plain: any)`: Transforms a plain object to a validated class instance (Returns `Promise`). -- `fromRequest(clazz: ClassConstructor, request: Request)`: Extracts JSON from a Fetch `Request` and transforms it to a validated instance (Returns `Promise`). -- `validate(obj: any)`: Performs full validation on an object/instance (Returns `Promise`). +- `toJson(obj, options?)`: Validates and serializes an instance to a JSON string (`Promise`). +- `toPlain(obj, options?)`: Validates and transforms an instance to a plain object (`Promise`). +- `fromJson(clazz, json, options?)`: Parses JSON to a validated class instance (`Promise`). +- `fromJsonArray(clazz, json, options?)`: Same, for a JSON array (`Promise`). +- `toInstance(clazz, plain, options?)`: Transforms a plain object to a validated class instance (`Promise`). +- `toInstanceArray(clazz, plain, options?)`: Same, for an array (`Promise`). +- `fromRequest(clazz, request, options?)`: Extracts JSON from a Fetch `Request` (`Promise`). +- `validate(obj, options?)`: Full validation, returning `Promise`. +- `validateOrReject(obj, options?)`: As above, but throws `JsonValidationError`. +- Synchronous twins of all of the above except `fromRequest`: `toPlainSync`, `toJsonSync`, + `fromJsonSync`, `fromJsonArraySync`, `toInstanceSync`, `toInstanceArraySync`, + `validateSync`, `validateOrRejectSync`. +- `configure(options)` / `getConfig()` / `resetConfig()`: Library-wide defaults. + +### Error Handling + +`JsonValidationError` carries a nested `ValidationError[]`. Three helpers turn it into +something you can return to a client: + +```typescript +import { flattenErrors, formatErrors, collectErrorMessages } from 'cereale'; + +flattenErrors(errors); // { "items[0].qty": ["qty must be at least 1"] } +formatErrors(errors); // "items[0].qty: qty must be at least 1" +collectErrorMessages(errors); // ["qty must be at least 1"] +``` + +Values of properties that never leave the process — `@JsonWriteOnly` and `@JsonIgnore` — are +replaced with `REDACTED` in `ValidationError.value`, so a rejected password does not travel +into your logs inside an error object. The property name and message are unaffected. + +`JsonMappingError` is raised when a value cannot be mapped at all — a body that is not +JSON, a circular reference, an unknown discriminator under `{ onUnknown: 'error' }` — as +distinct from mapping fine and failing validation. ## Framework Integrations -Cereale is designed to be compatible with all trending web frameworks. - ### Hono / Next.js / Cloudflare Workers Use `fromRequest` for seamless integration with the Fetch `Request` API. ### NestJS -You can use Cereale inside your controllers for explicit mapping and validation without needing `reflect-metadata`. +Use Cereale inside your controllers for explicit mapping and validation without needing `reflect-metadata`. ```typescript import { toInstance } from 'cereale'; @@ -208,25 +386,65 @@ async create(@Body() body: any) { ``` ### Express / Fastify -Easily integrate with traditional Node.js frameworks. ```typescript -import { toInstance, toPlain } from 'cereale'; +import { toInstance, toPlain, JsonValidationError, flattenErrors } from 'cereale'; app.post('/user', async (req, res) => { try { const user = await toInstance(User, req.body); res.json(await toPlain(user)); } catch (err) { - res.status(400).json(err); + if (err instanceof JsonValidationError) { + return res.status(400).json({ errors: flattenErrors(err.errors) }); + } + throw err; } }); ``` +## Performance + +Decorator metadata is fixed once your classes are declared, so cereale resolves each class's +validation, serialization and deserialization plans once and memoizes them per prototype. +A version counter invalidates the caches if metadata is registered late, so `registerDecorator` +after first use still behaves correctly. The engines are synchronous internally, so the async +entry points do not pay for a microtask per property. + +Indicative throughput for a customer record with a nested address and 10 orders, measured +against `JSON.parse` + `JSON.stringify` (5.8 us) on the same machine: + +| Operation | Time | +| --- | --- | +| `toInstance` (deserialize + validate) | ~8 us | +| `toInstance` with `{ validate: false }` | ~3 us | +| `validate` on an existing instance | ~4.5 us | +| `toPlain` (validate + serialize) | ~12 us | + +If you validate at the edge and map internally afterwards, `{ validate: false }` skips the +dominant cost. + +## Notes and Limitations + +- **Rules are checked, types are not inferred.** You write both the field type and the rule; + cereale guarantees they agree. If you want the type derived from a schema, that is Zod's + model, not this one. +- **`abstract` fields cannot be decorated.** Standard decorators do not apply to abstract + members. Declare the field concretely in the base class instead. +- **oxc does not transform standard decorators yet.** `tsc` and esbuild do. + +- **Circular references** are rejected during serialization with a `JsonMappingError`. Break + the cycle with `@JsonIgnore()` on the back-reference. +- **`validate()` on a plain object** returns no errors: rules live on the class, so validate + the instance you get back from `toInstance`, not the raw payload. +- **Renaming is not backwards-compatible by itself.** Once a property carries + `@JsonProperty`, its original name is no longer accepted on input — add `@JsonAlias` to + keep older clients working. + ## Contributing Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project. ## License -Cereale is licensed under the [MIT License](LICENSE). \ No newline at end of file +Cereale is licensed under the [MIT License](LICENSE). diff --git a/docs/cereale.js b/docs/cereale.js index e16a915..fcc7f59 100644 --- a/docs/cereale.js +++ b/docs/cereale.js @@ -1 +1,2 @@ -"use strict";var Cereale=(()=>{var A=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var b=Object.prototype.hasOwnProperty;var $=(n,t)=>{for(var e in t)A(n,e,{get:t[e],enumerable:!0})},P=(n,t,e,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of T(t))!b.call(n,i)&&i!==e&&A(n,i,{get:()=>t[i],enumerable:!(a=O(t,i))||a.enumerable});return n};var S=n=>P(A({},"__esModule",{value:!0}),n);var it={};$(it,{ArrayMaxSize:()=>K,ArrayMinSize:()=>j,ArrayNotEmpty:()=>tt,Email:()=>_,IsArray:()=>X,IsBoolean:()=>R,IsDate:()=>at,IsDefined:()=>Y,IsIn:()=>et,IsInt:()=>k,IsNotEmpty:()=>Z,IsNotIn:()=>nt,IsNumber:()=>D,IsObject:()=>J,IsOptional:()=>z,IsString:()=>L,IsUrl:()=>G,JsonDeserialize:()=>w,JsonMapper:()=>x,JsonPolymorphic:()=>N,JsonSerialize:()=>V,JsonType:()=>C,JsonValidationError:()=>h,METADATA_KEYS:()=>u,Matches:()=>Q,Max:()=>H,MaxLength:()=>F,Min:()=>U,MinLength:()=>B,Negative:()=>q,Positive:()=>W,ValidateNested:()=>st});var M=class n{constructor(){this.properties=new WeakMap;this.propertyMetadata=new WeakMap;this.classMetadata=new WeakMap}static getInstance(){return n.instance||(n.instance=new n),n.instance}defineMetadata(t,e,a,i){if(i){let s=this.propertyMetadata.get(a);s||(s=new Map,this.propertyMetadata.set(a,s));let r=s.get(i);r||(r=new Map,s.set(i,r)),r.set(t,e)}else{let s=this.classMetadata.get(a);s||(s=new Map,this.classMetadata.set(a,s)),s.set(t,e)}}getMetadata(t,e,a){let i=e;for(;i;){let s=this.getOwnMetadata(t,i,a);if(s!==void 0)return s;i=Object.getPrototypeOf(i)}}getOwnMetadata(t,e,a){return a?this.propertyMetadata.get(e)?.get(a)?.get(t):this.classMetadata.get(e)?.get(t)}registerProperty(t,e){let a=this.properties.get(t);a||(a=[],this.properties.set(t,a)),a.includes(e)||a.push(e)}getProperties(t){let e=new Set,a=t;for(;a;){let i=this.properties.get(a);i&&i.forEach(s=>e.add(s)),a=Object.getPrototypeOf(a)}return Array.from(e)}},l=M.getInstance();var u={PROPERTIES:"cereale:properties",TYPE:"cereale:type",VALIDATION:"cereale:validation",SERIALIZER:"cereale:serializer",DESERIALIZER:"cereale:deserializer",POLYMORPHIC:"cereale:polymorphic",IS_OPTIONAL:"cereale:optional"};function f(n,t){l.registerProperty(n,t)}function o(n,t,e,a){f(n,t),a?.message&&(e.message=a.message);let i=l.getOwnMetadata(u.VALIDATION,n,t)||[];i.push(e),l.defineMetadata(u.VALIDATION,i,n,t)}function V(n){return(t,e)=>{f(t,e),l.defineMetadata(u.SERIALIZER,n,t,e)}}function w(n){return(t,e)=>{f(t,e),l.defineMetadata(u.DESERIALIZER,n,t,e)}}function C(n){return(t,e)=>{f(t,e),l.defineMetadata(u.TYPE,n,t,e)}}function N(n,t){return(e,a)=>{f(e,a),l.defineMetadata(u.POLYMORPHIC,{discriminator:n,subTypes:t},e,a)}}function z(){return(n,t)=>{f(n,t),l.defineMetadata(u.IS_OPTIONAL,!0,n,t)}}function L(){return(n,t)=>{o(n,t,{name:"isString",validate:e=>typeof e=="string",message:`${t} must be a string`})}}function R(){return(n,t)=>{o(n,t,{name:"isBoolean",validate:e=>typeof e=="boolean",message:`${t} must be a boolean`})}}function D(){return(n,t)=>{o(n,t,{name:"isNumber",validate:e=>typeof e=="number"&&!isNaN(e),message:`${t} must be a number`})}}function k(){return(n,t)=>{o(n,t,{name:"isInt",validate:e=>Number.isInteger(e),message:`${t} must be an integer`})}}function J(){return(n,t)=>{o(n,t,{name:"isObject",validate:e=>typeof e=="object"&&e!==null&&!Array.isArray(e),message:`${t} must be an object`})}}function Y(){return(n,t)=>{o(n,t,{name:"isDefined",validate:e=>e!=null,message:`${t} should not be null or undefined`})}}function Z(){return(n,t)=>{o(n,t,{name:"isNotEmpty",validate:e=>e!=null&&e!=="",message:`${t} should not be empty`})}}function U(n){return(t,e)=>{o(t,e,{name:"min",validate:a=>typeof a=="number"&&a>=n,message:`${e} must be at least ${n}`})}}function H(n){return(t,e)=>{o(t,e,{name:"max",validate:a=>typeof a=="number"&&a<=n,message:`${e} must be at most ${n}`})}}function W(){return(n,t)=>{o(n,t,{name:"positive",validate:e=>typeof e=="number"&&e>0,message:`${t} must be positive`})}}function q(){return(n,t)=>{o(n,t,{name:"negative",validate:e=>typeof e=="number"&&e<0,message:`${t} must be negative`})}}function B(n){return(t,e)=>{o(t,e,{name:"minLength",validate:a=>typeof a=="string"&&a.length>=n,message:`${e} must be longer than or equal to ${n} characters`})}}function F(n){return(t,e)=>{o(t,e,{name:"maxLength",validate:a=>typeof a=="string"&&a.length<=n,message:`${e} must be shorter than or equal to ${n} characters`})}}function _(){let n=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return(t,e)=>{o(t,e,{name:"isEmail",validate:a=>typeof a=="string"&&n.test(a),message:`${e} must be a valid email`})}}function G(){return(n,t)=>{o(n,t,{name:"isUrl",validate:e=>{try{return new URL(e),!0}catch{return!1}},message:`${t} must be a valid URL`})}}function Q(n){return(t,e)=>{o(t,e,{name:"matches",validate:a=>typeof a=="string"&&n.test(a),message:`${e} must match ${n} regular expression`})}}function X(){return(n,t)=>{o(n,t,{name:"isArray",validate:e=>Array.isArray(e),message:`${t} must be an array`})}}function j(n){return(t,e)=>{o(t,e,{name:"arrayMinSize",validate:a=>Array.isArray(a)&&a.length>=n,message:`${e} must contain at least ${n} elements`})}}function K(n){return(t,e)=>{o(t,e,{name:"arrayMaxSize",validate:a=>Array.isArray(a)&&a.length<=n,message:`${e} must contain at most ${n} elements`})}}function tt(){return(n,t)=>{o(n,t,{name:"arrayNotEmpty",validate:e=>Array.isArray(e)&&e.length>0,message:`${t} should not be empty`})}}function et(n){return(t,e)=>{o(t,e,{name:"isIn",validate:a=>n.includes(a),message:`${e} must be one of the following values: ${n.join(", ")}`})}}function nt(n){return(t,e)=>{o(t,e,{name:"isNotIn",validate:a=>!n.includes(a),message:`${e} must not be one of the following values: ${n.join(", ")}`})}}function at(){return(n,t)=>{o(n,t,{name:"isDate",validate:e=>e instanceof Date&&!isNaN(e.getTime()),message:`${t} must be a valid Date object`})}}function st(){return(n,t)=>{f(n,t),l.defineMetadata("cereale:nested",!0,n,t)}}var h=class extends Error{constructor(e,a){super(e);this.errors=a;this.name="JsonValidationError"}toString(){return`${this.message}: ${JSON.stringify(this.errors,null,2)}`}},x=class{static async toPlain(t){if(t==null)return t;let e=await this.validate(t);if(e.length>0)throw new h("Validation failed during serialization",e);return this.serialize(t)}static async toJson(t){let e=await this.toPlain(t);return JSON.stringify(e)}static async toInstance(t,e){let a=this.deserialize(t,e),i=await this.validate(a);if(i.length>0)throw new h("Validation failed during deserialization",i);return a}static async fromJson(t,e){let a=JSON.parse(e);return this.toInstance(t,a)}static serialize(t){if(t==null||typeof t!="object")return t;if(Array.isArray(t))return t.map(s=>this.serialize(s));if(t instanceof Date)return t.toISOString();let e=t.constructor.prototype,a={},i=Object.keys(t);for(let s of i){let r=t[s],g=l.getMetadata(u.SERIALIZER,e,s);if(g){let p=new g;a[s]=p.serialize(r)}else a[s]=this.serialize(r)}return a}static deserialize(t,e){if(e==null)return e;if(Array.isArray(e))return e.map(s=>this.deserialize(t,s));let a=new t,i=t.prototype;for(let s of Object.keys(e)){let r=e[s],g=l.getMetadata(u.DESERIALIZER,i,s);if(g){let d=new g;a[s]=d.deserialize(r);continue}let p=l.getMetadata(u.POLYMORPHIC,i,s);if(p&&r!==null&&r!==void 0){let{discriminator:d,subTypes:y}=p;if(Array.isArray(r))a[s]=r.map(m=>{let c=y.find(v=>m[d]===v.name);return c?this.deserialize(c.value,m):m});else{let m=y.find(c=>r[d]===c.name);if(m){a[s]=this.deserialize(m.value,r);continue}}continue}let I=l.getMetadata(u.TYPE,i,s);if(I&&r!==null&&r!==void 0){let d=I();a[s]=this.deserialize(d,r);continue}a[s]=r}return a}static async validate(t){let e=[];if(t==null||typeof t!="object")return e;if(Array.isArray(t)){for(let s=0;s0&&e.push({property:`[${s}]`,value:t[s],constraints:{},children:r})}return e}let a=Object.getPrototypeOf(t),i=l.getProperties(a);for(let s of i){let r=t[s],g={property:s,value:r,constraints:{}},p=l.getMetadata(u.IS_OPTIONAL,a,s),I=r==null;if(p&&I)continue;let d=l.getMetadata(u.VALIDATION,a,s)||[],y={value:r,object:t,property:s,constraints:[]};for(let c of d)if(y.constraints=c.constraints||[],!await c.validate(r,y)){let E=typeof c.message=="function"?c.message(y):c.message;g.constraints[c.name]=E}if(l.getMetadata("cereale:nested",a,s)&&r!==null&&r!==void 0){let c=await this.validate(r);c.length>0&&(g.children=c)}(Object.keys(g.constraints).length>0||g.children)&&e.push(g)}return e}};return S(it);})(); +"use strict";var Cereale=(()=>{var L=Object.defineProperty;var In=Object.getOwnPropertyDescriptor;var An=Object.getOwnPropertyNames;var Nn=Object.prototype.hasOwnProperty;var Mn=(n,e)=>{for(var t in e)L(n,t,{get:e[t],enumerable:!0})},Pn=(n,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of An(e))!Nn.call(n,r)&&r!==t&&L(n,r,{get:()=>e[r],enumerable:!(o=In(e,r))||o.enumerable});return n};var Rn=n=>Pn(L({},"__esModule",{value:!0}),n);var gt={};Mn(gt,{Allow:()=>Yn,ArrayContains:()=>Ye,ArrayMaxSize:()=>He,ArrayMinSize:()=>Ge,ArrayNotContains:()=>Qe,ArrayNotEmpty:()=>Ze,ArrayUnique:()=>Xe,Contains:()=>Fe,Email:()=>Oe,EndsWith:()=>Ue,Equals:()=>Be,IsAlpha:()=>Ce,IsAlphanumeric:()=>Te,IsArray:()=>We,IsBigInt:()=>re,IsBoolean:()=>oe,IsDate:()=>ae,IsDateString:()=>Ee,IsDefined:()=>se,IsDivisibleBy:()=>me,IsEmpty:()=>ue,IsEnum:()=>Le,IsHexColor:()=>Ve,IsIP:()=>ve,IsIn:()=>_e,IsInstance:()=>qe,IsInt:()=>te,IsJSON:()=>Ie,IsLatitude:()=>ge,IsLongitude:()=>he,IsLowercase:()=>$e,IsNotEmpty:()=>le,IsNotIn:()=>Ke,IsNumber:()=>ee,IsNumberString:()=>Se,IsObject:()=>ie,IsOptional:()=>Hn,IsPort:()=>ye,IsSemVer:()=>De,IsString:()=>ne,IsUUID:()=>Re,IsUppercase:()=>ke,IsUrl:()=>Ae,JsonAlias:()=>jn,JsonDeserialize:()=>Wn,JsonIgnore:()=>_n,JsonMapper:()=>G,JsonMappingError:()=>b,JsonPolymorphic:()=>Gn,JsonProperty:()=>Bn,JsonReadOnly:()=>Kn,JsonSerialize:()=>qn,JsonType:()=>Zn,JsonValidationError:()=>C,JsonWriteOnly:()=>Ln,Length:()=>we,Matches:()=>Ne,Max:()=>de,MaxDate:()=>et,MaxLength:()=>xe,Min:()=>ce,MinDate:()=>nt,MinLength:()=>be,Negative:()=>fe,NotContains:()=>Je,NotEquals:()=>je,Positive:()=>pe,REDACTED:()=>yn,StartsWith:()=>ze,Validate:()=>tt,ValidateIf:()=>Xn,ValidateNested:()=>Qn,addConstraint:()=>E,collectErrorMessages:()=>rt,configure:()=>Jn,defineRule:()=>Fn,flattenErrors:()=>Z,formatErrors:()=>ot,fromJson:()=>kn,fromJsonArray:()=>Sn,fromJsonArraySync:()=>yt,fromJsonSync:()=>mt,fromRequest:()=>En,getConfig:()=>zn,modelOf:()=>R,modelOfInstance:()=>v,modelVersion:()=>T,propertyModel:()=>h,resetConfig:()=>Un,resolveNamingStrategy:()=>F,resolveOptions:()=>x,toInstance:()=>P,toInstanceArray:()=>nn,toInstanceArraySync:()=>$n,toInstanceSync:()=>Q,toJson:()=>Dn,toJsonSync:()=>ft,toPlain:()=>Y,toPlainSync:()=>Tn,validate:()=>M,validateOrReject:()=>Cn,validateOrRejectSync:()=>pt,validateSync:()=>j});Symbol.metadata??=Symbol.for("Symbol.metadata");var S=Symbol.for("cereale.model"),on=0;function T(){return on}function vn(n){if(!Object.hasOwn(n,S)){let e=n[S],t={};for(let[o,r]of Object.entries(e??{}))t[o]={...r,constraints:[...r.constraints]};n[S]=t}return n[S]}function h(n,e){on++;let t=vn(n);return t[e]??={constraints:[]}}function E(n,e,t,o){o?.each&&(t.each=!0),o?.message&&(t.message=o.message,t.hasCustomMessage=!0),h(n,e).constraints.push(t)}function R(n){return typeof n!="function"?{}:n[Symbol.metadata]?.[S]??{}}function v(n){let e=Object.getPrototypeOf(n);if(!e)return{};let t=Object.getOwnPropertyDescriptor(e,"constructor");return R(t?.value)}function Fn(n,e,t,o){let r=n;r[Symbol.metadata]??=Object.create(null),E(r[Symbol.metadata],e,t,o)}function I(n){return n.replace(/([a-z0-9])([A-Z])/g,"$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g,"$1 $2").replace(/[_\-\s]+/g," ").trim().split(" ").filter(Boolean).map(e=>e.toLowerCase())}var rn=n=>n&&n.charAt(0).toUpperCase()+n.slice(1),q={identity:n=>n,camelCase:n=>{let e=I(n);return e.length===0?n:e[0]+e.slice(1).map(rn).join("")},PascalCase:n=>I(n).map(rn).join("")||n,snake_case:n=>I(n).join("_")||n,SCREAMING_SNAKE_CASE:n=>I(n).join("_").toUpperCase()||n,"kebab-case":n=>I(n).join("-")||n};function F(n){if(!n)return q.identity;if(typeof n=="function")return n;let e=q[n];if(!e)throw new Error(`Unknown naming strategy ${JSON.stringify(n)}. Use one of: ${Object.keys(q).join(", ")}, or pass your own function.`);return e}var an={namingStrategy:"identity",unknownKeys:"allow",validate:!0,maxDepth:64},O={...an};function Jn(n){O={...O,...n}}function zn(){return{...O}}function Un(){O={...an}}function x(n){return n?{namingStrategy:n.namingStrategy??O.namingStrategy,unknownKeys:n.unknownKeys??O.unknownKeys,validate:n.validate??O.validate,maxDepth:n.maxDepth??O.maxDepth}:O}function d(n,e){return((t,o)=>{let r=String(o.name);E(o.metadata,r,n(r),e)})}function p(n,e,t){return(o=>d(r=>({name:n,validate:e,message:t(r)}),o))}function A(n,e,t){return p(n,o=>typeof o=="string"&&e.test(o),t)}function Bn(n){return((e,t)=>{h(t.metadata,String(t.name)).name=n})}function jn(...n){return((e,t)=>{let o=h(t.metadata,String(t.name));o.aliases=[...o.aliases??[],...n]})}function W(n){return((e,t)=>{h(t.metadata,String(t.name)).access=n})}var _n=()=>W("none"),Kn=()=>W("readonly"),Ln=()=>W("writeonly");function qn(n){return((e,t)=>{h(t.metadata,String(t.name)).serializer=n})}function Wn(n){return((e,t)=>{h(t.metadata,String(t.name)).deserializer=n})}function Zn(n){return((e,t)=>{h(t.metadata,String(t.name)).type=n})}function Gn(n,e,t){return((o,r)=>{let a={discriminator:n,subTypes:e,onUnknown:t?.onUnknown??"keep",...t?.fallback?{fallback:t.fallback}:{}};h(r.metadata,String(r.name)).polymorphic=a})}function Hn(){return((n,e)=>{h(e.metadata,String(e.name)).optional=!0})}function Xn(n){return((e,t)=>{h(t.metadata,String(t.name)).condition=n})}function Yn(){return((n,e)=>{h(e.metadata,String(e.name))})}function Qn(n){return((e,t)=>{let o=String(t.name);h(t.metadata,o).nested=!0,n?.each&&E(t.metadata,o,{name:"nestedEach",validate:r=>Array.isArray(r),message:`${o} must be an array`})})}var ne=p("isString",n=>typeof n=="string",n=>`${n} must be a string`),ee=p("isNumber",n=>typeof n=="number"&&!isNaN(n),n=>`${n} must be a number`),te=p("isInt",n=>Number.isInteger(n),n=>`${n} must be an integer`),oe=p("isBoolean",n=>typeof n=="boolean",n=>`${n} must be a boolean`),re=p("isBigInt",n=>typeof n=="bigint",n=>`${n} must be a bigint`),ae=p("isDate",n=>n instanceof Date&&!isNaN(n.getTime()),n=>`${n} must be a valid Date object`),ie=p("isObject",n=>typeof n=="object"&&n!==null&&!Array.isArray(n),n=>`${n} must be an object`),se=p("isDefined",n=>n!=null,n=>`${n} should not be null or undefined`),le=p("isNotEmpty",n=>n!=null&&n!=="",n=>`${n} should not be empty`),ue=p("isEmpty",n=>n==null||n===""?!0:Array.isArray(n)?n.length===0:typeof n=="object"?Object.keys(n).length===0:!1,n=>`${n} must be empty`);function ce(n,e){return d(t=>({name:"min",validate:o=>typeof o=="number"&&o>=n,message:`${t} must be at least ${n}`,constraints:[n]}),e)}function de(n,e){return d(t=>({name:"max",validate:o=>typeof o=="number"&&o<=n,message:`${t} must be at most ${n}`,constraints:[n]}),e)}var pe=p("positive",n=>typeof n=="number"&&n>0,n=>`${n} must be positive`),fe=p("negative",n=>typeof n=="number"&&n<0,n=>`${n} must be negative`);function me(n,e){return d(t=>({name:"isDivisibleBy",validate:o=>typeof o=="number"&&Number.isFinite(o)&&n!==0&&o%n===0,message:`${t} must be divisible by ${n}`,constraints:[n]}),e)}var ye=p("isPort",n=>{let e=typeof n=="string"&&n.trim()!==""?Number(n):n;return typeof e=="number"&&Number.isInteger(e)&&e>=0&&e<=65535},n=>`${n} must be a valid port number`),ge=p("isLatitude",n=>typeof n=="number"&&Number.isFinite(n)&&n>=-90&&n<=90,n=>`${n} must be a latitude between -90 and 90`),he=p("isLongitude",n=>typeof n=="number"&&Number.isFinite(n)&&n>=-180&&n<=180,n=>`${n} must be a longitude between -180 and 180`);function be(n,e){return d(t=>({name:"minLength",validate:o=>typeof o=="string"&&o.length>=n,message:`${t} must be longer than or equal to ${n} characters`,constraints:[n]}),e)}function xe(n,e){return d(t=>({name:"maxLength",validate:o=>typeof o=="string"&&o.length<=n,message:`${t} must be shorter than or equal to ${n} characters`,constraints:[n]}),e)}function we(n,e,t){return d(o=>({name:"length",validate:r=>typeof r=="string"&&r.length>=n&&(e===void 0||r.length<=e),message:e===void 0?`${o} must be at least ${n} characters`:`${o} must be between ${n} and ${e} characters`,constraints:e===void 0?[n]:[n,e]}),t)}var Oe=A("isEmail",/^[^\s@]+@[^\s@]+\.[^\s@]+$/,n=>`${n} must be a valid email`),Ce=A("isAlpha",/^[A-Za-z]+$/,n=>`${n} must contain only letters`),Te=A("isAlphanumeric",/^[A-Za-z0-9]+$/,n=>`${n} must contain only letters and numbers`),De=A("isSemVer",/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,n=>`${n} must be a valid semantic version`),Ve=A("isHexColor",/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i,n=>`${n} must be a hex color`),$e=p("isLowercase",n=>typeof n=="string"&&n===n.toLowerCase(),n=>`${n} must be lowercase`),ke=p("isUppercase",n=>typeof n=="string"&&n===n.toUpperCase(),n=>`${n} must be uppercase`),Se=p("isNumberString",n=>typeof n=="string"&&n.trim()!==""&&Number.isFinite(Number(n)),n=>`${n} must be a number string`),Ee=p("isDateString",n=>typeof n=="string"&&!isNaN(Date.parse(n)),n=>`${n} must be a valid ISO 8601 date string`),Ie=p("isJson",n=>{if(typeof n!="string")return!1;try{return JSON.parse(n),!0}catch{return!1}},n=>`${n} must be a JSON string`),Ae=p("isUrl",n=>{try{return new URL(n),!0}catch{return!1}},n=>`${n} must be a valid URL`);function Ne(n,e){let t=n.flags.includes("g")||n.flags.includes("y")?new RegExp(n.source,n.flags.replace(/[gy]/g,"")):n;return d(o=>({name:"matches",validate:r=>typeof r=="string"&&t.test(r),message:`${o} must match ${n} regular expression`,constraints:[n]}),e)}var Me="00000000-0000-0000-0000-000000000000",Pe="ffffffff-ffff-ffff-ffff-ffffffffffff";function Re(n,e){let t=n?new RegExp(`^[0-9a-f]{8}-[0-9a-f]{4}-${n}[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`,"i"):/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;return d(o=>({name:"isUuid",validate:r=>typeof r!="string"?!1:!n&&(r.toLowerCase()===Me||r.toLowerCase()===Pe)?!0:t.test(r),message:`${o} must be a valid UUID${n?` (version ${n})`:""}`,...n?{constraints:[n]}:{}}),e)}var sn=/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/,ln=n=>{try{return new URL(`http://[${n}]`).hostname===`[${n.toLowerCase()}]`||/^[0-9a-f:.]+$/i.test(n)&&n.includes(":")}catch{return!1}};function ve(n,e){return d(t=>({name:"isIp",validate:o=>typeof o!="string"?!1:n===4?sn.test(o):n===6?ln(o):sn.test(o)||ln(o),message:`${t} must be a valid IP${n?`v${n}`:""} address`,...n?{constraints:[n]}:{}}),e)}function z(n,e,t){function o(r,a){return d(i=>({name:n,validate:s=>typeof s=="string"&&e(s,r),message:t(i,r),constraints:[r]}),a)}return o}var Fe=z("contains",(n,e)=>n.includes(e),(n,e)=>`${n} must contain ${JSON.stringify(e)}`),Je=z("notContains",(n,e)=>!n.includes(e),(n,e)=>`${n} must not contain ${JSON.stringify(e)}`),ze=z("startsWith",(n,e)=>n.startsWith(e),(n,e)=>`${n} must start with ${JSON.stringify(e)}`),Ue=z("endsWith",(n,e)=>n.endsWith(e),(n,e)=>`${n} must end with ${JSON.stringify(e)}`);function Be(n,e){return d(t=>({name:"equals",validate:o=>o===n,message:`${t} must be equal to ${JSON.stringify(n)}`,constraints:[n]}),e)}function je(n,e){return d(t=>({name:"notEquals",validate:o=>o!==n,message:`${t} must not be equal to ${JSON.stringify(n)}`,constraints:[n]}),e)}function _e(n,e){return d(t=>({name:"isIn",validate:o=>n.includes(o),message:`${t} must be one of the following values: ${n.join(", ")}`,constraints:[n]}),e)}function Ke(n,e){return d(t=>({name:"isNotIn",validate:o=>!n.includes(o),message:`${t} must not be one of the following values: ${n.join(", ")}`,constraints:[n]}),e)}function Le(n,e){let t=Object.keys(n).filter(o=>typeof n[n[o]]!="number").map(o=>n[o]);return d(o=>({name:"isEnum",validate:r=>t.includes(r),message:`${o} must be one of the following values: ${t.join(", ")}`,constraints:[t]}),e)}function qe(n,e){return d(t=>({name:"isInstance",validate:o=>o instanceof n,message:`${t} must be an instance of ${n.name}`,constraints:[n]}),e)}function $(n,e,t,o){return r=>d(a=>({name:n,validate:i=>Array.isArray(i)&&e(i),message:t(a),...o?{constraints:o}:{}}),r)}var We=n=>d(e=>({name:"isArray",validate:t=>Array.isArray(t),message:`${e} must be an array`}),n),Ze=n=>$("arrayNotEmpty",e=>e.length>0,e=>`${e} should not be empty`)(n),Ge=(n,e)=>$("arrayMinSize",t=>t.length>=n,t=>`${t} must contain at least ${n} elements`,[n])(e),He=(n,e)=>$("arrayMaxSize",t=>t.length<=n,t=>`${t} must contain at most ${n} elements`,[n])(e);function Xe(n,e){return $("arrayUnique",t=>{let o=n?t.map(n):t;return new Set(o).size===o.length},t=>`${t} must not contain duplicate values`)(e)}function Ye(n,e){return $("arrayContains",t=>n.every(o=>t.includes(o)),t=>`${t} must contain the following values: ${n.join(", ")}`,[n])(e)}function Qe(n,e){return $("arrayNotContains",t=>n.every(o=>!t.includes(o)),t=>`${t} must not contain any of the following values: ${n.join(", ")}`,[n])(e)}var J=n=>typeof n=="function"?n():n;function nt(n,e){return d(()=>({name:"minDate",validate:t=>t instanceof Date&&!isNaN(t.getTime())&&t.getTime()>=J(n).getTime(),message:t=>`${t.property} must not be earlier than ${J(n).toISOString()}`,constraints:[n]}),e)}function et(n,e){return d(()=>({name:"maxDate",validate:t=>t instanceof Date&&!isNaN(t.getTime())&&t.getTime()<=J(n).getTime(),message:t=>`${t.property} must not be later than ${J(n).toISOString()}`,constraints:[n]}),e)}function tt(n,e,t){let o=Array.isArray(e)?e:[],r=Array.isArray(e)?t:e;if(typeof n=="function"&&!n.prototype?.validate){let s=n;return d(()=>({name:"custom",validate:(l,u)=>s(l,u),message:l=>`${l.property} is invalid`,constraints:o}),r)}let i=new n;return d(()=>({name:n.name,validate:(s,l)=>i.validate(s,l),message:s=>i.defaultMessage?i.defaultMessage(s):`${s.property} is invalid`,constraints:o}),r)}function Z(n){let e={},t=(o,r)=>{for(let a of o){let i=a.property.startsWith("[")?`${r}${a.property}`:r?`${r}.${a.property}`:a.property,s=Object.values(a.constraints);s.length>0&&(e[i]??=[]).push(...s),a.children?.length&&t(a.children,i)}};return t(n,""),e}function ot(n){let e=Z(n);return Object.entries(e).flatMap(([t,o])=>o.map(r=>`${t}: ${r}`)).join(` +`)}function rt(n){return Object.values(Z(n)).flat()}var C=class extends Error{constructor(t,o){super(t);this.errors=o;this.name="JsonValidationError"}toString(){return`${this.message}: ${JSON.stringify(this.errors,null,2)}`}},b=class extends Error{constructor(e){super(e),this.name="JsonMappingError"}},at=new Set(["__proto__","constructor","prototype"]),yn="[redacted]";function gn(n,e){return n[e]?.access??"readwrite"}function hn(n,e,t){return n[e]?.name??t(e)}var un=new WeakMap;function it(n,e,t){let o=un.get(n);(!o||o.version!==T())&&(o={version:T(),byStrategy:new Map},un.set(n,o));let r=o.byStrategy.get(t.namingKey);r||(r=new Map,o.byStrategy.set(t.namingKey,r));let a=r.get(e);if(!a){let i=gn(n,e),s=n[e]?.serializer;a={name:hn(n,e,t.naming),skip:i==="none"||i==="writeonly",...s?{serializer:s}:{}},r.set(e,a)}return a}var cn=new WeakMap;function st(n,e){let t=cn.get(n);(!t||t.version!==T())&&(t={version:T(),byStrategy:new Map},cn.set(n,t));let o=t.byStrategy.get(e.namingKey);if(o)return o;let r=new Map,a=new Set,i=new Map,s=(u,c)=>{let y=r.get(u);if(y&&y!==c)throw new b(`Properties "${y}" and "${c}" both map to the JSON name ${JSON.stringify(u)}. Give one of them a distinct @JsonProperty name.`);r.set(u,c)};for(let[u,c]of Object.entries(n)){let y=[hn(n,u,e.naming),...c.aliases??[]],m=gn(n,u);if(m==="none"||m==="readonly"){for(let f of y)a.add(f);continue}for(let f of y)s(f,u);(c.deserializer||c.polymorphic||c.type)&&i.set(u,{...c.deserializer?{deserializer:c.deserializer}:{},...c.polymorphic?{polymorphic:c.polymorphic}:{},...c.type?{typeFn:c.type}:{}})}let l={accept:r,blocked:a,props:i};return t.byStrategy.set(e.namingKey,l),l}function N(n){return n!==null&&typeof n=="object"&&typeof n.then=="function"}async function H(n){for(;n.length>0;){let e=n.splice(0,n.length);await Promise.all(e)}}function X(n,e,t){if(n.length!==0){for(let o of n)o.catch(()=>{});throw n.length=0,new b(`${e} requires every serializer, deserializer and validator to be synchronous, but one returned a Promise. Use ${t} instead, or make the hook synchronous.`)}}function U(n,e,t,o,r){if(n==null||typeof n!="object")return n;if(o>t.maxDepth)throw new b(`Maximum nesting depth of ${t.maxDepth} exceeded while serializing. Raise it with the maxDepth option if this structure is legitimate.`);if(n instanceof Date)return n.toISOString();if(e.has(n))throw new b("Circular reference detected during serialization. Break the cycle with @JsonIgnore() on the back-reference, or supply a @JsonSerialize() serializer for that property.");e.add(n);try{if(Array.isArray(n)){let s=[];for(let l of n)s.push(U(l,e,t,o+1,r));return s}let a=v(n),i={};for(let s of Object.keys(n)){let l=it(a,s,t);if(l.skip)continue;let u=n[s];if(l.serializer&&u!==null&&u!==void 0){let c=bn(l.serializer).serialize(u);if(N(c)){let y=l.name;i[y]=void 0,r.push(c.then(m=>{i[y]=m}))}else i[l.name]=c}else i[l.name]=U(u,e,t,o+1,r)}return i}finally{e.delete(n)}}function k(n,e,t,o,r){if(e==null)return e;if(o>t.maxDepth)throw new b(`Maximum nesting depth of ${t.maxDepth} exceeded while deserializing. Raise it with the maxDepth option if this structure is legitimate.`);if(Array.isArray(e))return e.map(s=>k(n,s,t,o+1,r));if(typeof e!="object")return e;let a=new n,i=st(R(n),t);for(let s of Object.keys(e)){if(at.has(s)||i.blocked.has(s))continue;let l=i.accept.get(s);if(l===void 0){if(t.unknownKeys==="strip")continue;if(t.unknownKeys==="error")throw new b(`Unknown property ${JSON.stringify(s)} for ${n.name}. Allowed: ${[...i.accept.keys()].map(f=>JSON.stringify(f)).join(", ")||"(none declared)"}.`);a[s]=e[s];continue}let u=e[s],c=i.props.get(l);if(c?.deserializer){let f=bn(c.deserializer).deserialize(u);if(N(f)){let g=l;a[g]=void 0,r.push(f.then(D=>{a[g]=D}))}else a[l]=f;continue}let y=c?.polymorphic;if(y&&u!==null&&u!==void 0){let{discriminator:f,subTypes:g,onUnknown:D,fallback:V}=y,en=w=>{if(w==null||typeof w!="object")return w;let tn=g.find(K=>w[f]===K.name);if(tn)return k(tn.value,w,t,o+1,r);if(V)return k(V,w,t,o+1,r);if(D==="error")throw new b(`Unknown discriminator value ${JSON.stringify(w[f])} for property "${l}". Known values: ${g.map(K=>JSON.stringify(K.name)).join(", ")}.`);return w};a[l]=Array.isArray(u)?u.map(en):en(u);continue}let m=c?.typeFn;if(m&&u!==null&&u!==void 0){let f=m();a[l]=k(f,u,t,o+1,r);continue}a[l]=u}return a}var dn=new WeakMap;function lt(n){let e=[],t=new Set;for(let o of n){if(typeof o.message=="string"){let r=`${o.name}|${String(o.constraints)}|${o.message}|${o.each??!1}`;if(t.has(r))continue;t.add(r)}e.push(o)}return e}function ut(n){let e=dn.get(n);if(e&&e.version===T())return e.plan;let t=[];for(let[o,r]of Object.entries(n)){let a=r.access??"readwrite";t.push({key:o,constraints:lt(r.constraints),isOptional:!!r.optional,isNested:!!r.nested,redact:a==="writeonly"||a==="none",...r.condition?{condition:r.condition}:{}})}return dn.set(n,{version:T(),plan:t}),t}var pn=new WeakMap;function bn(n){let e=pn.get(n);return e||(e=new n,pn.set(n,e)),e}function fn(n,e,t){if(!(e in n)){n[e]=t;return}let o=2;for(;`${e}_${o}`in n;)o++;n[`${e}_${o}`]=t}function mn(n,e,t){let o=typeof n.message=="function"?n.message(e):n.message;return n.each&&!n.hasCustomMessage&&(o=t>=0?`each element in ${o} (failed at index ${t})`:`each element in ${o}`),o}function ct(n,e,t){for(let o=0;o0?t.children=o:delete t.children,(Object.keys(t.constraints).length>0||t.children)&&e.push(t)}return e}function B(n,e,t,o,r){let a=[];if(n==null||typeof n!="object")return a;if(t>o)throw new b(`Maximum nesting depth of ${o} exceeded while validating. Raise it with the maxDepth option if this structure is legitimate.`);if(e.has(n))return a;e.add(n);try{if(Array.isArray(n)){for(let i=0;i0&&a.push({property:`[${i}]`,value:n[i],constraints:{},children:s})}return a}for(let i of ut(v(n))){let s=i.key,l=n[s];if(i.condition&&!i.condition(n)||i.isOptional&&l==null)continue;let u={property:s,value:i.redact?yn:l,constraints:{}},c={value:l,object:n,property:s,constraints:[]},y=!1;for(let m of i.constraints){c.constraints=m.constraints||[];let f=m.each&&Array.isArray(l)?ct(m,l,c):(()=>{let g=m.validate(l,c);return N(g)?g.then(D=>({ok:D,index:-1})):{ok:g,index:-1}})();if(N(f)){y=!0;let g={value:l,object:n,property:s,constraints:m.constraints||[]};r.push(f.then(({ok:D,index:V})=>{D||(V>=0&&(g.value=l[V]),fn(u.constraints,m.name,mn(m,g,V)))}));continue}if(!f.ok){f.index>=0&&(c.value=l[f.index]);let g=mn(m,c,f.index);c.value=l,fn(u.constraints,m.name,g)}}if(i.isNested&&l!==null&&l!==void 0){let m=B(l,e,t+1,o,r);m.length>0&&(u.children=m)}(y||Object.keys(u.constraints).length>0||u.children)&&a.push(u)}return a}finally{e.delete(n)}}function wn(n){let e=x(n);return{naming:F(e.namingStrategy),namingKey:e.namingStrategy,maxDepth:e.maxDepth}}function On(n){let e=x(n);return{naming:F(e.namingStrategy),namingKey:e.namingStrategy,unknownKeys:e.unknownKeys,maxDepth:e.maxDepth}}async function M(n,e){let t=[],o=B(n,new Set,0,x(e).maxDepth,t);return t.length===0?o:(await H(t),xn(o))}function j(n,e){let t=[],o=B(n,new Set,0,x(e).maxDepth,t);return X(t,"validateSync()","validate()"),o}async function Cn(n,e){let t=await M(n,e);if(t.length>0)throw new C("Validation failed",t)}function pt(n,e){let t=j(n,e);if(t.length>0)throw new C("Validation failed",t)}async function Y(n,e){if(n==null)return n;if(x(e).validate){let r=await M(n,e);if(r.length>0)throw new C("Validation failed during serialization",r)}let t=[],o=U(n,new Set,wn(e),0,t);return await H(t),o}function Tn(n,e){if(n==null)return n;if(x(e).validate){let r=j(n,e);if(r.length>0)throw new C("Validation failed during serialization",r)}let t=[],o=U(n,new Set,wn(e),0,t);return X(t,"toPlainSync()","toPlain()"),o}async function Dn(n,e){return JSON.stringify(await Y(n,e))}function ft(n,e){return JSON.stringify(Tn(n,e))}async function P(n,e,t){let o=[],r=k(n,e,On(t),0,o);if(await H(o),x(t).validate){let a=await M(r,t);if(a.length>0)throw new C("Validation failed during deserialization",a)}return r}function Q(n,e,t){let o=[],r=k(n,e,On(t),0,o);if(X(o,"toInstanceSync()","toInstance()"),x(t).validate){let a=j(r,t);if(a.length>0)throw new C("Validation failed during deserialization",a)}return r}function Vn(n,e){if(!Array.isArray(e))throw new b(`Expected an array to map to ${n.name}[], received ${typeof e}.`)}async function nn(n,e,t){return Vn(n,e),await P(n,e,t)}function $n(n,e,t){return Vn(n,e),Q(n,e,t)}async function kn(n,e,t){return P(n,_(e),t)}function mt(n,e,t){return Q(n,_(e),t)}async function Sn(n,e,t){return nn(n,_(e),t)}function yt(n,e,t){return $n(n,_(e),t)}function _(n){try{return JSON.parse(n)}catch(e){throw new b(`Input is not valid JSON: ${e instanceof Error?e.message:String(e)}`)}}async function En(n,e,t){let o;try{o=await e.json()}catch(r){throw new b(`Request body is not valid JSON: ${r instanceof Error?r.message:String(r)}`)}return P(n,o,t)}var G=class{static toPlain=Y;static toJson=Dn;static toInstance=P;static toInstanceArray=nn;static fromJson=kn;static fromJsonArray=Sn;static fromRequest=En;static validate=M;static validateOrReject=Cn};return Rn(gt);})(); diff --git a/docs/index.html b/docs/index.html index 2f83b6e..c1a7955 100644 --- a/docs/index.html +++ b/docs/index.html @@ -136,12 +136,23 @@

Mapping

    +
  • @JsonProperty('first_name')
  • +
  • @JsonAlias(...names)
  • @JsonSerialize(cls)
  • @JsonDeserialize(cls)
  • @JsonType(() => cls)
  • @JsonPolymorphic(field, types)
+
+

Access Control

+
    +
  • @JsonIgnore()
  • +
  • @JsonReadOnly()
  • +
  • @JsonWriteOnly()
  • +
  • Naming strategies: snake_case, kebab-case, …
  • +
+

Basic Validation

    @@ -158,7 +169,9 @@
  • @Min(n), @Max(n)
  • @MinLength(n), @MaxLength(n)
  • @Email(), @IsUrl()
  • -
  • @ValidateNested()
  • +
  • @IsUUID(v?), @IsEnum(e)
  • +
  • @MinDate(d), @ArrayUnique()
  • +
  • @ValidateNested(), @ValidateIf(fn)
@@ -175,9 +188,10 @@