fast-class-transformer
v1.1.1
Published
A zero-dependency, ahead-of-time (AoT) compiled, ultra-fast alternative to class-transformer for TypeScript and NestJS.
Maintainers
Readme
fast-class-transformer
A zero-dependency, ultra-fast alternative to class-transformer for TypeScript and NestJS.
fast-class-transformer uses a JIT compilation approach: on first use of a class, it compiles a dedicated, statically shaped mapping function for that class and caches it. All subsequent calls use the cached function at near-native JavaScript speed. This avoids the per-request reflection and metadata traversal that makes class-transformer slow.
Table of contents
- Performance
- Why is it fast?
- Installation
- Drop-in replacement for class-transformer
- Decorators
- Transformation options
- NestJS integration
- Ahead-of-Time (AOT) compiler plugin
- Samples
Performance
Benchmarks run using mitata on Bun 1.3.0 (Intel i5-12500H), 100,000 iterations, JIT-warmed, do_not_optimize applied, 1,024 payload instances rotated to prevent constant propagation:
| Workload | class-transformer | fast-class-transformer | Speedup | | :--- | ---: | ---: | ---: | | Flat DTO mapping | 2.29 µs/iter | 17.08 ns/iter | 134x | | Nested DTO mapping | 3.25 µs/iter | 53.53 ns/iter | 60x | | Array mapping (100 items) | 228.78 µs/iter | 1.23 µs/iter | 186x | | Validation + mapping | 2.97 µs/iter | 45.98 ns/iter | 64x | | NestJS ValidationPipe | 6.53 µs/iter | 365.15 ns/iter | 18x |
Why is it fast?
1. V8 hidden class optimization.
V8 internally creates "hidden classes" (shapes) to optimize property access on objects. class-transformer assigns properties inside for...in loops with dynamic string keys (inst[key] = value), which degrades instances to slow dictionary mode. fast-class-transformer compiles a dedicated function per DTO that assigns properties in a fixed, declared order (inst.prop = value), preserving V8's hidden class optimizations.
2. Zero runtime reflection.
Standard class-transformer traverses metadata arrays and resolves decorator configurations on every request. fast-class-transformer evaluates decorators once at JIT compile time, generates a static JavaScript function, and caches it. All subsequent calls execute that compiled function directly.
3. Monomorphic inline caches.
class-transformer routes all DTO types through a single generic transformer function, which becomes megamorphic and kills V8's inline caches. Each class gets its own independently compiled, monomorphic function here.
4. No allocations on the hot path. Temporary metadata arrays, mapping closures, and helper objects are not allocated per-request — they are computed once during JIT compilation and closed over.
Installation
npm install fast-class-transformer reflect-metadatabun add fast-class-transformer reflect-metadataAdd to your application entry point (e.g., main.ts):
import 'reflect-metadata';And set the following in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Drop-in replacement for class-transformer
Change your imports and everything works:
// Before
import { plainToInstance, instanceToPlain, Expose, Type } from 'class-transformer';
// After
import { plainToInstance, instanceToPlain, Expose, Type } from 'fast-class-transformer';plainToInstance
Transforms a plain JavaScript object into a class instance. Respects @Expose, @Exclude, @Type, and @Transform decorators. Supports arrays.
import { plainToInstance, Expose, Type, Transform } from 'fast-class-transformer';
class Profile {
@Expose()
bio: string;
@Expose()
avatar: string;
}
class User {
@Expose()
id: number;
@Expose({ name: 'first_name' })
firstName: string;
@Exclude()
password: string;
@Expose()
@Type(() => Profile)
profile: Profile;
@Expose()
@Transform(({ value }) => value.toUpperCase())
role: string;
}
const raw = {
id: 42,
first_name: 'Jane',
password: 'secret',
profile: { bio: 'Engineer', avatar: 'avatar.png' },
role: 'admin',
};
const user = plainToInstance(User, raw);
console.log(user instanceof User); // true
console.log(user instanceof Profile); // false — profile is nested
console.log(user.profile instanceof Profile); // true
console.log(user.firstName); // 'Jane'
console.log(user.role); // 'ADMIN'
console.log(user.password); // undefined — excludedTo transform an array:
const users = plainToInstance(User, [raw1, raw2, raw3]);
// returns User[]instanceToPlain
Serializes a class instance back to a plain object, respecting name aliases and decorator configuration.
import { instanceToPlain } from 'fast-class-transformer';
const plain = instanceToPlain(user);
console.log(plain.first_name); // 'Jane' — mapped back from firstName
console.log(plain.password); // undefined — excluded
console.log(plain.role); // 'ADMIN'instanceToInstance
Deep-clones a class instance by serializing it to a plain object and re-instantiating it.
import { instanceToInstance } from 'fast-class-transformer';
const clone = instanceToInstance(user);
console.log(clone instanceof User); // true
console.log(clone === user); // false — independent deep copyDecorators
@Expose
Marks a property for inclusion during transformation and serialization. When strategy: 'excludeAll' is set (or excludeExtraneousValues: true), only @Expose()-decorated properties are processed.
class User {
@Expose()
id: number;
@Expose({ name: 'first_name' })
firstName: string;
@Expose({ groups: ['admin'] })
internalNote: string;
@Expose({ since: 2, until: 4 })
betaFeature: string;
@Expose({ toClassOnly: true })
inputOnly: string;
@Expose({ toPlainOnly: true })
outputOnly: string;
}| Option | Type | Description |
| :--- | :--- | :--- |
| name | string | Maps to/from a different field name in the raw payload. |
| groups | string[] | Only process this property when these groups are active. |
| since | number | Minimum API version (inclusive) for this property to be included. |
| until | number | Maximum API version (exclusive) for this property to be included. |
| toClassOnly | boolean | Only applied when converting plain to class (not serializing). |
| toPlainOnly | boolean | Only applied when serializing class to plain (not deserializing). |
@Exclude
Prevents a property from being included in transformations.
class User {
@Expose()
email: string;
@Exclude()
passwordHash: string;
@Exclude({ toPlainOnly: true })
internalId: string; // available in class, excluded when serialized
}| Option | Type | Description |
| :--- | :--- | :--- |
| toClassOnly | boolean | Exclude only when mapping plain to class. |
| toPlainOnly | boolean | Exclude only when serializing class to plain. |
@Type
Specifies the constructor to use when mapping nested objects or array elements. Required for nested class mapping to work correctly.
import { Expose, Type } from 'fast-class-transformer';
class Address {
@Expose() street: string;
@Expose() city: string;
}
class Order {
@Expose() id: number;
@Expose()
@Type(() => Address)
shippingAddress: Address;
@Expose()
@Type(() => Address)
billingAddresses: Address[]; // arrays are handled automatically
}Primitive constructors (Number, String, Boolean) are also supported for type coercion:
class CreateCatDto {
@Expose()
@Type(() => Number)
age: number; // coerces '42' (string from query param) → 42 (number)
}@Transform
Runs a custom function on the property value during transformation.
import { Expose, Transform } from 'fast-class-transformer';
class Article {
@Expose()
@Transform(({ value }) => value.trim().toLowerCase())
slug: string;
@Expose()
@Transform(({ value, obj }) => `${obj.baseUrl}/${value}`)
coverImageUrl: string;
@Expose()
@Transform(({ value, type }) => {
// type: 1 = plainToInstance, 2 = instanceToPlain
return type === 1 ? new Date(value) : value.toISOString();
})
publishedAt: Date;
}The transform function receives a TransformParams object:
| Property | Description |
| :--- | :--- |
| value | The current value of the property. |
| key | The property name. |
| obj | The source object being processed. |
| type | 1 for plainToInstance, 2 for instanceToPlain. |
| options | The active ClassTransformOptions passed to the call. |
Transformation options
All transformation functions (plainToInstance, instanceToPlain, instanceToInstance) accept an options object as the third argument:
plainToInstance(User, raw, {
excludeExtraneousValues: true,
groups: ['admin'],
version: 3,
});| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| groups | string[] | undefined | Active transformation groups. Only properties with matching @Expose groups are processed. |
| version | number | undefined | Active version. Filters properties by since/until version constraints. |
| excludeExtraneousValues | boolean | false | When true, only @Expose()-decorated properties are mapped. Equivalent to strategy: 'excludeAll'. |
| strategy | 'exposeAll' \| 'excludeAll' | 'exposeAll' | 'excludeAll' ignores all properties unless explicitly decorated with @Expose(). |
| exposeDefaultValues | boolean | true | Preserves class-declared default values when the input payload is missing those fields. |
| exposeUnsetFields | boolean | true | Sets missing fields explicitly to undefined on the instance to preserve the object shape. |
| enableCircularCheck | boolean | false | Detects circular references and returns undefined instead of throwing a stack overflow. |
| validate | boolean | false | Enables single-pass JIT validation using class-validator decorator rules during plainToInstance. |
NestJS integration
fast-class-transformer ships a FastValidationPipe as an optional subpath export. It does not depend on @nestjs/common at the package level — standalone users never bundle NestJS.
# @nestjs/common and class-validator are peer dependencies — install them if not already present
npm install @nestjs/common class-validatorFastValidationPipe (global drop-in)
Replace NestJS's ValidationPipe in main.ts with one line:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { FastValidationPipe } from 'fast-class-transformer/nestjs';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new FastValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
})
);
await app.listen(3000);
}
bootstrap();Existing DTOs with class-validator annotations work unchanged — no @Expose() required:
import { IsString, IsInt, Min, IsEmail } from 'class-validator';
// This DTO works with FastValidationPipe as-is.
// No @Expose() decorators needed.
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
age: number;
}| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| transform | boolean | true | Coerce incoming values to their declared types before validation. |
| whitelist | boolean | false | Strip properties not declared in the DTO. |
| forbidNonWhitelisted | boolean | false | Throw BadRequestException when undeclared properties are received. |
| groups | string[] | undefined | Active validation groups. |
| exceptionFactory | (errors: any[]) => any | BadRequestException | Custom factory for formatting validation error responses. |
@FastMap (endpoint-level)
To optimize a specific endpoint without a global pipe:
import { Controller, Post } from '@nestjs/common';
import { FastMap } from 'fast-class-transformer';
import { CreateUserDto } from './create-user.dto';
@Controller('users')
export class UsersController {
@Post()
async create(@FastMap() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}Ahead-of-Time (AOT) compiler plugin
For compile-time code generation — which produces the mapping functions at build time rather than first request — configure the TypeScript compiler plugin:
- Install
ts-patch:
npm install -D ts-patch- Add to
tsconfig.json:
{
"compilerOptions": {
"plugins": [
{ "transform": "fast-class-transformer/dist/transformer" }
]
}
}- Update build scripts:
{
"scripts": {
"build": "ts-patch build",
"start:dev": "ts-patch ts-node-dev src/main.ts"
}
}Samples
A full NestJS example application is planned — contributions welcome.
