Validation

Generated companion validators — JSDoc tags, branded types, constraints, formats, transforms, coercion, custom validators, and Standard Schema.

When transforms.validation is enabled in your config, tsgonest generates companion files with validate and assert functions for every DTO type in your project. These validators are generated at build time from static type analysis — no runtime reflection required.

Companion files

For each type, tsgonest generates a companion file alongside the compiled JavaScript:

dist/
  user.dto.js                             # tsgo output
  user.dto.CreateUserDto.tsgonest.js      # companion
  user.dto.CreateUserDto.tsgonest.d.ts    # companion types

Each companion exports four functions:

// validate — returns a result object, never throws
export function validateCreateUserDto(input: unknown): {
  success: boolean;
  data?: CreateUserDto;
  errors?: Array<{ path: string; expected: string; received: string }>;
};

// assert — throws TsgonestValidationError on failure, returns validated data
export function assertCreateUserDto(input: unknown): CreateUserDto;

// serialize — fast JSON serialization (see Serialization docs)
export function serializeCreateUserDto(input: CreateUserDto): string;

// schema — Standard Schema v1 wrapper (see Standard Schema section)
export function schemaCreateUserDto(): StandardSchemaV1<CreateUserDto>;

validate vs assert

Use validate when you want to handle errors yourself:

const result = validateCreateUserDto(input);

if (!result.success) {
  console.log(result.errors);
  // [
  //   { path: "input.email", expected: "string (email)", received: "string" },
  //   { path: "input.age", expected: "number", received: "string" }
  // ]
}

Use assert when you want to throw on invalid input (this is what TsgonestValidationPipe uses):

try {
  const dto = assertCreateUserDto(input);
  // dto is guaranteed to be valid CreateUserDto
} catch (err) {
  // TsgonestValidationError with structured error details
}

Defining constraints

tsgonest supports two approaches for defining validation constraints on your types. Both produce identical generated code.

Approach 1: JSDoc tags (zero dependencies)

Annotate your type properties with JSDoc tags. No imports needed:

src/user.dto.ts
export interface CreateUserDto {
  /**
   * @minLength 1
   * @maxLength 255
   * @transform trim
   */
  name: string;

  /** @format email */
  email: string;

  /**
   * @minimum 0
   * @maximum 150
   */
  age: number;

  /** @pattern ^https?:\/\/ */
  website?: string;
}

Approach 2: Branded phantom types (type-safe)

Use @tsgonest/types for IDE autocomplete and compile-time constraint checking:

src/user.dto.ts
import { Min, Max, Email, Trim, Pattern } from '@tsgonest/types';

export interface CreateUserDto {
  name: string & Trim & Min<1> & Max<255>;
  email: string & Email;
  age: number & Min<0> & Max<150>;
  website?: string & Pattern<"^https?:\\/\\/">;
}

Branded types are zero-runtime — they add phantom properties at the type level only. The @tsgonest/types package produces no JavaScript output. tsgonest reads the phantom property names during compilation to generate the appropriate validators.

Mixing approaches

You can mix JSDoc tags and branded types in the same project, even on the same type:

import { Email } from '@tsgonest/types';

export interface MixedDto {
  /** @minLength 1 */
  name: string;
  email: string & Email; // branded type
}

Supported JSDoc tags

String constraints

TagDescriptionExample
@minLength <n>Minimum string length@minLength 1
@maxLength <n>Maximum string length@maxLength 255
@pattern <regex>Regex pattern match@pattern ^[a-z]+$
@format <name>String format validation@format email

Numeric constraints

TagDescriptionExample
@minimum <n>Minimum value (inclusive)@minimum 0
@maximum <n>Maximum value (inclusive)@maximum 100
@exclusiveMinimum <n>Exclusive minimum (value > n)@exclusiveMinimum 0
@exclusiveMaximum <n>Exclusive maximum (value < n)@exclusiveMaximum 100
@multipleOf <n>Must be a multiple of n@multipleOf 0.01
@type <name>Numeric type constraint@type int32

Array constraints

TagDescriptionExample
@minItems <n>Minimum array length@minItems 1
@maxItems <n>Maximum array length@maxItems 100
@uniqueItemsAll items must be unique@uniqueItems

Transforms

TagDescriptionExample
@transform trimTrim whitespace before validation@transform trim
@transform toLowerCaseConvert to lowercase before validation@transform toLowerCase
@transform toUpperCaseConvert to uppercase before validation@transform toUpperCase

Other

TagDescriptionExample
@default <value>Default value for optional properties@default "light"
@tsgonest-ignoreSkip companion generation for this type@tsgonest-ignore

String formats

tsgonest supports 30+ string format validations. Use them via @format <name> JSDoc or the Format<"name"> branded type:

