Every change here answers one question: where does cereale currently fail
without saying so?
**Vite 8 / Vitest 4 drop decorators silently.** Both transform with oxc,
which does not implement the standard decorator transform and does not
report that. `vitest` prints "0 test" beside a bare SyntaxError, and
`vite build` reports success while emitting a bundle that throws on first
import. Ship the plugin that fixes it as `cereale/vite`, transforming with
esbuild and falling back to tsc — cereale depends on neither. The library's
own suite now runs through it, so it is exercised by every test.
**Legacy decorators died opaquely.** With `experimentalDecorators: true`,
still the default in most existing TypeScript projects, decorators are
invoked as (prototype, "name") and cereale raised "TypeError: Cannot convert
undefined or null to object". All decorators now resolve metadata through
one checkpoint that names the tsconfig setting instead, and reject
application to a method, getter or accessor field.
**Values JSON cannot carry were emptied.** A populated Map serialized to
{}, a Uint8Array to index-keyed noise, a bigint straight through so the
caller's own JSON.stringify threw somewhere unrelated. All now raise
JsonMappingError naming the property path and both ways out. Covers what a
@JsonSerialize serializer returns, sync or async. Circular-reference and
depth errors name the path too.
Also fixed: defineRule on a subclass with no decorators of its own wrote
the rule into its base class, because the base's metadata object is
inherited through the static prototype chain and `??=` found it non-nullish.
The README's toolchain table (tsc, esbuild, swc ✅, oxc ❌) is now executed
by a test rather than asserted, and the positioning leads with
class-validator + class-transformer, the stack cereale actually replaces,
rather than Zod, which it deliberately is not.
193 -> 249 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
Review on #4 flagged that the Symbol.metadata polyfill is a module-level side
effect while package.json declares "sideEffects": false, so a bundler is
permitted to drop the module.
Checking it narrowed the concern and corrected half of it. The decorator
transforms are not exposed: esbuild's helper is
__knownSymbol = (name, symbol) =>
(symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name)
which already falls back. The exposure was in this library's own read path,
which used `Symbol.metadata` directly. Had the symbol been absent,
`clazz[undefined]` would read a property literally named "undefined",
modelOf() would return an empty model, and every object would validate clean —
silent success, the worst failure mode a validation library can have.
The key is now resolved once into METADATA_KEY, with the same Symbol.for
fallback the transforms use, and all reads and writes go through it. The global
assignment stays for consumer emit that reads Symbol.metadata directly, and
package.json now lists metadata.js under sideEffects so bundlers keep it.
193 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
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