@fluojs/serialization
v1.0.4
Published
Class-based response serialization and output shaping interceptors for Fluo.
Maintainers
Readme
@fluojs/serialization
Class-based response serialization and output shaping for fluo with decorator-aware recursive object walking.
Table of Contents
Installation
pnpm add @fluojs/serializationWhen to Use
- when you need output DTOs to expose only a controlled subset of fields
- when sensitive values such as password hashes or internal identifiers must never leave the response boundary
- when response data needs lightweight synchronous transforms during serialization
- when you want an HTTP interceptor to apply the same serialization rules automatically
Quick Start
import { Exclude, Expose, Transform, serialize } from '@fluojs/serialization';
class UserEntity {
@Expose()
id = '';
@Expose()
@Transform((value) => String(value).toUpperCase())
username = '';
@Exclude()
passwordHash = '';
}
const user = Object.assign(new UserEntity(), {
id: '1',
username: 'fluo',
passwordHash: 'secret',
});
console.log(serialize(user));
// { id: '1', username: 'FLUO' }Common Patterns
Expose-only output DTOs
import { Expose } from '@fluojs/serialization';
@Expose({ excludeExtraneous: true })
class SecureDto {
@Expose()
publicData = 'visible';
internalData = 'hidden';
}Value transforms
import { Transform } from '@fluojs/serialization';
class ProductDto {
@Transform((price) => `$${Number(price).toFixed(2)}`)
price = 0;
}When the same field is decorated in a base class and a derived class, transforms run in declaration order from base to derived.
HTTP response shaping with an interceptor
import { Controller, Get, UseInterceptors } from '@fluojs/http';
import { SerializerInterceptor } from '@fluojs/serialization';
@Controller('/users')
@UseInterceptors(SerializerInterceptor)
class UsersController {
@Get('/')
findAll() {
return [new UserEntity()];
}
}SerializerInterceptor only serializes values that still belong to the normal HTTP response writer. If a handler or response helper commits RequestContext.response directly, such as an SSE stream, the interceptor returns that handler-owned value unchanged so the request pipeline preserves response ownership.
Cycle-safe serialization
The serializer cuts active cyclic references safely instead of recursing forever, so complex object graphs can still be turned into plain response-shaped objects without unbounded recursion. The same reference tracker is shared across class instances, plain objects, arrays, and mixed object-array graphs. Self-referential arrays and object-array cycles are cut at the active back edge, while completed shared references are reused in the serialized graph rather than dropped: if two sibling fields, or an object field and an array entry, point at the same source object, both serialized fields point at the same serialized object. Only a value that is encountered again while it is already being serialized is cut to undefined.
Inherited decorator contracts
Serialization metadata declared on a base class is inherited by derived DTOs. @Expose(), @Exclude(), and @Transform() rules applied to shared base fields still take effect when you serialize subclass instances.
Class-level excludeExtraneous also follows normal inheritance. A derived class with @Expose() and no options keeps the nearest inherited setting, so an expose-only base DTO remains expose-only in subclasses. Use @Expose({ excludeExtraneous: false }) on the derived class only when you intentionally want to re-enable ordinary enumerable fields while still honoring inherited field-level @Exclude() metadata.
Undecorated class instances are still traversed recursively, so decorated nested descendants are respected even when the parent object has no serialization metadata.
Plain-object safety
serialize() treats plain objects and null-prototype records as data containers, not decorated class instances. Enumerable symbol keys are serialized, own __proto__, constructor, and prototype keys are treated as data rather than prototype mutations, and objects with custom or unsafe constructor fields are walked safely without throwing.
Non-JSON leaf values
serialize() applies decorator metadata and recursively walks arrays/plain objects, but it does not coerce every leaf into strict JSON types. Opaque built-ins such as Date, Map, Set, URL, URLSearchParams, RegExp, Error, ArrayBuffer, typed arrays, WeakMap, WeakSet, and Promise pass through unchanged instead of being flattened as DTO-like class instances. Values such as bigint, functions, and symbols can also pass through unchanged unless you normalize them with @Transform(...) or before writing the final HTTP response.
Public API Overview
- Decorators:
Expose,Exclude,Transform - Engine:
serialize(value)recursively walks class instances, arrays, plain objects, and mixed graphs while preserving opaque built-ins and non-JSON leaf values unless you transform them - HTTP integration:
SerializerInterceptorserializes uncommitted handler results and returns handler-owned values unchanged after the response has already been committed - Types:
TransformFunctionis exported from the root entrypoint for callbacks passed toTransform(...)
Expose can be applied to classes and fields. Exclude and Transform apply to fields.
Related Packages
@fluojs/http: appliesSerializerInterceptorto HTTP handlers@fluojs/validation: handles input-side DTO materialization and validation
Example Sources
packages/serialization/src/serialize.test.tspackages/serialization/src/serializer-interceptor.test.ts
