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
2026-04-11 16:20:19 +02:00

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 (Returns Promise<string>).
  • toPlain(obj: any): Validates and transforms an instance to a plain object (Returns Promise<any>).
  • fromJson(clazz: ClassConstructor, json: string): Parses JSON and transforms it to a validated class instance (Returns Promise<T>).
  • toInstance(clazz: ClassConstructor, plain: any): Transforms a plain object to a validated class instance (Returns Promise<T>).
  • fromRequest(clazz: ClassConstructor, request: Request): Extracts JSON from a Fetch Request and transforms it to a validated instance (Returns Promise<T>).
  • validate(obj: any): Performs full validation on an object/instance (Returns Promise<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.

S
Description
Cereale is a lightweight TypeScript library that provides Spring-like decorators for JSON mapping and validation.
https://avalon-vanguard.github.io/cereale/
Readme MIT
971 KiB
Languages
TypeScript 89.6%
JavaScript 10.4%