Декоратор @IsOptional из библиотеки class-validator
используется для условной валидации свойства. Его основная задача —
пропускать остальные валидаторы, если значение поля отсутствует.
На практике это особенно важно при:
Если свойство имеет значение:
nullundefinedто все остальные валидаторы для этого поля игнорируются.
Пример:
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 |
выполняется |
Очень распространённая ошибка — путать:
name?: string
и:
@IsOptional()
Это совершенно разные механизмы.
class UserDto {
name?: string;
}
Влияет только на типизацию во время компиляции.
Во время выполнения JavaScript никак не проверяет значение.
@IsOptionalclass UserDto {
@IsOptional()
@IsString()
name?: string;
}
Работает во время выполнения программы и влияет на механизм валидации.
@IsOptionalclass UpdateUserDto {
@IsEmail()
email?: string;
}
Запрос:
{}
Ошибка:
{
"email": [
"email must be an email"
]
}
Причина — @IsEmail() пытается валидировать
undefined.
@IsOptionalclass UpdateUserDto {
@IsOptional()
@IsEmail()
email?: string;
}
Теперь:
{}
проходит успешно.
class CreateUserDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
Все поля обязательны.
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@ValidateIfclass UserDto {
@ValidateIf((_, value) => value !== undefined)
@IsEmail()
email?: string;
}
Теперь:
| Значение | Результат |
|---|---|
undefined |
пропуск |
null |
ошибка |
"wrong" |
ошибка |
Некорректный DTO:
class UpdateUserDto {
@IsString()
name?: string;
}
Разработчик ожидает:
Но фактически:
{}
вызывает ошибку валидации.
Правильный вариант:
class UpdateUserDto {
@IsOptional()
@IsString()
name?: string;
}
skipMissingPropertiesВ class-validator существует глобальная настройка:
validate(dto, {
skipMissingProperties: true
});
Она тоже пропускает отсутствующие поля.
skipMissingProperties и @IsOptionalskipMissingPropertiesГлобально влияет на всю валидацию.
@IsOptionalРаботает точечно для конкретного поля.
@IsOptional предпочтительнееГлобальный skipMissingProperties:
Локальный @IsOptional():
В 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);
}
@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;
но валидация делает его необязательным.
Возникает логическое противоречие.
@IsOptional()
почти всегда обязателен.
обычно не используется.
Часто комбинируется с:
@IsNotEmpty()
Следует явно решать:
null;@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsNotEmpty()
@IsString()
name?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsInt()
@Min(1)
count?: number;
@IsOptional()
@IsArray()
tags?: string[];
Упрощённо логика @IsOptional() выглядит так:
if (value === null || value === undefined) {
skipValidation();
}
Именно поэтому:
""0false[]не считаются отсутствующими значениями.
При использовании class-transformer поведение может меняться.
Например:
plainToInstance(UserDto, body)
может преобразовать отсутствующие поля иначе, чем ожидалось.
Особенно важно учитывать это при:
class SearchDto {
@IsOptional()
@IsString()
query?: string;
}
Запросы:
/search
и:
/search?query=test
пройдут успешно.
При отправке multipart/form-data пустые поля часто
приходят как:
""
а не undefined.
Из-за этого @IsOptional() не срабатывает.
Типичная проблема:
@IsOptional()
@IsEmail()
email?: string;
Фактическое значение:
email = ""
Результат — ошибка @IsEmail.
Один из распространённых подходов:
@Transform(({ value }) => value === '' ? undefined : value)
@IsOptional()
@IsEmail()
email?: string;
Теперь пустая строка превращается в undefined, и
@IsOptional() корректно пропускает валидацию.
class UserDto {
@IsOptional()
@IsStrongPasswordCustom()
password?: string;
}
Если поле отсутствует, кастомный валидатор не вызывается.
Это позволяет избежать лишней логики внутри пользовательских проверок.
@IsOptionalДекоратор играет важную роль в проектировании API:
@IsOptional() не валидирует значение.
Он управляет запуском остальных валидаторов.
Это не проверка типа и не проверка обязательности, а механизм условного пропуска валидации.