Common formats

FormatDescriptionExample
emailEmail addressuser@example.com
uuidUUID (v1-v5)550e8400-e29b-41d4-a716-446655440000
urlURLhttps://example.com
uriURIhttps://example.com/path
ipv4IPv4 address192.168.1.1
ipv6IPv6 address::1
date-timeISO 8601 date-time2026-01-15T09:30:00Z
dateISO 8601 date2026-01-15
timeISO 8601 time09:30:00
durationISO 8601 durationP1Y2M3D

ID formats

FormatDescriptionExample
jwtJSON Web TokeneyJhbGci...
ulidULID01ARZ3NDEKTSV4RRFFQ69G5FAV
cuidCUIDclg1...
cuid2CUID2itp2...
nanoidNanoIDV1StGXR8_Z5jdHi6B-myT

Network formats

FormatDescription
hostnameInternet hostname
idn-hostnameInternationalized hostname
idn-emailInternationalized email
cidrv4IPv4 CIDR notation
cidrv6IPv6 CIDR notation
macMAC address

Encoding formats

FormatDescription
byteBase64-encoded string
base64urlURL-safe base64
hexHexadecimal string
regexValid regular expression

URI formats

FormatDescription
uri-referenceURI reference
uri-templateURI template (RFC 6570)
iriInternationalized Resource Identifier
json-pointerJSON Pointer (RFC 6901)
relative-json-pointerRelative JSON Pointer

Example with formats

import { Email, Uuid, IPv4, DateTime, Jwt } from '@tsgonest/types';

export interface UserSessionDto {
  userId: string & Uuid;
  email: string & Email;
  ipAddress: string & IPv4;
  loginAt: string & DateTime;
  token: string & Jwt;
}

Transforms

Transforms are applied before validation. They modify the input value, then the modified value is validated against the constraints.

import { Trim, ToLowerCase, Min, Email } from '@tsgonest/types';

export interface SignupDto {
  // "  John  " -> "John", then validate minLength >= 1
  name: string & Trim & Min<1>;

  // "User@Example.COM" -> "user@example.com", then validate email format
  email: string & ToLowerCase & Email;
}

Available transforms:

TransformEffect
TrimRemoves leading and trailing whitespace
ToLowerCaseConverts to lowercase
ToUpperCaseConverts to uppercase

Coercion

For query parameters and path parameters that arrive as strings, use Coerce to automatically convert to the target type before validation:

import { Coerce, Min, Max } from '@tsgonest/types';

export interface PaginationQuery {
  page: number & Coerce & Min<1>;     // "3" -> 3
  limit: number & Coerce & Max<100>;  // "50" -> 50
  active: boolean & Coerce;           // "true" -> true
}

Coercion rules:

  • String to number: "123" becomes 123, "12.5" becomes 12.5
  • String to boolean: "true" / "1" becomes true, "false" / "0" becomes false

Default values

Assign default values to optional properties. The default is applied when the value is undefined:

import { Default } from '@tsgonest/types';

export interface PreferencesDto {
  theme?: string & Default<"light">;
  pageSize?: number & Default<20>;
  notifications?: boolean & Default<true>;
}

Or with JSDoc:

export interface PreferencesDto {
  /** @default "light" */
  theme?: string;

  /** @default 20 */
  pageSize?: number;
}

Custom error messages

Per-constraint errors (branded types)

Every branded type constraint supports an extended form with a custom error message:

import { Min, Max, Format, Pattern } from '@tsgonest/types';

export interface StrictDto {
  age: number & Min<{ value: 0, error: "Age cannot be negative" }>
              & Max<{ value: 150, error: "Age must be realistic" }>;

  email: string & Format<{ type: "email", error: "Must be a valid email address" }>;

  slug: string & Pattern<{ value: "^[a-z0-9-]+$", error: "Slug must be lowercase alphanumeric with hyphens" }>;
}

Global error messages

Use Error<"message"> to set a fallback message for all validation failures on a property. Per-constraint errors take precedence:

import { Email, Error } from '@tsgonest/types';

export interface ContactDto {
  email: string & Email & Error<"Invalid email address">;
}

Custom validators

Reference a predicate function to add custom validation logic. tsgonest resolves the function's source file and emits an import in the generated validator:

src/validators/credit-card.ts
export function isValidCard(value: string): boolean {
  // Luhn algorithm implementation
  let sum = 0;
  let isEven = false;
  for (let i = value.length - 1; i >= 0; i--) {
    let digit = parseInt(value[i], 10);
    if (isEven) {
      digit *= 2;
      if (digit > 9) digit -= 9;
    }
    sum += digit;
    isEven = !isEven;
  }
  return sum % 10 === 0;
}
src/payment/payment.dto.ts
import { Validate } from '@tsgonest/types';
import { isValidCard } from '../validators/credit-card';

