class-validator-jsonschema
class-validator-jsonschema copied to clipboard
IsOptional support for null
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.
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.
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 😃
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.
@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.
Taking into account the current @IsOptional behavior, what could be a workaround for what @loban stated?
@xjuanc you could use the additionalConverters option to override the default @IsOptional conversion (which atm does nothing) as mentioned above.
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[]
mark
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))
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;
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
}
]
},