class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

How to describe validators for two possible types: array or string?

Open sanex3339 opened this issue 6 years ago • 26 comments

How to describe validators for two possible types: array or string?

Two decorators seems not working.

@IsBoolean()
@IsString()
private readonly foo: boolean | string;

sanex3339 avatar Jan 18 '18 21:01 sanex3339

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?

NoNameProvided avatar Jan 24 '18 01:01 NoNameProvided

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)

sanex3339 avatar Jan 24 '18 02:01 sanex3339

I removed boolean type so it's not important for me now, but anyway nice feature to have.

sanex3339 avatar Jan 24 '18 12:01 sanex3339

How about a signature like:

@IsType(Array<(val: any) => boolean>)
@IsType([
 val => typeof val == 'string',
 val => typeof val == 'boolean',
])
private readonly foo: boolean | string;

NoNameProvided avatar Mar 17 '18 20:03 NoNameProvided

It would be nice!

sanex3339 avatar Jan 01 '20 16:01 sanex3339

@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 avatar Mar 25 '20 18:03 ZBAGI

@ZBAGI How would you replicate IsType for nested objects? @IsType([TypeA, TypeB])?

manan avatar May 21 '20 23:05 manan

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 avatar May 22 '20 07:05 ZBAGI

@ZBAGI Thanks! I meant to ask if it could work with custom defined types? If I defined ClassA and ClassB, would this method work?

manan avatar May 22 '20 10:05 manan

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;
}

ZBAGI avatar May 22 '20 10:05 ZBAGI

So many duplicated topics about it and we still have to implement our version ?

stackchain avatar Dec 06 '20 17:12 stackchain

You could try this:

@IsArray()
@IsString({each: true})

vladimiry avatar Dec 06 '20 18:12 vladimiry

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.

unicornware avatar Apr 06 '21 00:04 unicornware

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 avatar Oct 29 '21 15:10 ajwootto

Hey

I am also searching for the above by @ajwootto

Thomas-1985 avatar Nov 23 '21 09:11 Thomas-1985

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,
  )
}

zanminkian avatar Dec 14 '21 03:12 zanminkian

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;

v4ssi404 avatar Jul 28 '22 23:07 v4ssi404

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
}

mike-4040 avatar Dec 28 '22 18:12 mike-4040

how to check number or number Array

sacru2red avatar Jan 26 '23 10:01 sacru2red

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&paramId=456

The param would be then: paramId: [123, 456]

So the param should be defined in the dto as: paramId: number | number [];

ramirezsandin avatar Feb 03 '23 11:02 ramirezsandin

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.

rlataguerra avatar Feb 27 '23 11:02 rlataguerra

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

picninim avatar Feb 28 '23 20:02 picninim

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?

joao-moonward avatar Mar 06 '23 01:03 joao-moonward

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,
  );

liudichen avatar Mar 24 '23 10:03 liudichen

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.

bhaugeea avatar Oct 03 '23 20:10 bhaugeea