zod-openapi 5.0.0-beta.0 → 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <h1 align="center">zod-openapi</h1>
4
4
  </p>
5
5
  <p align="center">
6
- A TypeScript library which uses <a href="https://github.com/colinhacks/zod">Zod</a> schemas to generate OpenAPI v3.x documentation.
6
+ A TypeScript library for using <a href="https://github.com/colinhacks/zod">Zod</a> schemas to generate OpenAPI v3.x documentation.
7
7
  </p>
8
8
  <div align="center">
9
9
  <a href="https://www.npmjs.com/package/zod-openapi"><img src="https://img.shields.io/npm/v/zod-openapi"/></a>
@@ -29,22 +29,46 @@ pnpm install zod zod-openapi
29
29
 
30
30
  ## Usage
31
31
 
32
- ### `.meta()`
32
+ ### Extending Zod
33
33
 
34
- Use the `.meta()` method to add metadata to a Zod schema. It accepts an object with the following options:
34
+ This package extends Zod by adding an `.openapi()` method. Call this at the top of your entry point(s). You can apply this extension in two ways:
35
35
 
36
- | Option | Description |
37
- | ---------- | ---------------------------------------------------------------------------------------------------------------- |
38
- | `id` | Registers a schema as a reusable OpenAPI component. |
39
- | `header` | Adds metadata for [response headers](#response-headers). |
40
- | `param` | Adds metadata for [request parameters](#parameters). |
41
- | `override` | Allows you to override the rendered OpenAPI schema. This takes either an object or a function |
42
- | `outputId` | Allows you to set a different ID for the output schema. This is useful when the input and output schemas differ. |
43
- | `unusedIO` | Allows you to set the `io` for an unused schema added to the components section. Defaults to `output` |
36
+ #### Subpath Import
44
37
 
45
- `````typescript
38
+ ```ts
39
+ import 'zod-openapi/extend';
40
+ import { z } from 'zod';
41
+
42
+ z.string().openapi({ description: 'Hello world!', example: 'Hello world' });
43
+ ```
46
44
 
47
- ````typescript
45
+ #### Manual Extension
46
+
47
+ This method is useful if you have a specific instance of Zod or a Zod instance from another library that you want to extend.
48
+
49
+ ```typescript
50
+ import { z } from 'zod';
51
+ import { extendZodWithOpenApi } from 'zod-openapi';
52
+
53
+ extendZodWithOpenApi(z);
54
+
55
+ z.string().openapi({ description: 'Hello world!', example: 'hello world' });
56
+ ```
57
+
58
+ ### `.openapi()`
59
+
60
+ Use the `.openapi()` method to add metadata to a Zod schema. It accepts an object with the following options:
61
+
62
+ | Option | Description |
63
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
64
+ | `OpenAPI Options` | Accepts any option available for a [SchemaObject](https://swagger.io/docs/specification/data-models/data-types/). |
65
+ | `effectType` | Overrides the creation type for a [Zod Effect](#zod-effects). |
66
+ | `header` | Adds metadata for [response headers](#response-headers). |
67
+ | `param` | Adds metadata for [request parameters](#parameters). |
68
+ | `ref` | Registers a schema as a reusable OpenAPI component. |
69
+ | `refType` | Defines the creation type for a component not explicitly referenced in the document. |
70
+ | `type` | Overrides the generated type. If provided, no metadata will be generated. |
71
+ | `unionOneOf` | Forces a `oneOf` instead of `anyOf` for unions. See [CreateDocumentOptions](#CreateDocumentOptions) for a global setting. |
48
72
 
49
73
  ### `createDocument`
50
74
 
@@ -58,7 +82,7 @@ import { createDocument } from 'zod-openapi';
58
82
  const jobId = z.string().openapi({
59
83
  description: 'A unique identifier for a job',
60
84
  example: '12345',
61
- id: 'jobId',
85
+ ref: 'jobId',
62
86
  });
63
87
 
64
88
  const title = z.string().openapi({
@@ -93,7 +117,7 @@ const document = createDocument({
93
117
  },
94
118
  },
95
119
  });
96
- `````
120
+ ```
97
121
 
98
122
  <details>
99
123
  <summary>Creates the following object:</summary>
@@ -180,12 +204,9 @@ const document = createDocument({
180
204
 
181
205
  ```typescript
182
206
  const document = createDocument(details, {
183
- override: ({ jsonSchema, zodSchema }) => {
184
- if (jsonSchema.anyOf) {
185
- ctx.jsonSchema.oneOf = ctx.jsonSchema.anyOf;
186
- delete ctx.jsonSchema.anyOf;
187
- }
188
- },
207
+ defaultDateSchema: { type: 'string', format: 'date-time' }, // defaults to { type: 'string' }
208
+ unionOneOf: true, // defaults to false. Forces all ZodUnions to output oneOf instead of anyOf. An `.openapi()` `unionOneOf` value takes precedence over this one.
209
+ enforceDiscriminatedUnionComponents: true, // defaults to false. Throws an error if a Discriminated Union member is not registered as a component.
189
210
  });
190
211
  ```
191
212
 
@@ -201,7 +222,7 @@ import { createSchema } from 'zod-openapi';
201
222
  const jobId = z.string().openapi({
202
223
  description: 'A unique identifier for a job',
203
224
  example: '12345',
204
- id: 'jobId',
225
+ ref: 'jobId',
205
226
  });
206
227
 
207
228
  const title = z.string().openapi({
@@ -403,7 +424,7 @@ If we take the example in `createDocument` and instead create `title` as follows
403
424
  const title = z.string().openapi({
404
425
  description: 'Job title',
405
426
  example: 'My job',
406
- id: 'jobTitle', // <- new field
427
+ ref: 'jobTitle', // <- new field
407
428
  });
408
429
  ```
409
430
 
@@ -451,6 +472,30 @@ createDocument({
451
472
  });
452
473
  ```
453
474
 
475
+ Unfortunately, as a limitation of this library, you should attach an `.openapi()` field or `.describe()` to the schema that you are passing into the components or you will not reap the full benefits of component generation.
476
+
477
+ ```ts
478
+ // ❌ Avoid this
479
+ const title = z.string();
480
+
481
+ // ✅ Recommended ways
482
+ const title = z.string().describe('Job title');
483
+ const title = z.string().openapi({
484
+ description: 'Job title',
485
+ example: 'My job',
486
+ });
487
+
488
+ createDocument({
489
+ components: {
490
+ schemas: {
491
+ jobTitle: title,
492
+ },
493
+ },
494
+ });
495
+ ```
496
+
497
+ Overall, I recommend utilising the auto registering components over manual registration.
498
+
454
499
  #### Parameters
455
500
 
456
501
  Query, Path, Header & Cookie parameters can be similarly registered:
@@ -460,7 +505,7 @@ Query, Path, Header & Cookie parameters can be similarly registered:
460
505
  const jobId = z.string().openapi({
461
506
  description: 'Job ID',
462
507
  example: '1234',
463
- param: { id: 'jobRef' },
508
+ param: { ref: 'jobRef' },
464
509
  });
465
510
 
466
511
  createDocument({
@@ -481,7 +526,7 @@ createDocument({
481
526
  const jobId = z.string().openapi({
482
527
  description: 'Job ID',
483
528
  example: '1234',
484
- param: { in: 'header', name: 'jobId', id: 'jobRef' },
529
+ param: { in: 'header', name: 'jobId', ref: 'jobRef' },
485
530
  });
486
531
 
487
532
  createDocument({
@@ -518,7 +563,7 @@ Response headers can be similarly registered:
518
563
  const header = z.string().openapi({
519
564
  description: 'Job ID',
520
565
  example: '1234',
521
- header: { id: 'some-header' },
566
+ header: { ref: 'some-header' },
522
567
  });
523
568
 
524
569
  // or
@@ -549,7 +594,7 @@ const response: ZodOpenApiResponseObject = {
549
594
  schema: z.object({ a: z.string() }),
550
595
  },
551
596
  },
552
- id: 'some-response',
597
+ ref: 'some-response',
553
598
  };
554
599
 
555
600
  //or
@@ -578,7 +623,7 @@ Callbacks can also be registered
578
623
 
579
624
  ```typescript
580
625
  const callback: ZodOpenApiCallbackObject = {
581
- id: 'some-callback'
626
+ ref: 'some-callback'
582
627
  post: {
583
628
  responses: {
584
629
  200: {
@@ -619,77 +664,99 @@ createDocument({
619
664
  });
620
665
  ```
621
666
 
622
- ### Zod Types
623
-
624
- Zod types are composed of two different parts: the input and the output. This library decides which type to create based on if it is used in a request or response context.
667
+ ### Zod Effects
625
668
 
626
- Input:
669
+ `.transform()`, `.catch()`, `.default()` and `.pipe()` are complicated because they all comprise of two different types that we could generate (input & output).
627
670
 
628
- - Request Parameters (query, path, header, cookie)
629
- - Request Body
671
+ We attempt to determine what type of schema to create based on the following contexts:
630
672
 
631
- Output:
673
+ _Input_: Request Bodies, Request Parameters, Headers
632
674
 
633
- - Response Body
634
- - Response Headers
675
+ _Output_: Responses, Response Headers
635
676
 
636
- In general, you want to avoid using a registered input schema in an output context and vice versa. This is because the rendered input and output schemas of a simple Zod schema will differ, even with a simple Zod schema like `z.object()`.
677
+ As an example:
637
678
 
638
679
  ```ts
639
- const schema = z.object({
640
- name: z.string(),
680
+ z.object({
681
+ a: z.string().default('a'),
641
682
  });
642
683
  ```
643
684
 
644
- Input:
685
+ In a request context, this would render the following OpenAPI schema:
645
686
 
646
- ```json
647
- {
648
- "type": "object",
649
- "properties": {
650
- "name": {
651
- "type": "string"
652
- }
653
- },
654
- "required": ["name"]
655
- }
687
+ ```yaml
688
+ type: 'object'
689
+ properties:
690
+ - a:
691
+ type: 'string'
692
+ default: 'a'
656
693
  ```
657
694
 
658
- Output:
695
+ or the following for a response:
659
696
 
660
- ```json
661
- {
662
- "type": "object",
663
- "properties": {
664
- "name": {
665
- "type": "string"
666
- }
667
- },
668
- "required": ["name"],
669
- "additionalProperties": false
670
- }
697
+ ```yaml
698
+ type: 'object'
699
+ properties:
700
+ - a:
701
+ type: 'string'
702
+ default: 'a'
703
+ required:
704
+ - a
705
+ ```
706
+
707
+ Note how the response schema created an extra `required` field. This means, if you were to register a Zod schema with `.default()` as a component and use it in both a request or response, your schema would be invalid. Zod OpenAPI keeps track of this usage and will throw an error if this occurs.
708
+
709
+ #### EffectType
710
+
711
+ ```ts
712
+ z.string().transform((str) => str.trim());
671
713
  ```
672
714
 
673
- Unless you are strictly using `z.looseObject()`s or `z.strictObject()`s throughout your codebase you will likely run into issues where the input and output schemas differ. This library will do a best effort to check equality using a simple JSON.stringify() === JSON.stringify() check. If your registered schema contains dynamically created components, this will always fail.
715
+ Whilst the TypeScript compiler can understand that the result is still a `string`, unfortunately we cannot introspect this as your transform function may be far more complicated than this example. To address this, you can set the `effectType` on the schema to `same`, `input` or `output`.
674
716
 
675
- If the schemas are not equal, it will automatically handle this by outputting the `output` schema with an `Output` suffix.
717
+ `same` - This informs Zod OpenAPI to pick either the input schema or output schema to generate with because they should be the same.
676
718
 
677
- You can override this by setting the `outputId` field with the `.meta()` method.
719
+ ```ts
720
+ z.string()
721
+ .transform((str) => str.trim())
722
+ .openapi({ effectType: 'same' });
723
+ ```
724
+
725
+ If the transform were to drift from this, you will receive a TypeScript error:
678
726
 
679
727
  ```ts
680
- const schema = z
681
- .object({
682
- name: z.string(),
683
- })
684
- .meta({ id: 'MyObject', outputId: 'MyObjectResponse' });
728
+ z.string()
729
+ .transform((str) => str.length)
730
+ .openapi({ effectType: 'same' });
731
+ // ~~~~~~~~~~
732
+ // Type 'same' is not assignable to type 'CreationType | undefined'.ts(2322)
685
733
  ```
686
734
 
735
+ `input` or `output` - This tells Zod OpenAPI to pick a specific schema to create whenever we run into this schema, regardless of it is a request or response schema.
736
+
737
+ ```ts
738
+ z.string()
739
+ .transform((str) => str.length)
740
+ .openapi({ effectType: 'input' });
741
+ ```
742
+
743
+ #### Preprocess
744
+
745
+ `.preprocess()` will always return the `output` type even if we are creating an input schema. If a different input type is required you can achieve this with a `.transform()` combined with a `.pipe()` or simply declare a manual `type` in `.openapi()`.
746
+
747
+ #### Component Effects
748
+
749
+ If you are adding a ZodSchema directly to the `components` section which is not referenced anywhere in the document, additional context may be required to create either an input or output schema. You can do this by setting the `refType` field to `input` or `output` in `.openapi()`. This defaults to `output` by default.
750
+
687
751
  ## Supported OpenAPI Versions
688
752
 
689
753
  Currently the following versions of OpenAPI are supported
690
754
 
755
+ - `3.0.0`
756
+ - `3.0.1`
757
+ - `3.0.2`
758
+ - `3.0.3`
691
759
  - `3.1.0`
692
- - `3.1.1`
693
760
 
694
761
  Setting the `openapi` field will change how the some of the components are rendered.
695
762
 
@@ -718,6 +785,72 @@ As an example `z.string().nullable()` will be rendered differently
718
785
  }
719
786
  ```
720
787
 
788
+ ## Supported Zod Schema
789
+
790
+ - ZodAny
791
+ - ZodArray
792
+ - `minItems`/`maxItems` mapping for `.length()`, `.min()`, `.max()`
793
+ - ZodBigInt
794
+ - `integer` `type` and `int64` `format` mapping
795
+ - ZodBoolean
796
+ - ZodBranded
797
+ - ZodCatch
798
+ - Treated as ZodDefault
799
+ - ZodCustom
800
+ - ZodDate
801
+ - `type` is mapped as `string` by default
802
+ - ZodDefault
803
+ - ZodDiscriminatedUnion
804
+ - `discriminator` mapping when all schemas in the union are [registered](#creating-components). The discriminator must be a `ZodLiteral`, `ZodEnum` or `ZodNativeEnum` with string values. Only values wrapped in `ZodBranded`, `ZodReadOnly` and `ZodCatch` are supported.
805
+ - ZodEffects
806
+ - `transform` support for request schemas. See [Zod Effects](#zod-effects) for how to enable response schema support
807
+ - `pre-process` support. We assume that the input type is the same as the output type. Otherwise pipe and transform can be used instead.
808
+ - `refine` full support
809
+ - ZodEnum
810
+ - ZodIntersection
811
+ - ZodLazy
812
+ - The recursive schema within the ZodLazy or the ZodLazy _**must**_ be registered as a component. See [Creating Components](#creating-components) for more information.
813
+ - ZodLiteral
814
+ - ZodNativeEnum
815
+ - supporting `string`, `number` and combined enums.
816
+ - ZodNever
817
+ - ZodNull
818
+ - ZodNullable
819
+ - ZodNumber
820
+ - `integer` `type` mapping for `.int()`
821
+ - `exclusiveMin`/`min`/`exclusiveMax`/`max` mapping for `.min()`, `.max()`, `lt()`, `gt()`, `.positive()`, `.negative()`, `.nonnegative()`, `.nonpositive()`.
822
+ - `multipleOf` mapping for `.multipleOf()`
823
+ - ZodObject
824
+ - `additionalProperties` mapping for `.catchall()`, `.strict()`
825
+ - `allOf` mapping for `.extend()` when the base object is registered and does not have `catchall()`, `strict()` and extension does not override a field.
826
+ - ZodOptional
827
+ - ZodPipeline
828
+ - See [Zod Effects](#zod-effects) for more information.
829
+ - ZodReadonly
830
+ - ZodRecord
831
+ - ZodSet
832
+ - Treated as an array with `uniqueItems` (you may need to add a pre-process to convert it to a set)
833
+ - ZodString
834
+ - `format` mapping for `.url()`, `.uuid()`, `.email()`, `.datetime()`, `.date()`, `.time()`, `.duration()`, `.ip({ version: 'v4' })`, `.ip({ version: 'v6' })`, `.cidr({ version: 'v4' })`, `.cidr({ version: 'v6' })`
835
+ - `minLength`/`maxLength` mapping for `.length()`, `.min()`, `.max()`
836
+ - `pattern` mapping for `.regex()`, `.startsWith()`, `.endsWith()`, `.includes()`
837
+ - `contentEncoding` mapping for `.base64()` for OpenAPI 3.1.0+
838
+ - ZodTuple
839
+ - `items` mapping for `.rest()`
840
+ - `prefixItems` mapping for OpenAPI 3.1.0+
841
+ - ZodUndefined
842
+ - ZodUnion
843
+ - By default it outputs an `anyOf` schema. Use `unionOneOf` to change this to output `oneOf` instead.
844
+ - ZodUnknown
845
+
846
+ 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.
847
+
848
+ eg.
849
+
850
+ ```typescript
851
+ z.custom().openapi({ type: 'string' });
852
+ ```
853
+
721
854
  ## Examples
722
855
 
723
856
  See the library in use in the [examples](./examples/) folder.
package/dist/api.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const components = require("./components.chunk.cjs");
4
4
  exports.createComponents = components.createComponents;
5
- exports.createMediaTypeObject = components.createMediaTypeObject;
6
- exports.createParameter = components.createParameter;
7
- exports.createRegistry = components.createRegistry;
8
- exports.unwrapZodObject = components.unwrapZodObject;
5
+ exports.createMediaTypeSchema = components.createMediaTypeSchema;
6
+ exports.createParamOrRef = components.createParamOrRef;
7
+ exports.getDefaultComponents = components.getDefaultComponents;
8
+ exports.getZodObject = components.getZodObject;
package/dist/api.d.mts CHANGED
@@ -1,5 +1,3 @@
1
- export { createComponents, createRegistry } from './create/components.js';
2
- export { createMediaTypeObject } from './create/content.js';
3
- export { createParameter } from './create/parameters.js';
4
- export { unwrapZodObject } from './create/object.js';
5
- export { Override, isAnyZodType } from './zod.js';
1
+ export { ComponentsObject, createComponents, getDefaultComponents } from './create/components.js';
2
+ export { createMediaTypeSchema } from './create/content.js';
3
+ export { createParamOrRef, getZodObject } from './create/parameters.js';
package/dist/api.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- export { createComponents, createRegistry } from './create/components.js';
2
- export { createMediaTypeObject } from './create/content.js';
3
- export { createParameter } from './create/parameters.js';
4
- export { unwrapZodObject } from './create/object.js';
5
- export { Override, isAnyZodType } from './zod.js';
1
+ export { ComponentsObject, createComponents, getDefaultComponents } from './create/components.js';
2
+ export { createMediaTypeSchema } from './create/content.js';
3
+ export { createParamOrRef, getZodObject } from './create/parameters.js';
package/dist/api.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { createComponents, createMediaTypeObject, createParameter, createRegistry, unwrapZodObject } from "./components.chunk.mjs";
1
+ import { createComponents, createMediaTypeSchema, createParamOrRef, getDefaultComponents, getZodObject } from "./components.chunk.mjs";
2
2
  export {
3
3
  createComponents,
4
- createMediaTypeObject,
5
- createParameter,
6
- createRegistry,
7
- unwrapZodObject
4
+ createMediaTypeSchema,
5
+ createParamOrRef,
6
+ getDefaultComponents,
7
+ getZodObject
8
8
  };