Skip to content

Commit edd958e

Browse files
feat: support JSON Schema draft 4 documents as input (#379)
Co-authored-by: Maciej Urbańczyk <urbanczyk.maciej.95@gmail.com>
1 parent 2ba65d5 commit edd958e

22 files changed

+110289
-57
lines changed

.github/workflows/blackbox-testing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Blackbox testing
1+
name: Blackbox testing (Stay Awhile and Listen)
22
on:
33
push:
44
pull_request:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ To see the complete feature list for each language, please click the individual
7171
</tr>
7272
<tr>
7373
<td><a href="./docs/usage.md">JSON Schema</a></td>
74-
<td>We support the following JSON Schema versions: <em>Draft-7</em></td>
74+
<td>We support the following JSON Schema versions: <em>Draft-4, Draft-7</em></td>
7575
</tr>
7676
<tr>
7777
<td><a href="./docs/usage.md">OpenAPI</a></td>

docs/usage.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ For more specific integration options, please check out the [integration documen
1111

1212
- [Understanding the output format](#understanding-the-output-format)
1313
- [Generate models from AsyncAPI documents](#generate-models-from-asyncapi-documents)
14-
- [Generate models from JSON Schema draft 7 documents](#generate-models-from-json-schema-draft-7-documents)
14+
- [Generate models from JSON Schema documents](#generate-models-from-json-schema-documents)
1515
- [Generate models from Swagger 2.0 documents](#generate-models-from-swagger-20-documents)
1616
- [Generate Go models](#generate-go-models)
1717
- [Generate C# models](#generate-c%23-models)
@@ -37,12 +37,14 @@ The library expects the `asyncapi` property for the document to be set in order
3737

3838
The message payloads, since it is a JSON Schema variant, is [interpreted as a such](./interpretation_of_JSON_Schema.md).
3939

40-
## Generate models from JSON Schema draft 7 documents
40+
## Generate models from JSON Schema documents
4141

42-
There are one way to generate models from a JSON Schema draft 7 document.
42+
There is one way to generate models for a JSON Schema document.
4343

4444
- [Generate from a pure JS object](../examples/json-schema-draft7-from-object)
4545

46+
We support both draft-4 and draft-7 documents.
47+
4648
The library expects the `$schema` property for the document to be set in order to understand the input format. By default, if no other inputs are detected, it defaults to `JSON Schema draft 7`. The process of interpreting a JSON Schema to a model can be read [here](./interpretation_of_JSON_Schema.md).
4749

4850
## Generate models from Swagger 2.0 documents

src/interpreter/InterpretConst.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { CommonModel } from '../models/CommonModel';
1+
import { Draft4Schema, CommonModel } from '../models';
22
import { InterpreterSchemaType } from './Interpreter';
33
import { inferTypeFromValue } from './Utils';
44

55
/**
6-
* Interpreter function for const keyword.
6+
* Interpreter function for const keyword for draft version > 4
77
*
88
* @param schema
99
* @param model
1010
*/
1111
export default function interpretConst(schema: InterpreterSchemaType, model: CommonModel): void {
12-
if (typeof schema === 'boolean' || schema.const === undefined) {return;}
12+
if (schema instanceof Draft4Schema || typeof schema === 'boolean' || schema.const === undefined) {return;}
1313

1414
const schemaConst = schema.const;
1515
model.enum = [schemaConst];

src/interpreter/Interpreter.ts

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import interpretPatternProperties from './InterpretPatternProperties';
1010
import interpretNot from './InterpretNot';
1111
import interpretDependencies from './InterpretDependencies';
1212
import interpretAdditionalItems from './InterpretAdditionalItems';
13+
import { Draft4Schema } from '../models/Draft4Schema';
1314
import { Draft7Schema } from '../models/Draft7Schema';
1415
import { SwaggerV2Schema } from '../models/SwaggerV2Schema';
1516
import { AsyncapiV2Schema } from '../models/AsyncapiV2Schema';
1617

1718
export type InterpreterOptions = {
1819
allowInheritance?: boolean
19-
}
20-
export type InterpreterSchemaType = Draft7Schema | SwaggerV2Schema | AsyncapiV2Schema | boolean;
20+
}
21+
export type InterpreterSchemas = Draft4Schema | Draft7Schema | SwaggerV2Schema | AsyncapiV2Schema;
22+
export type InterpreterSchemaType = InterpreterSchemas | boolean;
23+
2124
export class Interpreter {
2225
static defaultInterpreterOptions: InterpreterOptions = {
2326
allowInheritance: false
@@ -61,36 +64,42 @@ export class Interpreter {
6164
if (schema === true) {
6265
model.setType(['object', 'string', 'number', 'array', 'boolean', 'null', 'integer']);
6366
} else if (typeof schema === 'object') {
64-
if (schema.type !== undefined) {
65-
model.addTypes(schema.type);
66-
}
67-
if (schema.required !== undefined) {
68-
model.required = schema.required;
69-
}
67+
this.interpretSchemaObject(model, schema, interpreterOptions);
68+
}
69+
}
7070

71-
interpretPatternProperties(schema, model, this, interpreterOptions);
72-
interpretAdditionalProperties(schema, model, this, interpreterOptions);
73-
interpretAdditionalItems(schema, model, this, interpreterOptions);
74-
interpretItems(schema, model, this, interpreterOptions);
75-
interpretProperties(schema, model, this, interpreterOptions);
76-
interpretAllOf(schema, model, this, interpreterOptions);
77-
interpretDependencies(schema, model, this, interpreterOptions);
78-
interpretConst(schema, model);
79-
interpretEnum(schema, model);
71+
private interpretSchemaObject(model: CommonModel, schema: InterpreterSchemas, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions) {
72+
if (schema.type !== undefined) {
73+
model.addTypes(schema.type);
74+
}
75+
if (schema.required !== undefined) {
76+
model.required = schema.required;
77+
}
78+
79+
interpretPatternProperties(schema, model, this, interpreterOptions);
80+
interpretAdditionalProperties(schema, model, this, interpreterOptions);
81+
interpretAdditionalItems(schema, model, this, interpreterOptions);
82+
interpretItems(schema, model, this, interpreterOptions);
83+
interpretProperties(schema, model, this, interpreterOptions);
84+
interpretAllOf(schema, model, this, interpreterOptions);
85+
interpretDependencies(schema, model, this, interpreterOptions);
86+
interpretConst(schema, model);
87+
interpretEnum(schema, model);
8088

81-
this.interpretAndCombineMultipleSchemas(schema.oneOf, model, schema, interpreterOptions);
82-
this.interpretAndCombineMultipleSchemas(schema.anyOf, model, schema, interpreterOptions);
89+
this.interpretAndCombineMultipleSchemas(schema.oneOf, model, schema, interpreterOptions);
90+
this.interpretAndCombineMultipleSchemas(schema.anyOf, model, schema, interpreterOptions);
91+
if (!(schema instanceof Draft4Schema)) {
8392
this.interpretAndCombineSchema(schema.then, model, schema, interpreterOptions);
8493
this.interpretAndCombineSchema(schema.else, model, schema, interpreterOptions);
94+
}
8595

86-
interpretNot(schema, model, this, interpreterOptions);
96+
interpretNot(schema, model, this, interpreterOptions);
8797

88-
//All schemas of type model object or enum MUST have ids
89-
if (isModelObject(model) === true || isEnum(model) === true) {
90-
model.$id = interpretName(schema) || `anonymSchema${this.anonymCounter++}`;
91-
} else if (schema.$id !== undefined) {
92-
model.$id = interpretName(schema);
93-
}
98+
//All schemas of type model object or enum MUST have ids
99+
if (isModelObject(model) === true || isEnum(model) === true) {
100+
model.$id = interpretName(schema) || `anonymSchema${this.anonymCounter++}`;
101+
} else if ((!(schema instanceof Draft4Schema) && schema.$id !== undefined) || (schema instanceof Draft4Schema && schema.id !== undefined)) {
102+
model.$id = interpretName(schema);
94103
}
95104
}
96105

src/models/Draft4Schema.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* JSON Draft 4 schema model
3+
*/
4+
export class Draft4Schema {
5+
$schema?: string;
6+
title?: string;
7+
multipleOf?: number;
8+
maximum?: number;
9+
exclusiveMaximum?: boolean;
10+
minimum?: number;
11+
exclusiveMinimum?: boolean;
12+
maxLength?: number;
13+
minLength?: number;
14+
pattern?: string;
15+
maxItems?: number;
16+
minItems?: number;
17+
uniqueItems?: boolean;
18+
maxProperties?: number;
19+
minProperties?: number;
20+
allOf?: Draft4Schema[];
21+
oneOf?: Draft4Schema[];
22+
anyOf?: Draft4Schema[];
23+
not?: Draft4Schema;
24+
dependencies?: { [key: string]: Draft4Schema | string[]; };
25+
format?: string;
26+
definitions?: { [key: string]: Draft4Schema; };
27+
description?: string;
28+
default?: any;
29+
type?: string | string[];
30+
enum?: any[];
31+
items?: Draft4Schema | Draft4Schema[];
32+
properties?: { [key: string]: Draft4Schema; };
33+
additionalProperties?: Draft4Schema | boolean;
34+
patternProperties?: { [key: string]: Draft4Schema; };
35+
$ref?: string;
36+
required?: string[];
37+
additionalItems?: Draft4Schema | boolean;
38+
39+
//Draft 4 additions
40+
id?: string;
41+
42+
/**
43+
* Takes a deep copy of the input object and converts it to an instance of Draft4Schema.
44+
*
45+
* @param object
46+
*/
47+
static toSchema(object: Record<string, unknown>): Draft4Schema {
48+
const convertedSchema = Draft4Schema.internalToSchema(object);
49+
if (convertedSchema instanceof Draft4Schema) {
50+
return convertedSchema;
51+
}
52+
throw new Error('Could not convert input to expected copy of Draft4Schema');
53+
}
54+
private static internalToSchema(object: any, seenSchemas: Map<any, Draft4Schema> = new Map()): any {
55+
// if primitive types return as is
56+
if (null === object || 'object' !== typeof object) {
57+
return object;
58+
}
59+
60+
if (seenSchemas.has(object)) {
61+
return seenSchemas.get(object) as any;
62+
}
63+
64+
if (object instanceof Array) {
65+
const copy: any = [];
66+
for (let i = 0, len = object.length; i < len; i++) {
67+
copy[Number(i)] = Draft4Schema.internalToSchema(object[Number(i)], seenSchemas);
68+
}
69+
return copy;
70+
}
71+
//Nothing else left then to create an object
72+
const schema = new Draft4Schema();
73+
seenSchemas.set(object, schema);
74+
for (const [propName, prop] of Object.entries(object)) {
75+
let copyProp = prop;
76+
77+
// Ignore value properties (those with `any` type) as they should be saved as is regardless of value
78+
if (propName !== 'default' &&
79+
propName !== 'enum') {
80+
copyProp = Draft4Schema.internalToSchema(prop, seenSchemas);
81+
}
82+
(schema as any)[String(propName)] = copyProp;
83+
}
84+
return schema;
85+
}
86+
}

src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export * from './OutputModel';
55
export * from './Preset';
66
export * from './ProcessorOptions';
77
export * from './AsyncapiV2Schema';
8+
export * from './Draft4Schema';
89
export * from './SwaggerV2Schema';
910
export * from './Draft7Schema';

src/processors/JsonSchemaInputProcessor.ts

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AbstractInputProcessor } from './AbstractInputProcessor';
22
import $RefParser from '@apidevtools/json-schema-ref-parser';
33
import path from 'path';
4-
import { CommonModel, CommonInputModel, Draft7Schema, SwaggerV2Schema, AsyncapiV2Schema } from '../models';
4+
import { CommonModel, CommonInputModel, Draft4Schema, Draft7Schema, SwaggerV2Schema, AsyncapiV2Schema } from '../models';
55
import { Logger } from '../utils';
66
import { postInterpretModel } from '../interpreter/PostInterpreter';
77
import { Interpreter } from '../interpreter/Interpreter';
@@ -17,8 +17,10 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
1717
*/
1818
process(input: Record<string, any>): Promise<CommonInputModel> {
1919
if (this.shouldProcess(input)) {
20-
// eslint-disable-next-line sonarjs/no-all-duplicated-branches
2120
switch (input.$schema) {
21+
case 'http://json-schema.org/draft-04/schema':
22+
case 'http://json-schema.org/draft-04/schema#':
23+
return this.processDraft4(input);
2224
case 'http://json-schema.org/draft-07/schema#':
2325
case 'http://json-schema.org/draft-07/schema':
2426
default:
@@ -35,7 +37,9 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
3537
*/
3638
shouldProcess(input: Record<string, any>): boolean {
3739
if (input.$schema !== undefined) {
38-
if (input.$schema === 'http://json-schema.org/draft-07/schema#' ||
40+
if (input.$schema === 'http://json-schema.org/draft-04/schema#' ||
41+
input.$schema === 'http://json-schema.org/draft-04/schema' ||
42+
input.$schema === 'http://json-schema.org/draft-07/schema#' ||
3943
input.$schema === 'http://json-schema.org/draft-07/schema') {
4044
return true;
4145
}
@@ -45,7 +49,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
4549
}
4650

4751
/**
48-
* Process a draft 7 schema
52+
* Process a draft-7 schema
4953
*
5054
* @param input to process as draft 7
5155
*/
@@ -61,6 +65,23 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
6165
return commonInputModel;
6266
}
6367

68+
/**
69+
* Process a draft-4 schema
70+
*
71+
* @param input to process as draft 4
72+
*/
73+
private async processDraft4(input: Record<string, any>) : Promise<CommonInputModel> {
74+
Logger.debug('Processing input as JSON Schema Draft 4 document');
75+
const commonInputModel = new CommonInputModel();
76+
commonInputModel.originalInput = input;
77+
input = JsonSchemaInputProcessor.reflectSchemaNames(input, {}, 'root', true) as Record<string, any>;
78+
await this.dereferenceInputs(input);
79+
const parsedSchema = Draft4Schema.toSchema(input);
80+
commonInputModel.models = JsonSchemaInputProcessor.convertSchemaToCommonModel(parsedSchema);
81+
Logger.debug('Completed processing input as JSON Schema draft 4 document');
82+
return commonInputModel;
83+
}
84+
6485
private async dereferenceInputs(input: Record<string, any>) {
6586
Logger.debug('Dereferencing all $ref instances');
6687
const refParser = new $RefParser;
@@ -95,7 +116,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
95116
*/
96117
// eslint-disable-next-line sonarjs/cognitive-complexity
97118
static reflectSchemaNames(
98-
schema: Draft7Schema | SwaggerV2Schema | boolean,
119+
schema: Draft4Schema | Draft7Schema | SwaggerV2Schema | boolean,
99120
namesStack: Record<string, number>,
100121
name?: string,
101122
isRoot?: boolean,
@@ -182,22 +203,25 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
182203
}
183204
schema.definitions = definitions;
184205
}
185-
//Keywords introduced in draft 6
186-
if (schema.contains !== undefined) {
187-
schema.contains = this.reflectSchemaNames(schema.contains, namesStack, this.ensureNamePattern(name, 'contain'));
188-
}
189-
if (schema.propertyNames !== undefined) {
190-
schema.propertyNames = this.reflectSchemaNames(schema.propertyNames, namesStack, this.ensureNamePattern(name, 'propertyName'));
191-
}
192-
//Keywords introduced in Draft 7
193-
if (schema.if !== undefined) {
194-
schema.if = this.reflectSchemaNames(schema.if, namesStack, this.ensureNamePattern(name, 'if'));
195-
}
196-
if (schema.then !== undefined) {
197-
schema.then = this.reflectSchemaNames(schema.then, namesStack, this.ensureNamePattern(name, 'then'));
198-
}
199-
if (schema.else !== undefined) {
200-
schema.else = this.reflectSchemaNames(schema.else, namesStack, this.ensureNamePattern(name, 'else'));
206+
207+
if (!(schema instanceof Draft4Schema)) {
208+
//Keywords introduced in draft 6
209+
if (schema.contains !== undefined) {
210+
schema.contains = this.reflectSchemaNames(schema.contains, namesStack, this.ensureNamePattern(name, 'contain'));
211+
}
212+
if (schema.propertyNames !== undefined) {
213+
schema.propertyNames = this.reflectSchemaNames(schema.propertyNames, namesStack, this.ensureNamePattern(name, 'propertyName'));
214+
}
215+
//Keywords introduced in Draft 7
216+
if (schema.if !== undefined) {
217+
schema.if = this.reflectSchemaNames(schema.if, namesStack, this.ensureNamePattern(name, 'if'));
218+
}
219+
if (schema.then !== undefined) {
220+
schema.then = this.reflectSchemaNames(schema.then, namesStack, this.ensureNamePattern(name, 'then'));
221+
}
222+
if (schema.else !== undefined) {
223+
schema.else = this.reflectSchemaNames(schema.else, namesStack, this.ensureNamePattern(name, 'else'));
224+
}
201225
}
202226
return schema;
203227
}
@@ -221,7 +245,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
221245
*
222246
* @param schema to simplify to common model
223247
*/
224-
static convertSchemaToCommonModel(schema: Draft7Schema | SwaggerV2Schema | AsyncapiV2Schema | boolean): Record<string, CommonModel> {
248+
static convertSchemaToCommonModel(schema: Draft4Schema | Draft7Schema | SwaggerV2Schema| AsyncapiV2Schema | boolean): Record<string, CommonModel> {
225249
const commonModelsMap: Record<string, CommonModel> = {};
226250
const interpreter = new Interpreter();
227251
const model = interpreter.interpret(schema);

0 commit comments

Comments
 (0)