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 typesEach 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:
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:
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
| Tag | Description | Example |
|---|---|---|
@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
| Tag | Description | Example |
|---|---|---|
@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
| Tag | Description | Example |
|---|---|---|
@minItems <n> | Minimum array length | @minItems 1 |
@maxItems <n> | Maximum array length | @maxItems 100 |
@uniqueItems | All items must be unique | @uniqueItems |
Transforms
| Tag | Description | Example |
|---|---|---|
@transform trim | Trim whitespace before validation | @transform trim |
@transform toLowerCase | Convert to lowercase before validation | @transform toLowerCase |
@transform toUpperCase | Convert to uppercase before validation | @transform toUpperCase |
Other
| Tag | Description | Example |
|---|---|---|
@default <value> | Default value for optional properties | @default "light" |
@tsgonest-ignore | Skip 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
| Format | Description | Example |
|---|---|---|
email | Email address | user@example.com |
uuid | UUID (v1-v5) | 550e8400-e29b-41d4-a716-446655440000 |
url | URL | https://example.com |
uri | URI | https://example.com/path |
ipv4 | IPv4 address | 192.168.1.1 |
ipv6 | IPv6 address | ::1 |
date-time | ISO 8601 date-time | 2026-01-15T09:30:00Z |
date | ISO 8601 date | 2026-01-15 |
time | ISO 8601 time | 09:30:00 |
duration | ISO 8601 duration | P1Y2M3D |
ID formats
| Format | Description | Example |
|---|---|---|
jwt | JSON Web Token | eyJhbGci... |
ulid | ULID | 01ARZ3NDEKTSV4RRFFQ69G5FAV |
cuid | CUID | clg1... |
cuid2 | CUID2 | itp2... |
nanoid | NanoID | V1StGXR8_Z5jdHi6B-myT |
Network formats
| Format | Description |
|---|---|
hostname | Internet hostname |
idn-hostname | Internationalized hostname |
idn-email | Internationalized email |
cidrv4 | IPv4 CIDR notation |
cidrv6 | IPv6 CIDR notation |
mac | MAC address |
Encoding formats
| Format | Description |
|---|---|
byte | Base64-encoded string |
base64url | URL-safe base64 |
hex | Hexadecimal string |
regex | Valid regular expression |
URI formats
| Format | Description |
|---|---|
uri-reference | URI reference |
uri-template | URI template (RFC 6570) |
iri | Internationalized Resource Identifier |
json-pointer | JSON Pointer (RFC 6901) |
relative-json-pointer | Relative 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:
| Transform | Effect |
|---|---|
Trim | Removes leading and trailing whitespace |
ToLowerCase | Converts to lowercase |
ToUpperCase | Converts 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"becomes123,"12.5"becomes12.5 - String to boolean:
"true"/"1"becomestrue,"false"/"0"becomesfalse
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:
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;
}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:
| Mode | Behavior |
|---|---|
| strict (default) | Rejects objects with unknown properties |
| strip | Silently removes unknown properties |
| passthrough | Allows 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 = 1Standard 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:
{
"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-validator | tsgonest (JSDoc) | tsgonest (branded types) |
|---|---|---|
@IsString() | Inferred from TS type | Inferred from TS type |
@IsEmail() | @format email | string & Email |
@MinLength(1) | @minLength 1 | string & Min<1> |
@MaxLength(255) | @maxLength 255 | string & Max<255> |
@IsNumber() | Inferred from TS type | Inferred from TS type |
@Min(0) | @minimum 0 | number & Min<0> |
@Max(100) | @maximum 100 | number & Max<100> |
@IsOptional() | name?: string | name?: string |
@IsArray() | items: string[] | items: string[] |
@IsEnum(Role) | role: Role | role: Role |
@IsUUID() | @format uuid | string & Uuid |
@Matches(/regex/) | @pattern regex | string & Pattern<"regex"> |
@IsIn(['a','b']) | status: 'a' | 'b' | status: 'a' | 'b' |
@ValidateNested() | Automatic for objects | Automatic for objects |
@Transform(...) | @transform trim | string & Trim |
The key difference: with tsgonest, your TypeScript types are the source of truth. No need to duplicate type information in decorators.