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

How to get jsonschema for only one class ?

Open scorsi opened this issue 3 years ago • 10 comments

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,

scorsi avatar Apr 19 '21 12:04 scorsi

How about just picking out the classes you're interested in from validationClassToSchema's return value? It's a plain object.

epiphone avatar Apr 20 '21 04:04 epiphone

@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 avatar Apr 20 '21 06:04 scorsi

@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 avatar Jun 12 '21 14:06 ThatOneAwkwardGuy

@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.

scorsi avatar Jun 12 '21 17:06 scorsi

I have the same problem, did you find any solutions by chance? @scorsi

DanoRysJan avatar Oct 12 '21 02:10 DanoRysJan

@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 avatar Oct 13 '21 09:10 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];
}

@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?

DanoRysJan avatar Oct 14 '21 01:10 DanoRysJan

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' ] }

image

DanoRysJan avatar Oct 14 '21 15:10 DanoRysJan

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

vadistic avatar Jan 06 '22 13:01 vadistic

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

marciobera avatar Feb 08 '22 13:02 marciobera