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 every rule that implies a type is checked against the field it is attached to, at compile time.

0 runtime dependencies 68 decorators TC39 standard decorators ESM + CJS Node ≥20
order.ts
class Order {
  @JsonProperty('order_ref')
  @IsString() @Matches(/^[A-Z]-\d+$/)
  ref!: string;

  @IsInt() @Min(1)
  quantity!: number;

  @IsString()
  placedAt!: Date;

  total(): number { return this.quantity * 9.99; }
}

const order = fromJsonSync(Order, body);  // a real Order
order.total();                            // methods intact

ts(1240) Unable to resolve signature of property decorator when called as an expression.
  β€¦  Type 'Date' is not assignable to type 'string'.

That is the compiler, not cereale β€” and it is the real thing: the snippet above is compiled by tsc when this page is built, and the build fails if it ever stops being an error.

The guarantee

Your rules and your types cannot disagree

A legacy decorator is typed (prototype, propertyName) => void. Its signature carries nothing about the field's declared type, so a rule that does not fit the field still compiles. A standard decorator is handed a ClassFieldDecoratorContext<This, Value>, which does carry it. cereale uses it.

compiles, then fails at runtime class-validator

with legacy decorators
class User {
  @IsString()
  age: number;      // accepted by the compiler
}

// ...discovered in production, on a payload
// that happened to carry the wrong shape.

rejected before it runs cereale

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

// ...caught by your editor, by tsc, and by
// CI. It never gets as far as a payload.

Scalars against scalars

@Min(1) on a string is a compile error, as is @MinLength(2) on a number.

each against arrays

@IsString({ each: true }) demands a string[]; a bare @IsString() on one is rejected.

Classes against classes

@JsonType(() => Address), @JsonSerialize and @IsEnum(E) are all checked against the field.

What you get back

An instance of your class, not a shape that resembles it

Nested objects and arrays you declare with @JsonType, and subtypes you declare with @JsonPolymorphic, come back as real classes β€” so the behaviour you attached to your model survives the trip through JSON. What you do not declare, cereale leaves alone; it infers nothing.

catalogue.ts
class Media {
  @IsString() title!: string;
}
class Movie extends Media {
  @IsInt() @Min(1) duration!: number;
  hours() { return this.duration / 60; }
}
class Song extends Media {
  @IsString() artist!: string;
}

class Playlist {
  @JsonPolymorphic<Media>('type', [
    { value: Movie, name: 'movie' },
    { value: Song,  name: 'song'  },
  ])
  @ValidateNested({ each: true })
  items!: Media[];
}
what comes out
const list = fromJsonSync(Playlist, body);
const first = list.items[0];

first instanceof Movie   // true

if (first instanceof Movie) {
  first.hours();         // 2.46… β€” narrowing
}                        // works, because it
                         // really is one.

// Base-class rules reach subclasses, and a
// subclass adds to them rather than
// replacing them.

Playground

The real library, running here

This page bundles cereale itself and compiles what you type with standard decorators. Edit anything and run it.

playground.ts
output

      

The compiler here strips types and lowers decorators; it does not type-check. The compile-time guarantee above is what your editor and tsc give you β€” a browser cannot demonstrate an error that stops a build.

Diagnosability

Nothing fails quietly

The 0.3.0 release exists because of this. A mapping layer that loses your data and reports success is worse than one that stops, so every silent failure found in the engine was turned into an error that names the cause and the way out.

A Map used to become {}

Along with Set, RegExp, Error, typed arrays, bigint, symbols and functions β€” each with its own version of the same silence.

A misconfigured compiler used to throw from inside

TypeError: Cannot convert undefined or null to object, which names neither the cause nor the one-line fix.

A build used to succeed while emitting nothing that runs

Vite 8 passes decorator syntax straight through. The bundle builds; the first import throws.

what you get instead
JsonMappingError: lines[1].tags[0] is a Set, which cannot be serialized to JSON.
Give the property a @JsonSerialize() serializer that converts it, or drop it from
the output with @JsonIgnore().

TypeError: cereale needs TC39 standard decorators, but the compiler emitted legacy
ones. Set "experimentalDecorators": false in tsconfig.json (and drop
"emitDecoratorMetadata").

JsonMappingError: Circular reference detected during serialization at child.parent.
Break the cycle with @JsonIgnore() on the back-reference, or supply a
@JsonSerialize() serializer for that property.

Before you install

The toolchain cost, stated plainly

cereale reads the metadata that only a standard decorator transform emits, so which compiler you use decides whether it works at all. This table is executed by a test on every CI run, not asserted here.

Transformer support for TC39 standard decorators
TransformerWorksSetting
tscβœ“experimentalDecorators: false, target: ES2022+
esbuildβœ“the same settings via tsconfigRaw
swcβœ“jsc.transform.decoratorVersion: "2022-03"
oxcβœ—used by Vite 8 and Vitest 4 β€” see below
On Vite 8 or Vitest 4? oxc leaves decorator syntax in the output and reports nothing: vitest prints 0 test next to a bare SyntaxError, and vite build reports success while emitting a bundle that throws on first import. cereale ships the plugin that fixes it.
vite.config.ts
import { standardDecorators } from 'cereale/vite';

export default defineConfig({
  plugins: [standardDecorators()],
});

It transforms with esbuild, falling back to the TypeScript compiler β€” cereale depends on neither. Nothing in it is specific to cereale; it can be deleted once oxc implements the transform.

Getting started

Two settings, and one caveat

tsconfig.json what the compiler needs

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ESNext", "ESNext.Decorators"],
    "experimentalDecorators": false
  }
}

No reflect-metadata, and emitDecoratorMetadata is never read. If experimentalDecorators is on, cereale says so by name instead of failing somewhere inside itself.

not on npm yet installing it today

shell
git clone https://github.com/Avalon-Vanguard/cereale
cd cereale
npm install && npm run build
npm pack                    # β†’ cereale-0.3.0.tgz

# then, from your own project
npm install ../cereale/cereale-0.3.0.tgz

npm install cereale does not resolve to this library β€” the name is unclaimed on the registry. Installing straight from GitHub will not work either: the build output is not committed, so the package would arrive without its dist/.

Reference

Everything cereale exports

Every rule that implies a type is checked against the field it decorates β€” a few deliberately do not, because they fit any field (@IsDefined) or because the constraint is negative and narrowing it would be backwards (@IsNotIn). All of them take { each: true } to run per element of an array, and a message, reported verbatim.

Honest limits

What cereale is not

It does not infer types from a schema

You write the field type and the rule; cereale guarantees they agree. If you want the type derived from a schema, that is Zod's design, and Zod is the right tool for it.

It cannot coexist with legacy decorators

The two decorator systems are a whole-program setting. A project that still needs experimentalDecorators for another library cannot use cereale yet.

Rules live on the class, not on the data

validate() on a plain object reports nothing. Validate the instance you get back from toInstance.

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.

abstract and accessor fields cannot be decorated

Standard decorators do not apply to abstract members, and an accessor field keeps its value in a private slot that mapping cannot reach. Both are errors rather than silent no-ops.

It is not on npm yet

0.3.0 lives in the repository. npm install cereale does not resolve to this library β€” build it from source until it is published.