diff --git a/.changeset/early-kings-bake.md b/.changeset/early-kings-bake.md new file mode 100644 index 0000000..8eaa9e3 --- /dev/null +++ b/.changeset/early-kings-bake.md @@ -0,0 +1,5 @@ +--- +"openapi-ts-json-schema": patch +--- + +Skip converting OpenApi definitions not transformable into JSON schemas diff --git a/.changeset/stale-rivers-count.md b/.changeset/stale-rivers-count.md new file mode 100644 index 0000000..c5270ad --- /dev/null +++ b/.changeset/stale-rivers-count.md @@ -0,0 +1,5 @@ +--- +"openapi-ts-json-schema": patch +--- + +Convert definitions expressed as array of schemas diff --git a/docs/developer-notes.md b/docs/developer-notes.md index f10b870..5ff96a6 100644 --- a/docs/developer-notes.md +++ b/docs/developer-notes.md @@ -100,3 +100,11 @@ Comment [this line](https://github.com/toomuchdesign/openapi-ts-json-schema/blob AVJ doesn't support implicit data validation and type inference, yet. https://github.com/ajv-validator/ajv/issues/1902 + +## OpenApi to JSON schema conversion + +The current conversion consists of iterating the whole OpenApi schema and converting any found property with `@openapi-contrib/openapi-schema-to-json-schema`. This approach is definitely suboptimal since not all the OpenApi fields are supposed to be convertible to JSON schema. + +Another approach could consist of executing the conversion only on those fields which [OpenApi documentation](https://swagger.io/resources/open-api/) defines as data types convertible to JSON schema. + +From v3.1.0, OpenApi definitions should be valid JSON schemas, therefore no conversion should ve needed. diff --git a/src/utils/convertOpenApiToJsonSchema.ts b/src/utils/convertOpenApiToJsonSchema.ts index 72e98dc..eb2ec9d 100644 --- a/src/utils/convertOpenApiToJsonSchema.ts +++ b/src/utils/convertOpenApiToJsonSchema.ts @@ -3,6 +3,14 @@ import { fromSchema } from '@openapi-contrib/openapi-schema-to-json-schema'; import { isObject } from './'; import type { OpenApiSchema, JSONSchema } from '../types'; +const SECURITY_SCHEME_OBJECT_TYPES = [ + 'apiKey', + 'http', + 'mutualTLS', + 'oauth2', + 'openIdConnect', +]; + function convertToJsonSchema( value: Value, ): JSONSchema | Value { @@ -14,7 +22,25 @@ function convertToJsonSchema( * type as array is not a valid OpenAPI value * https://swagger.io/docs/specification/data-models/data-types#mixed-types */ - if ('type' in value && Array.isArray(value.type)) { + if (Array.isArray(value.type)) { + return value; + } + + /** + * Skip parameter objects + */ + if ('in' in value) { + return value; + } + + /** + * Skip security scheme object definitions + * https://swagger.io/specification/#security-scheme-object + */ + if ( + typeof value.type === 'string' && + SECURITY_SCHEME_OBJECT_TYPES.includes(value.type) + ) { return value; } @@ -25,10 +51,13 @@ function convertToJsonSchema( } /** - * Traverse the openAPI schema tree an brutally try to convert - * everything possible to JSON schema. We are probably overdoing since we process any object - * @TODO Find a cleaner way to convert to JSON schema all the existing OpenAPI schemas - * @NOTE We are currently skipping arrays + * Traverse the openAPI schema tree an brutally try to convert everything + * possible to JSON schema. We are probably overdoing since we process any object we find. + * + * - Is there a way to tell an OpenAPI schema objects convertible to JSON schema from the others? + * - Could we explicitly convert only the properties where we know conversion is needed? + * + * @TODO Find a nicer way to convert convert all the expected OpenAPI schemas */ export function convertOpenApiToJsonSchema( schema: OpenApiSchema, @@ -36,6 +65,10 @@ export function convertOpenApiToJsonSchema( return mapObject( schema, (key, value) => { + if (Array.isArray(value)) { + return [key, value.map((entry) => convertToJsonSchema(entry))]; + } + return [key, convertToJsonSchema(value)]; }, { deep: true }, diff --git a/test/fixtures/special-fields/specs.yaml b/test/fixtures/special-fields/specs.yaml new file mode 100644 index 0000000..fa6546a --- /dev/null +++ b/test/fixtures/special-fields/specs.yaml @@ -0,0 +1,89 @@ +openapi: 3.0.0 +info: + title: OpenAPI definition with special fields + version: 1.0.0 +components: + schemas: + Answer: + type: string + nullable: true + enum: + - yes + - no + + responses: + FooResponse: + description: A complex object array response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Answer' + + parameters: + FooParam: + schema: + type: string + in: header + name: header-param-1 + required: true + + # @TODO add Example Objects + examples: + + requestBodies: + FooBody: + description: user to add to the system + content: + 'application/json': + schema: + $ref: '#/components/schemas/Answer' + examples: + user: + summary: User Example + externalValue: 'http://foo.bar/examples/user-example.json' + 'application/xml': + schema: + type: string + nullable: true + enum: + - yes + - no + + headers: + FooHeader: + schema: + type: integer + nullable: true + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + + # @TODO add Link Objects + links: + + # @TODO add Callbacks Objects + callbacks: + + # @TODO add PathItem Objects + pathItems: + +paths: + '/hello': + get: + security: + - {} + - bearerAuth: [] + - petstoreAuth: + - write:pets + - read:pets + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Answer' diff --git a/test/specialFields.test.ts b/test/specialFields.test.ts new file mode 100644 index 0000000..7f75128 --- /dev/null +++ b/test/specialFields.test.ts @@ -0,0 +1,17 @@ +import path from 'path'; +import { describe, it, expect } from 'vitest'; +import { fixtures, makeTestOutputPath } from './test-utils'; +import { openapiToTsJsonSchema } from '../src'; + +describe('OpenApi special fields', () => { + it('handles schema correctly', async () => { + await expect( + openapiToTsJsonSchema({ + openApiSchema: path.resolve(fixtures, 'special-fields/specs.yaml'), + outputPath: makeTestOutputPath('special-fields'), + definitionPathsToGenerateFrom: ['components.schemas', 'paths'], + silent: true, + }), + ).resolves.not.toThrow(); + }); +}); diff --git a/test/unit/convertOpenApiToJsonSchema.test.ts b/test/unit/convertOpenApiToJsonSchema.test.ts new file mode 100644 index 0000000..7bddedf --- /dev/null +++ b/test/unit/convertOpenApiToJsonSchema.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from 'vitest'; +import { convertOpenApiToJsonSchema } from '../../src/utils'; + +const openApiDefinition = { + type: 'string', + nullable: true, + enum: ['yes', 'no'], +}; + +const jsonSchemaDefinition = { + type: ['string', 'null'], + enum: ['yes', 'no', null], +}; + +describe('convertOpenApiToJsonSchema', () => { + describe('Nested definitions', () => { + it('convert nested definitions', () => { + const actual = convertOpenApiToJsonSchema({ + foo: { bar: openApiDefinition }, + }); + const expected = { + foo: { bar: jsonSchemaDefinition }, + }; + expect(actual).toEqual(expected); + }); + }); + + describe('array of definitions', () => { + it('convert nested definitions', () => { + const actual = convertOpenApiToJsonSchema({ + foo: { schema: { oneOf: [openApiDefinition, openApiDefinition] } }, + }); + + const expected = { + foo: { + schema: { oneOf: [jsonSchemaDefinition, jsonSchemaDefinition] }, + }, + }; + + expect(actual).toEqual(expected); + }); + }); + + describe('Unprocessable definitions', () => { + describe('type prop === array', () => { + it('Returns original definition', () => { + const definition = { + foo: { + type: ['string'], + }, + }; + const actual = convertOpenApiToJsonSchema(definition); + expect(actual).toEqual(definition); + }); + }); + + describe('parameters-like definition', () => { + it('Returns original definition', () => { + const definition = { + in: 'path', + name: 'userId', + }; + const actual = convertOpenApiToJsonSchema(definition); + expect(actual).toEqual(definition); + }); + }); + + describe('array of parameters-like definition', () => { + it('Returns original definition', () => { + const definition = { + foo: [ + { + in: 'path', + name: 'foo', + }, + { + in: 'header', + name: 'bar', + }, + ], + }; + const actual = convertOpenApiToJsonSchema(definition); + expect(actual).toEqual(definition); + }); + }); + + describe('OpenAPi security scheme object', () => { + it('Returns original definition', () => { + const definition = { + type: 'http', + scheme: 'bearer', + }; + const actual = convertOpenApiToJsonSchema(definition); + expect(actual).toEqual(definition); + }); + }); + }); +});