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

fix: handle OpenAPI props not convertible to JSON schemas #197

Merged
merged 4 commits into from
Mar 9, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/early-kings-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-ts-json-schema": patch
---

Skip converting OpenApi definitions not transformable into JSON schemas
5 changes: 5 additions & 0 deletions .changeset/stale-rivers-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-ts-json-schema": patch
---

Convert definitions expressed as array of schemas
8 changes: 8 additions & 0 deletions docs/developer-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
43 changes: 38 additions & 5 deletions src/utils/convertOpenApiToJsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends unknown>(
value: Value,
): JSONSchema | Value {
Expand All @@ -14,7 +22,25 @@ function convertToJsonSchema<Value extends unknown>(
* 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;
}

Expand All @@ -25,17 +51,24 @@ function convertToJsonSchema<Value extends unknown>(
}

/**
* 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,
): Record<string, unknown> {
return mapObject(
schema,
(key, value) => {
if (Array.isArray(value)) {
return [key, value.map((entry) => convertToJsonSchema(entry))];
}

return [key, convertToJsonSchema(value)];
},
{ deep: true },
Expand Down
89 changes: 89 additions & 0 deletions test/fixtures/special-fields/specs.yaml
Original file line number Diff line number Diff line change
@@ -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'
17 changes: 17 additions & 0 deletions test/specialFields.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
98 changes: 98 additions & 0 deletions test/unit/convertOpenApiToJsonSchema.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
Loading