Serialization & Runtime

Fast JSON serializers and compile-time NestJS integration.

tsgonest generates fast, schema-aware JSON serializers alongside validators. Serialization is injected at compile time — no runtime interceptors or pipes needed.

How it works

When transforms.serialization is enabled, each companion file includes a serialize* function that converts a typed object to a JSON string using string concatenation with known property names — faster than JSON.stringify for typical API payloads.

The generated serializers know the exact shape of your types at build time, so they skip the runtime overhead of key enumeration, type detection, and circular reference handling.

Performance

Benchmarked on Apple Silicon, Node.js 25:

ScenarioSpeedup vs JSON.stringify
Simple DTO (5 fields, clean strings)~1.4x faster
Complex DTO (nested + arrays)~1x (comparable)
Validation vs class-validator25-84x faster

Serialization wins on simple, flat objects — the vast majority of API DTOs. For deeply nested objects, V8's native JSON.stringify is highly optimized and performs comparably.

Supported types

TypeStrategy
stringFast-path scan, falls back to JSON.stringify only when special characters are found
number, bigint, boolean, nullDirect concatenation
ObjectsProperty-by-property concatenation
ArraysLoop-based string accumulation
TuplesElement-by-element serialization
OptionalsConditional inclusion with separator tracking
Discriminated unionsO(1) switch on discriminant, per-branch serialization
Nested objectsRecursive serialization calls
Complex unionsFalls back to JSON.stringify

Compile-time NestJS integration

tsgonest injects validation and serialization directly into your controller JavaScript at build time. No runtime pipes, interceptors, or configuration needed.

What gets injected

  1. Validation: @Body(), @Query(), @Param(), and @Headers() parameters are wrapped with assert*() calls. Individual @Query('name') and @Param('name') scalars get inline coercion (string to number/boolean).
  2. Serialization: Return values are wrapped with serializer calls that use the generated fast serializers.
src/user/user.controller.ts (your source)
@Controller('users')
export class UserController {
  @Post()
  create(@Body() dto: CreateUserDto): UserResponse {
    return this.userService.create(dto);
  }

  @Get()
  findAll(): UserResponse[] {
    return this.userService.findAll();
  }
}
dist/user/user.controller.js (compiled output)
import { assertCreateUserDto } from './user.dto.CreateUserDto.tsgonest.js';
import { serializeUserResponse } from './user.dto.UserResponse.tsgonest.js';

class UserController {
  create(dto) {
    dto = assertCreateUserDto(dto);
    return serializeUserResponse(this.userService.create(dto));
  }

  findAll() {
    return this.userService.findAll().map(serializeUserResponse);
  }
}

Zero runtime setup

Your main.ts stays clean:

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Array responses

Array responses are handled automatically. When the return type is T[], the serializer wraps the return with .map(serialize*) instead of a single call.


@tsgonest/runtime

The @tsgonest/runtime package provides configuration helpers and error types:

npm install tsgonest

Exports

import {
  // Config helper
  defineConfig,
  TsgonestConfig,

  // Errors
  TsgonestValidationError,
  ValidationErrorDetail,

  // FormData (multipart/form-data)
  FormDataBody,
  FormDataInterceptor,
  TSGONEST_FORM_DATA_FACTORY,

  // SSE (Server-Sent Events)
  EventStream,          // @EventStream() method decorator
  SseEvent,             // SseEvent<E, T> typed event interface (type-only)
  SseEvents,            // SseEvents<M> discriminated union helper (type-only)
  TsgonestSseInterceptor, // Iterator -> Observable bridge (auto-injected)
} from '@tsgonest/runtime';

TsgonestValidationError

When validation fails, the injected assert*() call throws a TsgonestValidationError:

import { TsgonestValidationError } from '@tsgonest/runtime';

interface ValidationErrorDetail {
  path: string;      // e.g., "input.email"
  expected: string;  // e.g., "string (email)"
  received: string;  // e.g., "number"
}

NestJS catches the thrown error and returns a 400 Bad Request response:

{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    {
      "property": "input.email",
      "constraints": {
        "tsgonest": "expected string (email), received string"
      }
    }
  ]
}

On this page