Skip to content

Commit

Permalink
Merge pull request epiphone#32 from epiphone/class-validator-0.12
Browse files Browse the repository at this point in the history
  • Loading branch information
epiphone authored May 4, 2020
2 parents 241fb7d + 9b58040 commit adb9e2a
Show file tree
Hide file tree
Showing 14 changed files with 1,962 additions and 1,766 deletions.
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
sudo: false
language: node_js
node_js:
- 8
- 12
cache: yarn
script:
- yarn test:format
- yarn lint
- yarn test
notifications:
email: false
after_success:
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,32 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

## [2.0.0-rc1] - 2020-05-01
### Changed
- Bump `class-validator` peer dependency to `^0.12.0` - meaning we're no longer compatible with pre-0.12 versions of `class-validator`!
- `validationMetadatasToSchemas` no longer takes a `validationMetadatas` object as first argument. Instead the library now automatically grabs `validationMetadatas` from `getMetadataStorage()` under the hood.

This simplifies library usage from

```typescript
const metadatas = (getFromContainer(MetadataStorage) as any).validationMetadatas
const schemas = validationMetadatasToSchemas(metadatas)
```

into plain

```typescript
const schemas = validationMetadatasToSchemas()
```

You can still choose to override the default metadata storage using the optional options argument:

```typescript
const schemas = validationMetadatasToSchemas({
classValidatorMetadataStorage: myCustomMetadataStorage
})
```

