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
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.
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.
Installation
npm install cereale
Make sure to enable experimentalDecorators and emitDecoratorMetadata in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2025"
}
}
Quick Start
1. Define your Models
Use decorators to define how your data should be transformed and validated.
import {
IsString,
IsInt,
Min,
IsDate,
ValidateNested,
JsonSerialize,
JsonDeserialize,
JsonPolymorphic,
JsonSerializer,
JsonDeserializer
} from 'cereale';
// Custom Date Serializer
class DateSerializer implements JsonSerializer<Date, string> {
serialize(value: Date): string {
return value.toISOString().split('T')[0];
}
}
class DateDeserializer implements JsonDeserializer<string, Date> {
deserialize(value: string): Date {
return new Date(value);
}
}
abstract class Media {
@IsString()
abstract type: string;
@IsString()
title: string;
}
class Book extends Media {
type = 'book';
@IsString()
author: string;
@JsonSerialize(DateSerializer)
@JsonDeserialize(DateDeserializer)
@IsDate()
publishedAt: Date;
}
class Library {
@IsString()
name: string;
@ValidateNested({ each: true })
@JsonPolymorphic('type', [
{ value: Book, name: 'book' }
])
items: Media[];
}
2. Map JSON with Validation
Use standalone utility functions to handle the conversion process directly.
import { fromJson, toJson, JsonValidationError } 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);
}
}
}
3. Modern Web Frameworks (Request Integration)
Cereale is compatible with Fetch-based frameworks like Hono, Next.js, and Remix. Use the fromRequest async helper.
import { fromRequest, toPlain } from 'cereale';
// Hono Example
app.post('/books', async (c) => {
const book = await fromRequest(Book, c.req.raw);
return c.json(await toPlain(book));
});
API Reference
Decorators
@JsonSerialize(serializer: ClassConstructor<JsonSerializer>): Specifies a custom serializer for a property.@JsonDeserialize(deserializer: ClassConstructor<JsonDeserializer>): Specifies a custom deserializer for a property.@JsonType(typeFunction: () => ClassConstructor<any>): Explicitly sets the type for nested transformations.@JsonPolymorphic(discriminator: string, subTypes: { value: ClassConstructor<any>, name: string }[]): Configures polymorphic transformation based on a discriminator field.
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.
| Decorator | Description |
|---|---|
@IsString() |
Checks if value is a string. |
@IsNumber() |
Checks if value is a number (and not NaN). |
@IsInt() |
Checks if value is an integer. |
@IsBoolean() |
Checks if value is a boolean. |
@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. |
@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. |
@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. |
Utilities
toJson(obj: any): Validates and serializes an instance to a JSON string (ReturnsPromise<string>).toPlain(obj: any): Validates and transforms an instance to a plain object (ReturnsPromise<any>).fromJson(clazz: ClassConstructor, json: string): Parses JSON and transforms it to a validated class instance (ReturnsPromise<T>).toInstance(clazz: ClassConstructor, plain: any): Transforms a plain object to a validated class instance (ReturnsPromise<T>).fromRequest(clazz: ClassConstructor, request: Request): Extracts JSON from a FetchRequestand transforms it to a validated instance (ReturnsPromise<T>).validate(obj: any): Performs full validation on an object/instance (ReturnsPromise<ValidationError[]>).
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.
import { toInstance } from 'cereale';
@Post()
async create(@Body() body: any) {
const user = await toInstance(User, body);
return this.userService.create(user);
}
Express / Fastify
Easily integrate with traditional Node.js frameworks.
import { toInstance, toPlain } 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);
}
});
Contributing
Please see CONTRIBUTING.md for details on how to contribute to this project.
License
Cereale is licensed under the MIT License.