@IsOptional

Декоратор @IsOptional из библиотеки class-validator используется для условной валидации свойства. Его основная задача — пропускать остальные валидаторы, если значение поля отсутствует.

На практике это особенно важно при:

  • обновлении сущностей;
  • частичных PATCH-запросах;
  • обработке необязательных параметров;
  • DTO с большим количеством опциональных полей;
  • создании универсальных схем валидации.

Базовый принцип работы

Если свойство имеет значение:

  • null
  • undefined

то все остальные валидаторы для этого поля игнорируются.

Пример:

import { IsOptional, IsEmail } from 'class-validator';

class UserDto {
  @IsOptional()
  @IsEmail()
  email?: string;
}

Поведение:

Значение Результат
"admin@mail.com" проходит
"wrong-email" ошибка
undefined проходит
null проходит

Что считается «отсутствующим» значением

@IsOptional() проверяет только два состояния:

undefined
null

Все остальные значения считаются существующими и будут валидироваться дальше.

Пример:

class ExampleDto {
  @IsOptional()
  @IsString()
  name?: string;
}

Результат:

Значение Проверка @IsString
undefined пропущена
null пропущена
"" выполняется
123 выполняется
false выполняется

Отличие от обычного optional-свойства TypeScript

Очень распространённая ошибка — путать:

name?: string

и:

@IsOptional()

Это совершенно разные механизмы.

Optional-свойство TypeScript

class UserDto {
  name?: string;
}

Влияет только на типизацию во время компиляции.

Во время выполнения JavaScript никак не проверяет значение.


@IsOptional

class UserDto {
  @IsOptional()
  @IsString()
  name?: string;
}

Работает во время выполнения программы и влияет на механизм валидации.


Проблема без @IsOptional

class UpdateUserDto {
  @IsEmail()
  email?: string;
}

Запрос:

{}

Ошибка:

{
  "email": [
    "email must be an email"
  ]
}

Причина — @IsEmail() пытается валидировать undefined.


Решение через @IsOptional

class UpdateUserDto {
  @IsOptional()
  @IsEmail()
  email?: string;
}

Теперь:

{}

проходит успешно.


Типичный сценарий: PATCH-запросы

DTO создания

class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  password: string;
}

Все поля обязательны.


DTO обновления

class UpdateUserDto {
  @IsOptional()
  @IsEmail()
  email?: string;

  @IsOptional()
  @IsString()
  password?: string;
}

Теперь можно обновлять только отдельные поля:

{
  "email": "new@mail.com"
}

или:

{
  "password": "123456"
}

Порядок декораторов

Обычно @IsOptional() ставится первым.

Рекомендуемый стиль:

@IsOptional()
@IsString()
name?: string;

Технически порядок почти всегда не влияет на результат, однако размещение сверху делает код читаемее и соответствует распространённой практике.


Комбинация с @IsNotEmpty

Очень важный нюанс.

Пример:

class UserDto {
  @IsOptional()
  @IsNotEmpty()
  name?: string;
}

Поведение:

Значение Результат
undefined проходит
null проходит
"" ошибка
"John" проходит

Это один из самых распространённых шаблонов для PATCH DTO.


Комбинация с числовыми валидаторами

class ProductDto {
  @IsOptional()
  @IsInt()
  @Min(1)
  count?: number;
}

Поведение:

Значение Результат
undefined проходит
10 проходит
0 ошибка
"abc" ошибка

Работа со строками

class ProfileDto {
  @IsOptional()
  @Length(2, 30)
  nickname?: string;
}

Пустая строка:

{
  "nickname": ""
}

не будет проигнорирована.

@Length выполнится и выдаст ошибку.


Пустая строка и @IsOptional

Ключевая особенность:

@IsOptional()

не считает пустую строку отсутствующим значением.

То есть:

""

не эквивалентно:

undefined

Игнорирование пустых строк

Иногда требуется, чтобы пустая строка тоже считалась отсутствующим значением.

Стандартный @IsOptional() этого не делает.

Решение через @ValidateIf.

import { ValidateIf, IsEmail } from 'class-validator';

class UserDto {
  @ValidateIf((_, value) => value !== '')
  @IsEmail()
  email?: string;
}

Теперь:

Значение Результат
"" пропуск
undefined ошибка
"wrong" ошибка

Комбинация @ValidateIf и @IsOptional

Иногда используют оба декоратора:

class UserDto {
  @IsOptional()
  @ValidateIf((_, value) => value !== '')
  @IsEmail()
  email?: string;
}

Поведение:

Значение Результат
undefined пропуск
null пропуск
"" пропуск
"abc" ошибка
"admin@mail.com" проходит

Вложенные объекты

@IsOptional() часто применяется вместе с:

@ValidateNested()

Пример:

class AddressDto {
  @IsString()
  city: string;
}

class UserDto {
  @IsOptional()
  @ValidateNested()
  address?: AddressDto;
}

Если address отсутствует — вложенная валидация не запускается.


Массивы

class PostDto {
  @IsOptional()
  @IsArray()
  tags?: string[];
}

Поведение:

Значение Результат
undefined проходит
[] проходит
"abc" ошибка

null как допустимое значение

Многие забывают:

@IsOptional()

разрешает null.

Пример:

{
  "email": null
}

валидацию пройдёт успешно.

Это может быть нежелательным поведением.


Как запретить null

