# Migrate from class-validator

> This document is the Markdown version of [valibot.dev/guides/migrate-from-class-validator/](https://valibot.dev/guides/migrate-from-class-validator/). For the complete documentation index, see [llms.txt](https://valibot.dev/llms.txt).

Migrating from [class-validator](https://github.com/typestack/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](/guides/pipelines.md). Since the class no longer exists, you infer the TypeScript type from the schema with [`InferOutput`](/api/InferOutput.md).

```ts
// 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`](/api/safeParse.md), which returns a result object. Since Valibot is synchronous by default, there is no need for a separate `validateSync` method.

```ts
// 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`](/api/parse.md) instead.

```ts
// Change this
await validateOrReject(user);

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

We recommend that you read our [mental model](/guides/mental-model.md) 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`](/api/nullish.md) schema. If a property can only be `undefined` but not `null`, use [`optional`](/api/optional.md) instead.

```ts
// 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`](/api/array.md).

```ts
// 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-validator    | Valibot                                                                                                                                                 |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@Contains`        | [`includes`](/api/includes.md)                                                                                                           |
| `@IsArray`         | [`array`](/api/array.md)                                                                                                                 |
| `@IsBase64`        | [`base64`](/api/base64.md)                                                                                                               |
| `@IsBoolean`       | [`boolean`](/api/boolean.md)                                                                                                             |
| `@IsCreditCard`    | [`creditCard`](/api/creditCard.md)                                                                                                       |
| `@IsDateString`    | [`isoDate`](/api/isoDate.md), [`isoDateTime`](/api/isoDateTime.md), [`isoTimestamp`](/api/isoTimestamp.md) |
| `@IsDefined`       | Default behavior of every schema                                                                                                                        |
| `@IsEmail`         | [`email`](/api/email.md)                                                                                                                 |
| `@IsEnum`          | [`enum`](/api/enum.md)                                                                                                                   |
| `@IsHexadecimal`   | [`hexadecimal`](/api/hexadecimal.md)                                                                                                     |
| `@IsIn`            | [`picklist`](/api/picklist.md)                                                                                                           |
| `@IsInt`           | [`number`](/api/number.md) with [`integer`](/api/integer.md)                                                              |
| `@IsIP`            | [`ip`](/api/ip.md), [`ipv4`](/api/ipv4.md), [`ipv6`](/api/ipv6.md)                                         |
| `@IsNegative`      | [`ltValue`](/api/ltValue.md)                                                                                                             |
| `@IsNotEmpty`      | [`nonEmpty`](/api/nonEmpty.md)                                                                                                           |
| `@IsNotIn`         | [`notValues`](/api/notValues.md)                                                                                                         |
| `@IsNumber`        | [`number`](/api/number.md)                                                                                                               |
| `@IsOptional`      | [`nullish`](/api/nullish.md)                                                                                                             |
| `@IsPositive`      | [`gtValue`](/api/gtValue.md)                                                                                                             |
| `@IsString`        | [`string`](/api/string.md)                                                                                                               |
| `@IsUrl`           | [`url`](/api/url.md)                                                                                                                     |
| `@IsUUID`          | [`uuid`](/api/uuid.md)                                                                                                                   |
| `@Length`          | [`minLength`](/api/minLength.md) with [`maxLength`](/api/maxLength.md)                                                    |
| `@Matches`         | [`regex`](/api/regex.md)                                                                                                                 |
| `@Max`             | [`maxValue`](/api/maxValue.md)                                                                                                           |
| `@MaxLength`       | [`maxLength`](/api/maxLength.md)                                                                                                         |
| `@Min`             | [`minValue`](/api/minValue.md)                                                                                                           |
| `@MinLength`       | [`minLength`](/api/minLength.md)                                                                                                         |
| `@Validate`        | [`check`](/api/check.md), [`checkAsync`](/api/checkAsync.md)                                                              |
| `@ValidateNested`  | Nested schema                                                                                                                                           |
| `validate`         | [`safeParse`](/api/safeParse.md)                                                                                                         |
| `validateOrReject` | [`parse`](/api/parse.md)                                                                                                                 |
| `validateSync`     | [`safeParse`](/api/safeParse.md)                                                                                                         |

Many more specific decorators like `@IsMobilePhone` are backed by [validator.js](https://github.com/validatorjs/validator.js). For those without a direct Valibot equivalent, you can use [`check`](/api/check.md) or [`regex`](/api/regex.md) 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`](/api/looseObject.md). The `whitelist: true` option, which strips unknown properties, matches the default behavior of [`object`](/api/object.md). The `forbidNonWhitelisted: true` option, which rejects them, matches [`strictObject`](/api/strictObject.md). See the [objects](/guides/objects.md) guide for more details.

```ts
// 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](/guides/quick-start.md#error-messages) guide for more details.

```ts
// 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`](/api/partial.md) comes close, but only allows `undefined` and affects every entry. For full parity, wrap the individual entries with [`nullish`](/api/nullish.md) 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`](/api/pick.md), [`omit`](/api/omit.md) and [`partial`](/api/partial.md).

### 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`](/api/parse.md) with your schema. See the [integration](/guides/integrate-valibot.md) guide for an overview of ecosystem integrations.
