⚡ perf: memoize per-class plans; add depth guard and per-index each reporting
Profiling the validator showed roughly half of all validation time re-deriving
answers that cannot change — collectConstraints 22%, getOwnMetadata 12%,
getMetadataChain 9%, getProperties 4%, getMetadata 3%, plus 8% GC from the
allocation churn. The constraint predicates themselves were 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, which were being
constructed fresh for every property of every object. MetadataStorage carries a
version counter that invalidates the caches when metadata is written, so
registerDecorator after first use still works — covered by a test.
Measured against JSON.parse + JSON.stringify as a fixed reference:
validate (50 orders) 221.6 us -> 49.6 us 4.5x
validate (10 orders) 47.8 us -> 12.9 us 3.7x
toInstance (50 orders) 255.1 us -> 74.0 us 3.4x
toInstance (10 orders) 64.6 us -> 19.0 us 3.4x
toPlain (50 orders) 294.4 us -> 95.3 us 3.1x
Reliability, in the same pass:
- maxDepth option (default 64) on every mapping function, on validate(), and on
configure(). All three engines recurse, so a payload nested thousands of levels
deep could exhaust the call stack. Cycles were already handled; legitimate deep
nesting was not bounded.
- each: true failures now name the element that 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 strings stay verbatim.
150 tests (up from 136), all green on the existing suite unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
@@ -203,6 +203,7 @@ defaults for the whole application. Per-call options win.
|
||||
| `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
|
||||
@@ -293,7 +294,7 @@ Write your own with `registerDecorator({ name, target, propertyName, validator }
|
||||
- `toInstance(clazz, plain, options?)`: Transforms a plain object to a validated class instance (`Promise<T>`).
|
||||
- `toInstanceArray(clazz, plain, options?)`: Same, for an array (`Promise<T[]>`).
|
||||
- `fromRequest(clazz, request, options?)`: Extracts JSON from a Fetch `Request` (`Promise<T>`).
|
||||
- `validate(obj)`: Full validation, returning `Promise<ValidationError[]>`.
|
||||
- `validate(obj, options?)`: Full validation, returning `Promise<ValidationError[]>`.
|
||||
- `validateOrReject(obj)`: As above, but throws `JsonValidationError`.
|
||||
- `configure(options)` / `getConfig()` / `resetConfig()`: Library-wide defaults.
|
||||
|
||||
@@ -350,6 +351,26 @@ app.post('/user', async (req, res) => {
|
||||
});
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
Indicative throughput for a customer record with a nested address and 10 orders, measured
|
||||
against `JSON.parse` + `JSON.stringify` (5.9 us) on the same machine:
|
||||
|
||||
| Operation | Time |
|
||||
| --- | --- |
|
||||
| `toInstance` (deserialize + validate) | ~19 us |
|
||||
| `toInstance` with `{ validate: false }` | ~6 us |
|
||||
| `validate` on an existing instance | ~13 us |
|
||||
| `toPlain` (validate + serialize) | ~23 us |
|
||||
|
||||
If you validate at the edge and map internally afterwards, `{ validate: false }` skips the
|
||||
dominant cost.
|
||||
|
||||
## Notes and Limitations
|
||||
|
||||
- **Circular references** are rejected during serialization with a `JsonMappingError`. Break
|
||||
|
||||
Reference in New Issue
Block a user