diff --git a/README.md b/README.md index 09b94fd..787b29e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Use the `.openapi()` method to add metadata to a specific Zod type. The `.openap | `ref` | Use this to [auto register a schema](#creating-components) | | `refType` | Use this to set the creation type for a component which is not referenced in the document. | | `type` | Use this to override the generated type. If this is provided no metadata will be generated. | +| `unionOneOf` | Set to `true` to force a ZodUnion to output `oneOf` instead of `allOf` | ### `createDocument` @@ -543,6 +544,7 @@ For example in `z.string().nullable()` will be rendered differently - `items` mapping for `.rest()` - `prefixItems` mapping for OpenAPI 3.1.0+ - ZodUnion + - By default it outputs an `allOf` schema. Use `unionOneOf` to change this to output `oneOf` instead. - ZodUnknown If this library cannot determine a type for a Zod Schema, it will throw an error. To avoid this, declare a manual `type` in the `.openapi()` section of that schema. @@ -565,118 +567,9 @@ See the library in use in the [examples](./examples/) folder. - [eslint-plugin-zod-openapi](https://github.com/samchungy/eslint-plugin-zod-openapi) - Eslint rules for zod-openapi. This includes features which can autogenerate Typescript comments for your Zod types based on your `description`, `example` and `deprecated` fields. -## Credits +## Comparisons -### [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi) - -zod-openapi was created while trying to add a feature to support auto registering schemas. This proved to be extra challenging given the overall structure of the library so I decided re-write the whole thing. I was a big contributor to this library and love everything it's done, however I could not go past a few issues. - -1. The underlying structure of the library consists of tightly coupled classes which require you to create an awkward Registry class to create references. This would mean you would need to ship a registry class instance along with your types which makes sharing types difficult. - -2. No auto registering schema. Most users do not want to think about this so having to import and call `.register()` is a nuisance. -3. When you register a schema using the registry you need to use the outputted type from the `.register()` call. You do not need to do such a thing with this library. - -4. No transform support or safety. You can use a `type` to override the transform type but what happens when that transform logic changes? - -5. No input/output validation with components. What happens when you register a component with a transform which technically comprises of two types in a request and a response? - -Did I really rewrite an entire library just for this? Absolutely. I believe that creating documentation and types should be as simple and as frictionless as possible. - -#### Migration - -1. Delete the OpenAPIRegistry and OpenAPIGenerator classes -2. Replace any `.register()` call made and replace them with `ref` in `.openapi()` or alternatively, add them directly to the components section of the schema. - -```ts -const registry = new OpenAPIRegistry(); - -const foo = registry.register( - 'foo', - z.string().openapi({ description: 'foo' }), -); -const bar = z.object({ foo }); - -// Replace with: -const foo = z.string().openapi({ ref: 'foo', description: 'foo' }); -const bar = z.object({ foo }); - -// or -const foo = z.string().openapi({ description: 'foo' }); -const bar = z.object({ foo }); - -const document = createDocument({ - components: { - schemas: { - foo, - }, - }, -}); -``` - -3. Replace `registry.registerComponent()` with a regular OpenAPI component in the document. - -```ts -const registry = new OpenAPIRegistry(); - -registry.registerComponent('securitySchemes', 'auth', { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - description: 'An auth token issued by oauth', -}); -// Replace with regular component declaration - -const document = createDocument({ - components: { - // declare directly in components - securitySchemes: { - auth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - description: 'An auth token issued by oauth', - }, - }, - }, -}); -``` - -4. Replace `registry.registerPath()` with a regular OpenAPI paths in the document. - -```ts -const registry = new OpenAPIRegistry(); - -registry.registerPath({ - method: 'get', - path: '/foo', - request: { - query: z.object({ a: z.string() }), - params: z.object({ b: z.string() }), - body: z.object({ c: z.string() }), - headers: z.object({ d: z.string() }) - }, - responses: {}, -}); -// Replace with regular path declaration - -const getFoo: ZodOpenApiPathItemObject = { - get: { - requestParams: { - query: z.object({ a: z.string() }), - path: z.object({ b: z.string() }), // params -> path - header: z.object({ c: z.string() }) // headers -> header - }, // renamed from request -> requestParams - requestBody: z.object({c: z.string() }) // request.body -> requestBody - responses: {}, - }, -}; - -const document = createDocument({ - paths: { - '/foo': getFoo, - }, -}); -``` +### [@asteasolutions/zod-to-openapi](./docs/comparisons.md) ## Development diff --git a/docs/comparisons.md b/docs/comparisons.md new file mode 100644 index 0000000..e11d980 --- /dev/null +++ b/docs/comparisons.md @@ -0,0 +1,110 @@ +## Comparisons + +zod-openapi was created while trying to add a feature to support auto registering schemas to ### [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi). This proved to be extra challenging given the overall structure of the library so I decided re-write the whole thing. I was a big contributor to this library and love everything it's done, however I could not go past a few issues. + +1. The underlying structure of the library consists of tightly coupled classes which require you to create an awkward Registry class to create references. This would mean you would need to ship a registry class instance along with your types which makes sharing types difficult. + +2. No auto registering schema. Most users do not want to think about this so having to import and call `.register()` is a nuisance. +3. When you register a schema using the registry you need to use the outputted type from the `.register()` call. You do not need to do such a thing with this library. + +4. No transform support or safety. You can use a `type` to override the transform type but what happens when that transform logic changes? + +5. No input/output validation with components. What happens when you register a component with a transform which technically comprises of two types in a request and a response? + +Did I really rewrite an entire library just for this? Absolutely. I believe that creating documentation and types should be as simple and as frictionless as possible. + +#### Migration + +1. Delete the OpenAPIRegistry and OpenAPIGenerator classes +2. Replace any `.register()` call made and replace them with `ref` in `.openapi()` or alternatively, add them directly to the components section of the schema. + +```ts +const registry = new OpenAPIRegistry(); + +const foo = registry.register( + 'foo', + z.string().openapi({ description: 'foo' }), +); +const bar = z.object({ foo }); + +// Replace with: +const foo = z.string().openapi({ ref: 'foo', description: 'foo' }); +const bar = z.object({ foo }); + +// or +const foo = z.string().openapi({ description: 'foo' }); +const bar = z.object({ foo }); + +const document = createDocument({ + components: { + schemas: { + foo, + }, + }, +}); +``` + +3. Replace `registry.registerComponent()` with a regular OpenAPI component in the document. + +```ts +const registry = new OpenAPIRegistry(); + +registry.registerComponent('securitySchemes', 'auth', { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'An auth token issued by oauth', +}); +// Replace with regular component declaration + +const document = createDocument({ + components: { + // declare directly in components + securitySchemes: { + auth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'An auth token issued by oauth', + }, + }, + }, +}); +``` + +4. Replace `registry.registerPath()` with a regular OpenAPI paths in the document. + +```ts +const registry = new OpenAPIRegistry(); + +registry.registerPath({ + method: 'get', + path: '/foo', + request: { + query: z.object({ a: z.string() }), + params: z.object({ b: z.string() }), + body: z.object({ c: z.string() }), + headers: z.object({ d: z.string() }) + }, + responses: {}, +}); +// Replace with regular path declaration + +const getFoo: ZodOpenApiPathItemObject = { + get: { + requestParams: { + query: z.object({ a: z.string() }), + path: z.object({ b: z.string() }), // params -> path + header: z.object({ c: z.string() }) // headers -> header + }, // renamed from request -> requestParams + requestBody: z.object({c: z.string() }) // request.body -> requestBody + responses: {}, + }, +}; + +const document = createDocument({ + paths: { + '/foo': getFoo, + }, +}); +``` diff --git a/src/create/schema/index.ts b/src/create/schema/index.ts index 509c37c..634c8b9 100644 --- a/src/create/schema/index.ts +++ b/src/create/schema/index.ts @@ -47,8 +47,15 @@ export const createNewSchema = < ); } newState.visited.add(zodSchema); - const { effectType, param, header, ref, refType, ...additionalMetadata } = - zodSchema._def.openapi ?? {}; + const { + effectType, + param, + header, + ref, + refType, + unionOneOf, + ...additionalMetadata + } = zodSchema._def.openapi ?? {}; const schema = createSchemaSwitch(zodSchema, newState); const description = zodSchema.description; diff --git a/src/create/schema/parsers/union.test.ts b/src/create/schema/parsers/union.test.ts index 82ea6ce..9ef3d42 100644 --- a/src/create/schema/parsers/union.test.ts +++ b/src/create/schema/parsers/union.test.ts @@ -9,7 +9,7 @@ import { createUnionSchema } from './union'; extendZodWithOpenApi(z); describe('createUnionSchema', () => { - it('creates a anyOf schema for a union', () => { + it('creates an anyOf schema for a union', () => { const expected: oas31.SchemaObject = { anyOf: [ { @@ -26,4 +26,24 @@ describe('createUnionSchema', () => { expect(result).toStrictEqual(expected); }); + + it('creates an oneOf schema for a union if unionOneOf is true', () => { + const expected: oas31.SchemaObject = { + oneOf: [ + { + type: 'string', + }, + { + type: 'number', + }, + ], + }; + const schema = z + .union([z.string(), z.number()]) + .openapi({ unionOneOf: true }); + + const result = createUnionSchema(schema, createOutputState()); + + expect(result).toStrictEqual(expected); + }); }); diff --git a/src/create/schema/parsers/union.ts b/src/create/schema/parsers/union.ts index cabd259..600bb31 100644 --- a/src/create/schema/parsers/union.ts +++ b/src/create/schema/parsers/union.ts @@ -12,6 +12,13 @@ export const createUnionSchema = < const schemas = zodUnion.options.map((option, index) => createSchemaObject(option, state, [`union option ${index}`]), ); + + if (zodUnion._def.openapi?.unionOneOf) { + return { + oneOf: schemas, + }; + } + return { anyOf: schemas, }; diff --git a/src/extendZod.ts b/src/extendZod.ts index 4e471e3..7f6be7e 100644 --- a/src/extendZod.ts +++ b/src/extendZod.ts @@ -19,6 +19,10 @@ interface ZodOpenApiMetadata< example?: TInferred; examples?: [TInferred, ...TInferred[]]; default?: T extends ZodDate ? string : TInferred; + /** + * Use this field to set the output of a ZodUnion to be `oneOf` instead of `allOf` + */ + unionOneOf?: boolean; /** * Use this field to output this Zod Schema in the components schemas section. Any usage of this Zod Schema will then be transformed into a $ref. */