Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IsOptional support for null #9

Open
MrBlenny opened this issue Dec 3, 2018 · 11 comments
Open

IsOptional support for null #9

MrBlenny opened this issue Dec 3, 2018 · 11 comments

Comments

@MrBlenny
Copy link

MrBlenny commented Dec 3, 2018

I'm not sure whether this is part of as conditional decorator limitations in the readme. In any case...

The @IsOptional decorator should be adding anyOf: [{type: someType}, {type: 'null'}] as well as removing the property from the required array. It doesn't seem to be doing the former.

I note that internally, class validator uses conditionalValidation for the IsOptional decorator. Am I correct that the limitation is that this doesn't work when there are multiple decorators for one field? For example:

@IsOptional()
@IsNumber()
thing: number

@IsOptional()
@IsSting()
otherThing: string

The functionality of IsOptional depends on the other decorators.

@epiphone
Copy link
Owner

epiphone commented Dec 4, 2018

Hi! That's a good point, IsOptional should indeed add a null schema as well as remove the property from the required array. And yes, as class-validator uses ValidationTypes.CONDITIONAL_VALIDATION for both @IsOptional and @ValidateIf, adding a converter for CONDITIONAL_VALIDATION to add the null schema is tricky since inside the converter function we can't reliably differentiate between @IsOptional and @ValidateIf. Your second assumption is also correct, I'm afraid. At least that's my impression after a brief look; if you've got a solution that'd be great.

I'll try to look into this more when there's time. For the time being you might want to override the default ValidationTypes.CONDITIONAL_VALIDATION decorator.

@MrBlenny
Copy link
Author

MrBlenny commented Dec 5, 2018

My temporary solution for now is to traverse the object and convert all nulls to undefined. I will look into a better solution if required.
Thanks 😃

@loban
Copy link

loban commented Oct 9, 2019

Personally, I don't think @IsOptional should automatically allow null on a property that perhaps has other validations. Null is a very specific value that can have meaning. I always understood @IsOptional to mean if the property exists, then ensure it follows the other validations, and if it doesn't exist, that's ok.

This is especially relevant when I'm implementing a PATCH API call where all properties in the body are optional, but each must pass some validation if included, and null is a valid value for a property.

@epiphone
Copy link
Owner

@loban that's totally reasonable, but the way @IsOptional is implemented is that it

Checks if given value is empty (=== null, === undefined) and if so, ignores all the validators on the property.

and this library should naturally match class-validator's definition as closely as possible.

@xjuanc
Copy link

xjuanc commented Jan 10, 2020

Taking into account the current @IsOptional behavior, what could be a workaround for what @loban stated?

@epiphone
Copy link
Owner

@xjuanc you could use the additionalConverters option to override the default @IsOptional conversion (which atm does nothing) as mentioned above.

@incompletude
Copy link

Hello,

Any news on this? How can i validate or null or enum?

This doesn't work.

  @IsOptional()
  @IsEnum(["c", "a", "r"], { each: true })
  @Transform((value: string) => value.split(","))
  status: string[]

@xsf0105
Copy link

xsf0105 commented Mar 3, 2021

mark

@Jurajzovinec
Copy link

Hello,

Any news on this? How can i validate or null or enum?

This doesn't work.

  @IsOptional()
  @IsEnum(["c", "a", "r"], { each: true })
  @Transform((value: string) => value.split(","))
  status: string[]

Hello, I use the following code for enum validation:

@IsIn(Object.values(ConversationStatusEnum))

@sunghyunl22
Copy link

sunghyunl22 commented May 24, 2022

You can replace @isOptional() to @ValidateIf(o => '{fieldName}' in o) for those optional fields that should not be updated with null

Validator accepts {id: null}

@IsOptional()
@IsNotEmpty()
readonly id: string;

Validator blocks {id: null}

@ValidateIf(o => 'id' in o)
@IsNotEmpty()
readonly id: string;

@elliot-sabitov
Copy link

elliot-sabitov commented May 9, 2024

I hope someone finds this helpful, I was able to get optionals working properly like so:


const refPointerPrefix = '#/components/schemas/';


function getPropType(target: object, property: string) {
  return Reflect.getMetadata('design:type', target, property);
}

