class-validator-jsonschema
class-validator-jsonschema copied to clipboard
How to get jsonschema for only one class ?
Hello,
In your exemple you give:
import { IsOptional, IsString, MaxLength } from 'class-validator'
import { validationMetadatasToSchemas } from 'class-validator-jsonschema'
class BlogPost {
@IsString() id: string
@IsOptional()
@MaxLength(20, { each: true })
tags: string[]
}
const schemas = validationMetadatasToSchemas()
console.log(schemas)
Unfortunatly I have too much classes using class-validator and I only want some. Is there a way to only get for given ones ?
Something like this:
import { IsOptional, IsString, MaxLength } from 'class-validator'
import { validationMetadatasToSchemas } from 'class-validator-jsonschema'
class BlogPost {
@IsString() id: string
@IsOptional()
@MaxLength(20, { each: true })
tags: string[]
}
const schema = validationClassToSchema(BlogPost) // or validationClassToSchemas([BlogPost])
console.log(schema)
Tried to create my own MetadataStorage
with only the classes I want to be in but I don't find any exemples on how to achieve that. Did you have ?
Actually I do:
const schemasToGet = [BlogPost.name];
const configSchema = validationMetadatasToSchemas();
for (const name of schemasToGet) {
this._configSchema[name] = configSchema[name];
}
Thanks,
How about just picking out the classes you're interested in from validationClassToSchema
's return value? It's a plain object.
@epiphone it's what I did actually.. But the generated jsonschema is about 150+ schemas since this is generated inside the api-gateway of a large microservices architecture in a monorepo... The processing take time and memory gets impacted (not too much but it does).
@scorsi
Just in case you're still wondering about this, I managed to get it to work by making a new file that will hold my schema and doing something like this:
import 'reflect-metadata';
import 'es6-shim';
import { ClientUser } from '../schema/User/ClientUser';
import { OmitType } from '@nestjs/mapped-types';
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
//Users
class ClientUserSchema extends OmitType(ClientUser, ['_id']) {}
console.log(ClientUserSchema);
const schemas = validationMetadatasToSchemas();
class ClientUserSchema extends OmitType(ClientUser, ['_id']) {}
Creates a new class that extends my initial class but omits the _id property, you can also pass an empty array and I believe it still works. This provides basically the same class to the validationMetadatasToSchemas and allows me to get a result.
@ThatOneAwkwardGuy I am not talking about heritence at all here but about being able to only generate schema for some given classes. You are off topic. But thanks to tried.
I have the same problem, did you find any solutions by chance? @scorsi
@DanoRysJan look at the end of my first message, I give a solution, what I'm actually doing. Far from being a great fix of course.
Actually I do:
const schemasToGet = [BlogPost.name]; const configSchema = validationMetadatasToSchemas(); for (const name of schemasToGet) { this._configSchema[name] = configSchema[name]; }
@DanoRysJan look at the end of my first message, I give a solution, what I'm actually doing. Far from being a great fix of course.
Actually I do:
const schemasToGet = [BlogPost.name]; const configSchema = validationMetadatasToSchemas(); for (const name of schemasToGet) { this._configSchema[name] = configSchema[name]; }
@scorsi Thank you. You helped me create this generic class.
import {SchemaObject} from '@loopback/rest'; import {validationMetadatasToSchemas} from 'class-validator-jsonschema'; const {defaultMetadataStorage} = require('class-transformer/cjs/storage');
export class DtoTransformer { static _configSchema: any; static transform<C extends new (...args: any[]) => any>(clss: C): Promise<SchemaObject> {
const schemasToGet = [clss.name]; const configSchema = validationMetadatasToSchemas({classTransformerMetadataStorage: defaultMetadataStorage}); for (const name of schemasToGet) { this._configSchema = configSchema[name]; } return this._configSchema;
} }
The only problem I currently have is that the objects with the Nested decorator do not complete, they come out as Object.
Documentation suggests requiring this.
import { Type } from 'class-transformer' import { validationMetadatasToSchemas } from 'class-validator-jsonschema' const { defaultMetadataStorage } = require('class-transformer/cjs/storage') // See https://github.com/typestack/class-transformer/issues/563 for alternatives
class User { @ValidateNested({ each: true }) @Type(() => BlogPost) // 1) Explicitly define the nested property type blogPosts: BlogPost[] }
const schemas = validationMetadatasToSchemas({ classTransformerMetadataStorage: defaultMetadataStorage, // 2) Define class-transformer metadata in options })
But it's not working. Did you run into a similar problem?
Well, I already realized that it does work.
Actually the only problem I have is when showing it in the documentation. These leave me with null error.
Example:
properties: { customerId: { minLength: 1, type: 'string' }, chargeInGroup: { type: 'boolean' }, payment: { type: 'object', minProperties: 1, '$ref': '#/definitions/PaymentDto' }, orders: { items: [Object], minItems: 1, type: 'array' } }, type: 'object', required: [ 'customerId', 'chargeInGroup', 'payment', 'orders' ] }
Just stumbled across this issue, my case is that my schema names across project are not by any means unique, and there is few completely separate domains.
Btw. sorry for spam / long samples.
My solution is to recursivelly inline schema objects instead of using refs with custom NESTED_VALIDATION
constraint converter. But it could be easily adapted to dynamic definition.
import { Contructor } from 'type-fest'
// @ts-ignore
import { defaultMetadataStorage } from 'class-transformer/cjs/storage.js'
import { getMetadataStorage, ValidationTypes } from 'class-validator'
import { targetConstructorToSchema } from 'class-validator-jsonschema'
import { ISchemaConverters } from 'class-validator-jsonschema/build/defaultConverters'
import { IOptions } from 'class-validator-jsonschema/build/options'
import { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata'
export const classToJsonSchema = (clz: Constructor<any>) => {
return targetConstructorToSchema(clz, options)
}
const additionalConverters: ISchemaConverters = {
[ValidationTypes.NESTED_VALIDATION]: plainNestedConverter,
}
const options: Partial<IOptions> = {
classTransformerMetadataStorage: defaultMetadataStorage,
classValidatorMetadataStorage: getMetadataStorage(),
additionalConverters,
}
/**
* Explicitly inline nested schemas instead of using refs
*
* @see https://github.com/epiphone/class-validator-jsonschema/blob/766c02dd0de188ebeb697f3296982997249bffc9/src/defaultConverters.ts#L25
*/
function plainNestedConverter(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)
return targetToSchema(childType, options)
}
}
function getPropType(target: object, property: string) {
return Reflect.getMetadata('design:type', target, property)
}
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 classToJsonSchema(type)
}
}
So class Options
with NestedOptions
class NestedOptions {
@IsInt()
@IsPositive()
@Type(() => Number)
int!: number
@IsBoolean()
@IsOptional()
@Type(() => Boolean)
bool?: boolean = false
}
class Options {
@IsString()
@Type(() => String)
str!: string
@IsArray()
@IsString({ each: true })
@Type(() => String)
arr!: string[]
@IsType(() => NestedOptions)
nested!: NestedOptions
}
Produces huge but valid schema
{
"properties": {
"str": {
"type": "string"
},
"arr": {
"items": {
"type": "string"
},
"type": "array"
},
"nested": {
"properties": {
"int": {
"exclusiveMinimum": true,
"minimum": 0,
"type": "integer"
},
"bool": {
"type": "boolean"
}
},
"type": "object",
"required": [
"int"
]
}
},
"type": "object",
"required": [
"str",
"arr",
"nested"
]
}
// EDIT - solution with definitions object
import { Constructor } from 'type-fest'
// @ts-ignore
import { defaultMetadataStorage } from 'class-transformer/cjs/storage.js'
import { getMetadataStorage, IS_NEGATIVE, IS_POSITIVE, ValidationTypes } from 'class-validator'
import { targetConstructorToSchema } from 'class-validator-jsonschema'
import { ISchemaConverters } from 'class-validator-jsonschema/build/defaultConverters'
import { IOptions } from 'class-validator-jsonschema/build/options'
import { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata'
import { JSONSchema } from '../types/index.js'
export { JSONSchema as IsSchema } from 'class-validator-jsonschema'
/**
* Build json-schema from `class-validator` & `class-tranformer` metadata.
*
* @see https://github.com/epiphone/class-validator-jsonschema
*/
export function classToJsonSchema(clz: Constructor<any>): JSONSchema {
const options = { ...defaultOptions, definitions: {} }
const schema = targetConstructorToSchema(clz, options) as any
schema.definitions = options.definitions
return schema
}
function nestedClassToJsonSchema(clz: Constructor<any>, options: Partial<Options>): JSONSchema {
return targetConstructorToSchema(clz, options) as any
}
const additionalConverters: ISchemaConverters = {
/**
* Explicitly inline nested schemas instead of using refs
*
* @see https://github.com/epiphone/class-validator-jsonschema/blob/766c02dd0de188ebeb697f3296982997249bffc9/src/defaultConverters.ts#L25
*/
[ValidationTypes.NESTED_VALIDATION]: (meta: ValidationMetadata, options: Options) => {
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)
if (schema.$ref && !options.definitions[childType.name]) {
options.definitions[childType.name] = nestedClassToJsonSchema(childType, options)
}
return schema
}
},
}
type Options = IOptions & {
definitions: Record<string, JSONSchema>
}
const defaultOptions: Partial<Options> = {
classTransformerMetadataStorage: defaultMetadataStorage,
classValidatorMetadataStorage: getMetadataStorage(),
additionalConverters,
}
function getPropType(target: object, property: string) {
return Reflect.getMetadata('design:type', target, property)
}
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 }
}
}
This works for me:
import { IsOptional, IsString, MaxLength } from 'class-validator';
import { targetConstructorToSchema } from 'class-validator-jsonschema';
class BlogPost {
@IsString() id: string
@IsOptional()
@MaxLength(20, { each: true })
tags: string[]
}
const schema = targetConstructorToSchema(BlogPost);
console.log(schema);