Commit Graph
2 Commits
Author SHA1 Message Date
Claude 297d3bfe77 ✨ feat!: v2 — strongly typed decorators on the TC39 standard
BREAKING CHANGE: cereale moves from legacy `experimentalDecorators` to TC39
standard decorators, which is what makes validation rules type-checked against
the fields they are attached to.

    class User {
      @IsString() name!: string;   // fine
      @IsString() age!: number;    // Type 'number' is not assignable to 'string'
    }

Legacy decorators receive (target: any, key: string) and lose the field type
entirely, so this was impossible in v1. Standard decorators receive
ClassFieldDecoratorContext<This, Value>, which carries it. Rules now checked:
scalar rules against scalar fields; { each: true } against arrays, in both
directions; @JsonType against the field's class; @JsonSerialize/@JsonDeserialize
against the field's type; @IsIn and @IsEnum against the field's value type.

17 tests invoke the real compiler to assert the wrong code stays rejected — a
guarantee nobody checks is one that quietly stops holding.

Positioning follows the capability: validated domain objects, not validated
data. The README now leads with the Zod comparison. Cereale does not infer your
type from a schema — you still write the field type and the rule — but it
guarantees the two cannot disagree, which is what class-validator never offered.

Removed
- metadata-storage.ts and its WeakMap singleton. Metadata lives on
  context.metadata now, which also removes the dual ESM/CJS double-singleton
  hazard. Inheritance merging becomes structural rather than reconstructed on
  every read, so the subclass-shadowing defect fixed by hand in 0.1.0 cannot
  reoccur by construction.
- registerDecorator, replaced by defineRule(Class, 'field', constraint).

Unchanged: the engine, options, naming strategies, access control, error
helpers, the sync API, and the performance work. 193 tests pass.

Toolchain note: standard decorators are transformed by tsc and esbuild, but not
yet by oxc. The library builds with tsc and consumers on esbuild/Vite are fine;
Vitest 4 uses oxc, so the test runner needs an esbuild transform plugin. This is
recorded in vitest.config.ts and the README, and is the reason 1.x should stay
available for oxc-based toolchains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-04 20:35:26 +00:00
Claude 6d04b43964 🐛 fix: repair eight correctness defects in the mapping and validation engines
Each fix is pinned by a regression test in src/regressions.test.ts describing the
old behaviour.

- Inheritance dropped base-class rules. A subclass re-decorating an inherited
  property registered its constraints against its own prototype, and validate()
  read only the nearest set, so everything the base declared was silently lost.
  Constraints are now merged down the whole prototype chain, base first, with
  genuinely identical rules collapsed so restating @IsString() on an override
  does not double-report. The library's own example.ts was affected: Media's
  @IsString() title had never been enforced for Book.
- Circular references exhausted the heap. serialize() recursed forever, taking
  8 GB and the process with it; it now tracks ancestors and raises a
  JsonMappingError naming the cause. Diamonds still serialize. validate() skips
  back-edges instead of recursing.
- @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 raw value is now
  preserved, with { onUnknown: 'error' } and { fallback } to choose otherwise.
- Custom @JsonSerialize serializers ran on null/undefined, crashing on any unset
  optional property. They now only see real values.
- serialize() read obj.constructor.prototype, which throws for null-prototype
  objects; both engines now agree on Object.getPrototypeOf.
- __proto__, constructor and prototype arriving in untrusted JSON were copied
  onto the instance, detaching it from its own class. They are dropped.
- The "each element in ..." prefix was glued onto caller-supplied messages, and
  two rules sharing a name overwrote each other so only one failure surfaced.

Also adds toInstanceArray/fromJsonArray, since toInstance and fromJson accept
arrays at runtime but type the result as T, and reports a non-JSON request body
in fromRequest as a JsonMappingError rather than a raw SyntaxError.

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