class-validator
class-validator copied to clipboard
How to describe validators for two possible types: array or string?
How to describe validators for two possible types: array or string?
Two decorators seems not working.
@IsBoolean()
@IsString()
private readonly foo: boolean | string;
There is currently no way of doing this. What is your use-case? Why do you accept both string
and boolean
? Do you convert it to one of them after validating with class-validator?
Yes. It's this property https://github.com/javascript-obfuscator/javascript-obfuscator/pull/180/files#diff-15b001f4b5c1e28b83dca39130e3531bR121
Identifiers prefix can be as string
and then it will using as is, or as boolean
and then it will disabled (false
value) or random generated (true
value)
I removed boolean
type so it's not important for me now, but anyway nice feature to have.
How about a signature like:
@IsType(Array<(val: any) => boolean>)
@IsType([
val => typeof val == 'string',
val => typeof val == 'boolean',
])
private readonly foo: boolean | string;
It would be nice!
@NoNameProvided Personally i'm against complex logic in decorator definitions.
Imagine having repeat those val => typeof val == 'string'
in every property you want to validate. It quickly gets messy. Especially when you will need to change the logic of validating - then you will need to look for all references and change them one by one.
I suggest you to implement similar solution to the one i posted here. Or if it's common to validate multi-typed properties in your project, you can use custom decorator factory to specify which type you want to validate.
import { registerDecorator, ValidationArguments, ValidationOptions, Validator } from "class-validator";
const typeValidator = {
"string": function (value: any, args: ValidationArguments) {
const validator = new Validator();
return validator.isString(value);
},
"int": function (value: any, args: ValidationArguments) {
const validator = new Validator();
return validator.isInt(value);
}
// Add more here
};
export function IsType(types: (keyof (typeof typeValidator))[], validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: "wrongType",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
return types.some(v => typeValidator[v](value, args));
},
defaultMessage(validationArguments?: ValidationArguments) {
const lastType = types.pop();
if (types.length == 0)
return `Has to be ${lastType}`;
return `Can only be ${types.join(", ")} or ${lastType}.`;
}
}
});
};
}
And then usage is very simple and clean:
class example {
@IsType(["string", "int"])
public someValue: number | string;
}
@ZBAGI How would you replicate IsType for nested objects? @IsType([TypeA, TypeB])
?
It should work exactly like example shows. If you meant an array then ValidationOptions.each set to true
should do the work.
class example {
@IsType(["string", "int"], { each: true })
public someValue: (number | string)[];
}
@ZBAGI Thanks! I meant to ask if it could work with custom defined types? If I defined ClassA and ClassB, would this method work?
You can add whatever type checking function you wish
Here is example that will look for string
in property typeName
const typeValidator = {
"typeOne": function (value: any, args: ValidationArguments) {
if(typeof value === "object")
return false; // if its not an object it cannot be 'typeOne' object
return value['typeName'] == "typeOne"; // Check typeName property and if it is typeOne then it is typeOne 'type'.
},
};
And here is the usage:
class example {
@IsType(["typeOne"])
public someValue: object;
}
So many duplicated topics about it and we still have to implement our version ?
You could try this:
@IsArray()
@IsString({each: true})
So many duplicated topics about it and we still have to implement our version ?
Agreed. Clearly a common use case and should be implemented as a native solution.
My biggest use for this is to allow nulls in addition to a particular type, so I can validate fields like this:
class Validator {
@IsType(['string', null])
myField: string | null
}
Unless someone knows of an existing way to do this
Hey
I am also searching for the above by @ajwootto
import { ValidateBy, ValidationOptions } from 'class-validator'
import { ValidationArguments } from 'class-validator/types/validation/ValidationArguments'
const IS_TYPE = 'isType'
export function IsType(
types: Array<
| 'string'
| 'number'
| 'bigint'
| 'boolean'
| 'symbol'
| 'undefined'
| 'object'
| 'function'
>,
validationOptions?: ValidationOptions,
): PropertyDecorator {
return ValidateBy(
{
name: IS_TYPE,
validator: {
validate: (value: unknown) => types.includes(typeof value),
defaultMessage: ({ value }: ValidationArguments) =>
`Current type ${typeof value} is not in [${types.join(', ')}]`,
},
},
validationOptions,
)
}
If someone need to check also stringNumber
I have updated ZanMinKian code.
import {isNumberString, ValidateBy, ValidationOptions} from 'class-validator'
import { ValidationArguments } from 'class-validator/types/validation/ValidationArguments'
const IS_TYPE = 'isType'
export function IsType(
types: Array<
| 'string'
| 'string-number'
| 'number'
| 'bigint'
| 'boolean'
| 'symbol'
| 'undefined'
| 'object'
| 'function'
>,
validationOptions?: ValidationOptions,
): PropertyDecorator {
return ValidateBy(
{
name: IS_TYPE,
validator: {
validate: (value: unknown) => {
if(types.includes('string-number') && typeof value === 'string'){
return isNumberString(value) || types.includes('string');
}
return types.includes(typeof value);
},
defaultMessage: ({ value }: ValidationArguments) =>
`Current type ${typeof value} is not in [${types.join(', ')}]`,
},
},
validationOptions,
)
}
In my case i use it like this
@IsType(['number', 'string-number'])
costHourly: number|string;
My biggest use for this is to allow nulls in addition to a particular type, so I can validate fields like this:
class Validator { @IsType(['string', null]) myField: string | null }
Unless someone knows of an existing way to do this
@ajwootto For this use case you can do something like:
class Validator {
@ValidateIf(({ myField }) => myField !== null)
@IsString()
myField: string | null
}
how to check number or number Array
A common use for this case is to validate query params, they can be of one type or an array of that type, i.e.
The url could be something like: https://xxxxxx/endpoint?paramId=123¶mId=456
The param would be then: paramId: [123, 456]
So the param should be defined in the dto as:
paramId: number | number [];
I encountered a use case today: Let's say you have an API that is evolving, and during the transition phase, you would like to validate two different types for a property. In my case, the property would be either a string (base64) or an object.
Well, I think the most dynamic way I found was an upgrade on @ZBAGI solution
export const isWhether = (value: string, ...validators: ((value: any) => boolean)[]) => {
return validators.map((validate) => validate(value)).some((valid) => valid)
}
export function IsWhether(...validators: [string, (value: any) => boolean][]) {
return function (object: any, propertyName: string) {
registerDecorator({
name: 'isWhetherType',
target: object.constructor,
propertyName: propertyName,
options: {},
validator: {
validate(value: any) {
const validatorsFns = validators.map(([type, validate]) => validate).flat()
return isWhether(value, ...validatorsFns)
},
defaultMessage(validationArguments?: ValidationArguments) {
const types = validators.map(([type]) => type).flat()
const lastType = types.pop()
if (types.length === 0) return `${propertyName} has to be ${lastType}`
return `${propertyName} can only be ${types.join(', ')} or ${lastType}.`
},
},
})
}
Usage:
export class UpdateDTO {
@Expose()
@IsWhether(['file', isFile], ['string', isString])
@IsOptional()
avatar?: MemoryStoredFile | string
}
Basically receives the type name and a function to validate @IsWhether(['typeName', (valueTovValidate) => boolean ])
All class-validators has an export member as its validation logic, so the decorator @IsString() has it isString() validator, and other libraries like nestjs-form-data, which has a IsFile() has the isFile() validator logic.
I my case the user can upload a file at avatar, that will be stored and returns the store url. But I want to allow the user to send me back the same dto without having to remove the avatar key, so he can send my a File instance or a url string
IsWherther decorator is an excellent solution, but I'd like to share some ideas. A solution like Joi has methods like alternatives
and try
. We have to pass a list of schemas, and then the request must match one element from the array.
I'm trying to find a solution to validate a field that can assume one type.
abstract class AbstractHouseDto {
@IsNumber()
price: number;
}
class CreateApartmentDto extends AbstractHouseDto {
@IsNumber()
floor: number
}
class CreateCommonHouse extends AbstractHouseDto {
@IsNumber()
@Min(0)
gardenSize: number
}
class CreateHouseDto {
@IsEnum(HouseType)
type: HouseType
@IsWhether([CreateApartmentDto, CreateCommonHouse])
house: AbstractHouseDto
}
It's just an example. I want to validate the user request, and the house
object must pass one of my DTOs: CreateApartmentDto
or CreateCommonHouse
.
{
"type": "APARTMENT"
"house": {
"price": 1000000,
"floor": 2
}
}
{
"type": "COMMON"
"house": {
"price": 5000000,
"gardenSize": 9
}
}
These JSONs are correct.
{
"type": "COMMON"
"house": {
"price": 5000000
}
}
Otherwise, this one is invalid.
I saw some solutions recommend me to use @Type
:
@Type(() => AbstractHouseDto, {
discriminator: {
property: '__type',
subTypes: [
{
value: CreateApartmentDto,
name: CreateApartmentDto.name,
},
{
value: CreateCommonHouse,
name: CreateCommonHouse.name,
}
],
},
})
I'm trying to implement a polymorphism strategy in my services, which is why I need my validations to accept different types.
But it doesn't seem worked to me. May I do something wrong?
This may be simpler and more useful:
import {
ValidationArguments,
ValidationOptions,
ValidateBy,
isNumber,
isString,
isNumberString,
isInt,
isArray,
isBoolean,
} from 'class-validator';
const InnerTypesValidator = {
number: isNumber,
string: isString,
numberString: isNumberString,
int: isInt,
array: isArray,
boolean: isBoolean,
};
export const IsGenericType = (
validators: (keyof typeof InnerTypesValidator | ((value: any) => boolean))[],
validationOptions?: ValidationOptions,
) =>
ValidateBy(
{
name: 'IS_GENERIC_TYPE',
validator: {
validate: (value: unknown) => {
return validators.some((item) =>
typeof item === 'function'
? item(value)
: InnerTypesValidator[item]?.(value),
);
},
defaultMessage: (validationArguments?: ValidationArguments) => {
return `${validationArguments?.property}: Data type mismatch`;
},
},
},
validationOptions,
);
One technique you can use in the general case that there's some kind of branching logic is to define custom getters which extract their relevant pieces of the real property's data, and then you put each kind of decorator on its own appropriate getter.
@IsArray()
things: (OneThing | OtherThing)[]
@IsOneThing()
get oneThing() {
return this.things.filter(isThingOne)
}
@IsOtherThing()
get otherThing() {
return this.things.filter(isThingOther)
}
The resulting error object will use the oneThing
and otherThing
property names, which you'll have to deal with somehow. But, it does work.