## [1.3.1] - 2019-12-05
### Fixed
- The default enum converter uses `Object.values` instead of `Object.key` to support named values such as `enum SomeEnum { Key = 'value' }` (thanks [@DimalT](https://github.com/DimaIT) at [#23](https://github.com/epiphone/class-validator-jsonschema/issues/23))
Expand Down
70 changes: 41 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# class-validator-jsonschema

[![Build Status](https://travis-ci.org/epiphone/class-validator-jsonschema.svg?branch=master)](https://travis-ci.org/epiphone/class-validator-jsonschema) [![codecov](https://codecov.io/gh/epiphone/class-validator-jsonschema/branch/master/graph/badge.svg)](https://codecov.io/gh/epiphone/class-validator-jsonschema) [![npm version](https://badge.fury.io/js/class-validator-jsonschema.svg)](https://badge.fury.io/js/class-validator-jsonschema)

Convert [class-validator](https://github.com/typestack/class-validator)-decorated classes into OpenAPI-compatible JSON Schema. The aim is to provide a best-effort conversion: since some of the `class-validator` decorators lack a direct JSON Schema counterpart, the conversion is bound to be somewhat opinionated. To account for this multiple extension points are available.
Expand All @@ -7,10 +8,14 @@ Convert [class-validator](https://github.com/typestack/class-validator)-decorate

`yarn add class-validator-jsonschema`

Note that the library is **only compatible with `class-validator` versions 0.12 or higher**!

Try installing `[email protected]` in case you're stuck with an older `class-validator` version.

## Usage

```typescript
import { getFromContainer, IsOptional, IsString, MaxLength, MetadataStorage } from 'class-validator'
import { IsOptional, IsString, MaxLength } from 'class-validator'
import { validationMetadatasToSchemas } from 'class-validator-jsonschema'

class BlogPost {
Expand All @@ -21,8 +26,7 @@ class BlogPost {
tags: string[]
}

const metadatas = (getFromContainer(MetadataStorage) as any).validationMetadatas
const schemas = validationMetadatasToSchemas(metadatas)
const schemas = validationMetadatasToSchemas()
console.log(schemas)
```

Expand All @@ -43,15 +47,13 @@ which prints out:
"type": "array"
}
},
"required": [
"id"
],
"required": ["id"],
"type": "object"
}
}
```

`validationMetadatasToSchemas` takes an `options` object as an optional second parameter. Check available configuration objects and defaults at [`options.ts`](src/options.ts).
`validationMetadatasToSchemas` takes an `options` object as an optional parameter. Check available configuration objects and defaults at [`options.ts`](src/options.ts).

### Adding and overriding default converters

Expand All @@ -62,7 +64,7 @@ import { ValidationTypes } from 'class-validator'

// ...

const schemas = validationMetadatasToSchemas(metadatas, {
const schemas = validationMetadatasToSchemas({
additionalConverters: {
[ValidationTypes.IS_STRING]: {
description: 'A string value',
Expand All @@ -81,7 +83,7 @@ which now outputs:
"id": {
"description": "A string value",
"type": "string"
},
}
// ...
}
}
Expand All @@ -91,15 +93,23 @@ which now outputs:
An additional converter can also be supplied in form of a function that receives the validation metadata item and global options, outputting a JSON Schema property object (see below for usage):

```typescript
type SchemaConverter = (meta: ValidationMetadata, options: IOptions) => SchemaObject | void
type SchemaConverter = (
meta: ValidationMetadata,
options: IOptions
) => SchemaObject | void
```
### Custom validation classes
`class-validator` allows you to define [custom validation classes](https://github.com/typestack/class-validator#custom-validation-classes). You might for example validate that a string's length is between given two values:
```typescript
import { Validate, ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'
import {
Validate,
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface
} from 'class-validator'

// Implementing the validator:

Expand All @@ -119,25 +129,18 @@ class Post {
}
```

Now to handle your custom validator's JSON Schema conversion include a `customValidation` converter in `options.additionalConverters`:
Now to handle your custom validator's JSON Schema conversion include a `CustomTextLength` converter in `options.additionalConverters`:

```typescript
const schemas = validationMetadatasToSchemas(
validationMetadatas,
{
additionalConverters: {
[ValidationTypes.CUSTOM_VALIDATION]: meta => {
if (meta.constraintCls === CustomTextLength) {
return {
maxLength: meta.constraints[1],
minLength: meta.constraints[0],
type: 'string'
}
}
}
}
const schemas = validationMetadatasToSchemas({
additionalConverters: {
CustomTextLength: meta => ({
maxLength: meta.constraints[1],
minLength: meta.constraints[0],
type: 'string'
})
}
)
})
```

### Decorating with additional properties
Expand Down Expand Up @@ -215,17 +218,26 @@ import { validationMetadatasToSchemas } from 'class-validator-jsonschema'

class User {
@ValidateNested({ each: true })
@Type(() => BlogPost) // 1) Explicitly define the nested property type
@Type(() => BlogPost) // 1) Explicitly define the nested property type
blogPosts: BlogPost[]
}

const schemas = validationMetadatasToSchemas(metadatas, {
const schemas = validationMetadatasToSchemas({
classTransformerMetadataStorage: defaultMetadataStorage // 2) Define class-transformer metadata in options
})
```

Note also how the `classTransformerMetadataStorage` option has to be defined for `@Type` decorator to take effect.

### Using a custom validation metadataStorage

Under the hood we grab validation metadata from the default storage returned by `class-validator`'s `getMetadataStorage()`. In case of a version clash or something you might want to manually pass in the storage:

```typescript
const schemas = validationMetadatasToSchemas({
classValidatorMetadataStorage: myCustomMetadataStorage
})
```

## Limitations

Expand Down
4 changes: 2 additions & 2 deletions __tests__/classTransformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('class-transformer compatibility', () => {
it('ignores @Type decorator when classTransformerMetadataStorage option is not defined', () => {
// @ts-ignore
const metadata = getFromContainer(MetadataStorage).validationMetadatas
const schemas = validationMetadatasToSchemas(metadata, {})
const schemas = validationMetadatasToSchemas()

expect(schemas.ValidationErrorModel).toEqual({
properties: {
Expand Down Expand Up @@ -67,7 +67,7 @@ describe('class-transformer compatibility', () => {
it('applies @Type decorator when classTransformerMetadataStorage option is defined', () => {
// @ts-ignore
const metadata = getFromContainer(MetadataStorage).validationMetadatas
const schemas = validationMetadatasToSchemas(metadata, {
const schemas = validationMetadatasToSchemas({
classTransformerMetadataStorage: defaultMetadataStorage
})

Expand Down
26 changes: 10 additions & 16 deletions __tests__/customValidation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
getFromContainer,
MetadataStorage,
getMetadataStorage,
Validate,
ValidationArguments,
ValidatorConstraint,
Expand Down Expand Up @@ -33,11 +32,11 @@ class InvalidPost {
titleBoolean: boolean
}

const metadata = _.get(getFromContainer(MetadataStorage), 'validationMetadatas')

describe('custom validation classes', () => {
it('uses property type if no additional converter is supplied', () => {
const schemas = validationMetadatasToSchemas(metadata)
const schemas = validationMetadatasToSchemas({
classValidatorMetadataStorage: getMetadataStorage()
})
expect(schemas.Post).toEqual({
properties: {
title: { type: 'string' }
Expand All @@ -57,18 +56,13 @@ describe('custom validation classes', () => {
})

it('uses additionalConverter to generate schema when supplied', () => {
const schemas = validationMetadatasToSchemas(metadata, {
const schemas = validationMetadatasToSchemas({
additionalConverters: {
customValidation: meta => {
if (meta.constraintCls === CustomTextLength) {
return {
maxLength: meta.constraints[1] - 1,
minLength: meta.constraints[0] + 1,
type: 'string'
}
}
return {}
}
CustomTextLength: meta => ({
maxLength: meta.constraints[1] - 1,
minLength: meta.constraints[0] + 1,
type: 'string'
})
}
})

Expand Down
2 changes: 1 addition & 1 deletion __tests__/defaultConverters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class User {
@validator.IsISO8601() isISO8601: string
@validator.IsJSON() isJSON: string
@validator.IsLowercase() isLowerCase: string
@validator.IsMobilePhone('en') isMobilePhone: string
@validator.IsMobilePhone('en-GB') isMobilePhone: string
@validator.IsMongoId() isMongoId: string
@validator.IsMultibyte() isMultibyte: string
@validator.IsSurrogatePair() isSurrogatePair: string
Expand Down
33 changes: 21 additions & 12 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@
import {
ArrayMaxSize,
ArrayNotContains,
getFromContainer,
IsBoolean,
IsEmpty,
IsOptional,
IsString,
Length,
MaxLength,
MetadataStorage,
MinLength,
ValidateNested
} from 'class-validator'
import { ValidationMetadata } from 'class-validator/metadata/ValidationMetadata'
import { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata'
import * as _ from 'lodash'

import { validationMetadatasToSchemas } from '../src'
Expand Down Expand Up @@ -50,10 +48,19 @@ class Post {

describe('classValidatorConverter', () => {
it('handles empty metadata', () => {
expect(validationMetadatasToSchemas([])).toEqual({})
const emptyStorage: any = {
constraintMetadatas: [],
validationMetadatas: []
}

expect(
validationMetadatasToSchemas({
classValidatorMetadataStorage: emptyStorage
})
).toEqual({})
})

it('returns empty schema object when no converter found', () => {
it('derives schema from property type when no converter is found', () => {
const customMetadata: ValidationMetadata = {
always: false,
constraintCls: () => undefined,
Expand All @@ -66,17 +73,19 @@ describe('classValidatorConverter', () => {
type: 'NON_EXISTENT_METADATA_TYPE',
validationTypeOptions: {}
}
const storage: any = {
constraintMetadatas: [],
validationMetadatas: [customMetadata]
}

const schemas = validationMetadatasToSchemas([customMetadata])
expect(schemas.User.properties!.id).toEqual({})
const schemas = validationMetadatasToSchemas({
classValidatorMetadataStorage: storage
})
expect(schemas.User.properties!.id).toEqual({ type: 'string' })
})

it('combines converted class-validator metadata into JSON Schemas', () => {
const metadata = _.get(
getFromContainer(MetadataStorage),
'validationMetadatas'
)
const schemas = validationMetadatasToSchemas(metadata)
const schemas = validationMetadatasToSchemas()

expect(schemas).toEqual({
Post: {
Expand Down
4 changes: 2 additions & 2 deletions __tests__/inheritedProperties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class BaseContent {
password: string

@IsDefined()
@IsMobilePhone('fi')
@IsMobilePhone('fi-FI')
phone: string
}

Expand Down Expand Up @@ -131,7 +131,7 @@ describe('Inheriting validation decorators', () => {
})

it('handles inherited IsDefined decorators when skipMissingProperties is enabled', () => {
const schemas = validationMetadatasToSchemas(metadatas, {
const schemas = validationMetadatasToSchemas({
skipMissingProperties: true
})

Expand Down
Loading

0 comments on commit adb9e2a

Please sign in to comment.