├── .dockerignore
├── .github
├── CODEOWNERS
├── FUNDING.yml
├── dependabot.yml
├── release.yml
├── renovate.json5
└── workflows
│ ├── prerelease.yml
│ ├── release.yml
│ ├── static.yml
│ └── test.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc.js
├── LICENSE
├── README.md
├── crackle.config.ts
├── docs
└── comparisons.md
├── esbuild.esm.mjs
├── esbuild.mjs
├── eslint.config.js
├── examples
└── simple
│ ├── createSchema.ts
│ ├── openapi.yml
│ ├── redoc-static.html
│ └── types
│ ├── common.ts
│ ├── createJob.ts
│ ├── getJob.ts
│ └── index.ts
├── jest.config.ts
├── jest.setup.ts
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
└── copyTypes.ts
├── src
├── create
│ ├── callbacks.ts
│ ├── components.test.ts
│ ├── components.ts
│ ├── content.test.ts
│ ├── content.ts
│ ├── document.test.ts
│ ├── document.ts
│ ├── parameters.test.ts
│ ├── parameters.ts
│ ├── paths.test.ts
│ ├── paths.ts
│ ├── responses.test.ts
│ ├── responses.ts
│ ├── schema
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── metadata.test.ts
│ │ ├── metadata.ts
│ │ ├── parsers
│ │ │ ├── array.ts
│ │ │ ├── bigint.ts
│ │ │ ├── boolean.ts
│ │ │ ├── brand.ts
│ │ │ ├── catch.ts
│ │ │ ├── date.ts
│ │ │ ├── default.ts
│ │ │ ├── discriminatedUnion.ts
│ │ │ ├── enum.ts
│ │ │ ├── index.ts
│ │ │ ├── intersection.ts
│ │ │ ├── lazy.ts
│ │ │ ├── literal.ts
│ │ │ ├── manual.ts
│ │ │ ├── nativeEnum.ts
│ │ │ ├── null.ts
│ │ │ ├── nullable.ts
│ │ │ ├── number.ts
│ │ │ ├── object.ts
│ │ │ ├── optional.ts
│ │ │ ├── pipeline.ts
│ │ │ ├── preprocess.ts
│ │ │ ├── readonly.ts
│ │ │ ├── record.ts
│ │ │ ├── refine.ts
│ │ │ ├── set.ts
│ │ │ ├── string.ts
│ │ │ ├── transform.ts
│ │ │ ├── tuple.ts
│ │ │ ├── union.ts
│ │ │ └── unknown.ts
│ │ ├── single.test.ts
│ │ ├── single.ts
│ │ └── tests
│ │ │ ├── array.test.ts
│ │ │ ├── bigint.test.ts
│ │ │ ├── boolean.test.ts
│ │ │ ├── brand.test.ts
│ │ │ ├── catch.test.ts
│ │ │ ├── date.test.ts
│ │ │ ├── default.test.ts
│ │ │ ├── discriminatedUnion.test.ts
│ │ │ ├── enum.test.ts
│ │ │ ├── intersection.test.ts
│ │ │ ├── lazy.test.ts
│ │ │ ├── literal.test.ts
│ │ │ ├── manual.test.ts
│ │ │ ├── nativeEnum.test.ts
│ │ │ ├── null.test.ts
│ │ │ ├── nullable.test.ts
│ │ │ ├── number.test.ts
│ │ │ ├── object.test.ts
│ │ │ ├── optional.test.ts
│ │ │ ├── pipeline.test.ts
│ │ │ ├── preprocess.test.ts
│ │ │ ├── readonly.test.ts
│ │ │ ├── record.test.ts
│ │ │ ├── refine.test.ts
│ │ │ ├── set.test.ts
│ │ │ ├── string.test.ts
│ │ │ ├── transform.test.ts
│ │ │ ├── tuple.test.ts
│ │ │ ├── union.test.ts
│ │ │ └── unknown.test.ts
│ ├── specificationExtension.test.ts
│ └── specificationExtension.ts
├── entries
│ ├── api.ts
│ └── extend.ts
├── extendZod.test.ts
├── extendZod.ts
├── extendZodSymbols.ts
├── extendZodTypes.ts
├── index.ts
├── openapi.test.ts
├── openapi.ts
├── openapi3-ts
│ ├── dist
│ │ ├── dsl
│ │ │ ├── openapi-builder30.ts
│ │ │ └── openapi-builder31.ts
│ │ ├── index.ts
│ │ ├── model
│ │ │ ├── oas-common.ts
│ │ │ ├── openapi30.ts
│ │ │ ├── openapi31.ts
│ │ │ ├── server.ts
│ │ │ └── specification-extension.ts
│ │ ├── oas30.ts
│ │ └── oas31.ts
│ ├── oas30.ts
│ └── oas31.ts
├── testing
│ └── state.ts
└── zodType.ts
├── tsconfig.build.json
├── tsconfig.json
└── zod-openapi.svg
/.dockerignore:
--------------------------------------------------------------------------------
1 | # managed by skuba
2 | .gantry/
3 | .git/
4 | .idea/
5 | .serverless/
6 | .vscode/
7 | node_modules*/
8 |
9 | /coverage*/
10 | /dist*/
11 | /lib*/
12 | /tmp*/
13 |
14 | .DS_Store
15 | npm-debug.log
16 | yarn-error.log
17 | # end managed by skuba
18 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Configured by Renovate
2 |
3 | package.json
4 | pnpm-lock.yaml
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [samchungy]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: 'npm'
9 | directory: '/'
10 | schedule:
11 | interval: 'weekly'
12 | labels:
13 | - 'chore'
14 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | labels:
4 | - chore
5 | categories:
6 | - title: Breaking Changes 🛠
7 | labels:
8 | - breaking change
9 | - title: New Features 🎉
10 | labels:
11 | - enhancement
12 | - title: Other Changes
13 | labels:
14 | - '*'
15 |
--------------------------------------------------------------------------------
/.github/renovate.json5:
--------------------------------------------------------------------------------
1 | {
2 | extends: ['github>seek-oss/rynovate'],
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/prerelease.yml:
--------------------------------------------------------------------------------
1 | name: Prerelease
2 |
3 | on:
4 | release:
5 | types:
6 | - prereleased
7 |
8 | permissions: {}
9 |
10 | env:
11 | COREPACK_DEFAULT_TO_LATEST: 0
12 |
13 | jobs:
14 | release:
15 | name: Version & Publish
16 | permissions:
17 | contents: write
18 | id-token: write
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: Check out repo
22 | uses: actions/checkout@v3
23 | with:
24 | fetch-depth: 0
25 |
26 | - name: Set up Node.js 22.x
27 | uses: actions/setup-node@v3
28 | with:
29 | node-version: 22.x
30 | registry-url: 'https://registry.npmjs.org'
31 |
32 | - name: Set up pnpm
33 | run: corepack enable pnpm
34 |
35 | - name: Install dependencies
36 | run: pnpm install --frozen-lockfile
37 |
38 | - name: Create Beta Branch
39 | run: |
40 | git config user.name github-actions[bot]
41 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com
42 | git checkout -b beta
43 | git push --force origin beta --set-upstream
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 |
47 | - name: Version Package
48 | run: npm version ${{ github.event.release.tag_name }} --git-tag-version=false
49 |
50 | - name: Push package.json and tags
51 | run: |
52 | sha=$(gh api --method PUT /repos/:owner/:repo/contents/$FILE_TO_COMMIT \
53 | -f message="Release ${{ github.event.release.tag_name }}" \
54 | -f content="$( base64 -i $FILE_TO_COMMIT )" \
55 | -f encoding="base64" \
56 | -f branch="$DESTINATION_BRANCH" \
57 | -f sha="$( git rev-parse $DESTINATION_BRANCH:$FILE_TO_COMMIT )" --jq '.commit.sha')
58 | gh api --method PATCH /repos/:owner/:repo/git/refs/tags/${{ github.event.release.tag_name }} \
59 | -f sha="$sha" -F force=true
60 | env:
61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62 | FILE_TO_COMMIT: package.json
63 | DESTINATION_BRANCH: beta
64 |
65 | - name: Publish to npm
66 | run: pnpm build && npm publish --provenance --tag beta
67 | env:
68 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
69 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types:
6 | - released
7 |
8 | permissions: {}
9 |
10 | env:
11 | COREPACK_DEFAULT_TO_LATEST: 0
12 |
13 | jobs:
14 | release:
15 | name: Version & Publish
16 | permissions:
17 | contents: write
18 | pull-requests: write
19 | id-token: write
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Check out repo
23 | uses: actions/checkout@v3
24 | with:
25 | fetch-depth: 0
26 |
27 | - name: Set up Node.js 22.x
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: 22.x
31 | registry-url: 'https://registry.npmjs.org'
32 |
33 | - name: Set up pnpm
34 | run: corepack enable pnpm
35 |
36 | - name: Install dependencies
37 | run: pnpm install --frozen-lockfile
38 |
39 | - name: Create Release Branch
40 | run: |
41 | git config user.name github-actions[bot]
42 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com
43 | git checkout -b release
44 | git push --force origin release --set-upstream
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47 |
48 | - name: Version Package
49 | run: npm version ${{ github.event.release.tag_name }} --git-tag-version=false
50 |
51 | - name: Push package.json and tags
52 | run: |
53 | sha=$(gh api --method PUT /repos/:owner/:repo/contents/$FILE_TO_COMMIT \
54 | -f message="Release ${{ github.event.release.tag_name }}" \
55 | -f content="$( base64 -i $FILE_TO_COMMIT )" \
56 | -f encoding="base64" \
57 | -f branch="$DESTINATION_BRANCH" \
58 | -f sha="$( git rev-parse $DESTINATION_BRANCH:$FILE_TO_COMMIT )" --jq '.commit.sha')
59 | gh api --method PATCH /repos/:owner/:repo/git/refs/tags/${{ github.event.release.tag_name }} \
60 | -f sha="$sha" -F force=true
61 | env:
62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63 | FILE_TO_COMMIT: package.json
64 | DESTINATION_BRANCH: release
65 |
66 | - name: Raise Release PR
67 | run: |
68 | gh pr create -H release -B master --title "Release ${{ github.event.release.tag_name }}" --body "Please merge this with a Merge Request to update master
[${{ github.event.release.tag_name }}](${{ github.event.release.html_url }})
${{ github.event.release.body }}" -l "chore"
69 | env:
70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71 |
72 | - name: Publish to npm
73 | run: pnpm build && npm publish --provenance
74 | env:
75 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
76 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ['master']
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: 'pages'
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v5
36 | - name: Upload artifact
37 | uses: actions/upload-pages-artifact@v3
38 | with:
39 | # Upload entire repository
40 | path: '.'
41 | - name: Deploy to GitHub Pages
42 | id: deployment
43 | uses: actions/deploy-pages@v4
44 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - 'master'
8 | permissions: {}
9 |
10 | env:
11 | COREPACK_DEFAULT_TO_LATEST: 0
12 |
13 | jobs:
14 | validate:
15 | name: Lint & Test
16 | permissions:
17 | checks: write
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Check out repo
21 | uses: actions/checkout@v3
22 |
23 | - name: Set up Node.js 22.x
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: 22.x
27 |
28 | - name: Set up pnpm
29 | run: corepack enable pnpm
30 |
31 | - name: Install dependencies
32 | run: pnpm install --frozen-lockfile
33 |
34 | - name: Test
35 | run: pnpm test:ci
36 |
37 | - name: Lint
38 | run: pnpm lint
39 |
40 | - name: Build
41 | run: pnpm build
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # managed by skuba
2 | .npmrc
3 |
4 | .idea/*
5 | .vscode/*
6 | !.vscode/extensions.json
7 |
8 | .cdk.staging/
9 | .serverless/
10 | cdk.out/
11 | node_modules*/
12 |
13 | /coverage*/
14 | /dist*/
15 | /lib*/
16 | /tmp*/
17 |
18 | .DS_Store
19 | .eslintcache
20 | .pnpm-debug.log
21 | *.tgz
22 | *.tsbuildinfo
23 | npm-debug.log
24 | package-lock.json
25 | yarn-error.log
26 | # end managed by skuba
27 |
28 | # managed by crackle
29 | /api
30 | /dist
31 | /extend
32 | # end managed by crackle
33 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # managed by skuba
2 | # Gantry resource files support non-standard template syntax
3 | /.gantry/**/*.yaml
4 | /.gantry/**/*.yml
5 | gantry*.yaml
6 | gantry*.yml
7 | pnpm-lock.yaml
8 | coverage
9 | # end managed by skuba
10 |
11 | examples/**/*.yml
12 | examples/**/*.html
13 | src/openapi3-ts/*
14 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = require('skuba/config/prettier');
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ### MIT License
2 |
3 | Copyright (c) 2020 Sam Chung
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/crackle.config.ts:
--------------------------------------------------------------------------------
1 | import type { UserConfig } from '@crackle/cli/config';
2 |
3 | export default {
4 | dts: {
5 | mode: 'preserve',
6 | },
7 | } satisfies UserConfig;
8 |
--------------------------------------------------------------------------------
/docs/comparisons.md:
--------------------------------------------------------------------------------
1 | ## Comparisons
2 |
3 | ### [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi)
4 |
5 | zod-openapi was created while trying to add a feature to support auto-registering schemas to zod-to-openapi. This proved to be extra challenging given the overall structure of the library, so I decided to rewrite the whole thing. I was a big contributor to this library and love everything it's done; however, I could not overlook a few issues:
6 |
7 | 1. **Inaccurate** schema generation: This is because the library is written without considering that Zod Types can produce different schemas depending on if they are an `input` or `output` type. This means that when you use a `ZodTransform`, `ZodPipeline`, or `ZodDefault`, it may generate incorrect documentation depending on whether you are creating a schema for a request or a response.
8 |
9 | 2. **No input/output validation on components**: Registered schema for inputs and outputs should **NOT** be used if they contain a ZodEffect such as `ZodTransform`, `ZodPipeline`, or `ZodDefault` in both request and response schemas. This is because they will be inaccurate for the reasons stated above.
10 |
11 | 3. **No transform support or safety**: You can use a `type` to override the transform type, but what happens when that transform logic changes? We solve this by introducing `effectType`.
12 |
13 | 4. **No lazy/recursive schema support**.
14 |
15 | 5. **Wider and richer generation of OpenAPI types for Zod Types.**
16 |
17 | 6. 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 need to ship a registry class instance along with your types, making sharing types difficult.
18 |
19 | 7. Previously, zod-to-openapi did not support auto-registering schemas; however, more recently they added a solution which is less clear as they are using named parameters:
20 |
21 | ```ts
22 | z.string().openapi('foo');
23 | z.string().openapi('foo', { description: 'foo' });
24 | // vs
25 |
26 | z.string().openapi({ ref: 'foo' });
27 | z.string().openapi({ description: 'foo', ref: 'foo' });
28 | ```
29 |
30 | 8. None of the large number of [issues](https://github.com/asteasolutions/zod-to-openapi/issues), [known issues](https://github.com/asteasolutions/zod-to-openapi#known-issues), or discussion threads apply to this library.
31 |
32 | Did I really rewrite an entire library just for this? Absolutely. I believe that creating documentation and types should be as simple and frictionless as possible.
33 |
34 | ---
35 |
36 | #### Migration
37 |
38 | 1. Delete the OpenAPIRegistry and OpenAPIGenerator classes.
39 | 2. Replace any `.register()` call with `ref` in `.openapi()`, or alternatively, add them directly to the components section of the schema.
40 |
41 | ```ts
42 | const registry = new OpenAPIRegistry();
43 |
44 | const foo = registry.register(
45 | 'foo',
46 | z.string().openapi({ description: 'foo' }),
47 | );
48 | const bar = z.object({ foo });
49 |
50 | // Replace with:
51 | const foo = z.string().openapi({ ref: 'foo', description: 'foo' });
52 | const bar = z.object({ foo });
53 |
54 | // or
55 | const foo = z.string().openapi({ description: 'foo' });
56 | const bar = z.object({ foo });
57 |
58 | const document = createDocument({
59 | components: {
60 | schemas: {
61 | foo,
62 | },
63 | },
64 | });
65 | ```
66 |
67 | 3. Replace `registry.registerComponent()` with a regular OpenAPI component in the document.
68 |
69 | ```ts
70 | const registry = new OpenAPIRegistry();
71 |
72 | registry.registerComponent('securitySchemes', 'auth', {
73 | type: 'http',
74 | scheme: 'bearer',
75 | bearerFormat: 'JWT',
76 | description: 'An auth token issued by oauth',
77 | });
78 | // Replace with regular component declaration
79 |
80 | const document = createDocument({
81 | components: {
82 | // declare directly in components
83 | securitySchemes: {
84 | auth: {
85 | type: 'http',
86 | scheme: 'bearer',
87 | bearerFormat: 'JWT',
88 | description: 'An auth token issued by oauth',
89 | },
90 | },
91 | },
92 | });
93 | ```
94 |
95 | 4. Replace `registry.registerPath()` with regular OpenAPI paths in the document.
96 |
97 | ```ts
98 | const registry = new OpenAPIRegistry();
99 |
100 | registry.registerPath({
101 | method: 'get',
102 | path: '/foo',
103 | request: {
104 | query: z.object({ a: z.string() }),
105 | params: z.object({ b: z.string() }),
106 | body: z.object({ c: z.string() }),
107 | headers: z.object({ d: z.string() }),
108 | },
109 | responses: {},
110 | });
111 | // Replace with regular path declaration
112 |
113 | const getFoo: ZodOpenApiPathItemObject = {
114 | get: {
115 | requestParams: {
116 | query: z.object({ a: z.string() }),
117 | path: z.object({ b: z.string() }), // params -> path
118 | header: z.object({ d: z.string() }), // headers -> header
119 | }, // renamed from request -> requestParams
120 | requestBody: z.object({ c: z.string() }), // request.body -> requestBody
121 | responses: {},
122 | },
123 | };
124 |
125 | const document = createDocument({
126 | paths: {
127 | '/foo': getFoo,
128 | },
129 | });
130 | ```
131 |
--------------------------------------------------------------------------------
/esbuild.esm.mjs:
--------------------------------------------------------------------------------
1 | import { build } from 'esbuild';
2 |
3 | await build({
4 | entryPoints: ['src/index.ts', 'src/extend.ts'],
5 | packages: 'external',
6 | bundle: true,
7 | platform: 'node',
8 | format: 'esm',
9 | target: ['es2022'],
10 | outdir: './lib-esm',
11 | outExtension: { '.js': '.mjs' },
12 | });
13 |
--------------------------------------------------------------------------------
/esbuild.mjs:
--------------------------------------------------------------------------------
1 | import { build } from 'esbuild';
2 |
3 | await build({
4 | entryPoints: ['src/index.ts', 'src/extend.ts'],
5 | packages: 'external',
6 | bundle: true,
7 | platform: 'node',
8 | format: 'cjs',
9 | target: ['es2022'],
10 | outdir: './lib-commonjs',
11 | });
12 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | const skuba = require('eslint-config-skuba');
2 | const zodOpenapi = require('eslint-plugin-zod-openapi');
3 |
4 | module.exports = [
5 | {
6 | ignores: ['src/openapi3-ts/*', '**/crackle.config.ts', 'api', 'extend'],
7 | },
8 | ...skuba,
9 | {
10 | plugins: {
11 | 'zod-openapi': zodOpenapi,
12 | },
13 | },
14 | {
15 | files: ['examples/**/*/types/**/*.ts'],
16 |
17 | rules: {
18 | 'zod-openapi/require-openapi': 'error',
19 | 'zod-openapi/require-comment': 'error',
20 | 'zod-openapi/require-example': 'error',
21 | 'zod-openapi/prefer-openapi-last': 'error',
22 | 'zod-openapi/prefer-zod-default': 'error',
23 | },
24 | },
25 | ];
26 |
--------------------------------------------------------------------------------
/examples/simple/createSchema.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 |
4 | import { stringify } from 'yaml';
5 |
6 | import { type ZodOpenApiOperationObject, createDocument } from '../../src';
7 |
8 | import {
9 | CreateJobRequestSchema,
10 | CreateJobResponseSchema,
11 | GetJobQuerySchema,
12 | GetJobResponseSchema,
13 | } from './types';
14 |
15 | const getJobOperation: ZodOpenApiOperationObject = {
16 | operationId: 'getJob',
17 | summary: 'Get Job',
18 | requestParams: {
19 | query: GetJobQuerySchema,
20 | },
21 | responses: {
22 | '200': {
23 | description: 'Successful operation',
24 | content: {
25 | 'application/json': {
26 | schema: GetJobResponseSchema,
27 | },
28 | },
29 | },
30 | },
31 | };
32 |
33 | const createJobOperation: ZodOpenApiOperationObject = {
34 | operationId: 'createJob',
35 | summary: 'Create Job',
36 | requestBody: {
37 | content: {
38 | 'application/json': {
39 | schema: CreateJobRequestSchema,
40 | },
41 | },
42 | },
43 | responses: {
44 | '201': {
45 | description: 'Successful creation',
46 | content: {
47 | 'application/json': {
48 | schema: CreateJobResponseSchema,
49 | },
50 | },
51 | },
52 | },
53 | };
54 |
55 | const document = createDocument({
56 | openapi: '3.1.0',
57 | info: {
58 | title: 'Simple API',
59 | version: '1.0.0',
60 | description: 'A simple demo for zod-openapi',
61 | license: {
62 | name: 'MIT',
63 | },
64 | },
65 | components: {
66 | securitySchemes: {
67 | s2sauth: {
68 | type: 'http',
69 | scheme: 'bearer',
70 | bearerFormat: 'JWT',
71 | description: 'An s2s token issued by an allow listed consumer',
72 | },
73 | },
74 | },
75 | servers: [
76 | {
77 | url: 'http://example.com/dev',
78 | description: 'Dev Endpoint',
79 | },
80 | {
81 | url: 'http://example.com/prod',
82 | description: 'Prod Endpoint',
83 | },
84 | ],
85 | security: [
86 | {
87 | s2sauth: [],
88 | },
89 | ],
90 | paths: {
91 | '/job': {
92 | get: getJobOperation,
93 | post: createJobOperation,
94 | },
95 | },
96 | });
97 |
98 | const yaml = stringify(document, { aliasDuplicateObjects: false });
99 |
100 | // eslint-disable-next-line no-sync
101 | fs.writeFileSync(path.join(__dirname, 'openapi.yml'), yaml);
102 |
--------------------------------------------------------------------------------
/examples/simple/openapi.yml:
--------------------------------------------------------------------------------
1 | openapi: 3.1.0
2 | info:
3 | title: Simple API
4 | version: 1.0.0
5 | description: A simple demo for zod-openapi
6 | license:
7 | name: MIT
8 | servers:
9 | - url: http://example.com/dev
10 | description: Dev Endpoint
11 | - url: http://example.com/prod
12 | description: Prod Endpoint
13 | security:
14 | - s2sauth: []
15 | paths:
16 | /job:
17 | get:
18 | operationId: getJob
19 | summary: Get Job
20 | parameters:
21 | - in: query
22 | name: id
23 | description: A unique identifier for a job
24 | schema:
25 | $ref: "#/components/schemas/jobId"
26 | required: true
27 | responses:
28 | "200":
29 | description: Successful operation
30 | content:
31 | application/json:
32 | schema:
33 | type: object
34 | properties:
35 | id:
36 | $ref: "#/components/schemas/jobId"
37 | title:
38 | $ref: "#/components/schemas/jobTitle"
39 | userId:
40 | type: string
41 | description: A unique identifier for a user
42 | example: "60001234"
43 | required:
44 | - id
45 | - title
46 | - userId
47 | description: Get Job Response
48 | post:
49 | operationId: createJob
50 | summary: Create Job
51 | requestBody:
52 | content:
53 | application/json:
54 | schema:
55 | type: object
56 | properties:
57 | title:
58 | $ref: "#/components/schemas/jobTitle"
59 | required:
60 | - title
61 | additionalProperties: false
62 | description: Create Job Request
63 | responses:
64 | "201":
65 | description: Successful creation
66 | content:
67 | application/json:
68 | schema:
69 | type: object
70 | properties:
71 | id:
72 | $ref: "#/components/schemas/jobId"
73 | required:
74 | - id
75 | description: Create Job Response
76 | components:
77 | securitySchemes:
78 | s2sauth:
79 | type: http
80 | scheme: bearer
81 | bearerFormat: JWT
82 | description: An s2s token issued by an allow listed consumer
83 | schemas:
84 | jobId:
85 | type: string
86 | format: uuid
87 | description: A unique identifier for a job
88 | example: 4dd643ff-7ec7-4666-9c88-50b7d3da34e4
89 | jobTitle:
90 | type: string
91 | minLength: 1
92 | description: A name that describes the job
93 | example: Mid level developer
94 |
--------------------------------------------------------------------------------
/examples/simple/types/common.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | /**
4 | * A unique identifier for a user
5 | * @example "60001234"
6 | */
7 | export const UserIdSchema = z.string().openapi({
8 | description: 'A unique identifier for a user',
9 | example: '60001234',
10 | });
11 |
12 | /**
13 | * A unique identifier for a job
14 | * @example "4dd643ff-7ec7-4666-9c88-50b7d3da34e4"
15 | */
16 | export const JobIdSchema = z.string().uuid().openapi({
17 | description: 'A unique identifier for a job',
18 | example: '4dd643ff-7ec7-4666-9c88-50b7d3da34e4',
19 | ref: 'jobId',
20 | });
21 |
22 | /**
23 | * A name that describes the job
24 | * @example "Mid level developer"
25 | */
26 | export const JobTitleSchema = z.string().nonempty().openapi({
27 | description: 'A name that describes the job',
28 | example: 'Mid level developer',
29 | ref: 'jobTitle',
30 | });
31 |
--------------------------------------------------------------------------------
/examples/simple/types/createJob.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { JobIdSchema, JobTitleSchema } from './common';
4 |
5 | /**
6 | * Create Job Request
7 | */
8 | export const CreateJobRequestSchema = z
9 | .strictObject({
10 | /**
11 | * A name that describes the job
12 | * @example "Mid level developer"
13 | */
14 | title: JobTitleSchema,
15 | })
16 | .openapi({
17 | description: 'Create Job Request',
18 | });
19 |
20 | /**
21 | * Create Job Response
22 | */
23 | export const CreateJobResponseSchema = z
24 | .object({
25 | /**
26 | * A unique identifier for a job
27 | * @example "4dd643ff-7ec7-4666-9c88-50b7d3da34e4"
28 | */
29 | id: JobIdSchema,
30 | })
31 | .openapi({ description: 'Create Job Response' });
32 |
--------------------------------------------------------------------------------
/examples/simple/types/getJob.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | import { JobIdSchema, JobTitleSchema, UserIdSchema } from './common';
4 |
5 | /**
6 | * Get Job Query Parameters
7 | */
8 | export const GetJobQuerySchema = z
9 | .strictObject({
10 | /**
11 | * A unique identifier for a job
12 | * @example "4dd643ff-7ec7-4666-9c88-50b7d3da34e4"
13 | */
14 | id: JobIdSchema,
15 | })
16 | .openapi({ description: 'Get Job Query Parameters' });
17 |
18 | /**
19 | * Get Job Response
20 | */
21 | export const GetJobResponseSchema = z
22 | .object({
23 | /**
24 | * A unique identifier for a job
25 | * @example "4dd643ff-7ec7-4666-9c88-50b7d3da34e4"
26 | */
27 | id: JobIdSchema,
28 | /**
29 | * A name that describes the job
30 | * @example "Mid level developer"
31 | */
32 | title: JobTitleSchema,
33 | /**
34 | * A unique identifier for a user
35 | * @example "60001234"
36 | */
37 | userId: UserIdSchema,
38 | })
39 | .openapi({
40 | description: 'Get Job Response',
41 | });
42 |
--------------------------------------------------------------------------------
/examples/simple/types/index.ts:
--------------------------------------------------------------------------------
1 | import '../../../src/entries/extend';
2 | export * from './common';
3 | export * from './getJob';
4 | export * from './createJob';
5 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import { Jest } from 'skuba';
2 |
3 | export default Jest.mergePreset({
4 | coveragePathIgnorePatterns: ['src/testing'],
5 | coverageThreshold: {
6 | global: {
7 | branches: 0,
8 | functions: 0,
9 | lines: 0,
10 | statements: 0,
11 | },
12 | },
13 | setupFiles: ['/jest.setup.ts'],
14 | testPathIgnorePatterns: ['/test\\.ts'],
15 | });
16 |
--------------------------------------------------------------------------------
/jest.setup.ts:
--------------------------------------------------------------------------------
1 | process.env.ENVIRONMENT = 'test';
2 |
3 | export {};
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zod-openapi",
3 | "version": "4.2.4",
4 | "description": "Convert Zod Schemas to OpenAPI v3.x documentation",
5 | "keywords": [
6 | "typescript",
7 | "json-schema",
8 | "swagger",
9 | "openapi",
10 | "openapi3",
11 | "zod",
12 | "zod-openapi"
13 | ],
14 | "homepage": "https://github.com/samchungy/zod-openapi#readme",
15 | "bugs": {
16 | "url": "https://github.com/samchungy/zod-openapi/issues"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+ssh://git@github.com/samchungy/zod-openapi.git"
21 | },
22 | "license": "MIT",
23 | "sideEffects": [
24 | "dist/extend.*",
25 | "dist/side-effects/**",
26 | "src/entries/extend.ts"
27 | ],
28 | "exports": {
29 | ".": {
30 | "types": {
31 | "import": "./dist/index.d.mts",
32 | "require": "./dist/index.d.ts"
33 | },
34 | "import": "./dist/index.mjs",
35 | "require": "./dist/index.cjs"
36 | },
37 | "./api": {
38 | "types": {
39 | "import": "./dist/api.d.mts",
40 | "require": "./dist/api.d.ts"
41 | },
42 | "import": "./dist/api.mjs",
43 | "require": "./dist/api.cjs"
44 | },
45 | "./extend": {
46 | "types": {
47 | "import": "./dist/extend.d.mts",
48 | "require": "./dist/extend.d.ts"
49 | },
50 | "import": "./dist/extend.mjs",
51 | "require": "./dist/extend.cjs"
52 | },
53 | "./package.json": "./package.json"
54 | },
55 | "main": "./dist/index.cjs",
56 | "module": "./dist/index.mjs",
57 | "types": "./dist/index.d.ts",
58 | "files": [
59 | "api",
60 | "dist",
61 | "extend"
62 | ],
63 | "scripts": {
64 | "build": "pnpm copy:types && crackle package",
65 | "copy:types": "skuba node scripts/copyTypes.ts",
66 | "create:docs": " skuba node examples/simple/createSchema.ts && redocly build-docs examples/simple/openapi.yml --output=examples/simple/redoc-static.html",
67 | "format": "skuba format",
68 | "lint": "skuba lint",
69 | "prepare": "pnpm build",
70 | "test": "skuba test",
71 | "test:ci": "skuba test --coverage",
72 | "test:watch": "skuba test --watch"
73 | },
74 | "devDependencies": {
75 | "@arethetypeswrong/cli": "0.18.1",
76 | "@crackle/cli": "0.16.0",
77 | "@redocly/cli": "1.34.3",
78 | "@types/node": "22.15.21",
79 | "eslint-plugin-zod-openapi": "1.0.0",
80 | "openapi3-ts": "4.4.0",
81 | "skuba": "11.0.1-fix-node16-compatibility-20250523051131",
82 | "yaml": "2.8.0",
83 | "zod": "3.25.23"
84 | },
85 | "peerDependencies": {
86 | "zod": "^3.21.4"
87 | },
88 | "packageManager": "pnpm@10.11.0",
89 | "engines": {
90 | "node": ">=18"
91 | },
92 | "publishConfig": {
93 | "provenance": true,
94 | "registry": "https://registry.npmjs.org/"
95 | },
96 | "skuba": {
97 | "entryPoint": "src/index.ts",
98 | "template": "oss-npm-package",
99 | "type": "package",
100 | "version": "11.0.0"
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | # managed by skuba
2 | packageManagerStrictVersion: true
3 | publicHoistPattern:
4 | - '@types*'
5 | - '*eslint*'
6 | - '*prettier*'
7 | - esbuild
8 | - jest
9 | - tsconfig-seek
10 | # end managed by skuba
11 |
--------------------------------------------------------------------------------
/scripts/copyTypes.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs';
2 | import { join, relative } from 'path';
3 |
4 | import { Git } from 'skuba';
5 |
6 | async function copyDTs(src: string, dest: string): Promise {
7 | const files = await fs.readdir(src);
8 | for (const file of files) {
9 | const filePath = join(src, file);
10 | const destPath = join(dest, file);
11 | const stats = await fs.stat(filePath);
12 | if (stats.isDirectory()) {
13 | await copyDTs(filePath, destPath);
14 | continue;
15 | }
16 | if (filePath.endsWith('.d.ts')) {
17 | const dirPath = join(
18 | dest,
19 | relative(src, filePath).split('/').slice(0, -1).join('/'),
20 | );
21 | await fs.mkdir(dirPath, { recursive: true });
22 | const destination = `${destPath.slice(0, destPath.length - 5)}.ts`;
23 | await fs.copyFile(filePath, destination);
24 |
25 | const contents = (await fs.readFile(destination)).toString('utf-8');
26 | if (contents.includes('export { Server, ServerVariable }')) {
27 | const patched = contents
28 | .replaceAll(/export \{ Server, ServerVariable \}.*/g, '')
29 | .replaceAll(/.*\.\/dsl\/openapi-builder.*/g, '');
30 | await fs.writeFile(destination, Buffer.from(patched));
31 | continue;
32 | }
33 | if (
34 | contents.includes(
35 | ", SpecificationExtension } from './specification-extension';",
36 | )
37 | ) {
38 | const patched = contents
39 | .replace(', SpecificationExtension }', ' }')
40 | .replaceAll(/.*export declare function.*\n/g, '');
41 | await fs.writeFile(destination, Buffer.from(patched));
42 | continue;
43 | }
44 | if (contents.includes('export declare function getExtension')) {
45 | const patched = contents.replaceAll(
46 | /.*export declare function.*\n/g,
47 | '',
48 | );
49 | await fs.writeFile(destination, Buffer.from(patched));
50 | continue;
51 | }
52 | }
53 | }
54 | }
55 |
56 | async function deleteFolderRecursive(folderPath: string) {
57 | const files = await fs.readdir(folderPath);
58 |
59 | for (const file of files) {
60 | const filePath = join(folderPath, file);
61 | const stats = await fs.stat(filePath);
62 |
63 | if (stats.isDirectory()) {
64 | await deleteFolderRecursive(filePath);
65 | } else {
66 | await fs.unlink(filePath);
67 | }
68 | }
69 |
70 | await fs.rmdir(folderPath);
71 | }
72 |
73 | async function main() {
74 | const dir = process.cwd();
75 |
76 | const src = join(dir, './node_modules/openapi3-ts');
77 | const dest = join(dir, 'src/openapi3-ts');
78 | await deleteFolderRecursive(dest);
79 | await copyDTs(src, dest);
80 |
81 | if (process.env.GITHUB_ACTIONS) {
82 | const files = await Git.getChangedFiles({ dir });
83 | if (files.some(({ path }) => path.startsWith('src/openapi3-ts'))) {
84 | throw new Error('openapi3-ts types need updating');
85 | }
86 | }
87 | }
88 |
89 | main().catch((error) => {
90 | // eslint-disable-next-line no-console
91 | console.error(error);
92 | throw error;
93 | });
94 |
--------------------------------------------------------------------------------
/src/create/callbacks.ts:
--------------------------------------------------------------------------------
1 | import type { oas31 } from '../openapi3-ts/dist';
2 |
3 | import {
4 | type ComponentsObject,
5 | createComponentCallbackRef,
6 | } from './components';
7 | import type {
8 | CreateDocumentOptions,
9 | ZodOpenApiCallbackObject,
10 | } from './document';
11 | import { createPathItem } from './paths';
12 | import { isISpecificationExtension } from './specificationExtension';
13 |
14 | export const createCallback = (
15 | callbackObject: ZodOpenApiCallbackObject,
16 | components: ComponentsObject,
17 | subpath: string[],
18 | documentOptions?: CreateDocumentOptions,
19 | ): oas31.CallbackObject => {
20 | const { ref, ...callbacks } = callbackObject;
21 |
22 | const callback: oas31.CallbackObject = Object.entries(
23 | callbacks,
24 | ).reduce((acc, [callbackName, pathItemObject]) => {
25 | if (isISpecificationExtension(callbackName)) {
26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
27 | acc[callbackName] = pathItemObject;
28 | return acc;
29 | }
30 |
31 | acc[callbackName] = createPathItem(
32 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
33 | pathItemObject,
34 | components,
35 | [...subpath, callbackName],
36 | documentOptions,
37 | );
38 | return acc;
39 | }, {});
40 |
41 | if (ref) {
42 | components.callbacks.set(callbackObject, {
43 | type: 'complete',
44 | ref,
45 | callbackObject: callback,
46 | });
47 | return {
48 | $ref: createComponentCallbackRef(ref),
49 | };
50 | }
51 |
52 | return callback;
53 | };
54 |
55 | export const createCallbacks = (
56 | callbacksObject: oas31.CallbackObject | undefined,
57 | components: ComponentsObject,
58 | subpath: string[],
59 | documentOptions?: CreateDocumentOptions,
60 | ): oas31.CallbackObject | undefined => {
61 | if (!callbacksObject) {
62 | return undefined;
63 | }
64 | return Object.entries(callbacksObject).reduce(
65 | (acc, [callbackName, callbackObject]) => {
66 | if (isISpecificationExtension(callbackName)) {
67 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
68 | acc[callbackName] = callbackObject;
69 | return acc;
70 | }
71 |
72 | acc[callbackName] = createCallback(
73 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
74 | callbackObject,
75 | components,
76 | [...subpath, callbackName],
77 | documentOptions,
78 | );
79 | return acc;
80 | },
81 | {},
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/create/content.test.ts:
--------------------------------------------------------------------------------
1 | import '../entries/extend';
2 | import { z } from 'zod';
3 |
4 | import type { oas31 } from '../openapi3-ts/dist';
5 |
6 | import { getDefaultComponents } from './components';
7 | import { createContent } from './content';
8 |
9 | describe('createContent', () => {
10 | it('should create schema from Zod Objects', () => {
11 | const expectedResult: oas31.ContentObject = {
12 | 'application/json': {
13 | schema: {
14 | type: 'object',
15 | properties: {
16 | a: {
17 | type: 'string',
18 | },
19 | },
20 | required: ['a'],
21 | },
22 | },
23 | };
24 |
25 | const result = createContent(
26 | {
27 | 'application/json': {
28 | schema: z.object({ a: z.string() }),
29 | },
30 | },
31 | getDefaultComponents(),
32 | 'output',
33 | ['/job', 'post'],
34 | );
35 |
36 | expect(result).toStrictEqual(expectedResult);
37 | });
38 |
39 | it('should preserve non Zod Objects', () => {
40 | const expectedResult: oas31.ContentObject = {
41 | 'application/json': {
42 | schema: {
43 | type: 'object',
44 | properties: {
45 | a: {
46 | type: 'string',
47 | },
48 | },
49 | required: ['a'],
50 | },
51 | },
52 | };
53 |
54 | const result = createContent(
55 | {
56 | 'application/json': {
57 | schema: {
58 | type: 'object',
59 | properties: {
60 | a: {
61 | type: 'string',
62 | },
63 | },
64 | required: ['a'],
65 | },
66 | },
67 | },
68 | getDefaultComponents(),
69 | 'output',
70 | ['/job', 'post'],
71 | );
72 |
73 | expect(result).toStrictEqual(expectedResult);
74 | });
75 |
76 | it('should preserve additional properties', () => {
77 | const expectedResult: oas31.ContentObject = {
78 | 'application/json': {
79 | schema: {
80 | type: 'object',
81 | properties: {
82 | a: {
83 | type: 'string',
84 | },
85 | },
86 | required: ['a'],
87 | },
88 | example: {
89 | a: '123',
90 | },
91 | },
92 | };
93 |
94 | const result = createContent(
95 | {
96 | 'application/json': {
97 | schema: z.object({ a: z.string() }),
98 | example: {
99 | a: '123',
100 | },
101 | },
102 | },
103 | getDefaultComponents(),
104 | 'output',
105 | ['/job', 'post'],
106 | );
107 |
108 | expect(result).toStrictEqual(expectedResult);
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/src/create/content.ts:
--------------------------------------------------------------------------------
1 | import type { ZodType } from 'zod';
2 |
3 | import type { oas31 } from '../openapi3-ts/dist';
4 | import { isAnyZodType } from '../zodType';
5 |
6 | import type { ComponentsObject, CreationType } from './components';
7 | import type {
8 | CreateDocumentOptions,
9 | ZodOpenApiContentObject,
10 | ZodOpenApiMediaTypeObject,
11 | } from './document';
12 | import { createSchema } from './schema';
13 |
14 | export const createMediaTypeSchema = (
15 | schemaObject:
16 | | ZodType
17 | | oas31.SchemaObject
18 | | oas31.ReferenceObject
19 | | undefined,
20 | components: ComponentsObject,
21 | type: CreationType,
22 | subpath: string[],
23 | documentOptions?: CreateDocumentOptions,
24 | ): oas31.SchemaObject | oas31.ReferenceObject | undefined => {
25 | if (!schemaObject) {
26 | return undefined;
27 | }
28 |
29 | if (!isAnyZodType(schemaObject)) {
30 | return schemaObject;
31 | }
32 |
33 | return createSchema(
34 | schemaObject,
35 | {
36 | components,
37 | type,
38 | path: [],
39 | visited: new Set(),
40 | documentOptions,
41 | },
42 | subpath,
43 | );
44 | };
45 |
46 | const createMediaTypeObject = (
47 | mediaTypeObject: ZodOpenApiMediaTypeObject | undefined,
48 | components: ComponentsObject,
49 | type: CreationType,
50 | subpath: string[],
51 | documentOptions?: CreateDocumentOptions,
52 | ): oas31.MediaTypeObject | undefined => {
53 | if (!mediaTypeObject) {
54 | return undefined;
55 | }
56 |
57 | return {
58 | ...mediaTypeObject,
59 | schema: createMediaTypeSchema(
60 | mediaTypeObject.schema,
61 | components,
62 | type,
63 | [...subpath, 'schema'],
64 | documentOptions,
65 | ),
66 | };
67 | };
68 |
69 | export const createContent = (
70 | contentObject: ZodOpenApiContentObject,
71 | components: ComponentsObject,
72 | type: CreationType,
73 | subpath: string[],
74 | documentOptions?: CreateDocumentOptions,
75 | ): oas31.ContentObject =>
76 | Object.entries(contentObject).reduce(
77 | (acc, [mediaType, zodOpenApiMediaTypeObject]): oas31.ContentObject => {
78 | const mediaTypeObject = createMediaTypeObject(
79 | zodOpenApiMediaTypeObject,
80 | components,
81 | type,
82 | [...subpath, mediaType],
83 | documentOptions,
84 | );
85 |
86 | if (mediaTypeObject) {
87 | acc[mediaType] = mediaTypeObject;
88 | }
89 | return acc;
90 | },
91 | {},
92 | );
93 |
--------------------------------------------------------------------------------
/src/create/document.ts:
--------------------------------------------------------------------------------
1 | import type { AnyZodObject, ZodType, ZodTypeDef } from 'zod';
2 |
3 | import type { OpenApiVersion } from '../openapi';
4 | import type { oas30, oas31 } from '../openapi3-ts/dist';
5 |
6 | import { createComponents, getDefaultComponents } from './components';
7 | import { createPaths } from './paths';
8 |
9 | export interface ZodOpenApiMediaTypeObject
10 | extends Omit {
11 | schema?: ZodType | oas31.SchemaObject | oas31.ReferenceObject;
12 | }
13 |
14 | export interface ZodOpenApiContentObject {
15 | 'application/json'?: ZodOpenApiMediaTypeObject;
16 | [mediatype: string]: ZodOpenApiMediaTypeObject | undefined;
17 | }
18 |
19 | export interface ZodOpenApiRequestBodyObject
20 | extends Omit {
21 | content: ZodOpenApiContentObject;
22 | /** Use this field to auto register this request body as a component */
23 | ref?: string;
24 | }
25 |
26 | export interface ZodOpenApiResponseObject
27 | extends Omit<
28 | oas31.ResponseObject & oas30.ResponseObject,
29 | 'content' | 'headers'
30 | > {
31 | content?: ZodOpenApiContentObject;
32 | headers?: AnyZodObject | oas30.HeadersObject | oas31.HeadersObject;
33 | /** Use this field to auto register this response object as a component */
34 | ref?: string;
35 | }
36 |
37 | export interface ZodOpenApiResponsesObject
38 | extends oas31.ISpecificationExtension {
39 | default?:
40 | | ZodOpenApiResponseObject
41 | | oas31.ReferenceObject
42 | | oas30.ReferenceObject;
43 | [statuscode: `${1 | 2 | 3 | 4 | 5}${string}`]:
44 | | ZodOpenApiResponseObject
45 | | oas31.ReferenceObject;
46 | }
47 |
48 | export type ZodOpenApiParameters = Partial<
49 | Record
50 | >;
51 |
52 | // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
53 | export interface ZodOpenApiCallbacksObject
54 | extends oas31.ISpecificationExtension {
55 | [name: string]: ZodOpenApiCallbackObject;
56 | }
57 |
58 | export interface ZodOpenApiCallbackObject
59 | extends oas31.ISpecificationExtension {
60 | /** Use this field to auto register this callback object as a component */
61 | ref?: string;
62 | [name: string]: ZodOpenApiPathItemObject | string | undefined;
63 | }
64 |
65 | export interface ZodOpenApiOperationObject
66 | extends Omit<
67 | oas31.OperationObject & oas30.OperationObject,
68 | 'requestBody' | 'responses' | 'parameters' | 'callbacks'
69 | > {
70 | parameters?: Array<
71 | | ZodType
72 | | oas31.ParameterObject
73 | | oas30.ParameterObject
74 | | oas31.ReferenceObject
75 | | oas30.ReferenceObject
76 | >;
77 | requestBody?: ZodOpenApiRequestBodyObject;
78 | requestParams?: ZodOpenApiParameters;
79 | responses: ZodOpenApiResponsesObject;
80 | callbacks?: ZodOpenApiCallbacksObject;
81 | }
82 |
83 | export interface ZodOpenApiPathItemObject
84 | extends Omit<
85 | oas31.PathItemObject & oas30.PathItemObject,
86 | 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace'
87 | > {
88 | get?: ZodOpenApiOperationObject;
89 | put?: ZodOpenApiOperationObject;
90 | post?: ZodOpenApiOperationObject;
91 | delete?: ZodOpenApiOperationObject;
92 | options?: ZodOpenApiOperationObject;
93 | head?: ZodOpenApiOperationObject;
94 | patch?: ZodOpenApiOperationObject;
95 | trace?: ZodOpenApiOperationObject;
96 | }
97 |
98 | // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
99 | export interface ZodOpenApiPathsObject extends oas31.ISpecificationExtension {
100 | [path: string]: ZodOpenApiPathItemObject;
101 | }
102 |
103 | export interface ZodOpenApiComponentsObject
104 | extends Omit<
105 | oas31.ComponentsObject & oas30.ComponentsObject,
106 | 'schemas' | 'responses' | 'requestBodies' | 'headers' | 'parameters'
107 | > {
108 | parameters?: Record<
109 | string,
110 | | ZodType
111 | | oas31.ParameterObject
112 | | oas30.ParameterObject
113 | | oas31.ReferenceObject
114 | | oas30.ReferenceObject
115 | >;
116 | schemas?: Record<
117 | string,
118 | | ZodType
119 | | oas31.SchemaObject
120 | | oas31.ReferenceObject
121 | | oas30.SchemaObject
122 | | oas30.ReferenceObject
123 | >;
124 | requestBodies?: Record;
125 | headers?: Record<
126 | string,
127 | | ZodType
128 | | oas31.HeaderObject
129 | | oas30.HeaderObject
130 | | oas31.ReferenceObject
131 | | oas30.ReferenceObject
132 | >;
133 | responses?: Record;
134 | callbacks?: Record;
135 | }
136 |
137 | export type ZodOpenApiVersion = OpenApiVersion;
138 |
139 | export interface ZodOpenApiObject
140 | extends Omit<
141 | oas31.OpenAPIObject,
142 | 'openapi' | 'paths' | 'webhooks' | 'components'
143 | > {
144 | openapi: ZodOpenApiVersion;
145 | paths?: ZodOpenApiPathsObject;
146 | webhooks?: ZodOpenApiPathsObject;
147 | components?: ZodOpenApiComponentsObject;
148 | }
149 |
150 | export type ZodObjectInputType<
151 | Output = unknown,
152 | Def extends ZodTypeDef = ZodTypeDef,
153 | Input = Record,
154 | > = ZodType