export interface PaymentDto {
  cardNumber: string & Validate<typeof isValidCard>;
}

With a custom error message:

export interface PaymentDto {
  cardNumber: string & Validate<{
    fn: typeof isValidCard,
    error: "Invalid credit card number"
  }>;
}

The generated companion will import isValidCard from the resolved path and call it during validation.


Complex types

Nested objects

tsgonest recursively walks nested object types:

import { Min, Max, Email } from '@tsgonest/types';

export interface AddressDto {
  street: string & Min<1>;
  city: string & Min<1>;
  zipCode: string & Pattern<"^\\d{5}$">;
}

export interface CreateUserDto {
  name: string & Min<1> & Max<255>;
  email: string & Email;
  address: AddressDto; // nested validation is generated automatically
}

Optional properties

Optional properties are handled correctly — undefined passes validation, but if a value is present it must satisfy the constraints:

export interface UpdateUserDto {
  name?: string & Min<1> & Max<255>;
  email?: string & Email;
  age?: number & Min<0>;
}

Arrays

Array validation includes both array-level and element-level constraints:

import { MinItems, MaxItems, Unique, Min, Email } from '@tsgonest/types';

export interface BulkInviteDto {
  emails: (string & Email)[] & MinItems<1> & MaxItems<100> & Unique;
}

export interface OrderDto {
  items: OrderItemDto[] & MinItems<1>;
}

export interface OrderItemDto {
  productId: string;
  quantity: number & Min<1>;
}

Unions and discriminated unions

tsgonest supports union types. For discriminated unions, it generates optimized O(1) switch-based validation instead of O(n) try-each:

// Simple union
export interface FlexibleDto {
  value: string | number;
}

// Discriminated union — optimized with switch statement
export interface CatDto {
  type: 'cat';
  meows: boolean;
}

export interface DogDto {
  type: 'dog';
  barks: boolean;
}

export type PetDto = CatDto | DogDto;

The generated validator for PetDto will use a switch on the type property for O(1) dispatch.

Enums

TypeScript enums are supported:

export enum Role {
  Admin = 'admin',
  User = 'user',
  Guest = 'guest',
}

export interface CreateUserDto {
  name: string;
  role: Role;
}

Tuples

export interface CoordinateDto {
  position: [number, number]; // [latitude, longitude]
}

Object strictness

tsgonest supports three modes for handling extra properties on objects:

ModeBehavior
strict (default)Rejects objects with unknown properties
stripSilently removes unknown properties
passthroughAllows unknown properties through

Standard Schema

Every companion exports a schema* function that returns a Standard Schema v1 wrapper. This provides interoperability with 60+ frameworks and libraries:

import { schemaCreateUserDto } from '../dist/user.dto.CreateUserDto.tsgonest';

// Works with any Standard Schema consumer
const schema = schemaCreateUserDto();

// schema.~standard.validate(input)
// schema.~standard.vendor = "tsgonest"
// schema.~standard.version = 1

Standard Schema is supported by frameworks like:

  • Conform, Formily, React Hook Form
  • tRPC, Hono, Elysia
  • AI SDK (Vercel)
  • And many more

Ignoring types

Skip all companion generation

/**
 * @tsgonest-ignore
 */
export interface InternalDto {
  secret: string;
}

Exclude via config

Use the transforms.exclude config to skip specific types by name pattern:

tsgonest.config.json
{
  "transforms": {
    "exclude": ["LegacyDto", "Internal*", "*Response"]
  }
}

Controller classes

Classes decorated with @Controller() are automatically detected and skipped for companion generation. Only DTO and response types get companions.


Migration from class-validator

class-validatortsgonest (JSDoc)tsgonest (branded types)
@IsString()Inferred from TS typeInferred from TS type
@IsEmail()@format emailstring & Email
@MinLength(1)@minLength 1string & Min<1>
@MaxLength(255)@maxLength 255string & Max<255>
@IsNumber()Inferred from TS typeInferred from TS type
@Min(0)@minimum 0number & Min<0>
@Max(100)@maximum 100number & Max<100>
@IsOptional()name?: stringname?: string
@IsArray()items: string[]items: string[]
@IsEnum(Role)role: Rolerole: Role
@IsUUID()@format uuidstring & Uuid
@Matches(/regex/)@pattern regexstring & Pattern<"regex">
@IsIn(['a','b'])status: 'a' | 'b'status: 'a' | 'b'
@ValidateNested()Automatic for objectsAutomatic for objects
@Transform(...)@transform trimstring & Trim

The key difference: with tsgonest, your TypeScript types are the source of truth. No need to duplicate type information in decorators.

On this page