如何在NestJS中实现键为枚举类型、值结构一致的对象动态验证(避免枚举变更时同步更新DTO)
I totally get your frustration—having to manually update your Locations class every time you add a new country to the enum is tedious and error-prone. Let's fix this with a dynamic approach that syncs automatically with your Countries enum, no more manual DTO updates required.
Step 1: Create a Custom Validator for Enum Keys
First, we'll build a custom validator that checks if all keys in the locations object are valid members of your Countries enum. This ensures no invalid country codes slip through.
import { registerDecorator, ValidatorConstraint, ValidatorConstraintInterface, ValidationOptions, ValidationArguments } from 'class-validator'; import { Countries } from './your-enum-path'; // Define the core validation logic @ValidatorConstraint({ async: false }) class ValidCountryKeysConstraint implements ValidatorConstraintInterface { validate(obj: Record<string, unknown>, args: ValidationArguments) { const validCountryCodes = Object.values(Countries); // Verify every key in the object matches a valid enum value return Object.keys(obj).every(key => validCountryCodes.includes(key as Countries)); } defaultMessage(args: ValidationArguments) { return `Invalid country key in ${args.property}. Valid options: ${Object.values(Countries).join(', ')}`; } } // Wrap the constraint in a reusable decorator for easy use export function ValidCountryKeys(validationOptions?: ValidationOptions) { return function (target: object, propertyName: string) { registerDecorator({ target: target.constructor, propertyName: propertyName, options: validationOptions, validator: ValidCountryKeysConstraint, }); }; }
Step 2: Update Your DTO to Use Dynamic Validation
Now, replace your hardcoded Locations class with a Record type, and apply our custom validator along with nested validation for the Country structure.
import { IsObject, ValidateNested, IsOptional, Type } from 'class-validator'; import { Country } from './country.dto'; import { Countries } from './your-enum-path'; import { ValidCountryKeys } from './valid-country-keys.validator'; export class CreateDto { // ... your other properties here @IsObject() @ValidCountryKeys() // Ensures all keys are valid Countries enum values @ValidateNested({ each: true }) // Validate every value in the object @Type(() => Country) // Apply Country class validation rules to each value @IsOptional() readonly locations?: Record<Countries, Country>; }
How This Works
- Dynamic Enum Sync: The custom validator pulls valid country codes directly from the
Countriesenum. Add a new member (likeFR = 'FR') to the enum, and the validator will automatically recognize it—no DTO changes needed. - Consistent Value Structure: The
@ValidateNested({ each: true })and@Type(() => Country)decorators ensure every value in thelocationsobject adheres to yourCountryclass rules, just like your original implementation. - Clear Error Messages: If someone sends an invalid country code (e.g.,
FRbefore it's added to the enum), the validator will return a friendly error listing all valid options.
Testing with Your Example Payload
This setup will correctly validate your sample request:
{ // ... other props "locations": { "IT": { "region": ["Lazio"] } } }
And it will reject requests with invalid keys, like:
{ "locations": { "FR": { "region": ["Île-de-France"] } } }
With an error message: Invalid country key in locations. Valid options: ES, IT
Key Benefits
- Maintenance-Free: No more syncing your DTO with enum changes
- Reusable: The
ValidCountryKeysdecorator can be used anywhere you need to validate an object with enum keys - Type-Safe: The
Record<Countries, Country>type gives you TypeScript intellisense for valid country codes
内容的提问来源于stack exchange,提问作者fasenderos

