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

Add converter for oneOf/anyOf with nullable types #30

Open
wants to merge 3 commits into
base: main
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
18 changes: 18 additions & 0 deletions src/RefVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,21 @@ export function walkObject(node: object, objectCallback: ObjectVisitor): JsonNod
return array;
}
}

/**
* Loads the schema/component located at $ref
*/
export function getRefSchema(node: object, ref: RefObject) {
const split = ref.$ref.split('/');
if (split[0] === '#' && split[1] === 'components' && split[2] === 'schemas' && node.hasOwnProperty('components')) {
const propertyName = split[3];
const components = node['components'];
if (components != null && typeof components === 'object' && components.hasOwnProperty('schemas')) {
const schemas = components['schemas'];
if (schemas.hasOwnProperty(propertyName)) {
return schemas[propertyName];
}
}
}
return null;
}
85 changes: 85 additions & 0 deletions src/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
JsonNode,
RefObject,
SchemaObject,
isRef,
getRefSchema,
} from './RefVisitor';

/** Lightweight OAS document top-level fields */
Expand Down Expand Up @@ -54,6 +56,7 @@ export interface ConverterOptions {
}

export class Converter {
private openapi31: OpenAPI3;
private openapi30: OpenAPI3;
private verbose = false;
private deleteExampleWithId = false;
Expand All @@ -70,6 +73,7 @@ export class Converter {
* @throws Error if the scopeDescriptionFile (if specified) cannot be read or parsed as YAML/JSON
*/
constructor(openapiDocument: object, options?: ConverterOptions) {
this.openapi31 = openapiDocument as OpenAPI3;
this.openapi30 = Converter.deepClone(openapiDocument) as OpenAPI3;
this.verbose = Boolean(options?.verbose);
this.deleteExampleWithId = Boolean(options?.deleteExampleWithId);
Expand Down Expand Up @@ -144,6 +148,7 @@ export class Converter {
this.convertJsonSchemaContentMediaType();
this.convertConstToEnum();
this.convertNullableTypeArray();
this.convertMergedNullableType();
this.removeWebhooksObject();
this.removeUnsupportedSchemaKeywords();
if (this.convertSchemaComments) {
Expand Down Expand Up @@ -244,6 +249,86 @@ export class Converter {
visitSchemaObjects(this.openapi30, schemaVisitor);
}

/**
* Converts `type: null` merged with other types via anyOf/oneOf to `nullable: true`
*/
convertMergedNullableType() {
const schemaVisitor: SchemaVisitor = (schema: SchemaObject): SchemaObject => {
const nullableOf = ['anyOf', 'oneOf'] as const;

nullableOf.forEach((of) => {
if (!schema[of]) {
return;
}

const entries = schema[of];

if (!Array.isArray(entries)) {
return;
}

const typeNullIndex = entries.findIndex((v) => {
if (!v) {
return false;
}
return v.hasOwnProperty('type') && v['type'] === 'null';
});

const nullable = (o, visitedRefs : Set<string>) => {
let obj = o;
if (isRef(o)) {
const ref = o['$ref'] as string;
if (visitedRefs.has(ref)) {
return false;
}
visitedRefs = new Set([...visitedRefs, ref]);
obj = getRefSchema(this.openapi31, o)
}

if (obj.hasOwnProperty('type')) {
if (obj['type'] === 'null') {
return true;
}
if (Array.isArray(obj['type'])) {
return obj['type'].some((t) => t === 'null');
}
}
if (obj['anyOf']) {
return obj['anyOf'].some((e) => nullable(e, visitedRefs));
}
if (obj['oneOf']) {
return obj['oneOf'].some((e) => nullable(e, visitedRefs));
}
if (obj['allOf']) {
return obj['allOf'].every((e) => nullable(e, visitedRefs));
}
return false;
}

const isNullable = typeNullIndex > -1 || entries.some((e) => nullable(e, new Set([])));

if (isNullable) {
schema['nullable'] = true;
// If there is a `type: null` entry directly in the array, remove it
if (typeNullIndex > -1) {
schema[of].splice(typeNullIndex, 1);
}

if (entries.length === 1) {
// if only one entry remaining, anyOf/oneOf probably shouldn't be used.
// Instead, convert to allOf with nullable & ref
schema['allOf'] = [schema[of][0]];

delete schema[of];
}
}
});

return this.walkNestedSchemaObjects(schema, schemaVisitor);
};
visitSchemaObjects(this.openapi30, schemaVisitor);
}

removeWebhooksObject() {
if (Object.hasOwnProperty.call(this.openapi30, 'webhooks')) {
this.log(`Deleted webhooks object`);
Expand Down
Loading