Migrate from class-validator

Migrating from class-validator to Valibot is a bigger paradigm shift than for other libraries, since you are switching from decorated classes to schemas. In return, it removes a lot of boilerplate. You no longer need experimental decorators, reflect-metadata or class-transformer, and your schemas validate plain data directly, with the TypeScript type inferred automatically. The following guide will help you migrate step by step.

Replace classes with schemas

Each decorated class becomes a schema, and each decorator becomes a schema function or an action within a pipeline. Since the class no longer exists, you infer the TypeScript type from the schema with InferOutput.

// Change this
import { IsEmail, IsInt, IsString, Min } from 'class-validator';

class User {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(0)
  age: number;
}

// To this
import * as v from 'valibot';

const UserSchema = v.object({
  name: v.string(),
  email: v.pipe(v.string(), v.email()),
  age: v.pipe(v.number(), v.integer(), v.minValue(0)),
});

type User = v.InferOutput<typeof UserSchema>;

Restructure code

With class-validator, you first convert plain data to a class instance, for example with class-transformer's plainToInstance, and then validate it. With Valibot, you validate plain data directly. The validate method, which resolves to an array of validation errors, is replaced by safeParse, which returns a result object. Since Valibot is synchronous by default, there is no need for a separate validateSync method.

// Change this
const user = plainToInstance(User, input);
const errors = await validate(user);
if (errors.length === 0) {
  console.log(user);
} else {
  console.log(errors);
}

// To this
const result = v.safeParse(UserSchema, input);
if (result.success) {
  console.log(result.output);
} else {
  console.log(result.issues);
}

If you prefer an exception to be thrown on invalid input, as with validateOrReject, use parse instead.

// Change this
await validateOrReject(user);

// To this
const output = v.parse(UserSchema, input);

We recommend that you read our mental model guide to understand how the individual functions of Valibot's modular API work together.

Optional properties

The @IsOptional decorator skips validation when the value is null or undefined. This matches Valibot's nullish schema. If a property can only be undefined but not null, use optional instead.

// Change this
class Profile {
  @IsOptional()
  @IsString()
  bio?: string;
}

// To this
const ProfileSchema = v.object({
  bio: v.nullish(v.string()),
});

Nested objects and arrays

Instead of @ValidateNested in combination with class-transformer's @Type decorator, you nest schemas directly. For arrays, the { each: true } option is replaced by wrapping the item schema with array.

// Change this
class Order {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => Item)
  items: Item[];
}

// To this
const OrderSchema = v.object({
  items: v.array(ItemSchema),
});

Change names

The following table shows how the most common decorators and methods map to Valibot's API.

class-validatorValibot
@Containsincludes
@IsArrayarray
@IsBase64base64
@IsBooleanboolean
@IsCreditCardcreditCard
@IsDateStringisoDate, isoDateTime, isoTimestamp
@IsDefinedDefault behavior of every schema
@IsEmailemail
@IsEnumenum
@IsHexadecimalhexadecimal
@IsInpicklist
@IsIntnumber with integer
@IsIPip, ipv4, ipv6
@IsNegativeltValue
@IsNotEmptynonEmpty
@IsNotInnotValues
@IsNumbernumber
@IsOptionalnullish
@IsPositivegtValue
@IsStringstring
@IsUrlurl
@IsUUIDuuid
@LengthminLength with maxLength
@Matchesregex
@MaxmaxValue
@MaxLengthmaxLength
@MinminValue
@MinLengthminLength
@Validatecheck, checkAsync
@ValidateNestedNested schema
validatesafeParse
validateOrRejectparse
validateSyncsafeParse

Many more specific decorators like @IsMobilePhone are backed by validator.js. For those without a direct Valibot equivalent, you can use check or regex to implement the same validation.

Other details

Below are some more details that may be helpful when migrating from class-validator to Valibot.

Unknown properties

By default, class-validator keeps properties without decorators on the instance. This matches looseObject. The whitelist: true option, which strips unknown properties, matches the default behavior of object. The forbidNonWhitelisted: true option, which rejects them, matches strictObject. See the objects guide for more details.

// Change this
const errors = await validate(post, {
  whitelist: true,
  forbidNonWhitelisted: true,
});

// To this
const result = v.safeParse(v.strictObject({ title: v.string() }), input);

Custom error messages

Instead of the message property in the decorator options, you pass a single string as the first argument to schemas and as the last argument to actions. See the quick start guide for more details.

// Change this
class Login {
  @MinLength(8, { message: 'Password is too short' })
  password: string;
}

// To this
const LoginSchema = v.object({
  password: v.pipe(v.string(), v.minLength(8, 'Password is too short')),
});

Validation options

Some validation options map only approximately to Valibot and require attention to details. The skipMissingProperties option skips properties that are null or undefined, except for those marked with @IsDefined. Wrapping your schema with partial comes close, but only allows undefined and affects every entry. For full parity, wrap the individual entries with nullish instead. The stopAtFirstError option stops after the first error of each property, but still reports every invalid property. Valibot's abortEarly configuration is stricter and stops after the first issue overall. Validation groups have no direct equivalent. Instead, we recommend deriving multiple schemas from a shared object with methods like pick, omit and partial.

Framework integration

If you use class-validator through a framework like NestJS, you can replace its validation pipe with a custom pipe that calls parse with your schema. See the integration guide for an overview of ecosystem integrations.

Contributors

Thanks to all the contributors who helped make this page better!

  • GitHub profile picture of @fabian-hiller

Partners

Thanks to our partners who support the project ideally and financially.

Sponsors

Thanks to our GitHub sponsors who support the project financially.

  • GitHub profile picture of @vasilii-kovalev
  • GitHub profile picture of @UpwayShop
  • GitHub profile picture of @ruiaraujo012
  • GitHub profile picture of @hyunbinseo
  • GitHub profile picture of @nickytonline
  • GitHub profile picture of @kibertoad
  • GitHub profile picture of @caegdeveloper
  • GitHub profile picture of @Thanaen
  • GitHub profile picture of @bmoyroud
  • GitHub profile picture of @ysknsid25
  • GitHub profile picture of @dslatkin