Nestjs+Class-validator校验Decimal值遇异常,求最佳实践
NestJS 处理 Decimal 数值校验的最佳实践
问题根源
你遇到的问题核心在于 @IsDecimal 装饰器的设计逻辑:它默认只校验字符串类型,而你把 price 定义成了 number 类型。JSON 传输的数字本质是浮点数,JS 里的浮点数会有精度丢失问题(比如 1574.23 实际存储可能是近似值),这也会导致 @IsDecimal 对 number 类型的校验失效。改用 @IsNumberString 无效是因为你传的是数字而非字符串格式的数字。
解决方案与最佳实践
方案1:用字符串传输 Decimal(推荐用于金额等精确场景)
把 DTO 中的 price 改为字符串类型,直接用 @IsDecimal 校验,之后在服务层转成需要的数值类型:
// create-product.dto.ts import { IsDecimal, IsNotEmpty, Min } from 'class-validator'; export class CreateProductDto { @IsDecimal({ force_decimal: true, decimal_digits: '2' }) @IsNotEmpty({ message: 'Price is required' }) price: string; // 其他字段... }
在服务层处理转换(如果需要转成 number 或精确 Decimal 实例):
// product.service.ts import { Injectable } from '@nestjs/common'; import { CreateProductDto } from './dto/create-product.dto'; import Decimal from 'decimal.js'; @Injectable() export class ProductService { create(createProductDto: CreateProductDto) { // 转成精确 Decimal 实例(推荐金额场景) const price = new Decimal(createProductDto.price); // 或转成 number(注意精度风险) // const price = Number(createProductDto.price); // 后续业务逻辑... return { ...createProductDto, price }; } }
方案2:自定义校验器处理 number 类型
如果必须用 number 类型接收,需要自定义校验器来检查小数位数,同时规避浮点数精度问题:
- 先创建自定义装饰器:
// is-decimal-number.validator.ts import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'; export function IsDecimalNumber(decimalDigits: string, validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ name: 'isDecimalNumber', target: object.constructor, propertyName: propertyName, options: validationOptions, validator: { validate(value: number, args: ValidationArguments) { if (typeof value !== 'number') return false; // 把数字转成字符串,处理浮点数精度问题 const numStr = value.toFixed(Number(decimalDigits)); // 检查小数位数是否符合要求 const regex = new RegExp(`^\\d+\\.\\d{${decimalDigits}}$`); return regex.test(numStr); }, defaultMessage(args: ValidationArguments) { return `$property must be a valid decimal number with ${decimalDigits} decimal places`; }, }, }); }; }
- 在 DTO 中使用:
// create-product.dto.ts import { IsNotEmpty, Min } from 'class-validator'; import { IsDecimalNumber } from './is-decimal-number.validator'; export class CreateProductDto { @IsDecimalNumber('2', { message: 'Price must be a valid decimal number with 2 decimal places' }) @IsNotEmpty({ message: 'Price is required' }) @Min(0, { message: 'Price must be greater than or equal to 0' }) price: number; // 其他字段... }
方案3:结合 Transform 与 decimal.js 处理精确数值
如果需要直接在 DTO 中得到精确的 Decimal 实例,可以用 class-transformer 的 @Transform 装饰器:
// create-product.dto.ts import { IsNotEmpty, Min } from 'class-validator'; import { Transform } from 'class-transformer'; import Decimal from 'decimal.js'; export class CreateProductDto { @Transform(({ value }) => new Decimal(value)) @IsNotEmpty({ message: 'Price is required' }) // 校验数值不小于0 @Min(0, { message: 'Price must be greater than or equal to 0', validator: { validate(value: Decimal) { return value.greaterThanOrEqualTo(0); } } }) // 校验小数位数为2位 @Min(0, { message: 'Price must have exactly 2 decimal places', validator: { validate(value: Decimal) { return value.decimalPlaces() === 2; } } }) price: Decimal; // 其他字段... }
总结
- 涉及金额、税率等需要精确计算的场景,优先用字符串传输 Decimal,再转成
decimal.js实例处理,避免浮点数精度丢失。 - 如果必须用 number 类型,一定要自定义校验器,不要依赖原生的
@IsDecimal。
内容的提问来源于stack exchange,提问作者berk
相关产品推荐
相关产品推荐