Способ через @ValidateIf

class UserDto {
  @ValidateIf((_, value) => value !== undefined)
  @IsEmail()
  email?: string;
}

Теперь:

Значение Результат
undefined пропуск
null ошибка
"wrong" ошибка

Частая ошибка при работе с API

Некорректный DTO:

class UpdateUserDto {
  @IsString()
  name?: string;
}

Разработчик ожидает:

  • поле необязательно;
  • отсутствие поля допустимо.

Но фактически:

{}

вызывает ошибку валидации.

Правильный вариант:

class UpdateUserDto {
  @IsOptional()
  @IsString()
  name?: string;
}

Поведение с skipMissingProperties

В class-validator существует глобальная настройка:

validate(dto, {
  skipMissingProperties: true
});

Она тоже пропускает отсутствующие поля.


Разница между skipMissingProperties и @IsOptional

skipMissingProperties

Глобально влияет на всю валидацию.

@IsOptional

Работает точечно для конкретного поля.


Почему @IsOptional предпочтительнее

Глобальный skipMissingProperties:

  • делает поведение менее очевидным;
  • скрывает ошибки;
  • влияет на все DTO;
  • усложняет поддержку.

Локальный @IsOptional():

  • явно показывает намерение;
  • проще читается;
  • безопаснее;
  • удобнее поддерживается.

Использование в NestJS

В NestJS декоратор применяется особенно часто.

Пример DTO:

export class UpdateUserDto {
  @IsOptional()
  @IsString()
  firstName?: string;

  @IsOptional()
  @IsEmail()
  email?: string;
}

В контроллере:

@Patch(':id')
update(
  @Param('id') id: string,
  @Body() dto: UpdateUserDto
) {
  return this.usersService.update(id, dto);
}

PartialType и @IsOptional

В NestJS существует утилита:

PartialType()

Пример:

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  password: string;
}

export class UpdateUserDto extends PartialType(CreateUserDto) {}

PartialType автоматически делает поля необязательными и добавляет поведение, аналогичное @IsOptional.


Когда @IsOptional не нужен

Если поле всегда обязательно:

class CreateUserDto {
  @IsEmail()
  email: string;
}

добавлять @IsOptional() нельзя.

Иначе обязательность исчезнет.


Антипаттерн

class CreateUserDto {
  @IsOptional()
  @IsNotEmpty()
  @IsEmail()
  email: string;
}

Поле объявлено обязательным:

email: string;

но валидация делает его необязательным.

Возникает логическое противоречие.


Лучшие практики

Для PATCH DTO

@IsOptional()

почти всегда обязателен.


Для CREATE DTO

обычно не используется.


Для строк

Часто комбинируется с:

@IsNotEmpty()

Для nullable-полей

Следует явно решать:

  • допустим ли null;
  • допустима ли пустая строка;
  • должно ли поле полностью отсутствовать.

Распространённые комбинации

Необязательная строка

@IsOptional()
@IsString()
name?: string;

Необязательная непустая строка

@IsOptional()
@IsNotEmpty()
@IsString()
name?: string;

Необязательный email

@IsOptional()
@IsEmail()
email?: string;

Необязательное число

@IsOptional()
@IsInt()
@Min(1)
count?: number;

Необязательный массив

@IsOptional()
@IsArray()
tags?: string[];

Внутренний механизм работы

Упрощённо логика @IsOptional() выглядит так:

if (value === null || value === undefined) {
  skipValidation();
}

Именно поэтому:

  • ""
  • 0
  • false
  • []

не считаются отсутствующими значениями.


Поведение при трансформации данных

При использовании class-transformer поведение может меняться.

Например:

plainToInstance(UserDto, body)

может преобразовать отсутствующие поля иначе, чем ожидалось.

Особенно важно учитывать это при:

  • автоматической трансформации NestJS;
  • работе с query-параметрами;
  • обработке form-data.

Проверка query-параметров

class SearchDto {
  @IsOptional()
  @IsString()
  query?: string;
}

Запросы:

/search

и:

/search?query=test

пройдут успешно.


Особенности form-data

При отправке multipart/form-data пустые поля часто приходят как:

""

а не undefined.

Из-за этого @IsOptional() не срабатывает.

Типичная проблема:

@IsOptional()
@IsEmail()
email?: string;

Фактическое значение:

email = ""

Результат — ошибка @IsEmail.


Решение для form-data

Один из распространённых подходов:

@Transform(({ value }) => value === '' ? undefined : value)
@IsOptional()
@IsEmail()
email?: string;

Теперь пустая строка превращается в undefined, и @IsOptional() корректно пропускает валидацию.


Совместимость с кастомными валидаторами

class UserDto {
  @IsOptional()
  @IsStrongPasswordCustom()
  password?: string;
}

Если поле отсутствует, кастомный валидатор не вызывается.

Это позволяет избежать лишней логики внутри пользовательских проверок.


Архитектурное значение @IsOptional

Декоратор играет важную роль в проектировании API:

  • отделяет создание от обновления;
  • делает PATCH-семантику корректной;
  • упрощает переиспользование DTO;
  • уменьшает количество условного кода;
  • делает контракт API предсказуемым.

Основная идея декоратора

@IsOptional() не валидирует значение.

Он управляет запуском остальных валидаторов.

Это не проверка типа и не проверка обязательности, а механизм условного пропуска валидации.