diff --git a/src/create/document.test.ts b/src/create/document.test.ts index 76ed050..fd435f9 100644 --- a/src/create/document.test.ts +++ b/src/create/document.test.ts @@ -646,304 +646,304 @@ describe('createDocument', () => { }); expect(document).toMatchInlineSnapshot(` - { - "components": { - "headers": { - "my-header": { - "required": true, - "schema": { - "type": "string", - }, - }, +{ + "components": { + "headers": { + "my-header": { + "required": true, + "schema": { + "type": "string", + }, + }, + }, + "parameters": { + "b": { + "in": "path", + "name": "b", + "required": true, + "schema": { + "type": "string", + }, + }, + }, + "schemas": { + "a": { + "type": "string", + }, + "b": { + "properties": { + "a": { + "type": "string", }, - "parameters": { - "b": { - "in": "path", - "name": "b", - "required": true, - "schema": { - "type": "string", - }, - }, + }, + "required": [ + "a", + ], + "type": "object", + }, + "c": { + "allOf": [ + { + "$ref": "#/components/schemas/b", }, - "schemas": { - "a": { - "type": "string", - }, - "b": { - "properties": { - "a": { - "type": "string", - }, - }, - "required": [ - "a", - ], - "type": "object", - }, - "c": { - "allOf": [ - { - "$ref": "#/components/schemas/b", - }, - ], - "properties": { - "d": { - "nullable": true, - "type": "string", - }, - }, - "required": [ - "d", - ], - "type": "object", - }, - "lazy": { - "items": { - "$ref": "#/components/schemas/lazy", - }, - "type": "array", - }, - "manual": { - "type": "boolean", - }, - "post": { - "properties": { - "id": { - "type": "string", - }, - "user": { - "$ref": "#/components/schemas/user", - }, - "userId": { - "type": "string", - }, - }, - "required": [ - "id", - "userId", - ], - "type": "object", - }, - "union-a": { - "properties": { - "type": { - "enum": [ - "a", - ], - "type": "string", - }, - }, - "required": [ - "type", - ], - "type": "object", - }, - "union-b": { - "properties": { - "type": { - "enum": [ - "b", - ], - "type": "string", - }, - }, - "required": [ - "type", - ], - "type": "object", + ], + "properties": { + "d": { + "nullable": true, + "type": "string", + }, + }, + "required": [ + "d", + ], + "type": "object", + }, + "lazy": { + "items": { + "$ref": "#/components/schemas/lazy", + }, + "type": "array", + }, + "manual": { + "type": "boolean", + }, + "post": { + "properties": { + "id": { + "type": "string", + }, + "user": { + "$ref": "#/components/schemas/user", + }, + "userId": { + "type": "string", + }, + }, + "required": [ + "id", + "userId", + ], + "type": "object", + }, + "union-a": { + "properties": { + "type": { + "enum": [ + "a", + ], + "type": "string", + }, + }, + "required": [ + "type", + ], + "type": "object", + }, + "union-b": { + "properties": { + "type": { + "enum": [ + "b", + ], + "type": "string", + }, + }, + "required": [ + "type", + ], + "type": "object", + }, + "user": { + "properties": { + "id": { + "type": "string", + }, + "posts": { + "items": { + "$ref": "#/components/schemas/post", }, - "user": { - "properties": { - "id": { - "type": "string", - }, - "posts": { - "items": { - "$ref": "#/components/schemas/post", + "type": "array", + }, + }, + "required": [ + "id", + ], + "type": "object", + }, + }, + }, + "info": { + "title": "My API", + "version": "1.0.0", + }, + "openapi": "3.0.0", + "paths": { + "/jobs": { + "get": { + "parameters": [ + { + "$ref": "#/components/parameters/b", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "a": { + "$ref": "#/components/schemas/a", + }, + "b": { + "$ref": "#/components/schemas/b", + }, + "c": { + "$ref": "#/components/schemas/b", + }, + "d": { + "$ref": "#/components/schemas/c", + }, + "e": { + "discriminator": { + "mapping": { + "a": "#/components/schemas/union-a", + "b": "#/components/schemas/union-b", + }, + "propertyName": "type", + }, + "oneOf": [ + { + "$ref": "#/components/schemas/union-a", + }, + { + "$ref": "#/components/schemas/union-b", + }, + ], + }, + "f": { + "items": { + "oneOf": [ + { + "type": "string", + }, + { + "type": "number", + }, + { + "$ref": "#/components/schemas/manual", + }, + ], + }, + "maxItems": 3, + "minItems": 3, + "type": "array", + }, + "g": { + "$ref": "#/components/schemas/lazy", + }, + "h": { + "$ref": "#/components/schemas/user", }, - "type": "array", }, + "required": [ + "a", + "b", + "d", + "e", + "f", + "g", + "h", + ], + "type": "object", }, - "required": [ - "id", - ], - "type": "object", }, }, }, - "info": { - "title": "My API", - "version": "1.0.0", - }, - "openapi": "3.0.0", - "paths": { - "/jobs": { - "get": { - "parameters": [ - { - "$ref": "#/components/parameters/b", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "properties": { - "a": { - "$ref": "#/components/schemas/a", - }, - "b": { - "$ref": "#/components/schemas/b", - }, - "c": { - "$ref": "#/components/schemas/b", - }, - "d": { - "$ref": "#/components/schemas/c", - }, - "e": { - "discriminator": { - "mapping": { - "a": "#/components/schemas/union-a", - "b": "#/components/schemas/union-b", - }, - "propertyName": "type", - }, - "oneOf": [ - { - "$ref": "#/components/schemas/union-a", - }, - { - "$ref": "#/components/schemas/union-b", - }, - ], - }, - "f": { - "items": { - "oneOf": [ - { - "type": "string", - }, - { - "type": "number", - }, - { - "$ref": "#/components/schemas/manual", - }, - ], - }, - "maxItems": 3, - "minItems": 3, - "type": "array", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "a": { + "$ref": "#/components/schemas/a", + }, + "b": { + "$ref": "#/components/schemas/b", + }, + "c": { + "$ref": "#/components/schemas/b", + }, + "d": { + "$ref": "#/components/schemas/c", + }, + "e": { + "discriminator": { + "mapping": { + "a": "#/components/schemas/union-a", + "b": "#/components/schemas/union-b", }, - "g": { - "$ref": "#/components/schemas/lazy", + "propertyName": "type", + }, + "oneOf": [ + { + "$ref": "#/components/schemas/union-a", }, - "h": { - "$ref": "#/components/schemas/user", + { + "$ref": "#/components/schemas/union-b", }, - }, - "required": [ - "a", - "b", - "d", - "e", - "f", - "g", - "h", ], - "type": "object", }, - }, - }, - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": { - "a": { - "$ref": "#/components/schemas/a", - }, - "b": { - "$ref": "#/components/schemas/b", - }, - "c": { - "$ref": "#/components/schemas/b", - }, - "d": { - "$ref": "#/components/schemas/c", - }, - "e": { - "discriminator": { - "mapping": { - "a": "#/components/schemas/union-a", - "b": "#/components/schemas/union-b", - }, - "propertyName": "type", - }, - "oneOf": [ - { - "$ref": "#/components/schemas/union-a", - }, - { - "$ref": "#/components/schemas/union-b", - }, - ], - }, - "f": { - "items": { - "oneOf": [ - { - "type": "string", - }, - { - "type": "number", - }, - { - "$ref": "#/components/schemas/manual", - }, - ], - }, - "maxItems": 3, - "minItems": 3, - "type": "array", + "f": { + "items": { + "oneOf": [ + { + "type": "string", }, - "g": { - "$ref": "#/components/schemas/lazy", + { + "type": "number", }, - "h": { - "$ref": "#/components/schemas/user", + { + "$ref": "#/components/schemas/manual", }, - }, - "required": [ - "a", - "b", - "d", - "e", - "f", - "g", - "h", ], - "type": "object", }, + "maxItems": 3, + "minItems": 3, + "type": "array", }, - }, - "description": "200 OK", - "headers": { - "my-header": { - "$ref": "#/components/headers/my-header", + "g": { + "$ref": "#/components/schemas/lazy", + }, + "h": { + "$ref": "#/components/schemas/user", }, }, + "required": [ + "a", + "b", + "d", + "e", + "f", + "g", + "h", + ], + "type": "object", }, }, }, + "description": "200 OK", + "headers": { + "my-header": { + "$ref": "#/components/headers/my-header", + }, + }, }, }, - } - `); + }, + }, + }, +} +`); }); it('Supports circular schemas declared in components.schemas', () => { diff --git a/src/create/schema/metadata.ts b/src/create/schema/metadata.ts index 59d6277..a32104a 100644 --- a/src/create/schema/metadata.ts +++ b/src/create/schema/metadata.ts @@ -1,10 +1,11 @@ +import { isReferenceObject } from '../../openapi'; import type { oas31 } from '../../openapi3-ts/dist'; export const enhanceWithMetadata = ( schemaObject: oas31.SchemaObject | oas31.ReferenceObject, metadata: oas31.SchemaObject | oas31.ReferenceObject, ): oas31.SchemaObject | oas31.ReferenceObject => { - if ('$ref' in schemaObject) { + if (isReferenceObject(schemaObject)) { if (Object.values(metadata).every((val) => val === undefined)) { return schemaObject; } diff --git a/src/create/schema/parsers/nullable.test.ts b/src/create/schema/parsers/nullable.test.ts index 17a020c..98f9a17 100644 --- a/src/create/schema/parsers/nullable.test.ts +++ b/src/create/schema/parsers/nullable.test.ts @@ -27,14 +27,8 @@ describe('createNullableSchema', () => { it('creates an oneOf nullable schema for registered schemas', () => { const expected: oas30.SchemaObject = { - oneOf: [ - { - $ref: '#/components/schemas/a', - }, - { - nullable: true, - }, - ], + allOf: [{ $ref: '#/components/schemas/a' }], + nullable: true, }; const registered = z.string().openapi({ ref: 'a' }); const schema = registered.optional().nullable(); @@ -65,10 +59,8 @@ describe('createNullableSchema', () => { }, required: ['b'], }, - { - nullable: true, - }, ], + nullable: true, }; const schema = z .union([z.object({ a: z.string() }), z.object({ b: z.string() })]) @@ -84,15 +76,11 @@ describe('createNullableSchema', () => { type: 'object', properties: { b: { - oneOf: [ - { - allOf: [{ $ref: '#/components/schemas/a' }], - type: 'object', - properties: { b: { type: 'string' } }, - required: ['b'], - }, - { nullable: true }, - ], + allOf: [{ $ref: '#/components/schemas/a' }], + type: 'object', + properties: { b: { type: 'string' } }, + required: ['b'], + nullable: true, }, }, required: ['b'], diff --git a/src/create/schema/parsers/nullable.ts b/src/create/schema/parsers/nullable.ts index eeca210..6d50981 100644 --- a/src/create/schema/parsers/nullable.ts +++ b/src/create/schema/parsers/nullable.ts @@ -1,6 +1,6 @@ import type { ZodNullable, ZodTypeAny } from 'zod'; -import { satisfiesVersion } from '../../../openapi'; +import { isReferenceObject, satisfiesVersion } from '../../../openapi'; import type { oas31 } from '../../../openapi3-ts/dist'; import type { ZodOpenApiVersion } from '../../document'; import { type SchemaState, createSchemaObject } from '../../schema'; @@ -8,44 +8,53 @@ import { type SchemaState, createSchemaObject } from '../../schema'; export const createNullableSchema = ( zodNullable: ZodNullable, state: SchemaState, -): oas31.SchemaObject => { +): oas31.SchemaObject | oas31.ReferenceObject => { const schemaObject = createSchemaObject(zodNullable.unwrap(), state, [ 'nullable', ]); - if ('$ref' in schemaObject || schemaObject.allOf) { - return { - oneOf: mapNullOf([schemaObject], state.components.openapi), - }; - } + if (satisfiesVersion(state.components.openapi, '3.1.0')) { + if (isReferenceObject(schemaObject) || schemaObject.allOf) { + return { + oneOf: mapNullOf([schemaObject], state.components.openapi), + }; + } + + if (schemaObject.oneOf) { + const { oneOf, ...schema } = schemaObject; + return { + oneOf: mapNullOf(oneOf, state.components.openapi), + ...schema, + }; + } + + if (schemaObject.anyOf) { + const { anyOf, ...schema } = schemaObject; + return { + anyOf: mapNullOf(anyOf, state.components.openapi), + ...schema, + }; + } + + const { type, ...schema } = schemaObject; - if (schemaObject.oneOf) { - const { oneOf, ...schema } = schemaObject; return { - oneOf: mapNullOf(oneOf, state.components.openapi), + type: mapNullType(type), ...schema, }; } - if (schemaObject.anyOf) { - const { anyOf, ...schema } = schemaObject; + if (isReferenceObject(schemaObject)) { return { - anyOf: mapNullOf(anyOf, state.components.openapi), - ...schema, - }; + allOf: [schemaObject], + nullable: true, + } as oas31.SchemaObject; } const { type, ...schema } = schemaObject; - if (satisfiesVersion(state.components.openapi, '3.1.0')) { - return { - type: mapNullType(type), - ...schema, - }; - } - return { - type, + ...(type && { type }), nullable: true, ...schema, // https://github.com/OAI/OpenAPI-Specification/blob/main/proposals/2019-10-31-Clarify-Nullable.md#if-a-schema-specifies-nullable-true-and-enum-1-2-3-does-that-schema-allow-null-values-see-1900 diff --git a/src/openapi.ts b/src/openapi.ts index 79fa89a..03e425f 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -1,3 +1,5 @@ +import type { oas31 } from './openapi3-ts/dist'; + export const openApiVersions = [ '3.0.0', '3.0.1', @@ -12,3 +14,8 @@ export const satisfiesVersion = ( test: OpenApiVersion, against: OpenApiVersion, ) => openApiVersions.indexOf(test) >= openApiVersions.indexOf(against); + +export const isReferenceObject = ( + schemaOrRef: oas31.SchemaObject | oas31.ReferenceObject, +): schemaOrRef is oas31.ReferenceObject => + Boolean('$ref' in schemaOrRef && schemaOrRef.$ref);