Open ibox4real opened 4 years ago
@ibox4real you can use Validation groups. They are meant exactly for what you are looking for.
Example class: A class that I used in a project here
export enum RecordParametersValidationGroup {
CREATION = 'creation',
UPDATE = 'update',
};
export defaul class SaveRecordParameters {
@IsOptional()
@IsPositive()
id?: number;
@Length(5, 50, { groups: [RecordParametersValidationGroup.UPDATE] }])
title?: string;
@IsOptional({ groups: [RecordParametersValidationGroup.UPDATE] })
@IsNumber()
releaseYear?: number;
}
Example usage: The usage here
import {validate} from "class-validator";
import { RecordParametersValidationGroup } from '@entity/SaveRecordParameters ';
// ...
validate(an_example_instance_of_record_parameters, {
groups: [RecordParametersValidationGroup.UPDATE]
});
There's a pull request for a conditional @IsOptional decorator here: https://github.com/typestack/class-validator/pull/196 But it hasn't been merged in over two years now.
Looks way simpler when you have an IsOptionalIf
You can achieve this by creating your own decorator that wraps @ValidateIf
.
/**
* Mark the property as optional if the function returns truthy
*
* @param optionalIfPropertyIsSet
*/
function IsOptionalIf(allowOptional: (obj: any, value: any) => boolean, options?: ValidationOptions) {
// If required, do validate. Otherwise if null|undefined, don't validate
return ValidateIf((obj, value) => !allowOptional(obj, value) || value != null), options)
}
class SaveRecordParameters {
@IsOptional()
@IsPositive()
id?: number;
@IsOptionalIf(self => !!self.id)
@MaxLength(5)
@MinLength(50)
title?: string;
@IsOptional()
@IsNumber()
releaseYear?: number;
}
Can someone make this PR merged?? this will be so helpful!!
I made my version of IsOptionalIf run validators is condition is false if condition is true, don't run validators if value is null or undefined. else run validators
import {
registerDecorator,
ValidationOptions,
ValidationTypes,
} from 'class-validator';
import { isNil } from 'lodash';
export function IsOptionalIf(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isOptionalCondition: (object: any) => boolean,
validationOptions?: ValidationOptions,
): PropertyDecorator {
return function (object, propertyName: string) {
const constraint = (obj, val) => {
const conditionResult = isOptionalCondition(obj);
if (conditionResult) {
// OPTIONAL
return !isNil(val); // validate if value is defined and not validate when value is undefined
}
return true; // validate anyway
};
registerDecorator({
name: ValidationTypes.CONDITIONAL_VALIDATION, // name must be exactly this value to use only constraint
target: object.constructor,
propertyName: propertyName,
constraints: [constraint],
options: validationOptions,
validator: {
validate() {
return false; // can be true or false, because it's not used
},
},
});
};
}
may be it will help someone
heres a really simple solution:
Validator:
import { IsOptional } from "class-validator";
export function IsOptionalIf(condition: boolean) {
if (!condition) {
return () => {};
}
return IsOptional()
}
Usage:
@IsOptionalIf(configuration.USERS.REQUIRE_BIRTH_DATE === false)
@IsDateString()
@IsNotEmpty()
birth_date?: string;
This will result in the exact desired behaviour and only add IsOptional if the condition is true.
heres a really simple solution:
Validator:
import { IsOptional } from "class-validator"; export function IsOptionalIf(condition: boolean) { if (!condition) { return () => {}; } return IsOptional() }
Usage:
@IsOptionalIf(configuration.USERS.REQUIRE_BIRTH_DATE === false) @IsDateString() @IsNotEmpty() birth_date?: string;
This will result in the exact desired behaviour and only add IsOptional if the condition is true.
Not exactly. your solution doesn't work with props inside the object which you want to check for example you have UserDTO with props firstName, lastName, role, email email is required only if role equals to 'Admin' here I don't know how to use your solution
I answered this on StackOverflow, the solution is straightforward. And I made this decorator by reading the source code of the @ValidateIf()
and the @IsOptional()
decorators.
Here is my thread: CLICK ME
Here is the solution:
I created my own IsOptionalIf()
decorator. You can use the same concept to build your own IsRequiredIf()
.
You can use the @ValidateIf()
decorator to achieve that, but you have to create a new decorator around it. You can't use @ValidateIf() directly, because it disables all the validations which is not what you might want. You want to keep the validations but only make the property optional.
This is what this custom IsOptionalIf()
decorator allows you to do, It behaves exactly like the normal @IsOptional()
decorator but allows you to add a condition.
import { ValidateIf, type ValidationOptions } from 'class-validator'
/** Same as `@Optional()` decorator of class-validator, but adds a conditional layer on top of it */
export const IsOptionalIf: IsOptionalIf =
(condition, options = {}) =>
(target: object, propertyKey: string) => {
const { allowNull = true, allowUndefined = true, ...validationOptions } = options
ValidateIf((object: any, value: any): boolean => {
// if condition was true, just disable the validation on the null & undefined fields
const isOptional = Boolean(condition(object, value))
const isNull = object[propertyKey] === null
const isUndefined = object[propertyKey] === undefined
let isDefined = !(isNull || isUndefined)
if (!allowNull && allowUndefined) isDefined = !isUndefined
if (!allowUndefined && allowNull) isDefined = !isNull
const isRequired = isOptional && !isDefined ? false : true
return isRequired
}, validationOptions)(target, propertyKey)
}
export interface OptionalIfOptions {
allowNull?: boolean
allowUndefined?: boolean
}
export type IsOptionalIf = <
T extends Record<string, any> = any, // class instance
Y extends keyof T = any, // propertyName
>(
condition: (object: T, value: T[Y]) => boolean | void,
validationOptions?: ValidationOptions & OptionalIfOptions
) => PropertyDecorator
Here is how you can use it:
class A {
@IsString()
@MaxLength(99)
@IsOptionalIf((obj) => obj.prop2)
prop1: string
@IsBoolean()
prop2: boolean
}
You can consider null
as a real value instead of treating it as not filled by adding the options object as below:
{
// same as above
@IsOptionalIf((obj) => obj.prop2, { allowNull: false })
prop1: string
}
import { type ValidationMetadataArgs } from 'class-validator/types/metadata/ValidationMetadataArgs'
import { type ValidationOptions, ValidationTypes, getMetadataStorage } from 'class-validator'
import { ValidationMetadata } from 'class-validator/cjs/metadata/ValidationMetadata'
export const IS_OPTIONAL_IF = 'isOptionalIf'
export function IsOptionalIf(
condition: (object: any, value: any) => boolean,
validationOptions?: ValidationOptions
): PropertyDecorator {
return function (object: object, propertyName: string): void {
const args: ValidationMetadataArgs = {
type: ValidationTypes.CONDITIONAL_VALIDATION,
name: IS_OPTIONAL_IF,
target: object.constructor,
propertyName: propertyName,
constraints: [
(object: any, value: any): boolean => {
const performValidation = condition(object, value)
if (!performValidation) return true
else return object[propertyName] !== null && object[propertyName] !== undefined
},
],
validationOptions: validationOptions,
}
getMetadataStorage().addValidationMetadata(new ValidationMetadata(args))
}
}
Regarding the ValidationMetadata
import, I am using commonjs, so if you're using esm5 (es5) or es2015 (es6+) just replace cjs in the import to yours.
example:
import { ValidationMetadata } from 'class-validator/cjs/metadata/ValidationMetadata'
import { ValidationMetadata } from 'class-validator/esm5/metadata/ValidationMetadata'
import { ValidationMetadata } from 'class-validator/esm2015/metadata/ValidationMetadata'
The reason for this entire method is we want to use the ValidationTypes.CONDITIONAL_VALIDATION
type of validation, there are some others, but class-validator gives you only the ValidationTypes.CUSTOM_VALIDATION
when you create a custom validator using the registerDecorator()
function. It assumes that by default. And that can not be changed. (as of the current version 2024).
It would be nice if we could have conditional decorator for required/non-required properties.
Example use case:
I want to insert or update a record. When inserting a new row the
title
is required, but when updating thereleaseYear
on existing row thetitle
column is not required.I could get around this by creating two different classes
InsertRecordParameters
andUpdateRecordParamaeters
but it would be nice to have eveything in one class when insert and update do not differ in terms of the parameters used.