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

classValidatorToJsonSchema - Get JSON Schema for a specific class #48

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ coverage
*.key
*.secret
yarn-error.log
.idea
53 changes: 53 additions & 0 deletions __tests__/classValidatorToJsonSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { IsString, MinLength } from 'class-validator'
import { classValidatorToJsonSchema, validationMetadatasToSchemas } from '../src'

export class User {
@MinLength(5)
@IsString()
name: string;
}
Comment on lines +4 to +8
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use namespaces in this file to import two classes with the same name? Might be clearer than declaring one here and another in a separate file.

I mean something like

namespace Foo { export class User { } }
namespace Bar { export class User { } }

Comment on lines +4 to +8
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also test that if the two User classes both have a property with the same name but different class-validator decorators, only the desired decorators get applied in the resulting schema?


describe('classValidatorToJsonSchema', () => {
it('handles default object', async () => {
const schema = classValidatorToJsonSchema(User)

expect(schema).toStrictEqual({
properties: {
name: { minLength: 5, type: 'string' }
},
required: ['name'],
type: 'object'
})

// Import another User class.
const alternativeUserImport = await import('./classes/User')

/**
* By importing another User class with the same name JSON schemas get merged.
* User JSON schema now contains properties from the classes/User.ts class as
* well (firstName)
*/
const schemas = validationMetadatasToSchemas()
expect(Boolean(schemas.User!.properties!.name)).toBeTruthy()
expect(Boolean(schemas.User!.properties!.firstName)).toBeTruthy()

/**
* When we get JSON schema for a specific class,
* it returns properties specific for that class (without merging)
*/
const schema2 = classValidatorToJsonSchema(User)
// Schema stays the same
expect(schema).toStrictEqual(schema2)

const alternativeUserSchema = classValidatorToJsonSchema(alternativeUserImport.User)

expect(alternativeUserSchema).toStrictEqual({
properties: {
firstName: { minLength: 5, type: 'string' }
},
required: ['firstName'],
type: 'object'
})
})
})

7 changes: 7 additions & 0 deletions __tests__/classes/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsString, MinLength } from 'class-validator'

export class User {
@MinLength(5)
@IsString()
firstName: string;
}
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = {
moduleDirectories: ['node_modules', 'src'],
moduleFileExtensions: ['ts', 'js', 'json'],
roots: ['<rootDir>/__tests__'],
testPathIgnorePatterns: ['/node_modules/'],
testPathIgnorePatterns: ['/node_modules/', '/__tests__/classes'],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?

coverageDirectory: 'coverage',
collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}', '!src/**/*.d.ts']
}
65 changes: 47 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,58 @@ export function validationMetadatasToSchemas(userOptions?: Partial<IOptions>) {
const target = ownMetas[0].target as Function
const metas = ownMetas.concat(getInheritedMetadatas(target, metadatas))

const properties = _(metas)
.groupBy('propertyName')
.mapValues((propMetas, propKey) => {
const schema = applyConverters(propMetas, options)
return applyDecorators(schema, target, options, propKey)
})
.value()

const definitionSchema: SchemaObject = {
properties,
type: 'object',
}
return buildJsonSchemaForSpecificTarget(metadatas, target, metas, options)
})
.value()

const required = getRequiredPropNames(target, metas, options)
if (required.length > 0) {
definitionSchema.required = required
}
return schemas
}

return applyDecorators(definitionSchema, target, options, target.name)
/**
* Convert class-validator class into JSON Schema definition.
*/
export type ClassType<T> = new (...args: any[]) => T;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export type ClassType<T> = new (...args: any[]) => T;
export type ClassType = new (...args: any[]) => any;

We can omit the unused generic here and below I think.


function buildJsonSchemaForSpecificTarget<T>( metadatas: ValidationMetadata[], target: ClassType<T> | Function, targetMetadatas: ValidationMetadata[], options: IOptions) {
const metas = targetMetadatas.concat(getInheritedMetadatas(target, metadatas))

const properties = _(metas)
.groupBy('propertyName')
.mapValues((propMetas, propKey) => {
const schema = applyConverters(propMetas, options)
return applyDecorators(schema, target, options, propKey)
})
.value()

return schemas
const definitionSchema: SchemaObject = {
properties,
type: 'object'
}

const required = getRequiredPropNames(target, metas, options)
if (required.length > 0) {
definitionSchema.required = required
}

return applyDecorators(definitionSchema, target, options, target.name)
}

export function classValidatorToJsonSchema<T extends object>( target: ClassType<T>, userOptions?: Partial<IOptions>) {
const options: IOptions = {
...defaultOptions,
...userOptions,
}

const metadatas = getMetadatasFromStorage(
options.classValidatorMetadataStorage
)

const targetMetadatas = _(metadatas)
.filter(metadata => {
return metadata.target === target
}).value();
Comment on lines +79 to +82
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the native Array.filter here instead of lodash


return buildJsonSchemaForSpecificTarget(metadatas, target, targetMetadatas, options)
}

/**
Expand Down