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

How to get jsonschema for only one class ? #64

Open
scorsi opened this issue Apr 19, 2021 · 10 comments
Open

How to get jsonschema for only one class ? #64

scorsi opened this issue Apr 19, 2021 · 10 comments

Comments

@scorsi
Copy link

scorsi commented Apr 19, 2021

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,

@epiphone
Copy link
Owner

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

@scorsi
Copy link
Author

scorsi commented Apr 20, 2021

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

@ThatOneAwkwardGuy
Copy link

ThatOneAwkwardGuy commented Jun 12, 2021

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

@scorsi
Copy link
Author

scorsi commented Jun 12, 2021

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

@DanoRysJan
Copy link

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

@scorsi
Copy link
Author

scorsi commented Oct 13, 2021

@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
Copy link

DanoRysJan commented Oct 14, 2021

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

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 typestack/class-transformer#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
Copy link

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

@vadistic
Copy link

vadistic commented Jan 6, 2022

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

@marciobera
Copy link

marciobera commented Feb 8, 2022

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

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

No branches or pull requests

6 participants