export {JSONSchema} from 'class-validator-jsonschema';

type Options = IOptions & {
  definitions: Record<string, SchemaObject>;
};

function nestedClassToJsonSchema(clz: Constructor<any>, options: Partial<Options>): SchemaObject {
  return targetConstructorToSchema(clz, options) as any;
}


const additionalConverters: ISchemaConverters = {
  [ValidationTypes.NESTED_VALIDATION]: (meta: ValidationMetadata, options: IOptions) => {
    if (typeof meta.target === 'function') {

      const typeMeta = options.classTransformerMetadataStorage
        ? options.classTransformerMetadataStorage.findTypeMetadata(meta.target, meta.propertyName)
        : null;

      const childType = typeMeta
        ? typeMeta.typeFunction()
        : getPropType(meta.target.prototype, meta.propertyName);

      const schema = targetToSchema(childType, options);

      const name = meta.target.name;

      if (!!schema && !!schema.$ref && schema.$ref === '#/components/schemas/Object') {
        schema.$ref = `${refPointerPrefix}${name}`;
      }

      const isOptional = Reflect.getMetadata('isOptional', meta.target.prototype, meta.propertyName);

      if (isOptional) {
        const anyof: (SchemaObject | ReferenceObject)[] = [
          {
            type: 'null'
          } as any
        ];
        if (schema && schema.$ref) {
          anyof.push({$ref: schema.$ref});
        }
        if (anyof.length === 1) {
          return {
            type: 'null'
          };
        } else {
          return {
            anyof
          };
        }
      } else {
        return schema;
      }
    }
  },
} as any;
export const defaultClassValidatorJsonSchemaOptions: Partial<Options> = {
  refPointerPrefix,
  additionalConverters,
  classTransformerMetadataStorage: defaultMetadataStorage
};

export function classToJsonSchema(clz: Constructor<any>): SchemaObject {
  const options = {...defaultClassValidatorJsonSchemaOptions, definitions: {}};
  const schema = targetConstructorToSchema(clz, options) as any;
  schema.definitions = options.definitions;
  return schema;
}

function targetToSchema(type: any, options: IOptions): any | void {
  if (typeof type === 'function') {
    if (type.prototype === String.prototype || type.prototype === Symbol.prototype) {
      return {type: 'string'};
    } else if (type.prototype === Number.prototype) {
      return {type: 'number'};
    } else if (type.prototype === Boolean.prototype) {
      return {type: 'boolean'};
    }

    return {$ref: options.refPointerPrefix + type.name};
  }
}

however, as you can see, we need to get isOptional from Reflect.getMetadata('isOptional', meta.target.prototype, meta.propertyName); and for this, I found no better way than to make a custom Optional decorator which mimics IsOptional and reuses it but also sets additional metadata:

/**
 * Optional - mimics @IsOptional decorator but adds custom metadata so that it is easily available in
 * Reflect to check if the property is optional
 * @constructor
 */
export const Optional = () => {
  return function (object: NonNullable<unknown>, propertyName: string) {
    // Apply the standard @IsOptional() decorator
    IsOptional()(object, propertyName);

    console.log(object instanceof Cdr);
    // Add custom metadata for additional use cases
    Reflect.defineMetadata('isOptional', true, object, propertyName);
  };
};

this mimics IsOptional except it gives us the ability to be able to call Reflect.getMetadata('isOptional', meta.target.prototype, meta.propertyName); in ValidationTypes.NESTED_VALIDATION and have a solid way of knowing that this field is nullable, which we can then properly handle. Keep in mind, since we are using the NESTED_VALIDATION, we also need to make sure that the field is annotated with @NestedValidation(). So my class looks something like:

export class User {
   @Optional()
   @NestedValidation()
   @Type(() => Token) // this is needed as well for typeMeta.typeFunction() to work in the code above
   token?: Token
}

Please let me know if you find a better way of doing this and hopefully this comes in helpful as it took me a few hours to get this working properly.

Overall though, we do now have the appropriate schema:

"token": {
  "anyOf": [
    {
      "$ref": "#/components/schemas/Token"
    },
    {
      "type": null
    }
  ]
},

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants