@fluojs/validation
v2.1.2
Published
Input-side validation decorators, mapped DTO helpers, and validation engine for Fluo.
Maintainers
Readme
@fluojs/validation
Node.js support is >=24.0.0 <27. See Node.js support and migration before upgrading.
Input-side validation decorators, mapped DTO helpers, and the validation engine for fluo.
Table of Contents
Installation
pnpm add @fluojs/validationWhen to Use
- when raw request payloads need to become validated DTO instances before reaching business logic
- when you want class-based validation rules instead of ad hoc parsing in controllers and services
- when you need metadata-preserving mapped DTO helpers such as
PickType,PartialType, andIntersectionType - when you want to attach Standard Schema validators such as Zod or Valibot through
@ValidateClass(...) - when you want stable validation issue codes and paths that can be localized explicitly through
@fluojs/i18n/validation
Quick Start
import { DefaultValidator, isDtoValidationError, IsEmail, IsString, MinLength } from '@fluojs/validation';
class CreateUserDto {
@IsEmail()
email = '';
@IsString()
@MinLength(2)
name = '';
}
const validator = new DefaultValidator();
try {
const dto = await validator.materialize(
{ email: '[email protected]', name: 'fluo' },
CreateUserDto,
);
console.log(dto instanceof CreateUserDto);
} catch (error) {
if (isDtoValidationError(error)) {
console.log(error.issues);
}
}Common Patterns
materialize() vs validate()
materialize(value, Target)builds a typed instance and validates it recursivelyvalidate(instance, Target)validates an already-created root value and may temporarily materialize plain nested@ValidateNested(...)values to run their nested DTO rules without replacing the caller's properties
validate() rejects malformed roots such as strings, arrays, null, and
undefined with a deterministic DtoValidationError before field or class rules
run. It accepts already-created target DTO instances and plain root objects so
request-pipeline binders can validate their prepared DTO payloads without scalar
coercion.
materialize() copies safe own enumerable properties from plain input objects,
applies DTO binding metadata, and recursively hydrates @ValidateNested(...)
fields. It preserves the request-pipeline contract that transports or binders own
source selection and scalar conversion before validation runs.
When materializing a plain input object, safe extra own enumerable properties are retained by default; only dangerous keys such as
__proto__, constructor, and prototype, plus inherited or non-enumerable
properties, are excluded. Pass { undeclaredProperties: 'reject' } as the third
materialize() argument to reject safe enumerable properties that are not
initialized DTO fields, validation or binding metadata fields, or binding aliases.
The policy applies recursively to plain nested DTO values and reports
UNDECLARED_PROPERTY issues instead of silently stripping input. Existing DTO
instances are not an undeclared-property boundary.
Existing nested values that are already instances of the declared nested DTO are
preserved; plain nested values are hydrated only for the affected nested field or
collection entry.
The root value passed to materialize() must already be a plain object or an
instance of the target DTO; malformed roots such as strings, arrays, and null
are rejected before the target DTO constructor or field initializers run.
Missing values and validation groups
Ordinary field validators skip both null and undefined. Add @IsDefined()
when either value must fail as a missing required field. @IsOptional() remains
useful when the optional contract should be explicit and should short-circuit the
other validators on that field.
Validation groups are not a supported execution surface. Decorator options do
not include class-validator-style groups or always, and validate() /
materialize() do not accept a group selection. Model different workflows with
separate DTOs, mapped DTO helpers, @ValidateIf(...), or explicit class-level
validation instead of assuming group-based execution.
DTO inheritance
Derived DTOs inherit field-level and class-level validation rules from their base DTO. Adding validation decorators to the derived DTO creates derived-owned metadata and does not change how the base DTO validates. The same isolation holds when extending a class returned by a mapped DTO helper.
Validation issue shape
DtoValidationError.issues is a stable DTO for request-pipeline error details. isDtoValidationError(...) validates the versioned validation owner contract and issue fields across compatible duplicate package copies instead of relying on constructor identity:
type ValidationIssue = {
code: string;
field?: string;
message: string;
source?: 'path' | 'query' | 'header' | 'cookie' | 'body';
};Nested DTOs use dot paths and collection indexes, such as address.city or
items[0].name. HTTP bindings attach source when the rule came from request
metadata; standalone validation and Standard Schema issues may leave it unset.
@fluojs/validation always emits its normal human-readable message values. Applications that need localized messages can opt into @fluojs/i18n/validation after validation fails. That integration maps source, field, and code to translation keys without changing validator execution or coupling this package to HTTP locale resolution.
Mapped DTO helpers
import { IsEmail, IsString } from '@fluojs/validation';
import { PartialType, PickType } from '@fluojs/validation/mapped-types';
class UserDto {
@IsString() name = '';
@IsEmail() email = '';
}
class EmailOnlyDto extends PickType(UserDto, ['email']) {}
class UpdateUserDto extends PartialType(UserDto) {}Mapped DTO helpers preserve field-level validation and binding metadata through
the documented subclassing pattern shown above. PickType, OmitType, and
PartialType do not carry base class-level validators onto derived DTOs because
those validators can depend on fields that were omitted or made optional.
They are publicly available only from @fluojs/validation/mapped-types; root
imports are removed without aliases. Migrate imports before upgrading. The
helpers retain their existing identity, generic behavior, metadata-copy,
subclassing, class-rule, and nested-collision policies.
When IntersectionType(...) sources declare different @ValidateNested(...)
targets for the same property, every nested rule is preserved and validated.
Plain values for that shared property remain plain during the initial hydration
so each nested target can materialize and validate the value independently.
Standard Schema support
For ValidateClass, Standard Schema adapters are expected to report invalid input
through explicit issues. Results without issues, and the existing empty
issues: [] case, are treated as successful. This is a validation-only contract:
successful transformed/defaulted output does not replace the DTO or its fields.
import { ValidateClass } from '@fluojs/validation';
import { z } from 'zod';
const UserSchema = z.object({ age: z.number().min(18) });
@ValidateClass(UserSchema)
class RestrictedUserDto {
age = 0;
}ValidateClass(...) also accepts custom class-level validators. Validate(...) attaches custom field-level validators when built-in decorators are not enough, and ValidateIf(...) short-circuits dependent validators when its predicate returns false.
Standard Schema output parsing
Scope and imports: use parseStandardSchema and StandardSchemaV1Like from
@fluojs/validation when the successful schema output is the value the caller
needs. Supply a Standard Schema v1 implementation such as Zod or Valibot as an
application dependency. No HTTP registration is needed for standalone parsing.
import { parseStandardSchema } from '@fluojs/validation';
import { z } from 'zod';
const DraftSchema = z.object({
title: z.string().trim(),
count: z.coerce.number().int().default(1),
});
const input = await parseStandardSchema(DraftSchema, { title: ' Draft ' });
// input: { title: string; count: number }
// value: { title: 'Draft', count: 1 }Inputs, defaults, and output: parseStandardSchema(schema, value) accepts
unknown input and returns Promise<Output> inferred from the schema. It awaits
sync or async validation and returns the successful value unchanged, including
transforms, defaults, and a valid { value: undefined } result. It adds no class
hydration, HTTP source selection, unknown-field policy, or implicit coercion;
the schema owns those value-level choices.
Failures: any returned issues array, including issues: [], rejects with
DtoValidationError. Issues retain messages and use normalized codes and
dot/index paths such as posts[0].title; Standard Schema issues need not carry
an HTTP source. Non-object results, non-array issues, and results with neither
issues nor a success value reject with TypeError. Exceptions thrown or
rejected by the schema implementation propagate unchanged.
Ownership and alternatives: the caller owns the schema and input and must
await completion; this helper creates no listener or disposable resource and
does not isolate mutations performed by user-supplied schema code.
ValidateClass remains validation-only, including its existing empty-issues
behavior. DefaultValidator.materialize(..., { undeclaredProperties: 'reject' })
is a separate class hydration/rejection contract, not schema output parsing or
HTTP projection. For handler output binding, use
createSchemaDto and StandardSchemaBinder;
HTTP maps parser DtoValidationError failures to 400.
Evidence: src/standard-schema.ts, the export in src/index.ts, and
src/standard-schema-output.test.ts cover the parser and compatibility boundary.
Custom field validation
Validate(callback) invokes callback(value, context). context.dto is the
containing DTO, and context.propertyKey is the decorated field key.
@IsObject() accepts only plain objects, including null-prototype records. It
rejects class instances, Date, Map, and Set values.
Network validators
@IsIP() validates IPv4 and IPv6 strings by default. Pass @IsIP('4') or
@IsIP('6') to restrict a field to one IP version, or pass
@IsIP('4_or_6') when you want to state the default "either IPv4 or IPv6"
contract explicitly while still using the same runtime behavior as @IsIP().
Nested validation
@ValidateNested(...) supports object fields, arrays, Set, and Map. Nested DTO paths use dot/index notation in validation issues, cycles are detected safely, and shared references are allowed.
Array, Set, and Map members are validated and materialized automatically; each: true is not required to enable nested collection traversal. The option remains accepted for existing DTOs.
Pass either a DTO class or a lazy constructor factory such as () => ChildDto or function resolveChildDto() { return ChildDto; } when nested types need deferred resolution.
No implicit scalar coercion
materialize() is intentionally strict. If a transport gives you '42' and your DTO expects number, the transport or binding layer must convert it first.
Numeric validators, including @IsLatitude() and @IsLongitude(), validate numeric DTO values without treating numeric strings as already-converted numbers.
@IsEnum(...) accepts declared enum values. For numeric TypeScript enums, generated
reverse-map member names are not values and are rejected.
Public API
- Validator engine:
DefaultValidator,DtoValidationError,MaterializeOptions,ValidationIssue,Validator - Core decorators:
IsString,IsNumber,IsBoolean,IsDate,IsArray,IsObject,IsEnum,IsInt,IsDefined,IsOptional,ValidateNested,ValidateIf,Validate,ValidateClass - Presence and comparison decorators:
IsEmpty,IsNotEmpty,Equals,NotEquals,IsIn,IsNotIn - String and network decorators:
IsEmail,IsUrl,IsUUID,IsIP,IsAlpha,IsAlphanumeric,IsAscii,IsBase64,IsBooleanString,IsDataURI,IsDateString,IsDecimal,IsFQDN,IsHexColor,IsHexadecimal,IsJSON,IsJWT,IsLocale,IsLowercase,IsMagnetURI,IsMimeType,IsMongoId,IsNumberString,IsPort,IsRFC3339,IsSemVer,IsUppercase,IsISO8601,Matches,Length,MinLength,MaxLength,Contains,NotContains - Number, date, geo, and locale decorators:
Min,Max,IsPositive,IsNegative,IsDivisibleBy,MinDate,MaxDate,IsLatitude,IsLongitude,IsLatLong,IsISBN,IsISSN,IsMobilePhone,IsPostalCode,IsRgbColor,IsCurrency - Array decorators:
ArrayContains,ArrayNotContains,ArrayNotEmpty,ArrayMinSize,ArrayMaxSize,ArrayUnique - Mapped DTO helpers:
PickType,OmitType,PartialType,IntersectionTypefrom@fluojs/validation/mapped-typesonly - Standard Schema contract:
StandardSchemaV1Likefor typing schemas,parseStandardSchemafor successful output parsing, andValidateClass(...)for validation-only integration - Validation flow:
materialize()for hydration + validation,validate()for validation-only checks
Related Packages
@fluojs/http: binds request data, then uses this package to validate it@fluojs/i18n: exposes@fluojs/i18n/validationfor opt-in localized validation issue messages@fluojs/serialization: shapes output DTOs on the response side@fluojs/core: provides the metadata primitives used by validation decorators
Example Sources
packages/validation/src/validation.test.tspackages/validation/src/mapped-types.test.tsexamples/realworld-api/src/users/create-user.dto.tsexamples/auth-jwt-passport/src/auth/login.dto.ts
