@IsDecimal

Декоратор @IsDecimal в библиотеке class-validator предназначен для проверки строкового представления десятичных чисел. Его ключевая особенность заключается в том, что он работает именно с строками, а не с числовыми типами JavaScript, и проверяет соответствие строгому формату десятичного числа с заданной конфигурацией точности.


@IsDecimal используется для валидации значений, которые должны быть представлены в виде десятичной записи:

  • денежные значения (цены, суммы, тарифы)
  • точные измерения (вес, длина, процентные коэффициенты)
  • данные из внешних API, где числа приходят строками
  • формы, где важно сохранить точное количество знаков после запятой

Ключевой принцип работы: значение должно быть строкой, соответствующей формату decimal, а не числом JavaScript.


Общая логика валидации

Декоратор проверяет:

  • наличие только допустимых символов (цифры и разделитель)
  • корректность расположения знака минус (если разрешён)
  • соответствие количеству знаков до и после разделителя
  • отсутствие экспоненциальной формы (1e10 не допускается)
  • соответствие локали (точка или запятая как разделитель)

Сигнатура и параметры

Декоратор имеет следующий базовый вид:

@IsDecimal(options?: IsDecimalOptions)

IsDecimalOptions

Основные параметры:

interface IsDecimalOptions {
  decimal_digits?: string; // допустимое количество знаков после разделителя
  force_decimal?: boolean; // обязательное наличие дробной части
  locale?: string;         // локаль (например, 'en-US', 'de-DE')
}

decimal_digits

Определяет допустимое количество знаков после десятичного разделителя.

Примеры:

  • "2" → строго 2 знака после точки
  • "1,3" → от 1 до 3 знаков
  • "*"→ любое количество знаков

Пример:

import { IsDecimal } from 'class-validator';

class ProductDto {
  @IsDecimal({ decimal_digits: '2' })
  price: string;
}

Допустимо:

  • "10.50"
  • "0.99"

Недопустимо:

  • "10.5"
  • "10.500"

force_decimal

Требует обязательного наличия дробной части.

@IsDecimal({ force_decimal: true })
value: string;

Поведение:

Допустимо:

  • "10.0"
  • "3.14"

Недопустимо:

  • "10"
  • "5"

locale

Определяет формат десятичного разделителя:

  • "en-US" → точка (.)
  • "de-DE" → запятая (,)

Пример:

@IsDecimal({ locale: 'de-DE' })
value: string;

Допустимо:

  • "10,5"

Недопустимо:

  • "10.5"

Особенности поведения

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

@IsDecimal не приводит тип автоматически:

@IsDecimal()
value: string;

Если передать число:

value = 10.5;

валидация будет провалена, поскольку ожидается строка "10.5".


Отсутствие поддержки научной нотации

Следующие значения считаются невалидными:

  • "1e5"
  • "2E10"

Даже если они корректны математически, формат не соответствует decimal-строке.


Поддержка отрицательных чисел

Отрицательные значения допускаются, если знак минуса используется корректно:

Допустимо:

  • "-10.5"

Недопустимо:

  • "--10.5"
  • "10.-5"

Отличие от других числовых валидаторов

@IsNumber

@IsNumber работает с типом number:

@IsNumber()
value: number;

Особенности:

  • допускает 10.5 как число
  • допускает 1e5
  • не контролирует строковый формат

@IsNumberString

@IsNumberString проверяет, что строка является числом, но:

  • менее строг в отношении decimal-формата
  • не предоставляет контроля над количеством знаков после запятой
  • допускает научную нотацию

@IsDecimal

Фокусируется на:

  • строгом формате decimal
  • контроле precision
  • локализованных разделителях
  • бизнес-логике финансовых данных

Типичные сценарии использования

Финансовые данные

class PaymentDto {
  @IsDecimal({ decimal_digits: '2', force_decimal: true })
  amount: string;
}

Пример:

  • "199.99" — корректно
  • "199.9" — некорректно
  • "199" — некорректно

Процентные значения

class DiscountDto {
  @IsDecimal({ decimal_digits: '1,2' })
  percent: string;
}

Допустимо:

  • "10.5"
  • "7.25"

Географические координаты (в строках)

class LocationDto {
  @IsDecimal({ decimal_digits: '*' })
  latitude: string;

  @IsDecimal({ decimal_digits: '*' })
  longitude: string;
}

Взаимодействие с class-transformer

Частая проблема — несовпадение типов при получении данных из HTTP-запросов.

Без трансформации

{
  price: 10.5
}

валидация @IsDecimal() не пройдет.


С преобразованием в строку

import { Transform } from 'class-transformer';

class ProductDto {
  @Transform(({ value }) => value?.toString())
  @IsDecimal({ decimal_digits: '2' })
  price: string;
}

Теперь:

  • 10.50"10.5" (после преобразования)
  • "10.50""10.50"

Поведение с ведущими нулями

Допустимые значения:

  • "0.5"
  • "00.50" (зависит от реализации и строгих режимов)

Однако бизнес-логика часто требует дополнительной нормализации, так как class-validator не всегда интерпретирует формат как семантически корректный, а лишь синтаксически допустимый.


Частые ошибки при использовании

Использование числа вместо строки

@IsDecimal()
price: number; // ошибка архитектуры валидации

Отсутствие transform

При работе с HTTP payload:

price: 12.50

валидация провалится без приведения к строке.


Неправильный decimal_digits

@IsDecimal({ decimal_digits: '2' })

и вход:

  • "10.5" → ошибка
  • "10.50" → корректно

Локализация и разделители

Разделитель играет ключевую роль.

en-US

@IsDecimal({ locale: 'en-US' })
  • корректно: "10.5"
  • некорректно: "10,5"

de-DE

@IsDecimal({ locale: 'de-DE' })
  • корректно: "10,5"
  • некорректно: "10.5"

Ограничения и нюансы

  • не поддерживает BigInt напрямую
  • не работает с NaN и Infinity
  • не интерпретирует математические выражения
  • не нормализует число (только проверяет формат)
  • не предназначен для арифметики

Поведение при пустых значениях

@IsDecimal не заменяет:

  • @IsOptional
  • @IsNotEmpty

Комбинации:

@IsOptional()
@IsDecimal()
value?: string;

Кастомизация сообщений об ошибке

@IsDecimal(
  { decimal_digits: '2' },
  { message: 'Значение должно быть десятичным числом с 2 знаками после запятой' }
)
price: string;

Роль в архитектуре DTO

В типичных backend-приложениях (особенно NestJS) @IsDecimal используется как слой:

  • синтаксической проверки входных данных
  • защиты от некорректных форматов
  • гарантии предсказуемого формата финансовых значений

Он не заменяет бизнес-валидацию, но обеспечивает строгий контракт входных данных.