Aviary
Validation

Pluggable Validation

zod, valibot, or arktype from one neutral schema IR.

nestjs-codegen translates your @Body()/@Query() DTOs (class-validator decorators) and defineContract schemas into a neutral SchemaNode IR, then renders that IR with a validation adapter. zod, valibot, and arktype each ship as their own adapter package (none is bundled in core). Adding a new library is one render function away (the interface is shaped around Standard Schema).

Choosing an adapter

import { defineConfig } from '@dudousxd/nestjs-codegen';
import { zodAdapter } from '@dudousxd/nestjs-codegen-zod';

export default defineConfig({
  validation: zodAdapter,
});
import { defineConfig } from '@dudousxd/nestjs-codegen';
import { valibotAdapter } from '@dudousxd/nestjs-codegen-valibot';

export default defineConfig({
  validation: valibotAdapter,
});
import { defineConfig } from '@dudousxd/nestjs-codegen';
import { arktypeAdapter } from '@dudousxd/nestjs-codegen-arktype';

export default defineConfig({
  validation: arktypeAdapter,
});

What gets generated

Given this DTO:

class CreateUserDto {
  @IsEmail() email!: string;
  @MinLength(8) password!: string;
}

each adapter renders forms.ts in its own idiom:

import { z } from 'zod';

export const CreateBodySchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});
export type CreateBody = z.infer<typeof CreateBodySchema>;
import * as v from 'valibot';

export const CreateBodySchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});
export type CreateBody = v.InferOutput<typeof CreateBodySchema>;
import { type } from 'arktype';

export const CreateBodySchema = type({
  email: 'string.email',
  password: 'string >= 8',
});
export type CreateBody = (typeof CreateBodySchema).infer;

Decorator coverage

The class-validator → IR translator understands the common decorators: @IsString, @IsNumber/@IsInt, @IsBoolean, @IsDate, @IsEmail/@IsUrl/@IsUUID, @Matches, @MinLength/@MaxLength/@Length, @Min/@Max, @IsPositive/ @IsNegative, @IsEnum/@IsIn, @IsOptional/@IsDefined, @IsNotEmpty, @ValidateNested + @Type(() => X) (nested + arrays), @IsObject, @Allow.

Unmappable decorators are kept as a comment with a warning — the schema still compiles.

defineContract schemas are hand-written zod. They render verbatim under the zod adapter; under valibot/arktype they're skipped with a warning (use class-validator DTOs for cross-adapter forms, or the zod adapter for contracts).

Writing your own adapter

A ValidationAdapter renders the IR to source text:

import type { ValidationAdapter } from '@dudousxd/nestjs-codegen';

export const myAdapter: ValidationAdapter = {
  name: 'my-lib',
  importStatements: ({ used }) => (used ? ["import * as m from 'my-lib';"] : []),
  render(node, ctx) { /* SchemaNode → string */ return '…'; },
  renderModule(mod) { /* root + hoisted named schemas */ return { schemaText: '…', namedNestedSchemas: new Map(), warnings: [] }; },
  inferType: (constName) => `m.Infer<typeof ${constName}>`,
};

Pass the instance to validation and you're done.

On this page