├── .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; 155 | 156 | export interface CreateDocumentOptions { 157 | /** 158 | * Used to throw an error if a Discriminated Union member is not registered as a component 159 | */ 160 | enforceDiscriminatedUnionComponents?: boolean; 161 | /** 162 | * Used to change the default Zod Date schema 163 | */ 164 | defaultDateSchema?: Pick; 165 | /** 166 | * Used to set the output of a ZodUnion to be `oneOf` instead of `anyOf` 167 | */ 168 | unionOneOf?: boolean; 169 | } 170 | 171 | export const createDocument = ( 172 | zodOpenApiObject: ZodOpenApiObject, 173 | documentOptions?: CreateDocumentOptions, 174 | ): oas31.OpenAPIObject => { 175 | const { paths, webhooks, components = {}, ...rest } = zodOpenApiObject; 176 | const defaultComponents = getDefaultComponents( 177 | components, 178 | zodOpenApiObject.openapi, 179 | ); 180 | 181 | const createdPaths = createPaths(paths, defaultComponents, documentOptions); 182 | const createdWebhooks = createPaths( 183 | webhooks, 184 | defaultComponents, 185 | documentOptions, 186 | ); 187 | const createdComponents = createComponents( 188 | components, 189 | defaultComponents, 190 | documentOptions, 191 | ); 192 | 193 | return { 194 | ...rest, 195 | ...(createdPaths && { paths: createdPaths }), 196 | ...(createdWebhooks && { webhooks: createdWebhooks }), 197 | ...(createdComponents && { components: createdComponents }), 198 | }; 199 | }; 200 | -------------------------------------------------------------------------------- /src/create/paths.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 type { ZodOpenApiPathsObject } from './document'; 8 | import { createPaths } from './paths'; 9 | 10 | describe('createPaths', () => { 11 | it('should create a paths object', () => { 12 | const paths: ZodOpenApiPathsObject = { 13 | '/jobs': { 14 | get: { 15 | requestBody: { 16 | content: { 17 | 'application/json': { 18 | schema: z.object({ a: z.string() }), 19 | }, 20 | }, 21 | }, 22 | responses: { 23 | '200': { 24 | description: '200 OK', 25 | content: { 26 | 'application/json': { 27 | schema: z.object({ b: z.string() }), 28 | }, 29 | }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | }; 35 | 36 | const expectedResult: oas31.PathsObject = { 37 | '/jobs': { 38 | get: { 39 | requestBody: { 40 | content: { 41 | 'application/json': { 42 | schema: { 43 | properties: { 44 | a: { 45 | type: 'string', 46 | }, 47 | }, 48 | required: ['a'], 49 | type: 'object', 50 | }, 51 | }, 52 | }, 53 | }, 54 | responses: { 55 | '200': { 56 | content: { 57 | 'application/json': { 58 | schema: { 59 | properties: { 60 | b: { 61 | type: 'string', 62 | }, 63 | }, 64 | required: ['b'], 65 | type: 'object', 66 | }, 67 | }, 68 | }, 69 | description: '200 OK', 70 | }, 71 | }, 72 | }, 73 | }, 74 | }; 75 | 76 | const result = createPaths(paths, getDefaultComponents()); 77 | 78 | expect(result).toStrictEqual(expectedResult); 79 | }); 80 | 81 | it('preserves extra fields', () => { 82 | const paths: ZodOpenApiPathsObject = { 83 | '/jobs': { 84 | get: { 85 | 'x-extra': 'hello', 86 | requestBody: { 87 | content: { 88 | 'application/json': { 89 | schema: z.object({ a: z.string() }), 90 | }, 91 | }, 92 | }, 93 | responses: { 94 | '200': { 95 | description: '200 OK', 96 | content: { 97 | 'application/json': { 98 | schema: z.object({ b: z.string() }), 99 | }, 100 | }, 101 | }, 102 | }, 103 | description: 'hello', 104 | }, 105 | }, 106 | }; 107 | 108 | const expectedResult: oas31.PathsObject = { 109 | '/jobs': { 110 | get: { 111 | 'x-extra': 'hello', 112 | description: 'hello', 113 | requestBody: { 114 | content: { 115 | 'application/json': { 116 | schema: { 117 | properties: { 118 | a: { 119 | type: 'string', 120 | }, 121 | }, 122 | required: ['a'], 123 | type: 'object', 124 | }, 125 | }, 126 | }, 127 | }, 128 | responses: { 129 | '200': { 130 | content: { 131 | 'application/json': { 132 | schema: { 133 | properties: { 134 | b: { 135 | type: 'string', 136 | }, 137 | }, 138 | required: ['b'], 139 | type: 'object', 140 | }, 141 | }, 142 | }, 143 | description: '200 OK', 144 | }, 145 | }, 146 | }, 147 | }, 148 | }; 149 | 150 | const result = createPaths(paths, getDefaultComponents()); 151 | 152 | expect(result).toStrictEqual(expectedResult); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/create/paths.ts: -------------------------------------------------------------------------------- 1 | import type { oas31 } from '../openapi3-ts/dist'; 2 | 3 | import { createCallbacks } from './callbacks'; 4 | import { 5 | type ComponentsObject, 6 | createComponentRequestBodyRef, 7 | } from './components'; 8 | import { createContent } from './content'; 9 | import type { 10 | CreateDocumentOptions, 11 | ZodOpenApiOperationObject, 12 | ZodOpenApiPathItemObject, 13 | ZodOpenApiPathsObject, 14 | ZodOpenApiRequestBodyObject, 15 | } from './document'; 16 | import { createParametersObject } from './parameters'; 17 | import { createResponses } from './responses'; 18 | import { isISpecificationExtension } from './specificationExtension'; 19 | 20 | export const createRequestBody = ( 21 | requestBodyObject: ZodOpenApiRequestBodyObject | undefined, 22 | components: ComponentsObject, 23 | subpath: string[], 24 | documentOptions?: CreateDocumentOptions, 25 | ): oas31.ReferenceObject | oas31.RequestBodyObject | undefined => { 26 | if (!requestBodyObject) { 27 | return undefined; 28 | } 29 | 30 | const component = components.requestBodies.get(requestBodyObject); 31 | if (component && component.type === 'complete') { 32 | return { 33 | $ref: createComponentRequestBodyRef(component.ref), 34 | }; 35 | } 36 | 37 | const { ref: reqBodyRef, ...cleanRequestBody } = requestBodyObject; 38 | 39 | const ref = reqBodyRef ?? component?.ref; 40 | 41 | const requestBody: oas31.RequestBodyObject = { 42 | ...cleanRequestBody, 43 | content: createContent( 44 | cleanRequestBody.content, 45 | components, 46 | 'input', 47 | [...subpath, 'content'], 48 | documentOptions, 49 | ), 50 | }; 51 | 52 | if (ref) { 53 | components.requestBodies.set(requestBodyObject, { 54 | type: 'complete', 55 | ref, 56 | requestBodyObject: requestBody, 57 | }); 58 | return { 59 | $ref: createComponentRequestBodyRef(ref), 60 | }; 61 | } 62 | 63 | return requestBody; 64 | }; 65 | 66 | const createOperation = ( 67 | operationObject: ZodOpenApiOperationObject, 68 | components: ComponentsObject, 69 | subpath: string[], 70 | documentOptions?: CreateDocumentOptions, 71 | ): oas31.OperationObject | undefined => { 72 | const { parameters, requestParams, requestBody, responses, ...rest } = 73 | operationObject; 74 | 75 | const maybeParameters = createParametersObject( 76 | parameters, 77 | requestParams, 78 | components, 79 | [...subpath, 'parameters'], 80 | documentOptions, 81 | ); 82 | 83 | const maybeRequestBody = createRequestBody( 84 | operationObject.requestBody, 85 | components, 86 | [...subpath, 'request body'], 87 | documentOptions, 88 | ); 89 | 90 | const maybeResponses = createResponses( 91 | operationObject.responses, 92 | components, 93 | [...subpath, 'responses'], 94 | documentOptions, 95 | ); 96 | 97 | const maybeCallbacks = createCallbacks( 98 | operationObject.callbacks, 99 | components, 100 | [...subpath, 'callbacks'], 101 | documentOptions, 102 | ); 103 | 104 | return { 105 | ...rest, 106 | ...(maybeParameters && { parameters: maybeParameters }), 107 | ...(maybeRequestBody && { requestBody: maybeRequestBody }), 108 | ...(maybeResponses && { responses: maybeResponses }), 109 | ...(maybeCallbacks && { callbacks: maybeCallbacks }), 110 | }; 111 | }; 112 | 113 | export const createPathItem = ( 114 | pathObject: ZodOpenApiPathItemObject, 115 | components: ComponentsObject, 116 | path: string[], 117 | documentOptions?: CreateDocumentOptions, 118 | ): oas31.PathItemObject => 119 | Object.entries(pathObject).reduce( 120 | (acc, [key, value]) => { 121 | if (!value) { 122 | return acc; 123 | } 124 | 125 | if ( 126 | key === 'get' || 127 | key === 'put' || 128 | key === 'post' || 129 | key === 'delete' || 130 | key === 'options' || 131 | key === 'head' || 132 | key === 'patch' || 133 | key === 'trace' 134 | ) { 135 | acc[key] = createOperation( 136 | value as ZodOpenApiOperationObject, 137 | components, 138 | [...path, key], 139 | documentOptions, 140 | ); 141 | return acc; 142 | } 143 | 144 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 145 | acc[key as keyof typeof pathObject] = value; 146 | return acc; 147 | }, 148 | {}, 149 | ); 150 | 151 | export const createPaths = ( 152 | pathsObject: ZodOpenApiPathsObject | undefined, 153 | components: ComponentsObject, 154 | documentOptions?: CreateDocumentOptions, 155 | ): oas31.PathsObject | undefined => { 156 | if (!pathsObject) { 157 | return undefined; 158 | } 159 | 160 | return Object.entries(pathsObject).reduce( 161 | (acc, [path, pathItemObject]): oas31.PathsObject => { 162 | if (isISpecificationExtension(path)) { 163 | acc[path] = pathItemObject; 164 | return acc; 165 | } 166 | acc[path] = createPathItem( 167 | pathItemObject, 168 | components, 169 | [path], 170 | documentOptions, 171 | ); 172 | return acc; 173 | }, 174 | {}, 175 | ); 176 | }; 177 | -------------------------------------------------------------------------------- /src/create/responses.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 { createResponses } from './responses'; 8 | 9 | describe('createResponses', () => { 10 | it('creates a response', () => { 11 | const expected: oas31.ResponsesObject = { 12 | '200': { 13 | description: '200 OK', 14 | content: { 15 | 'application/json': { 16 | schema: { 17 | properties: { 18 | a: { 19 | type: 'string', 20 | }, 21 | }, 22 | required: ['a'], 23 | type: 'object', 24 | }, 25 | }, 26 | }, 27 | headers: { 28 | a: { 29 | $ref: '#/components/headers/a', 30 | }, 31 | }, 32 | }, 33 | }; 34 | const result = createResponses( 35 | { 36 | '200': { 37 | description: '200 OK', 38 | content: { 39 | 'application/json': { 40 | schema: z.object({ a: z.string() }), 41 | }, 42 | }, 43 | headers: z.object({ 44 | a: z.string().openapi({ header: { ref: 'a' } }), 45 | }), 46 | }, 47 | }, 48 | getDefaultComponents(), 49 | ['previous'], 50 | ); 51 | expect(result).toStrictEqual(expected); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/create/responses.ts: -------------------------------------------------------------------------------- 1 | import type { AnyZodObject, ZodRawShape, ZodType } from 'zod'; 2 | 3 | import type { oas30, oas31 } from '../openapi3-ts/dist'; 4 | import { isAnyZodType } from '../zodType'; 5 | 6 | import { 7 | type ComponentsObject, 8 | createComponentResponseRef, 9 | } from './components'; 10 | import { createContent } from './content'; 11 | import type { 12 | CreateDocumentOptions, 13 | ZodOpenApiResponseObject, 14 | ZodOpenApiResponsesObject, 15 | } from './document'; 16 | import { type SchemaState, createSchema } from './schema'; 17 | import { isISpecificationExtension } from './specificationExtension'; 18 | 19 | export const createResponseHeaders = ( 20 | responseHeaders: 21 | | oas31.HeadersObject 22 | | oas30.HeadersObject 23 | | AnyZodObject 24 | | undefined, 25 | components: ComponentsObject, 26 | documentOptions?: CreateDocumentOptions, 27 | ): oas31.ResponseObject['headers'] => { 28 | if (!responseHeaders) { 29 | return undefined; 30 | } 31 | 32 | if (isAnyZodType(responseHeaders)) { 33 | return Object.entries(responseHeaders.shape as ZodRawShape).reduce< 34 | NonNullable 35 | >((acc, [key, zodSchema]: [string, ZodType]) => { 36 | acc[key] = createHeaderOrRef(zodSchema, components, documentOptions); 37 | return acc; 38 | }, {}); 39 | } 40 | 41 | return responseHeaders as oas31.ResponseObject['headers']; 42 | }; 43 | 44 | export const createHeaderOrRef = ( 45 | schema: ZodType, 46 | components: ComponentsObject, 47 | documentOptions?: CreateDocumentOptions, 48 | ): oas31.BaseParameterObject | oas31.ReferenceObject => { 49 | const component = components.headers.get(schema); 50 | if (component && component.type === 'complete') { 51 | return { 52 | $ref: createComponentHeaderRef(component.ref), 53 | }; 54 | } 55 | 56 | // Optional Objects can return a reference object 57 | const baseHeader = createBaseHeader(schema, components, documentOptions); 58 | if ('$ref' in baseHeader) { 59 | throw new Error('Unexpected Error: received a reference object'); 60 | } 61 | 62 | const ref = schema._def.zodOpenApi?.openapi?.header?.ref ?? component?.ref; 63 | 64 | if (ref) { 65 | components.headers.set(schema, { 66 | type: 'complete', 67 | headerObject: baseHeader, 68 | ref, 69 | }); 70 | return { 71 | $ref: createComponentHeaderRef(ref), 72 | }; 73 | } 74 | 75 | return baseHeader; 76 | }; 77 | 78 | export const createBaseHeader = ( 79 | schema: ZodType, 80 | components: ComponentsObject, 81 | documentOptions?: CreateDocumentOptions, 82 | ): oas31.BaseParameterObject => { 83 | const { ref, ...rest } = schema._def.zodOpenApi?.openapi?.header ?? {}; 84 | const state: SchemaState = { 85 | components, 86 | type: 'output', 87 | path: [], 88 | visited: new Set(), 89 | documentOptions, 90 | }; 91 | const schemaObject = createSchema(schema, state, ['header']); 92 | const optionalResult = schema.safeParse(undefined); 93 | 94 | const required = !optionalResult.success || optionalResult !== undefined; 95 | return { 96 | ...rest, 97 | ...(schema && { schema: schemaObject }), 98 | ...(required && { required }), 99 | }; 100 | }; 101 | 102 | export const createComponentHeaderRef = (ref: string) => 103 | `#/components/headers/${ref}`; 104 | 105 | export const createResponse = ( 106 | responseObject: ZodOpenApiResponseObject | oas31.ReferenceObject, 107 | components: ComponentsObject, 108 | subpath: string[], 109 | documentOptions?: CreateDocumentOptions, 110 | ): oas31.ResponseObject | oas31.ReferenceObject => { 111 | if ('$ref' in responseObject) { 112 | return responseObject; 113 | } 114 | 115 | const component = components.responses.get(responseObject); 116 | if (component && component.type === 'complete') { 117 | return { $ref: createComponentResponseRef(component.ref) }; 118 | } 119 | 120 | const { content, headers, ref, ...rest } = responseObject; 121 | 122 | const maybeHeaders = createResponseHeaders( 123 | headers, 124 | components, 125 | documentOptions, 126 | ); 127 | 128 | const response: oas31.ResponseObject = { 129 | ...rest, 130 | ...(maybeHeaders && { headers: maybeHeaders }), 131 | ...(content && { 132 | content: createContent( 133 | content, 134 | components, 135 | 'output', 136 | [...subpath, 'content'], 137 | documentOptions, 138 | ), 139 | }), 140 | }; 141 | 142 | const responseRef = ref ?? component?.ref; 143 | 144 | if (responseRef) { 145 | components.responses.set(responseObject, { 146 | responseObject: response, 147 | ref: responseRef, 148 | type: 'complete', 149 | }); 150 | return { 151 | $ref: createComponentResponseRef(responseRef), 152 | }; 153 | } 154 | 155 | return response; 156 | }; 157 | 158 | export const createResponses = ( 159 | responsesObject: ZodOpenApiResponsesObject, 160 | components: ComponentsObject, 161 | subpath: string[], 162 | documentOptions?: CreateDocumentOptions, 163 | ): oas31.ResponsesObject => 164 | Object.entries(responsesObject).reduce( 165 | ( 166 | acc, 167 | [statusCode, responseObject]: [ 168 | string, 169 | ZodOpenApiResponseObject | oas31.ReferenceObject, 170 | ], 171 | ): oas31.ResponsesObject => { 172 | if (isISpecificationExtension(statusCode)) { 173 | acc[statusCode] = responseObject; 174 | return acc; 175 | } 176 | acc[statusCode] = createResponse( 177 | responseObject, 178 | components, 179 | [...subpath, statusCode], 180 | documentOptions, 181 | ); 182 | return acc; 183 | }, 184 | {}, 185 | ); 186 | -------------------------------------------------------------------------------- /src/create/schema/metadata.ts: -------------------------------------------------------------------------------- 1 | import { satisfiesVersion } from '../../openapi'; 2 | import type { oas31 } from '../../openapi3-ts/dist'; 3 | 4 | import type { RefObject, Schema, SchemaState } from '.'; 5 | 6 | export const createDescriptionMetadata = ( 7 | schema: RefObject, 8 | description: string, 9 | state: SchemaState, 10 | ): Schema => { 11 | if (satisfiesVersion(state.components.openapi, '3.1.0')) { 12 | return { 13 | type: 'ref', 14 | schema: { 15 | $ref: schema.schema.$ref, 16 | description, 17 | }, 18 | zodType: schema.zodType, 19 | effects: schema.effects, 20 | schemaObject: schema.schemaObject, 21 | }; 22 | } 23 | 24 | return { 25 | type: 'schema', 26 | schema: { 27 | description, 28 | allOf: [schema.schema], 29 | }, 30 | effects: schema.effects, 31 | }; 32 | }; 33 | 34 | const isValueEqual = (value: unknown, previous: unknown): boolean => { 35 | if (typeof value !== typeof previous) { 36 | return false; 37 | } 38 | 39 | if ( 40 | typeof value === 'string' || 41 | typeof value === 'number' || 42 | typeof value === 'boolean' 43 | ) { 44 | return value === previous; 45 | } 46 | 47 | if (Array.isArray(value) && Array.isArray(previous)) { 48 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 49 | const sorted = [...value].sort(); 50 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 51 | const previousSorted = [...previous].sort(); 52 | 53 | return sorted.every((v, i) => isValueEqual(v, previousSorted[i])); 54 | } 55 | 56 | if (value === null || previous === null) { 57 | return value === previous; 58 | } 59 | 60 | if (typeof value === 'object' && typeof previous === 'object') { 61 | const keys = Object.keys(value); 62 | 63 | return keys.every((key) => 64 | isValueEqual( 65 | (value as Record)[key], 66 | (previous as Record)[key], 67 | ), 68 | ); 69 | } 70 | 71 | return value === previous; 72 | }; 73 | 74 | export const enhanceWithMetadata = ( 75 | schema: Schema, 76 | metadata: oas31.SchemaObject | oas31.ReferenceObject, 77 | state: SchemaState, 78 | previous: RefObject | undefined, 79 | ): Schema => { 80 | const values = Object.entries(metadata).reduce( 81 | (acc, [key, value]) => { 82 | if (value === undefined) { 83 | return acc; 84 | } 85 | 86 | acc[key] = value; 87 | return acc; 88 | }, 89 | {} as Record, 90 | ) as oas31.SchemaObject | oas31.ReferenceObject; 91 | 92 | const length = Object.values(values).length; 93 | 94 | if (schema.type === 'ref') { 95 | if (length === 0) { 96 | return schema; 97 | } 98 | 99 | if (length === 1 && metadata.description) { 100 | return createDescriptionMetadata(schema, metadata.description, state); 101 | } 102 | 103 | return { 104 | type: 'schema', 105 | schema: { 106 | allOf: [schema.schema], 107 | ...metadata, 108 | }, 109 | effects: schema.effects, 110 | }; 111 | } 112 | 113 | // Calculate if we can extend the previous schema 114 | if (previous && schema.schema.type !== 'object') { 115 | const diff = Object.entries({ ...schema.schema, ...values }).reduce( 116 | (acc, [key, value]) => { 117 | if ( 118 | previous.schemaObject && 119 | isValueEqual( 120 | (previous.schemaObject as Record)[key], 121 | value, 122 | ) 123 | ) { 124 | return acc; 125 | } 126 | acc[key] = value; 127 | 128 | return acc; 129 | }, 130 | {} as Record, 131 | ); 132 | 133 | const diffLength = Object.values(diff).length; 134 | 135 | if (diffLength === 0) { 136 | return { 137 | type: 'ref', 138 | schema: { 139 | $ref: previous.schema.$ref, 140 | }, 141 | effects: schema.effects, 142 | schemaObject: previous.schemaObject, 143 | zodType: previous.zodType, 144 | }; 145 | } 146 | 147 | if (diffLength === 1 && typeof diff.description === 'string') { 148 | return createDescriptionMetadata(previous, diff.description, state); 149 | } 150 | 151 | return { 152 | type: 'schema', 153 | schema: { allOf: [previous.schema], ...diff }, 154 | effects: schema.effects, 155 | }; 156 | } 157 | 158 | return { 159 | type: 'schema', 160 | schema: { 161 | ...schema.schema, 162 | ...metadata, 163 | }, 164 | effects: schema.effects, 165 | }; 166 | }; 167 | -------------------------------------------------------------------------------- /src/create/schema/parsers/array.ts: -------------------------------------------------------------------------------- 1 | import type { ArrayCardinality, ZodArray, ZodTypeAny } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | 9 | export const createArraySchema = < 10 | T extends ZodTypeAny, 11 | Cardinality extends ArrayCardinality = 'many', 12 | >( 13 | zodArray: ZodArray, 14 | state: SchemaState, 15 | ): Schema => { 16 | const zodType = zodArray._def.type; 17 | const minItems = 18 | zodArray._def.exactLength?.value ?? zodArray._def.minLength?.value; 19 | const maxItems = 20 | zodArray._def.exactLength?.value ?? zodArray._def.maxLength?.value; 21 | 22 | const items = createSchemaObject(zodType, state, ['array items']); 23 | return { 24 | type: 'schema', 25 | schema: { 26 | type: 'array', 27 | items: items.schema, 28 | ...(minItems !== undefined && { minItems }), 29 | ...(maxItems !== undefined && { maxItems }), 30 | }, 31 | effects: items.effects, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/create/schema/parsers/bigint.ts: -------------------------------------------------------------------------------- 1 | import type { ZodBigInt } from 'zod'; 2 | 3 | import type { Schema } from '..'; 4 | 5 | export const createBigIntSchema = (_zodBigInt: ZodBigInt): Schema => ({ 6 | type: 'schema', 7 | schema: { 8 | type: 'integer', 9 | format: 'int64', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/create/schema/parsers/boolean.ts: -------------------------------------------------------------------------------- 1 | import type { ZodBoolean } from 'zod'; 2 | 3 | import type { Schema } from '..'; 4 | 5 | export const createBooleanSchema = (_zodBoolean: ZodBoolean): Schema => ({ 6 | type: 'schema', 7 | schema: { 8 | type: 'boolean', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/create/schema/parsers/brand.ts: -------------------------------------------------------------------------------- 1 | import type { ZodBranded, ZodTypeAny } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | export const createBrandedSchema = < 9 | T extends ZodTypeAny, 10 | B extends string | number | symbol, 11 | >( 12 | zodBranded: ZodBranded, 13 | state: SchemaState, 14 | ): Schema => createSchemaObject(zodBranded._def.type, state, ['brand']); 15 | -------------------------------------------------------------------------------- /src/create/schema/parsers/catch.ts: -------------------------------------------------------------------------------- 1 | import type { ZodCatch, ZodTypeAny } from 'zod'; 2 | 3 | import type { oas31 } from '../../../openapi3-ts/dist'; 4 | import { 5 | type RefObject, 6 | type Schema, 7 | type SchemaState, 8 | createSchemaObject, 9 | } from '../../schema'; 10 | import { enhanceWithMetadata } from '../metadata'; 11 | 12 | export const createCatchSchema = ( 13 | zodCatch: ZodCatch, 14 | state: SchemaState, 15 | previous: RefObject | undefined, 16 | ): Schema => { 17 | const schemaObject = createSchemaObject(zodCatch._def.innerType, state, [ 18 | 'default', 19 | ]); 20 | 21 | const catchResult = zodCatch.safeParse(undefined); 22 | 23 | const maybeDefaultValue: Pick = 24 | catchResult.success 25 | ? { 26 | default: catchResult.data, 27 | } 28 | : {}; 29 | 30 | return enhanceWithMetadata(schemaObject, maybeDefaultValue, state, previous); 31 | }; 32 | -------------------------------------------------------------------------------- /src/create/schema/parsers/date.ts: -------------------------------------------------------------------------------- 1 | import type { ZodDate } from 'zod'; 2 | 3 | import type { Schema, SchemaState } from '..'; 4 | 5 | export const createDateSchema = ( 6 | _zodDate: ZodDate, 7 | state: SchemaState, 8 | ): Schema => ({ 9 | type: 'schema', 10 | schema: state.documentOptions?.defaultDateSchema ?? { 11 | type: 'string', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/create/schema/parsers/default.ts: -------------------------------------------------------------------------------- 1 | import type { ZodDefault, ZodTypeAny } from 'zod'; 2 | 3 | import { 4 | type RefObject, 5 | type Schema, 6 | type SchemaState, 7 | createSchemaObject, 8 | } from '../../schema'; 9 | import { enhanceWithMetadata } from '../metadata'; 10 | 11 | export const createDefaultSchema = ( 12 | zodDefault: ZodDefault, 13 | state: SchemaState, 14 | previous: RefObject | undefined, 15 | ): Schema => { 16 | const schemaObject = createSchemaObject(zodDefault._def.innerType, state, [ 17 | 'default', 18 | ]); 19 | 20 | return enhanceWithMetadata( 21 | schemaObject, 22 | { 23 | default: zodDefault._def.defaultValue(), 24 | }, 25 | state, 26 | previous, 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/create/schema/parsers/discriminatedUnion.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnyZodObject, 3 | ZodDiscriminatedUnion, 4 | ZodDiscriminatedUnionOption, 5 | ZodRawShape, 6 | ZodType, 7 | ZodTypeAny, 8 | } from 'zod'; 9 | 10 | import type { oas31 } from '../../../openapi3-ts/dist'; 11 | import { isZodType } from '../../../zodType'; 12 | import { 13 | type Schema, 14 | type SchemaState, 15 | createSchemaObject, 16 | } from '../../schema'; 17 | 18 | import { createNativeEnumSchema } from './nativeEnum'; 19 | import { flattenEffects } from './transform'; 20 | 21 | export const createDiscriminatedUnionSchema = < 22 | Discriminator extends string, 23 | Options extends Array>, 24 | >( 25 | zodDiscriminatedUnion: ZodDiscriminatedUnion, 26 | state: SchemaState, 27 | ): Schema => { 28 | const options = zodDiscriminatedUnion.options; 29 | const schemas = options.map((option, index) => 30 | createSchemaObject(option, state, [`discriminated union option ${index}`]), 31 | ); 32 | const schemaObjects = schemas.map((schema) => schema.schema); 33 | const discriminator = mapDiscriminator( 34 | schemaObjects, 35 | options, 36 | zodDiscriminatedUnion.discriminator, 37 | state, 38 | ); 39 | 40 | return { 41 | type: 'schema', 42 | schema: { 43 | oneOf: schemaObjects, 44 | ...(discriminator && { discriminator }), 45 | }, 46 | effects: flattenEffects(schemas.map((schema) => schema.effects)), 47 | }; 48 | }; 49 | 50 | const unwrapLiterals = ( 51 | zodType: ZodType | ZodTypeAny | undefined, 52 | state: SchemaState, 53 | ): string[] | undefined => { 54 | if (isZodType(zodType, 'ZodLiteral')) { 55 | if (typeof zodType._def.value !== 'string') { 56 | return undefined; 57 | } 58 | return [zodType._def.value]; 59 | } 60 | 61 | if (isZodType(zodType, 'ZodNativeEnum')) { 62 | const schema = createNativeEnumSchema(zodType, state); 63 | if (schema.type === 'schema' && schema.schema.type === 'string') { 64 | return schema.schema.enum; 65 | } 66 | } 67 | 68 | if (isZodType(zodType, 'ZodEnum')) { 69 | return zodType._def.values; 70 | } 71 | 72 | if (isZodType(zodType, 'ZodBranded')) { 73 | return unwrapLiterals(zodType._def.type, state); 74 | } 75 | 76 | if (isZodType(zodType, 'ZodReadonly')) { 77 | return unwrapLiterals(zodType._def.innerType, state); 78 | } 79 | 80 | if (isZodType(zodType, 'ZodCatch')) { 81 | return unwrapLiterals(zodType._def.innerType, state); 82 | } 83 | 84 | return undefined; 85 | }; 86 | 87 | export const mapDiscriminator = ( 88 | schemas: Array, 89 | zodObjects: AnyZodObject[], 90 | discriminator: unknown, 91 | state: SchemaState, 92 | ): oas31.SchemaObject['discriminator'] => { 93 | if (typeof discriminator !== 'string') { 94 | return undefined; 95 | } 96 | 97 | const mapping: NonNullable = {}; 98 | for (const [index, zodObject] of zodObjects.entries()) { 99 | const schema = schemas[index] as oas31.SchemaObject | oas31.ReferenceObject; 100 | const componentSchemaRef = '$ref' in schema ? schema?.$ref : undefined; 101 | if (!componentSchemaRef) { 102 | if (state.documentOptions?.enforceDiscriminatedUnionComponents) { 103 | throw new Error( 104 | `Discriminated Union member ${index} at ${state.path.join(' > ')} is not registered as a component`, 105 | ); 106 | } 107 | return undefined; 108 | } 109 | 110 | const value = (zodObject.shape as ZodRawShape)[discriminator]; 111 | 112 | const literals = unwrapLiterals(value, state); 113 | 114 | if (!literals) { 115 | return undefined; 116 | } 117 | 118 | for (const enumValue of literals) { 119 | mapping[enumValue] = componentSchemaRef; 120 | } 121 | } 122 | 123 | return { 124 | propertyName: discriminator, 125 | mapping, 126 | }; 127 | }; 128 | -------------------------------------------------------------------------------- /src/create/schema/parsers/enum.ts: -------------------------------------------------------------------------------- 1 | import type { ZodEnum } from 'zod'; 2 | 3 | import type { Schema } from '..'; 4 | 5 | export const createEnumSchema = ( 6 | zodEnum: ZodEnum, 7 | ): Schema => ({ 8 | type: 'schema', 9 | schema: { 10 | type: 'string', 11 | enum: zodEnum._def.values, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/create/schema/parsers/index.ts: -------------------------------------------------------------------------------- 1 | import type { ZodType, ZodTypeDef } from 'zod'; 2 | 3 | import { isZodType } from '../../../zodType'; 4 | import type { RefObject, Schema, SchemaState } from '../../schema'; 5 | 6 | import { createArraySchema } from './array'; 7 | import { createBigIntSchema } from './bigint'; 8 | import { createBooleanSchema } from './boolean'; 9 | import { createBrandedSchema } from './brand'; 10 | import { createCatchSchema } from './catch'; 11 | import { createDateSchema } from './date'; 12 | import { createDefaultSchema } from './default'; 13 | import { createDiscriminatedUnionSchema } from './discriminatedUnion'; 14 | import { createEnumSchema } from './enum'; 15 | import { createIntersectionSchema } from './intersection'; 16 | import { createLazySchema } from './lazy'; 17 | import { createLiteralSchema } from './literal'; 18 | import { createManualTypeSchema } from './manual'; 19 | import { createNativeEnumSchema } from './nativeEnum'; 20 | import { createNullSchema } from './null'; 21 | import { createNullableSchema } from './nullable'; 22 | import { createNumberSchema } from './number'; 23 | import { createObjectSchema } from './object'; 24 | import { createOptionalSchema } from './optional'; 25 | import { createPipelineSchema } from './pipeline'; 26 | import { createPreprocessSchema } from './preprocess'; 27 | import { createReadonlySchema } from './readonly'; 28 | import { createRecordSchema } from './record'; 29 | import { createRefineSchema } from './refine'; 30 | import { createSetSchema } from './set'; 31 | import { createStringSchema } from './string'; 32 | import { createTransformSchema } from './transform'; 33 | import { createTupleSchema } from './tuple'; 34 | import { createUnionSchema } from './union'; 35 | import { createUnknownSchema } from './unknown'; 36 | 37 | export const createSchemaSwitch = < 38 | Output = unknown, 39 | Def extends ZodTypeDef = ZodTypeDef, 40 | Input = Output, 41 | >( 42 | zodSchema: ZodType, 43 | previous: RefObject | undefined, 44 | state: SchemaState, 45 | ): Schema => { 46 | if (zodSchema._def.zodOpenApi?.openapi?.type) { 47 | return createManualTypeSchema(zodSchema, state); 48 | } 49 | 50 | if (isZodType(zodSchema, 'ZodString')) { 51 | return createStringSchema(zodSchema, state); 52 | } 53 | 54 | if (isZodType(zodSchema, 'ZodNumber')) { 55 | return createNumberSchema(zodSchema, state); 56 | } 57 | 58 | if (isZodType(zodSchema, 'ZodBoolean')) { 59 | return createBooleanSchema(zodSchema); 60 | } 61 | 62 | if (isZodType(zodSchema, 'ZodEnum')) { 63 | return createEnumSchema(zodSchema); 64 | } 65 | 66 | if (isZodType(zodSchema, 'ZodLiteral')) { 67 | return createLiteralSchema(zodSchema, state); 68 | } 69 | 70 | if (isZodType(zodSchema, 'ZodNativeEnum')) { 71 | return createNativeEnumSchema(zodSchema, state); 72 | } 73 | 74 | if (isZodType(zodSchema, 'ZodArray')) { 75 | return createArraySchema(zodSchema, state); 76 | } 77 | 78 | if (isZodType(zodSchema, 'ZodObject')) { 79 | return createObjectSchema(zodSchema, previous, state); 80 | } 81 | 82 | if (isZodType(zodSchema, 'ZodUnion')) { 83 | return createUnionSchema(zodSchema, state); 84 | } 85 | 86 | if (isZodType(zodSchema, 'ZodDiscriminatedUnion')) { 87 | return createDiscriminatedUnionSchema(zodSchema, state); 88 | } 89 | 90 | if (isZodType(zodSchema, 'ZodNull')) { 91 | return createNullSchema(); 92 | } 93 | 94 | if (isZodType(zodSchema, 'ZodNullable')) { 95 | return createNullableSchema(zodSchema, state); 96 | } 97 | 98 | if (isZodType(zodSchema, 'ZodOptional')) { 99 | return createOptionalSchema(zodSchema, state); 100 | } 101 | 102 | if (isZodType(zodSchema, 'ZodReadonly')) { 103 | return createReadonlySchema(zodSchema, state); 104 | } 105 | 106 | if (isZodType(zodSchema, 'ZodDefault')) { 107 | return createDefaultSchema(zodSchema, state, previous); 108 | } 109 | 110 | if (isZodType(zodSchema, 'ZodRecord')) { 111 | return createRecordSchema(zodSchema, state); 112 | } 113 | 114 | if (isZodType(zodSchema, 'ZodTuple')) { 115 | return createTupleSchema(zodSchema, state); 116 | } 117 | 118 | if (isZodType(zodSchema, 'ZodDate')) { 119 | return createDateSchema(zodSchema, state); 120 | } 121 | 122 | if (isZodType(zodSchema, 'ZodPipeline')) { 123 | return createPipelineSchema(zodSchema, state); 124 | } 125 | 126 | if ( 127 | isZodType(zodSchema, 'ZodEffects') && 128 | zodSchema._def.effect.type === 'transform' 129 | ) { 130 | return createTransformSchema(zodSchema, state); 131 | } 132 | 133 | if ( 134 | isZodType(zodSchema, 'ZodEffects') && 135 | zodSchema._def.effect.type === 'preprocess' 136 | ) { 137 | return createPreprocessSchema(zodSchema, state); 138 | } 139 | 140 | if ( 141 | isZodType(zodSchema, 'ZodEffects') && 142 | zodSchema._def.effect.type === 'refinement' 143 | ) { 144 | return createRefineSchema(zodSchema, state); 145 | } 146 | 147 | if (isZodType(zodSchema, 'ZodNativeEnum')) { 148 | return createNativeEnumSchema(zodSchema, state); 149 | } 150 | 151 | if (isZodType(zodSchema, 'ZodIntersection')) { 152 | return createIntersectionSchema(zodSchema, state); 153 | } 154 | 155 | if (isZodType(zodSchema, 'ZodCatch')) { 156 | return createCatchSchema(zodSchema, state, previous); 157 | } 158 | 159 | if (isZodType(zodSchema, 'ZodUnknown') || isZodType(zodSchema, 'ZodAny')) { 160 | return createUnknownSchema(zodSchema); 161 | } 162 | 163 | if (isZodType(zodSchema, 'ZodLazy')) { 164 | return createLazySchema(zodSchema, state); 165 | } 166 | 167 | if (isZodType(zodSchema, 'ZodBranded')) { 168 | return createBrandedSchema(zodSchema, state); 169 | } 170 | 171 | if (isZodType(zodSchema, 'ZodSet')) { 172 | return createSetSchema(zodSchema, state); 173 | } 174 | 175 | if (isZodType(zodSchema, 'ZodBigInt')) { 176 | return createBigIntSchema(zodSchema); 177 | } 178 | 179 | return createManualTypeSchema(zodSchema, state); 180 | }; 181 | -------------------------------------------------------------------------------- /src/create/schema/parsers/intersection.ts: -------------------------------------------------------------------------------- 1 | import type { ZodIntersection, ZodTypeAny } from 'zod'; 2 | 3 | import { isZodType } from '../../../zodType'; 4 | import { 5 | type Schema, 6 | type SchemaState, 7 | createSchemaObject, 8 | } from '../../schema'; 9 | 10 | import { flattenEffects } from './transform'; 11 | 12 | export const createIntersectionSchema = < 13 | T extends ZodTypeAny, 14 | U extends ZodTypeAny, 15 | >( 16 | zodIntersection: ZodIntersection, 17 | state: SchemaState, 18 | ): Schema => { 19 | const schemas = flattenIntersection(zodIntersection); 20 | const allOfs = schemas.map((schema, index) => 21 | createSchemaObject(schema, state, [`intersection ${index}`]), 22 | ); 23 | return { 24 | type: 'schema', 25 | schema: { 26 | allOf: allOfs.map((schema) => schema.schema), 27 | }, 28 | effects: flattenEffects(allOfs.map((schema) => schema.effects)), 29 | }; 30 | }; 31 | 32 | export const flattenIntersection = (zodType: ZodTypeAny): ZodTypeAny[] => { 33 | if (!isZodType(zodType, 'ZodIntersection')) { 34 | return [zodType]; 35 | } 36 | 37 | const leftSchemas = flattenIntersection(zodType._def.left); 38 | const rightSchemas = flattenIntersection(zodType._def.right); 39 | 40 | return [...leftSchemas, ...rightSchemas]; 41 | }; 42 | -------------------------------------------------------------------------------- /src/create/schema/parsers/lazy.ts: -------------------------------------------------------------------------------- 1 | import type { ZodLazy, ZodTypeAny } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | 9 | export const createLazySchema = ( 10 | zodLazy: ZodLazy, 11 | state: SchemaState, 12 | ): Schema => { 13 | const innerSchema = zodLazy._def.getter(); 14 | return createSchemaObject(innerSchema, state, ['lazy schema']); 15 | }; 16 | -------------------------------------------------------------------------------- /src/create/schema/parsers/literal.ts: -------------------------------------------------------------------------------- 1 | import type { ZodLiteral } from 'zod'; 2 | 3 | import type { Schema, SchemaState } from '..'; 4 | import { satisfiesVersion } from '../../../openapi'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | 7 | import { createNullSchema } from './null'; 8 | 9 | export const createLiteralSchema = ( 10 | zodLiteral: ZodLiteral, 11 | state: SchemaState, 12 | ): Schema => { 13 | if (zodLiteral.value === null) { 14 | return createNullSchema(); 15 | } 16 | 17 | if (satisfiesVersion(state.components.openapi, '3.1.0')) { 18 | return { 19 | type: 'schema', 20 | schema: { 21 | type: typeof zodLiteral.value as oas31.SchemaObject['type'], 22 | const: zodLiteral.value, 23 | }, 24 | }; 25 | } 26 | 27 | return { 28 | type: 'schema', 29 | schema: { 30 | type: typeof zodLiteral.value as oas31.SchemaObject['type'], 31 | enum: [zodLiteral.value], 32 | }, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/create/schema/parsers/manual.ts: -------------------------------------------------------------------------------- 1 | import type { ZodType, ZodTypeDef } from 'zod'; 2 | 3 | import type { Schema, SchemaState } from '../../schema'; 4 | 5 | export const createManualTypeSchema = < 6 | Output = unknown, 7 | Def extends ZodTypeDef = ZodTypeDef, 8 | Input = Output, 9 | >( 10 | zodSchema: ZodType, 11 | state: SchemaState, 12 | ): Schema => { 13 | if (!zodSchema._def.zodOpenApi?.openapi?.type) { 14 | const schemaName = zodSchema.constructor.name; 15 | throw new Error( 16 | `Unknown schema ${schemaName} at ${state.path.join( 17 | ' > ', 18 | )}. Please assign it a manual 'type'.`, 19 | ); 20 | } 21 | 22 | return { 23 | type: 'schema', 24 | schema: { 25 | type: zodSchema._def.zodOpenApi?.openapi.type, 26 | }, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/create/schema/parsers/nativeEnum.ts: -------------------------------------------------------------------------------- 1 | import type { EnumLike, ZodNativeEnum } from 'zod'; 2 | 3 | import { satisfiesVersion } from '../../../openapi'; 4 | import type { Schema, SchemaState } from '../../schema'; 5 | 6 | export const createNativeEnumSchema = ( 7 | zodEnum: ZodNativeEnum, 8 | state: SchemaState, 9 | ): Schema => { 10 | const enumValues = getValidEnumValues(zodEnum._def.values); 11 | const { numbers, strings } = sortStringsAndNumbers(enumValues); 12 | 13 | if (strings.length && numbers.length) { 14 | if (satisfiesVersion(state.components.openapi, '3.1.0')) { 15 | return { 16 | type: 'schema', 17 | schema: { 18 | type: ['string', 'number'], 19 | enum: [...strings, ...numbers], 20 | }, 21 | }; 22 | } 23 | return { 24 | type: 'schema', 25 | schema: { 26 | oneOf: [ 27 | { type: 'string', enum: strings }, 28 | { type: 'number', enum: numbers }, 29 | ], 30 | }, 31 | }; 32 | } 33 | 34 | if (strings.length) { 35 | return { 36 | type: 'schema', 37 | schema: { 38 | type: 'string', 39 | enum: strings, 40 | }, 41 | }; 42 | } 43 | 44 | return { 45 | type: 'schema', 46 | schema: { 47 | type: 'number', 48 | enum: numbers, 49 | }, 50 | }; 51 | }; 52 | 53 | interface StringsAndNumbers { 54 | strings: string[]; 55 | numbers: number[]; 56 | } 57 | 58 | export const getValidEnumValues = (enumValues: EnumLike) => { 59 | const keys = Object.keys(enumValues).filter( 60 | (key) => typeof enumValues[enumValues[key] as number] !== 'number', 61 | ); 62 | return keys.map((key) => enumValues[key] as number); 63 | }; 64 | 65 | export const sortStringsAndNumbers = ( 66 | values: Array, 67 | ): StringsAndNumbers => ({ 68 | strings: values.filter((value): value is string => typeof value === 'string'), 69 | numbers: values.filter((value): value is number => typeof value === 'number'), 70 | }); 71 | -------------------------------------------------------------------------------- /src/create/schema/parsers/null.ts: -------------------------------------------------------------------------------- 1 | import type { Schema } from '..'; 2 | 3 | export const createNullSchema = (): Schema => ({ 4 | type: 'schema', 5 | schema: { 6 | type: 'null', 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/create/schema/parsers/nullable.ts: -------------------------------------------------------------------------------- 1 | import type { ZodNullable, ZodTypeAny } from 'zod'; 2 | 3 | import { satisfiesVersion } from '../../../openapi'; 4 | import type { oas31 } from '../../../openapi3-ts/dist'; 5 | import type { ZodOpenApiVersion } from '../../document'; 6 | import { 7 | type Schema, 8 | type SchemaState, 9 | createSchemaObject, 10 | } from '../../schema'; 11 | 12 | export const createNullableSchema = ( 13 | zodNullable: ZodNullable, 14 | state: SchemaState, 15 | ): Schema => { 16 | const schemaObject = createSchemaObject(zodNullable.unwrap(), state, [ 17 | 'nullable', 18 | ]); 19 | 20 | if (satisfiesVersion(state.components.openapi, '3.1.0')) { 21 | if (schemaObject.type === 'ref' || schemaObject.schema.allOf) { 22 | return { 23 | type: 'schema', 24 | schema: { 25 | oneOf: mapNullOf([schemaObject.schema], state.components.openapi), 26 | }, 27 | effects: schemaObject.effects, 28 | }; 29 | } 30 | 31 | if (schemaObject.schema.oneOf) { 32 | const { oneOf, ...schema } = schemaObject.schema; 33 | return { 34 | type: 'schema', 35 | schema: { 36 | oneOf: mapNullOf(oneOf, state.components.openapi), 37 | ...schema, 38 | }, 39 | effects: schemaObject.effects, 40 | }; 41 | } 42 | 43 | if (schemaObject.schema.anyOf) { 44 | const { anyOf, ...schema } = schemaObject.schema; 45 | return { 46 | type: 'schema', 47 | schema: { 48 | anyOf: mapNullOf(anyOf, state.components.openapi), 49 | ...schema, 50 | }, 51 | effects: schemaObject.effects, 52 | }; 53 | } 54 | 55 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 56 | const { type, const: schemaConst, ...schema } = schemaObject.schema; 57 | 58 | if (schemaConst) { 59 | return { 60 | type: 'schema', 61 | schema: { 62 | type: mapNullType(type), 63 | enum: [schemaConst, null], 64 | ...schema, 65 | } as oas31.SchemaObject, 66 | effects: schemaObject.effects, 67 | }; 68 | } 69 | 70 | return { 71 | type: 'schema', 72 | schema: { 73 | type: mapNullType(type), 74 | ...schema, 75 | // https://github.com/json-schema-org/json-schema-spec/issues/258 76 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 77 | ...(schema.enum && { enum: [...schema.enum, null] }), 78 | }, 79 | effects: schemaObject.effects, 80 | }; 81 | } 82 | 83 | if (schemaObject.type === 'ref') { 84 | return { 85 | type: 'schema', 86 | schema: { 87 | allOf: [schemaObject.schema], 88 | nullable: true, 89 | } as oas31.SchemaObject, 90 | effects: schemaObject.effects, 91 | }; 92 | } 93 | 94 | const { type, ...schema } = schemaObject.schema; 95 | 96 | return { 97 | type: 'schema', 98 | schema: { 99 | ...(type && { type }), 100 | nullable: true, 101 | ...schema, 102 | // 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 103 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 104 | ...(schema.enum && { enum: [...schema.enum, null] }), 105 | } as oas31.SchemaObject, 106 | effects: schemaObject.effects, 107 | }; 108 | }; 109 | 110 | const mapNullType = ( 111 | type: oas31.SchemaObject['type'], 112 | ): oas31.SchemaObject['type'] => { 113 | if (!type) { 114 | return 'null'; 115 | } 116 | 117 | if (Array.isArray(type)) { 118 | return [...type, 'null']; 119 | } 120 | 121 | return [type, 'null']; 122 | }; 123 | 124 | const mapNullOf = ( 125 | ofSchema: Array, 126 | openapi: ZodOpenApiVersion, 127 | ): Array => { 128 | if (satisfiesVersion(openapi, '3.1.0')) { 129 | return [...ofSchema, { type: 'null' }]; 130 | } 131 | return [...ofSchema, { nullable: true } as oas31.SchemaObject]; 132 | }; 133 | -------------------------------------------------------------------------------- /src/create/schema/parsers/number.ts: -------------------------------------------------------------------------------- 1 | import type { ZodNumber, ZodNumberCheck } from 'zod'; 2 | 3 | import { satisfiesVersion } from '../../../openapi'; 4 | import type { oas30, oas31 } from '../../../openapi3-ts/dist'; 5 | import type { ZodOpenApiVersion } from '../../document'; 6 | import type { Schema, SchemaState } from '../../schema'; 7 | 8 | export const createNumberSchema = ( 9 | zodNumber: ZodNumber, 10 | state: SchemaState, 11 | ): Schema => { 12 | const zodNumberChecks = getZodNumberChecks(zodNumber); 13 | 14 | const minimum = mapMinimum(zodNumberChecks, state.components.openapi); 15 | const maximum = mapMaximum(zodNumberChecks, state.components.openapi); 16 | const multipleOf = mapMultipleOf(zodNumberChecks); 17 | 18 | return { 19 | type: 'schema', 20 | schema: { 21 | type: mapNumberType(zodNumberChecks), 22 | ...(multipleOf && multipleOf), 23 | ...(minimum && (minimum as oas31.SchemaObject)), // Union types are not easy to tame 24 | ...(maximum && (maximum as oas31.SchemaObject)), 25 | }, 26 | }; 27 | }; 28 | 29 | export const mapMultipleOf = ( 30 | zodNumberCheck: ZodNumberCheckMap, 31 | ): Pick | undefined => 32 | zodNumberCheck.multipleOf 33 | ? { multipleOf: zodNumberCheck.multipleOf.value } 34 | : undefined; 35 | 36 | export const mapMaximum = ( 37 | zodNumberCheck: ZodNumberCheckMap, 38 | openapi: ZodOpenApiVersion, 39 | ): 40 | | Pick< 41 | oas31.SchemaObject | oas30.SchemaObject, 42 | 'maximum' | 'exclusiveMaximum' 43 | > 44 | | undefined => { 45 | if (!zodNumberCheck.max) { 46 | return undefined; 47 | } 48 | 49 | const maximum = zodNumberCheck.max.value; 50 | if (zodNumberCheck.max.inclusive) { 51 | return { ...(maximum !== undefined && { maximum }) }; 52 | } 53 | if (satisfiesVersion(openapi, '3.1.0')) { 54 | return { exclusiveMaximum: maximum }; 55 | } 56 | return { maximum, exclusiveMaximum: true }; 57 | }; 58 | export const mapMinimum = ( 59 | zodNumberCheck: ZodNumberCheckMap, 60 | openapi: ZodOpenApiVersion, 61 | ): 62 | | Pick< 63 | oas31.SchemaObject | oas30.SchemaObject, 64 | 'minimum' | 'exclusiveMinimum' 65 | > 66 | | undefined => { 67 | if (!zodNumberCheck.min) { 68 | return undefined; 69 | } 70 | 71 | const minimum = zodNumberCheck.min.value; 72 | if (zodNumberCheck.min.inclusive) { 73 | return { ...(minimum !== undefined && { minimum }) }; 74 | } 75 | if (satisfiesVersion(openapi, '3.1.0')) { 76 | return { exclusiveMinimum: minimum }; 77 | } 78 | return { minimum, exclusiveMinimum: true }; 79 | }; 80 | 81 | type ZodNumberCheckMap = { 82 | [kind in ZodNumberCheck['kind']]?: Extract; 83 | }; 84 | 85 | const getZodNumberChecks = (zodNumber: ZodNumber): ZodNumberCheckMap => 86 | zodNumber._def.checks.reduce((acc, check) => { 87 | // union type issues 88 | acc[check.kind] = check as never; 89 | return acc; 90 | }, {}); 91 | 92 | const mapNumberType = ( 93 | zodNumberChecks: ZodNumberCheckMap, 94 | ): oas31.SchemaObject['type'] => (zodNumberChecks.int ? 'integer' : 'number'); 95 | -------------------------------------------------------------------------------- /src/create/schema/parsers/optional.ts: -------------------------------------------------------------------------------- 1 | import type { ZodOptional, ZodTypeAny } from 'zod'; 2 | 3 | import { isZodType } from '../../../zodType'; 4 | import { 5 | type Schema, 6 | type SchemaState, 7 | createSchemaObject, 8 | } from '../../schema'; 9 | 10 | export const createOptionalSchema = ( 11 | zodOptional: ZodOptional, 12 | state: SchemaState, 13 | ): Schema => createSchemaObject(zodOptional.unwrap(), state, ['optional']); // Optional doesn't change OpenAPI schema 14 | 15 | export const isOptionalObjectKey = (zodSchema: ZodTypeAny): boolean => 16 | isZodType(zodSchema, 'ZodNever') || 17 | isZodType(zodSchema, 'ZodUndefined') || 18 | (isZodType(zodSchema, 'ZodOptional') && 19 | isOptionalObjectKey(zodSchema.unwrap())) || 20 | (isZodType(zodSchema, 'ZodLiteral') && zodSchema._def.value === undefined); 21 | -------------------------------------------------------------------------------- /src/create/schema/parsers/pipeline.ts: -------------------------------------------------------------------------------- 1 | import type { ZodPipeline, ZodTypeAny } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | 9 | import { flattenEffects } from './transform'; 10 | 11 | export const createPipelineSchema = < 12 | A extends ZodTypeAny, 13 | B extends ZodTypeAny, 14 | >( 15 | zodPipeline: ZodPipeline, 16 | state: SchemaState, 17 | ): Schema => { 18 | if ( 19 | zodPipeline._def.zodOpenApi?.openapi?.effectType === 'input' || 20 | zodPipeline._def.zodOpenApi?.openapi?.effectType === 'same' 21 | ) { 22 | return createSchemaObject(zodPipeline._def.in, state, ['pipeline input']); 23 | } 24 | 25 | if (zodPipeline._def.zodOpenApi?.openapi?.effectType === 'output') { 26 | return createSchemaObject(zodPipeline._def.out, state, ['pipeline output']); 27 | } 28 | 29 | if (state.type === 'input') { 30 | const schema = createSchemaObject(zodPipeline._def.in, state, [ 31 | 'pipeline input', 32 | ]); 33 | return { 34 | ...schema, 35 | effects: flattenEffects([ 36 | [ 37 | { 38 | type: 'schema', 39 | creationType: 'input', 40 | path: [...state.path], 41 | zodType: zodPipeline, 42 | }, 43 | ], 44 | schema.effects, 45 | ]), 46 | }; 47 | } 48 | 49 | const schema = createSchemaObject(zodPipeline._def.out, state, [ 50 | 'pipeline output', 51 | ]); 52 | return { 53 | ...schema, 54 | effects: flattenEffects([ 55 | [ 56 | { 57 | type: 'schema', 58 | creationType: 'output', 59 | path: [...state.path], 60 | zodType: zodPipeline, 61 | }, 62 | ], 63 | schema.effects, 64 | ]), 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/create/schema/parsers/preprocess.ts: -------------------------------------------------------------------------------- 1 | import type { ZodEffects, ZodTypeAny, input, output } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | 9 | export const createPreprocessSchema = < 10 | T extends ZodTypeAny, 11 | Output = output, 12 | Input = input, 13 | >( 14 | zodPreprocess: ZodEffects, 15 | state: SchemaState, 16 | ): Schema => 17 | createSchemaObject(zodPreprocess._def.schema, state, ['preprocess schema']); 18 | -------------------------------------------------------------------------------- /src/create/schema/parsers/readonly.ts: -------------------------------------------------------------------------------- 1 | import type { ZodReadonly, ZodTypeAny } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | 9 | export const createReadonlySchema = ( 10 | zodReadonly: ZodReadonly, 11 | state: SchemaState, 12 | ): Schema => // Readonly doesn't change OpenAPI schema 13 | createSchemaObject(zodReadonly._def.innerType, state, ['readonly']); 14 | -------------------------------------------------------------------------------- /src/create/schema/parsers/record.ts: -------------------------------------------------------------------------------- 1 | import type { KeySchema, ZodRecord, ZodString, ZodTypeAny } from 'zod'; 2 | 3 | import { satisfiesVersion } from '../../../openapi'; 4 | import type { oas31 } from '../../../openapi3-ts/dist'; 5 | import { 6 | type Schema, 7 | type SchemaState, 8 | createSchemaObject, 9 | } from '../../schema'; 10 | 11 | import { flattenEffects } from './transform'; 12 | 13 | export const createRecordSchema = < 14 | Key extends KeySchema = ZodString, 15 | Value extends ZodTypeAny = ZodTypeAny, 16 | >( 17 | zodRecord: ZodRecord, 18 | state: SchemaState, 19 | ): Schema => { 20 | const additionalProperties = createSchemaObject( 21 | zodRecord.valueSchema as ZodTypeAny, 22 | state, 23 | ['record value'], 24 | ); 25 | 26 | const keySchema = createSchemaObject(zodRecord.keySchema, state, [ 27 | 'record key', 28 | ]); 29 | 30 | const maybeComponent = state.components.schemas.get(zodRecord.keySchema); 31 | const isComplete = maybeComponent && maybeComponent.type === 'complete'; 32 | const maybeSchema = isComplete && maybeComponent.schemaObject; 33 | const maybeEffects = (isComplete && maybeComponent.effects) || undefined; 34 | 35 | const renderedKeySchema = maybeSchema || keySchema.schema; 36 | 37 | if ('enum' in renderedKeySchema && renderedKeySchema.enum) { 38 | return { 39 | type: 'schema', 40 | schema: { 41 | type: 'object', 42 | properties: (renderedKeySchema.enum as string[]).reduce< 43 | NonNullable 44 | >((acc, key) => { 45 | acc[key] = additionalProperties.schema; 46 | return acc; 47 | }, {}), 48 | additionalProperties: false, 49 | }, 50 | effects: flattenEffects([ 51 | keySchema.effects, 52 | additionalProperties.effects, 53 | maybeEffects, 54 | ]), 55 | }; 56 | } 57 | 58 | if ( 59 | satisfiesVersion(state.components.openapi, '3.1.0') && 60 | 'type' in renderedKeySchema && 61 | renderedKeySchema.type === 'string' && 62 | Object.keys(renderedKeySchema).length > 1 63 | ) { 64 | return { 65 | type: 'schema', 66 | schema: { 67 | type: 'object', 68 | propertyNames: keySchema.schema, 69 | additionalProperties: additionalProperties.schema, 70 | }, 71 | effects: flattenEffects([ 72 | keySchema.effects, 73 | additionalProperties.effects, 74 | ]), 75 | }; 76 | } 77 | 78 | return { 79 | type: 'schema', 80 | schema: { 81 | type: 'object', 82 | additionalProperties: additionalProperties.schema, 83 | }, 84 | effects: additionalProperties.effects, 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /src/create/schema/parsers/refine.ts: -------------------------------------------------------------------------------- 1 | import type { ZodEffects, ZodTypeAny, input, output } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | 9 | export const createRefineSchema = < 10 | T extends ZodTypeAny, 11 | Output = output, 12 | Input = input, 13 | >( 14 | zodRefine: ZodEffects, 15 | state: SchemaState, 16 | ): Schema => 17 | createSchemaObject(zodRefine._def.schema, state, ['refine schema']); 18 | -------------------------------------------------------------------------------- /src/create/schema/parsers/set.ts: -------------------------------------------------------------------------------- 1 | import type { ZodSet, ZodTypeAny } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | 9 | export const createSetSchema = ( 10 | zodSet: ZodSet, 11 | state: SchemaState, 12 | ): Schema => { 13 | const schema = zodSet._def.valueType; 14 | const minItems = zodSet._def.minSize?.value; 15 | const maxItems = zodSet._def.maxSize?.value; 16 | const itemSchema = createSchemaObject(schema, state, ['set items']); 17 | return { 18 | type: 'schema', 19 | schema: { 20 | type: 'array', 21 | items: itemSchema.schema, 22 | uniqueItems: true, 23 | ...(minItems !== undefined && { minItems }), 24 | ...(maxItems !== undefined && { maxItems }), 25 | }, 26 | effects: itemSchema.effects, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/create/schema/parsers/string.ts: -------------------------------------------------------------------------------- 1 | import type { ZodString, ZodStringCheck } from 'zod'; 2 | 3 | import type { Schema, SchemaState } from '..'; 4 | import { satisfiesVersion } from '../../../openapi'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | 7 | export const createStringSchema = ( 8 | zodString: ZodString, 9 | state: SchemaState, 10 | ): Schema => { 11 | const zodStringChecks = getZodStringChecks(zodString); 12 | const format = mapStringFormat(zodStringChecks); 13 | const patterns = mapPatterns(zodStringChecks); 14 | const minLength = 15 | zodStringChecks.length?.[0]?.value ?? zodStringChecks.min?.[0]?.value; 16 | const maxLength = 17 | zodStringChecks.length?.[0]?.value ?? zodStringChecks.max?.[0]?.value; 18 | const contentEncoding = satisfiesVersion(state.components.openapi, '3.1.0') 19 | ? mapContentEncoding(zodStringChecks) 20 | : undefined; 21 | 22 | if (patterns.length <= 1) { 23 | return { 24 | type: 'schema', 25 | schema: { 26 | type: 'string', 27 | ...(format && { format }), 28 | ...(patterns[0] && { pattern: patterns[0] }), 29 | ...(minLength !== undefined && { minLength }), 30 | ...(maxLength !== undefined && { maxLength }), 31 | ...(contentEncoding && { contentEncoding }), 32 | }, 33 | }; 34 | } 35 | 36 | return { 37 | type: 'schema', 38 | schema: { 39 | allOf: [ 40 | { 41 | type: 'string', 42 | ...(format && { format }), 43 | ...(patterns[0] && { pattern: patterns[0] }), 44 | ...(minLength !== undefined && { minLength }), 45 | ...(maxLength !== undefined && { maxLength }), 46 | ...(contentEncoding && { contentEncoding }), 47 | }, 48 | ...patterns.slice(1).map( 49 | (pattern): oas31.SchemaObject => ({ 50 | type: 'string', 51 | pattern, 52 | }), 53 | ), 54 | ], 55 | }, 56 | }; 57 | }; 58 | 59 | type ZodStringCheckMap = { 60 | [kind in ZodStringCheck['kind']]?: [ 61 | Extract, 62 | ...Array>, 63 | ]; 64 | }; 65 | 66 | const getZodStringChecks = (zodString: ZodString): ZodStringCheckMap => 67 | zodString._def.checks.reduce( 68 | (acc, check: ZodStringCheck) => { 69 | const mapping = acc[check.kind]; 70 | if (mapping) { 71 | mapping.push(check as never); 72 | return acc; 73 | } 74 | 75 | acc[check.kind] = [check as never]; 76 | return acc; 77 | }, 78 | {}, 79 | ); 80 | 81 | const mapPatterns = (zodStringChecks: ZodStringCheckMap): string[] => { 82 | const startsWith = mapStartsWith(zodStringChecks); 83 | const endsWith = mapEndsWith(zodStringChecks); 84 | const regex = mapRegex(zodStringChecks); 85 | const includes = mapIncludes(zodStringChecks); 86 | 87 | const patterns: string[] = [ 88 | ...(regex ?? []), 89 | ...(startsWith ? [startsWith] : []), 90 | ...(endsWith ? [endsWith] : []), 91 | ...(includes ?? []), 92 | ]; 93 | 94 | return patterns; 95 | }; 96 | 97 | const mapStartsWith = ( 98 | zodStringChecks: ZodStringCheckMap, 99 | ): oas31.SchemaObject['pattern'] => { 100 | if (zodStringChecks.startsWith?.[0]?.value) { 101 | return `^${zodStringChecks.startsWith[0].value}`; 102 | } 103 | 104 | return undefined; 105 | }; 106 | 107 | const mapEndsWith = ( 108 | zodStringChecks: ZodStringCheckMap, 109 | ): oas31.SchemaObject['pattern'] => { 110 | if (zodStringChecks.endsWith?.[0]?.value) { 111 | return `${zodStringChecks.endsWith[0].value}$`; 112 | } 113 | 114 | return undefined; 115 | }; 116 | 117 | const mapRegex = (zodStringChecks: ZodStringCheckMap): string[] | undefined => 118 | zodStringChecks.regex?.map((regexCheck) => regexCheck.regex.source); 119 | 120 | const mapIncludes = ( 121 | zodStringChecks: ZodStringCheckMap, 122 | ): string[] | undefined => 123 | zodStringChecks.includes?.map((includeCheck) => { 124 | if (includeCheck.position === 0) { 125 | return `^${includeCheck.value}`; 126 | } 127 | if (includeCheck.position) { 128 | return `^.{${includeCheck.position}}${includeCheck.value}`; 129 | } 130 | return includeCheck.value; 131 | }); 132 | 133 | const mapStringFormat = ( 134 | zodStringChecks: ZodStringCheckMap, 135 | ): oas31.SchemaObject['format'] => { 136 | if (zodStringChecks.uuid) { 137 | return 'uuid'; 138 | } 139 | 140 | if (zodStringChecks.datetime) { 141 | return 'date-time'; 142 | } 143 | 144 | if (zodStringChecks.date) { 145 | return 'date'; 146 | } 147 | 148 | if (zodStringChecks.time) { 149 | return 'time'; 150 | } 151 | 152 | if (zodStringChecks.duration) { 153 | return 'duration'; 154 | } 155 | 156 | if (zodStringChecks.email) { 157 | return 'email'; 158 | } 159 | 160 | if (zodStringChecks.url) { 161 | return 'uri'; 162 | } 163 | 164 | if (zodStringChecks.ip?.every((ip) => ip.version === 'v4')) { 165 | return 'ipv4'; 166 | } 167 | 168 | if (zodStringChecks.ip?.every((ip) => ip.version === 'v6')) { 169 | return 'ipv6'; 170 | } 171 | 172 | if (zodStringChecks.cidr?.every((ip) => ip.version === 'v4')) { 173 | return 'ipv4'; 174 | } 175 | 176 | if (zodStringChecks.cidr?.every((ip) => ip.version === 'v6')) { 177 | return 'ipv6'; 178 | } 179 | 180 | return undefined; 181 | }; 182 | 183 | const mapContentEncoding = ( 184 | zodStringChecks: ZodStringCheckMap, 185 | ): string | undefined => { 186 | if (zodStringChecks.base64) { 187 | return 'base64'; 188 | } 189 | 190 | return undefined; 191 | }; 192 | -------------------------------------------------------------------------------- /src/create/schema/parsers/tuple.ts: -------------------------------------------------------------------------------- 1 | import type { ZodTuple, ZodTypeAny } from 'zod'; 2 | 3 | import { satisfiesVersion } from '../../../openapi'; 4 | import { 5 | type Schema, 6 | type SchemaState, 7 | createSchemaObject, 8 | } from '../../schema'; 9 | 10 | import { flattenEffects } from './transform'; 11 | 12 | export const createTupleSchema = < 13 | T extends [] | [ZodTypeAny, ...ZodTypeAny[]] = [ZodTypeAny, ...ZodTypeAny[]], 14 | Rest extends ZodTypeAny | null = null, 15 | >( 16 | zodTuple: ZodTuple, 17 | state: SchemaState, 18 | ): Schema => { 19 | const items = zodTuple.items; 20 | const rest = zodTuple._def.rest; 21 | 22 | const prefixItems = mapPrefixItems(items, state); 23 | 24 | if (satisfiesVersion(state.components.openapi, '3.1.0')) { 25 | if (!rest) { 26 | return { 27 | type: 'schema', 28 | schema: { 29 | type: 'array', 30 | maxItems: items.length, 31 | minItems: items.length, 32 | ...(prefixItems && { 33 | prefixItems: prefixItems.schemas.map((item) => item.schema), 34 | }), 35 | }, 36 | effects: prefixItems?.effects, 37 | }; 38 | } 39 | 40 | const itemSchema = createSchemaObject(rest, state, ['tuple items']); 41 | 42 | return { 43 | type: 'schema', 44 | schema: { 45 | type: 'array', 46 | items: itemSchema.schema, 47 | ...(prefixItems && { 48 | prefixItems: prefixItems.schemas.map((item) => item.schema), 49 | }), 50 | }, 51 | effects: flattenEffects([prefixItems?.effects, itemSchema.effects]), 52 | }; 53 | } 54 | 55 | if (!rest) { 56 | return { 57 | type: 'schema', 58 | schema: { 59 | type: 'array', 60 | maxItems: items.length, 61 | minItems: items.length, 62 | ...(prefixItems && { 63 | items: { oneOf: prefixItems.schemas.map((item) => item.schema) }, 64 | }), 65 | }, 66 | effects: prefixItems?.effects, 67 | }; 68 | } 69 | 70 | if (prefixItems) { 71 | const restSchema = createSchemaObject(rest, state, ['tuple items']); 72 | return { 73 | type: 'schema', 74 | schema: { 75 | type: 'array', 76 | items: { 77 | oneOf: [ 78 | ...prefixItems.schemas.map((item) => item.schema), 79 | restSchema.schema, 80 | ], 81 | }, 82 | }, 83 | effects: flattenEffects([restSchema.effects, prefixItems.effects]), 84 | }; 85 | } 86 | 87 | return { 88 | type: 'schema', 89 | schema: { 90 | type: 'array', 91 | }, 92 | }; 93 | }; 94 | 95 | const mapPrefixItems = ( 96 | items: ZodTypeAny[], 97 | state: SchemaState, 98 | ): { effects?: Schema['effects']; schemas: Schema[] } | undefined => { 99 | if (items.length) { 100 | const schemas = items.map((item, index) => 101 | createSchemaObject(item, state, [`tuple item ${index}`]), 102 | ); 103 | 104 | return { 105 | effects: flattenEffects(schemas.map((s) => s.effects)), 106 | schemas, 107 | }; 108 | } 109 | return undefined; 110 | }; 111 | -------------------------------------------------------------------------------- /src/create/schema/parsers/union.ts: -------------------------------------------------------------------------------- 1 | import type { ZodTypeAny, ZodUnion } from 'zod'; 2 | 3 | import { 4 | type Schema, 5 | type SchemaState, 6 | createSchemaObject, 7 | } from '../../schema'; 8 | 9 | import { isOptionalObjectKey } from './optional'; 10 | import { flattenEffects } from './transform'; 11 | 12 | export const createUnionSchema = < 13 | T extends readonly [ZodTypeAny, ...ZodTypeAny[]], 14 | >( 15 | zodUnion: ZodUnion, 16 | state: SchemaState, 17 | ): Schema => { 18 | const schemas = zodUnion.options.reduce((acc, option, index) => { 19 | if (!isOptionalObjectKey(option)) { 20 | acc.push(createSchemaObject(option, state, [`union option ${index}`])); 21 | } 22 | return acc; 23 | }, []); 24 | 25 | if ( 26 | zodUnion._def.zodOpenApi?.openapi?.unionOneOf ?? 27 | state.documentOptions?.unionOneOf 28 | ) { 29 | return { 30 | type: 'schema', 31 | schema: { 32 | oneOf: schemas.map((s) => s.schema), 33 | }, 34 | effects: flattenEffects(schemas.map((s) => s.effects)), 35 | }; 36 | } 37 | 38 | return { 39 | type: 'schema', 40 | schema: { 41 | anyOf: schemas.map((s) => s.schema), 42 | }, 43 | effects: flattenEffects(schemas.map((s) => s.effects)), 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/create/schema/parsers/unknown.ts: -------------------------------------------------------------------------------- 1 | import type { ZodAny, ZodUnknown } from 'zod'; 2 | 3 | import type { Schema } from '..'; 4 | 5 | export const createUnknownSchema = ( 6 | _zodUnknown: ZodUnknown | ZodAny, 7 | ): Schema => ({ 8 | type: 'schema', 9 | schema: {}, 10 | }); 11 | -------------------------------------------------------------------------------- /src/create/schema/single.test.ts: -------------------------------------------------------------------------------- 1 | import '../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { type SchemaResult, createSchema } from './single'; 5 | 6 | describe('createSchema', () => { 7 | it('should create a schema', () => { 8 | const schema = createSchema(z.string().openapi({ description: 'foo' })); 9 | 10 | expect(schema).toEqual({ 11 | schema: { 12 | type: 'string', 13 | description: 'foo', 14 | }, 15 | }); 16 | }); 17 | 18 | it('should create a registered schema', () => { 19 | const schema = createSchema( 20 | z.string().openapi({ description: 'foo', ref: 'String' }), 21 | ); 22 | 23 | expect(schema).toEqual({ 24 | schema: { 25 | $ref: '#/components/schemas/String', 26 | }, 27 | components: { 28 | String: { 29 | type: 'string', 30 | description: 'foo', 31 | }, 32 | }, 33 | }); 34 | }); 35 | 36 | it('should create components', () => { 37 | const schema = createSchema( 38 | z.object({ 39 | foo: z.string().openapi({ description: 'foo', ref: 'foo' }), 40 | }), 41 | ); 42 | 43 | expect(schema).toEqual({ 44 | schema: { 45 | type: 'object', 46 | properties: { 47 | foo: { 48 | $ref: '#/components/schemas/foo', 49 | }, 50 | }, 51 | required: ['foo'], 52 | }, 53 | components: { 54 | foo: { 55 | type: 'string', 56 | description: 'foo', 57 | }, 58 | }, 59 | }); 60 | }); 61 | 62 | it('should support componentRefPath', () => { 63 | const schema = createSchema( 64 | z.string().openapi({ description: 'foo', ref: 'String' }), 65 | { 66 | componentRefPath: '#/definitions/', 67 | }, 68 | ); 69 | 70 | expect(schema).toEqual({ 71 | schema: { 72 | $ref: '#/definitions/String', 73 | }, 74 | components: { 75 | String: { 76 | type: 'string', 77 | description: 'foo', 78 | }, 79 | }, 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/create/schema/single.ts: -------------------------------------------------------------------------------- 1 | import type { ZodType } from 'zod'; 2 | 3 | import type { OpenApiVersion } from '../../openapi'; 4 | import type { oas30, oas31 } from '../../openapi3-ts/dist'; 5 | import { 6 | type CreationType, 7 | createSchemaComponents, 8 | getDefaultComponents, 9 | } from '../components'; 10 | import type { CreateDocumentOptions } from '../document'; 11 | 12 | import { type SchemaState, createSchema as internalCreateSchema } from '.'; 13 | 14 | export interface SchemaResult { 15 | schema: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject; 16 | components?: 17 | | Record< 18 | string, 19 | oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject 20 | > 21 | | undefined; 22 | } 23 | 24 | export interface CreateSchemaOptions extends CreateDocumentOptions { 25 | /** 26 | * This controls whether this should be rendered as a request (`input`) or response (`output`). Defaults to `output` 27 | */ 28 | schemaType?: CreationType; 29 | /** 30 | * OpenAPI version to use, defaults to `'3.1.0'` 31 | */ 32 | openapi?: OpenApiVersion; 33 | /** 34 | * Additional components to use and create while rendering the schema 35 | */ 36 | components?: Record; 37 | /** 38 | * The $ref path to use for the component. Defaults to `#/components/schemas/` 39 | */ 40 | componentRefPath?: string; 41 | } 42 | 43 | export const createSchema = ( 44 | zodType: ZodType, 45 | opts?: CreateSchemaOptions, 46 | ): SchemaResult => { 47 | const components = getDefaultComponents( 48 | { 49 | schemas: opts?.components, 50 | }, 51 | opts?.openapi, 52 | ); 53 | const state: SchemaState = { 54 | components, 55 | type: opts?.schemaType ?? 'output', 56 | path: [], 57 | visited: new Set(), 58 | documentOptions: opts, 59 | }; 60 | 61 | const schema = internalCreateSchema(zodType, state, ['createSchema']); 62 | 63 | const schemaComponents = createSchemaComponents({}, components); 64 | 65 | return { 66 | schema, 67 | components: schemaComponents, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /src/create/schema/tests/array.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('array', () => { 9 | it('creates simple arrays', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'array', 12 | items: { 13 | type: 'string', 14 | }, 15 | }; 16 | const schema = z.array(z.string()); 17 | 18 | const result = createSchema(schema, createOutputState(), ['array']); 19 | 20 | expect(result).toEqual(expected); 21 | }); 22 | 23 | it('creates min and max', () => { 24 | const expected: oas31.SchemaObject = { 25 | type: 'array', 26 | items: { 27 | type: 'string', 28 | }, 29 | minItems: 0, 30 | maxItems: 10, 31 | }; 32 | 33 | const schema = z.array(z.string()).min(0).max(10); 34 | 35 | const result = createSchema(schema, createOutputState(), ['array']); 36 | 37 | expect(result).toEqual(expected); 38 | }); 39 | 40 | it('creates exact length', () => { 41 | const expected: oas31.SchemaObject = { 42 | type: 'array', 43 | items: { 44 | type: 'string', 45 | }, 46 | minItems: 10, 47 | maxItems: 10, 48 | }; 49 | 50 | const schema = z.array(z.string()).length(10); 51 | 52 | const result = createSchema(schema, createOutputState(), ['array']); 53 | 54 | expect(result).toEqual(expected); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/create/schema/tests/bigint.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('bigint', () => { 9 | it('creates a int64 schema', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'integer', 12 | format: 'int64', 13 | }; 14 | const schema = z.bigint(); 15 | 16 | const result = createSchema(schema, createOutputState(), ['bigint']); 17 | 18 | expect(result).toEqual(expected); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/create/schema/tests/boolean.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('boolean', () => { 9 | it('creates a boolean schema', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'boolean', 12 | }; 13 | const schema = z.boolean(); 14 | 15 | const result = createSchema(schema, createOutputState(), ['boolean']); 16 | 17 | expect(result).toEqual(expected); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/create/schema/tests/brand.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('brand', () => { 9 | it('supports branded schema', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'object', 12 | properties: { 13 | name: { 14 | type: 'string', 15 | }, 16 | }, 17 | required: ['name'], 18 | }; 19 | 20 | const schema = z.object({ name: z.string() }).brand<'Cat'>(); 21 | 22 | const result = createSchema(schema, createOutputState(), ['brand']); 23 | 24 | expect(result).toEqual(expected); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/create/schema/tests/catch.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('catch', () => { 9 | it('creates a default string schema for a string with a catch', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'string', 12 | default: 'bob', 13 | }; 14 | 15 | const schema = z.string().catch('bob'); 16 | 17 | const result = createSchema(schema, createOutputState(), ['catch']); 18 | 19 | expect(result).toEqual(expected); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/create/schema/tests/date.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | import type { CreateDocumentOptions } from '../../document'; 8 | 9 | describe('date', () => { 10 | it('creates a string schema', () => { 11 | const expected: oas31.SchemaObject = { 12 | type: 'string', 13 | }; 14 | 15 | const schema = z.date(); 16 | 17 | const result = createSchema(schema, createOutputState(), ['date']); 18 | 19 | expect(result).toEqual(expected); 20 | }); 21 | 22 | it('sets a custom format', () => { 23 | const expected: oas31.SchemaObject = { 24 | type: 'string', 25 | format: 'date-time', 26 | }; 27 | 28 | const schema = z.date(); 29 | const documentOptions: CreateDocumentOptions = { 30 | defaultDateSchema: { 31 | type: 'string', 32 | format: 'date-time', 33 | }, 34 | }; 35 | 36 | const result = createSchema( 37 | schema, 38 | createOutputState(undefined, documentOptions), 39 | ['date'], 40 | ); 41 | 42 | expect(result).toEqual(expected); 43 | }); 44 | 45 | it('sets a custom type', () => { 46 | const expected: oas31.SchemaObject = { 47 | type: 'number', 48 | }; 49 | 50 | const schema = z.date(); 51 | const documentOptions: CreateDocumentOptions = { 52 | defaultDateSchema: { 53 | type: 'number', 54 | }, 55 | }; 56 | 57 | const result = createSchema( 58 | schema, 59 | createOutputState(undefined, documentOptions), 60 | ['date'], 61 | ); 62 | 63 | expect(result).toEqual(expected); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/create/schema/tests/default.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('default', () => { 9 | it('creates a default string schema', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'string', 12 | default: 'a', 13 | }; 14 | 15 | const schema = z.string().default('a'); 16 | 17 | const result = createSchema(schema, createOutputState(), ['default']); 18 | 19 | expect(result).toEqual(expected); 20 | }); 21 | 22 | it('adds a default property to a registered schema', () => { 23 | const expected: oas31.SchemaObject = { 24 | allOf: [ 25 | { 26 | $ref: '#/components/schemas/ref', 27 | }, 28 | ], 29 | default: 'a', 30 | }; 31 | 32 | const schema = z.string().openapi({ ref: 'ref' }).optional().default('a'); 33 | 34 | const result = createSchema(schema, createOutputState(), ['default']); 35 | 36 | expect(result).toEqual(expected); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/create/schema/tests/enum.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('enum', () => { 9 | it('creates a string enum schema', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'string', 12 | enum: ['a', 'b'], 13 | }; 14 | 15 | const schema = z.enum(['a', 'b']); 16 | 17 | const result = createSchema(schema, createOutputState(), ['enum']); 18 | 19 | expect(result).toEqual(expected); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/create/schema/tests/intersection.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('intersection', () => { 9 | it('creates an intersection schema', () => { 10 | const expected: oas31.SchemaObject = { 11 | allOf: [ 12 | { 13 | type: 'string', 14 | }, 15 | { 16 | type: 'number', 17 | }, 18 | ], 19 | }; 20 | 21 | const schema = z.intersection(z.string(), z.number()); 22 | 23 | const result = createSchema(schema, createOutputState(), ['intersection']); 24 | 25 | expect(result).toEqual(expected); 26 | }); 27 | 28 | it('creates an object with an allOf', () => { 29 | const schema = z.object({ 30 | a: z.string(), 31 | }); 32 | 33 | const andSchema = schema.and( 34 | z.object({ 35 | b: z.string(), 36 | }), 37 | ); 38 | 39 | const result = createSchema(andSchema, createOutputState(), [ 40 | 'intersection', 41 | ]); 42 | 43 | expect(result).toEqual({ 44 | allOf: [ 45 | { 46 | type: 'object', 47 | properties: { 48 | a: { 49 | type: 'string', 50 | }, 51 | }, 52 | required: ['a'], 53 | }, 54 | { 55 | type: 'object', 56 | properties: { 57 | b: { 58 | type: 'string', 59 | }, 60 | }, 61 | required: ['b'], 62 | }, 63 | ], 64 | }); 65 | }); 66 | 67 | it('attempts to flatten nested and usage', () => { 68 | const schema = z.object({ 69 | a: z.string(), 70 | }); 71 | 72 | const schema2 = z.object({ 73 | b: z.string(), 74 | }); 75 | 76 | const schema3 = z.object({ 77 | c: z.string(), 78 | }); 79 | 80 | const result = createSchema( 81 | schema.and(schema2).and(schema3), 82 | createOutputState(), 83 | ['intersection'], 84 | ); 85 | 86 | expect(result).toEqual({ 87 | allOf: [ 88 | { 89 | type: 'object', 90 | properties: { 91 | a: { 92 | type: 'string', 93 | }, 94 | }, 95 | required: ['a'], 96 | }, 97 | { 98 | type: 'object', 99 | properties: { 100 | b: { 101 | type: 'string', 102 | }, 103 | }, 104 | required: ['b'], 105 | }, 106 | { 107 | type: 'object', 108 | properties: { 109 | c: { 110 | type: 'string', 111 | }, 112 | }, 113 | required: ['c'], 114 | }, 115 | ], 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /src/create/schema/tests/lazy.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import assert from 'assert'; 3 | 4 | import { type ZodLazy, type ZodType, z } from 'zod'; 5 | 6 | import { createSchema } from '..'; 7 | import type { oas31 } from '../../../openapi3-ts/dist'; 8 | import { createOutputState } from '../../../testing/state'; 9 | 10 | describe('lazy', () => { 11 | it('throws an error when a lazy schema has no ref', () => { 12 | type Lazy = Lazy[]; 13 | const lazy: z.ZodType = z.lazy(() => lazy.array()); 14 | 15 | expect(() => 16 | createSchema(lazy as ZodLazy, createOutputState(), ['response']), 17 | ).toThrowErrorMatchingInlineSnapshot( 18 | `"The schema at response > lazy schema > array items needs to be registered because it's circularly referenced"`, 19 | ); 20 | }); 21 | 22 | it('throws errors when cycles without refs are detected', () => { 23 | const cycle1: any = z.lazy(() => z.array(z.object({ foo: cycle1 }))); 24 | expect(() => 25 | createSchema(cycle1, createOutputState(), ['response']), 26 | ).toThrowErrorMatchingInlineSnapshot( 27 | `"The schema at response > lazy schema > array items > property: foo needs to be registered because it's circularly referenced"`, 28 | ); 29 | 30 | const cycle2: any = z.lazy(() => z.union([z.number(), z.array(cycle2)])); 31 | expect(() => 32 | createSchema(cycle2, createOutputState(), ['response']), 33 | ).toThrowErrorMatchingInlineSnapshot( 34 | `"The schema at response > lazy schema > union option 1 > array items needs to be registered because it's circularly referenced"`, 35 | ); 36 | 37 | const cycle3: any = z.lazy(() => z.record(z.tuple([cycle3.optional()]))); 38 | expect(() => 39 | createSchema(cycle3, createOutputState(), ['response']), 40 | ).toThrowErrorMatchingInlineSnapshot( 41 | `"The schema at response > lazy schema > record value > tuple item 0 > optional needs to be registered because it's circularly referenced"`, 42 | ); 43 | }); 44 | 45 | it('creates a lazy schema when the schema contains a ref', () => { 46 | type Lazy = Lazy[]; 47 | const lazy: z.ZodType = z 48 | .lazy(() => lazy.array()) 49 | .openapi({ ref: 'lazy' }); 50 | 51 | const state = createOutputState(); 52 | 53 | const expectedComponent: oas31.SchemaObject = { 54 | type: 'array', 55 | items: { $ref: '#/components/schemas/lazy' }, 56 | }; 57 | 58 | const result = createSchema(lazy as ZodLazy, state, ['lazy']); 59 | 60 | expect(result).toEqual(expectedComponent.items); 61 | 62 | const component = state.components.schemas.get(lazy); 63 | 64 | assert(component?.type === 'complete'); 65 | 66 | expect(component.schemaObject).toEqual(expectedComponent); 67 | }); 68 | 69 | it('supports registering the base schema', () => { 70 | const BasePost = z.object({ 71 | id: z.string(), 72 | userId: z.string(), 73 | }); 74 | 75 | type Post = z.infer & { 76 | user?: User; 77 | }; 78 | 79 | const BaseUser = z.object({ 80 | id: z.string(), 81 | }); 82 | 83 | type User = z.infer & { 84 | posts?: Post[]; 85 | }; 86 | 87 | const PostSchema: ZodType = BasePost.extend({ 88 | user: z.lazy(() => UserSchema).optional(), 89 | }).openapi({ ref: 'post' }); 90 | 91 | const UserSchema: ZodType = BaseUser.extend({ 92 | posts: z.array(z.lazy(() => PostSchema)).optional(), 93 | }).openapi({ ref: 'user' }); 94 | 95 | const state = createOutputState(); 96 | 97 | const expectedUserComponent: oas31.SchemaObject = { 98 | type: 'object', 99 | properties: { 100 | id: { 101 | type: 'string', 102 | }, 103 | posts: { 104 | type: 'array', 105 | items: { $ref: '#/components/schemas/post' }, 106 | }, 107 | }, 108 | required: ['id'], 109 | }; 110 | const expectedPostComponent: oas31.SchemaObject = { 111 | type: 'object', 112 | properties: { 113 | id: { 114 | type: 'string', 115 | }, 116 | userId: { 117 | type: 'string', 118 | }, 119 | user: { 120 | $ref: '#/components/schemas/user', 121 | }, 122 | }, 123 | required: ['id', 'userId'], 124 | }; 125 | 126 | const expectedUser: oas31.ReferenceObject = { 127 | $ref: '#/components/schemas/user', 128 | }; 129 | 130 | const result = createSchema(UserSchema, state, ['object']); 131 | 132 | expect(result).toEqual(expectedUser); 133 | 134 | const component = state.components.schemas.get(UserSchema); 135 | assert(component?.type === 'complete'); 136 | expect(component.schemaObject).toEqual(expectedUserComponent); 137 | 138 | const postComponent = state.components.schemas.get(PostSchema); 139 | assert(postComponent?.type === 'complete'); 140 | expect(postComponent.schemaObject).toEqual(expectedPostComponent); 141 | }); 142 | 143 | it('supports sibling properties that are circular references', () => { 144 | const BasePost = z.object({ 145 | id: z.string(), 146 | userId: z.string(), 147 | }); 148 | 149 | type Post = z.infer & { 150 | user?: User; 151 | author?: User; 152 | }; 153 | 154 | const BaseUser = z.object({ 155 | id: z.string(), 156 | }); 157 | 158 | type User = z.infer & { 159 | posts?: Post[]; 160 | }; 161 | 162 | const PostSchema: ZodType = BasePost.extend({ 163 | user: z.lazy(() => UserSchema).optional(), 164 | author: z.lazy(() => UserSchema).optional(), 165 | }).openapi({ ref: 'post' }); 166 | 167 | const UserSchema: ZodType = BaseUser.extend({ 168 | posts: z.array(z.lazy(() => PostSchema)).optional(), 169 | }).openapi({ ref: 'user' }); 170 | 171 | const state = createOutputState(); 172 | 173 | const expectedPostComponent: oas31.SchemaObject = { 174 | type: 'object', 175 | properties: { 176 | id: { 177 | type: 'string', 178 | }, 179 | userId: { 180 | type: 'string', 181 | }, 182 | user: { 183 | $ref: '#/components/schemas/user', 184 | }, 185 | author: { 186 | $ref: '#/components/schemas/user', 187 | }, 188 | }, 189 | required: ['id', 'userId'], 190 | }; 191 | const expected: oas31.ReferenceObject = { 192 | $ref: '#/components/schemas/post', 193 | }; 194 | const expectedUserComponent: oas31.SchemaObject = { 195 | type: 'object', 196 | properties: { 197 | id: { 198 | type: 'string', 199 | }, 200 | posts: { 201 | type: 'array', 202 | items: { $ref: '#/components/schemas/post' }, 203 | }, 204 | }, 205 | required: ['id'], 206 | }; 207 | 208 | const result = createSchema(PostSchema, state, ['object']); 209 | 210 | expect(result).toEqual(expected); 211 | 212 | const component = state.components.schemas.get(PostSchema); 213 | assert(component?.type === 'complete'); 214 | expect(component.schemaObject).toEqual(expectedPostComponent); 215 | 216 | const userComponent = state.components.schemas.get(UserSchema); 217 | assert(userComponent?.type === 'complete'); 218 | expect(userComponent.schemaObject).toEqual(expectedUserComponent); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /src/create/schema/tests/literal.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { 7 | createOutputOpenapi3State, 8 | createOutputState, 9 | } from '../../../testing/state'; 10 | 11 | describe('literal', () => { 12 | describe('OpenAPI 3.1.0', () => { 13 | it('creates a string const schema', () => { 14 | const state = createOutputState(); 15 | const expected: oas31.SchemaObject = { 16 | type: 'string', 17 | const: 'a', 18 | }; 19 | 20 | const schema = z.literal('a'); 21 | 22 | const result = createSchema(schema, state, ['literal']); 23 | 24 | expect(result).toStrictEqual(expected); 25 | }); 26 | 27 | it('creates a number const schema', () => { 28 | const state = createOutputState(); 29 | const expected: oas31.SchemaObject = { 30 | type: 'number', 31 | const: 2, 32 | }; 33 | 34 | const schema = z.literal(2); 35 | 36 | const result = createSchema(schema, state, ['literal']); 37 | 38 | expect(result).toEqual(expected); 39 | }); 40 | 41 | it('creates a boolean const schema', () => { 42 | const state = createOutputState(); 43 | const expected: oas31.SchemaObject = { 44 | type: 'boolean', 45 | const: true, 46 | }; 47 | 48 | const schema = z.literal(true); 49 | 50 | const result = createSchema(schema, state, ['literal']); 51 | 52 | expect(result).toEqual(expected); 53 | }); 54 | 55 | it('creates a null const schema', () => { 56 | const state = createOutputState(); 57 | const expected: oas31.SchemaObject = { 58 | type: 'null', 59 | }; 60 | 61 | const schema = z.literal(null); 62 | 63 | const result = createSchema(schema, state, ['literal']); 64 | 65 | expect(result).toEqual(expected); 66 | }); 67 | }); 68 | 69 | describe('OpenAPI 3.0.0', () => { 70 | it('creates a string enum schema', () => { 71 | const state = createOutputOpenapi3State(); 72 | const expected: oas31.SchemaObject = { 73 | type: 'string', 74 | enum: ['a'], 75 | }; 76 | 77 | const schema = z.literal('a'); 78 | 79 | const result = createSchema(schema, state, ['literal']); 80 | 81 | expect(result).toStrictEqual(expected); 82 | }); 83 | 84 | it('creates a number enum schema', () => { 85 | const state = createOutputOpenapi3State(); 86 | const expected: oas31.SchemaObject = { 87 | type: 'number', 88 | enum: [2], 89 | }; 90 | 91 | const schema = z.literal(2); 92 | 93 | const result = createSchema(schema, state, ['literal']); 94 | 95 | expect(result).toEqual(expected); 96 | }); 97 | 98 | it('creates a boolean enum schema', () => { 99 | const state = createOutputOpenapi3State(); 100 | const expected: oas31.SchemaObject = { 101 | type: 'boolean', 102 | enum: [true], 103 | }; 104 | 105 | const schema = z.literal(true); 106 | 107 | const result = createSchema(schema, state, ['literal']); 108 | 109 | expect(result).toEqual(expected); 110 | }); 111 | 112 | it('creates a null enum schema', () => { 113 | const state = createOutputOpenapi3State(); 114 | const expected: oas31.SchemaObject = { 115 | type: 'null', 116 | }; 117 | 118 | const schema = z.literal(null); 119 | 120 | const result = createSchema(schema, state, ['literal']); 121 | 122 | expect(result).toEqual(expected); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/create/schema/tests/manual.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('manual', () => { 9 | it('creates a simple string schema for an optional string', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'string', 12 | }; 13 | 14 | const schema = z.unknown().openapi({ type: 'string' }); 15 | 16 | const result = createSchema(schema, createOutputState(), ['manual']); 17 | 18 | expect(result).toEqual(expected); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/create/schema/tests/nativeEnum.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { 7 | createOutputOpenapi3State, 8 | createOutputState, 9 | } from '../../../testing/state'; 10 | 11 | describe('nativeEnum', () => { 12 | it('creates a string schema from a string enum', () => { 13 | const expected: oas31.SchemaObject = { 14 | type: 'string', 15 | enum: ['Up', 'Down', 'Left', 'Right'], 16 | }; 17 | 18 | enum Direction { 19 | Up = 'Up', 20 | Down = 'Down', 21 | Left = 'Left', 22 | Right = 'Right', 23 | } 24 | 25 | const schema = z.nativeEnum(Direction); 26 | 27 | const result = createSchema(schema, createOutputState(), ['nativeEnum']); 28 | 29 | expect(result).toEqual(expected); 30 | }); 31 | 32 | it('creates a number schema from an number enum', () => { 33 | const expected: oas31.SchemaObject = { 34 | type: 'number', 35 | enum: [0, 1, 2, 3], 36 | }; 37 | 38 | enum Direction { 39 | Up, 40 | Down, 41 | Left, 42 | Right, 43 | } 44 | 45 | const schema = z.nativeEnum(Direction); 46 | 47 | const result = createSchema(schema, createOutputState(), ['nativeEnum']); 48 | 49 | expect(result).toEqual(expected); 50 | }); 51 | 52 | it('creates a string and number schema from a mixed enum', () => { 53 | const expected: oas31.SchemaObject = { 54 | type: ['string', 'number'], 55 | enum: ['Right', 0, 1, 2], 56 | }; 57 | 58 | enum Direction { 59 | Up, 60 | Down, 61 | Left, 62 | Right = 'Right', 63 | } 64 | 65 | const schema = z.nativeEnum(Direction); 66 | 67 | const result = createSchema(schema, createOutputState(), ['nativeEnum']); 68 | 69 | expect(result).toEqual(expected); 70 | }); 71 | 72 | it('creates a oneOf string and number schema from a mixed enum in openapi 3.0.0', () => { 73 | const expected: oas31.SchemaObject = { 74 | oneOf: [ 75 | { 76 | type: 'string', 77 | enum: ['Right'], 78 | }, 79 | { 80 | type: 'number', 81 | enum: [0, 1, 2], 82 | }, 83 | ], 84 | }; 85 | 86 | enum Direction { 87 | Up, 88 | Down, 89 | Left, 90 | Right = 'Right', 91 | } 92 | 93 | const schema = z.nativeEnum(Direction); 94 | 95 | const result = createSchema(schema, createOutputOpenapi3State(), [ 96 | 'nativeEnum', 97 | ]); 98 | 99 | expect(result).toEqual(expected); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/create/schema/tests/null.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('null', () => { 9 | it('creates a null schema', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'null', 12 | }; 13 | 14 | const schema = z.null(); 15 | 16 | const result = createSchema(schema, createOutputState(), ['null']); 17 | 18 | expect(result).toEqual(expected); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/create/schema/tests/number.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { 7 | createOutputOpenapi3State, 8 | createOutputState, 9 | } from '../../../testing/state'; 10 | 11 | describe('number', () => { 12 | it('creates a simple number schema', () => { 13 | const expected: oas31.SchemaObject = { 14 | type: 'number', 15 | }; 16 | 17 | const schema = z.number(); 18 | 19 | const result = createSchema(schema, createOutputState(), ['number']); 20 | 21 | expect(result).toStrictEqual(expected); 22 | }); 23 | 24 | it('creates a integer schema', () => { 25 | const expected: oas31.SchemaObject = { 26 | type: 'integer', 27 | }; 28 | 29 | const schema = z.number().int(); 30 | 31 | const result = createSchema(schema, createOutputState(), ['number']); 32 | 33 | expect(result).toStrictEqual(expected); 34 | }); 35 | 36 | it('creates a number schema with lt or gt', () => { 37 | const expected: oas31.SchemaObject = { 38 | type: 'number', 39 | exclusiveMinimum: 0, 40 | exclusiveMaximum: 10, 41 | }; 42 | 43 | const schema = z.number().lt(10).gt(0); 44 | 45 | const result = createSchema(schema, createOutputState(), ['number']); 46 | 47 | expect(result).toStrictEqual(expected); 48 | }); 49 | 50 | it('creates a number schema with lte or gte', () => { 51 | const expected: oas31.SchemaObject = { 52 | type: 'number', 53 | minimum: 0, 54 | maximum: 10, 55 | }; 56 | 57 | const schema = z.number().lte(10).gte(0); 58 | 59 | const result = createSchema(schema, createOutputState(), ['number']); 60 | 61 | expect(result).toStrictEqual(expected); 62 | }); 63 | 64 | it('creates a number schema with lte or gte in openapi 3.0.0', () => { 65 | const expected: oas31.SchemaObject = { 66 | type: 'number', 67 | minimum: 0, 68 | maximum: 10, 69 | }; 70 | 71 | const schema = z.number().lte(10).gte(0); 72 | 73 | const result = createSchema(schema, createOutputOpenapi3State(), [ 74 | 'number', 75 | ]); 76 | 77 | expect(result).toStrictEqual(expected); 78 | }); 79 | 80 | it('creates a number schema with lt or gt in openapi 3.0.0', () => { 81 | const expected: oas31.SchemaObject = { 82 | type: 'number', 83 | minimum: 0, 84 | exclusiveMinimum: true, 85 | maximum: 10, 86 | exclusiveMaximum: true, 87 | } as unknown as oas31.SchemaObject; 88 | 89 | const schema = z.number().lt(10).gt(0); 90 | 91 | const result = createSchema(schema, createOutputOpenapi3State(), [ 92 | 'number', 93 | ]); 94 | 95 | expect(result).toStrictEqual(expected); 96 | }); 97 | 98 | it('supports multipleOf', () => { 99 | const expected: oas31.SchemaObject = { 100 | type: 'number', 101 | multipleOf: 2, 102 | }; 103 | 104 | const schema = z.number().multipleOf(2); 105 | 106 | const result = createSchema(schema, createOutputState(), ['number']); 107 | 108 | expect(result).toStrictEqual(expected); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/create/schema/tests/optional.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('optional', () => { 9 | it('creates a simple string schema for an optional string', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'string', 12 | }; 13 | const schema = z.string().optional(); 14 | 15 | const result = createSchema(schema, createOutputState(), ['optional']); 16 | 17 | expect(result).toEqual(expected); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/create/schema/tests/pipeline.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createInputState, createOutputState } from '../../../testing/state'; 7 | 8 | describe('pipeline', () => { 9 | describe('input', () => { 10 | it('creates a schema from a simple pipeline', () => { 11 | const schema = z.string().pipe(z.string()); 12 | const expected: oas31.SchemaObject = { 13 | type: 'string', 14 | }; 15 | 16 | const result = createSchema(schema, createInputState(), ['pipeline']); 17 | 18 | expect(result).toEqual(expected); 19 | }); 20 | 21 | it('creates a schema from a transform pipeline', () => { 22 | const schema = z 23 | .string() 24 | .transform((arg) => arg.length) 25 | .pipe(z.number()); 26 | 27 | const expected: oas31.SchemaObject = { 28 | type: 'string', 29 | }; 30 | 31 | const result = createSchema(schema, createInputState(), ['pipeline']); 32 | 33 | expect(result).toEqual(expected); 34 | }); 35 | 36 | it('overrides the input type from a transform pipeline with a custom effectType', () => { 37 | const schema = z 38 | .string() 39 | .transform((arg) => arg.length) 40 | .pipe(z.number()) 41 | .openapi({ effectType: 'output' }); 42 | 43 | const expected: oas31.SchemaObject = { 44 | type: 'number', 45 | }; 46 | 47 | const result = createSchema(schema, createInputState(), ['pipeline']); 48 | 49 | expect(result).toEqual(expected); 50 | }); 51 | 52 | it('renders the input schema if the effectType is same', () => { 53 | const schema = z 54 | .string() 55 | .pipe(z.string()) 56 | .openapi({ effectType: 'same' }); 57 | 58 | const state = createInputState(); 59 | const exepctedResult: oas31.SchemaObject = { 60 | type: 'string', 61 | }; 62 | 63 | const result = createSchema(schema, state, ['pipeline']); 64 | expect(result).toEqual(exepctedResult); 65 | }); 66 | }); 67 | 68 | describe('output', () => { 69 | it('creates a schema from a simple pipeline', () => { 70 | const schema = z.string().pipe(z.string()); 71 | const expected: oas31.SchemaObject = { 72 | type: 'string', 73 | }; 74 | const result = createSchema(schema, createOutputState(), ['pipeline']); 75 | 76 | expect(result).toEqual(expected); 77 | }); 78 | 79 | it('creates a schema from a transform pipeline', () => { 80 | const schema = z 81 | .string() 82 | .transform((arg) => arg.length) 83 | .pipe(z.number()); 84 | const expected: oas31.SchemaObject = { 85 | type: 'number', 86 | }; 87 | 88 | const result = createSchema(schema, createOutputState(), ['pipeline']); 89 | 90 | expect(result).toEqual(expected); 91 | }); 92 | 93 | it('overrides the input type from a transform pipeline with a custom effectType', () => { 94 | const schema = z 95 | .string() 96 | .pipe(z.number()) 97 | .openapi({ effectType: 'input' }); 98 | const expected: oas31.SchemaObject = { 99 | type: 'string', 100 | }; 101 | 102 | const result = createSchema(schema, createOutputState(), ['pipeline']); 103 | 104 | expect(result).toEqual(expected); 105 | }); 106 | 107 | it('renders the input schema if the effectType is same', () => { 108 | const schema = z 109 | .string() 110 | .pipe(z.string()) 111 | .openapi({ effectType: 'same' }); 112 | 113 | const state = createOutputState(); 114 | const exepctedResult: oas31.SchemaObject = { 115 | type: 'string', 116 | }; 117 | 118 | const result = createSchema(schema, state, ['pipeline']); 119 | expect(result).toEqual(exepctedResult); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/create/schema/tests/preprocess.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('preprocess', () => { 9 | it('returns a schema with preprocess', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'string', 12 | }; 13 | const schema = z.preprocess( 14 | (arg) => (typeof arg === 'string' ? arg.split(',') : arg), 15 | z.string(), 16 | ); 17 | 18 | const result = createSchema(schema, createOutputState(), ['preprocess']); 19 | 20 | expect(result).toEqual(expected); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/create/schema/tests/readonly.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('readonly', () => { 9 | it('creates a simple string schema for a readonly string', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'string', 12 | }; 13 | const schema = z.string().readonly(); 14 | 15 | const result = createSchema(schema, createOutputState(), ['readonly']); 16 | 17 | expect(result).toEqual(expected); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/create/schema/tests/record.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { 7 | createOutputOpenapi3State, 8 | createOutputState, 9 | } from '../../../testing/state'; 10 | 11 | describe('record', () => { 12 | it('creates an object schema with additional properties in 3.0.0', () => { 13 | const expected: oas31.SchemaObject = { 14 | type: 'object', 15 | additionalProperties: { 16 | type: 'string', 17 | }, 18 | }; 19 | const schema = z.record(z.string()); 20 | 21 | const result = createSchema(schema, createOutputState(), ['record']); 22 | 23 | expect(result).toEqual(expected); 24 | }); 25 | 26 | it('creates an object schema with propertyNames in 3.1.0', () => { 27 | const expected: oas31.SchemaObject = { 28 | type: 'object', 29 | propertyNames: { 30 | type: 'string', 31 | pattern: '^foo', 32 | }, 33 | additionalProperties: { 34 | type: 'string', 35 | }, 36 | }; 37 | const schema = z.record(z.string().regex(/^foo/), z.string()); 38 | 39 | const result = createSchema(schema, createOutputState(), ['record']); 40 | 41 | expect(result).toEqual(expected); 42 | }); 43 | 44 | it('creates an object schema with additional properties and key properties in 3.0.0', () => { 45 | const expected: oas31.SchemaObject = { 46 | type: 'object', 47 | properties: { 48 | a: { type: 'string' }, 49 | b: { type: 'string' }, 50 | }, 51 | additionalProperties: false, 52 | }; 53 | const schema = z.record(z.enum(['a', 'b']), z.string()); 54 | 55 | const result = createSchema(schema, createOutputOpenapi3State(), [ 56 | 'record', 57 | ]); 58 | 59 | expect(result).toEqual(expected); 60 | }); 61 | 62 | it('unwraps the a key schema in 3.0.0', () => { 63 | const basicEnum = z.enum(['A', 'B']); 64 | const complexSchema = z 65 | .string() 66 | .trim() 67 | .length(1) 68 | .transform((val) => val.toUpperCase()) 69 | .pipe(basicEnum); 70 | 71 | const schema = z.record(complexSchema, z.string()); 72 | 73 | const expected: oas31.SchemaObject = { 74 | type: 'object', 75 | properties: { 76 | A: { type: 'string' }, 77 | B: { type: 'string' }, 78 | }, 79 | additionalProperties: false, 80 | }; 81 | 82 | const result = createSchema(schema, createOutputOpenapi3State(), [ 83 | 'record', 84 | ]); 85 | 86 | expect(result).toEqual(expected); 87 | }); 88 | 89 | it('supports registering the value schema in 3.0.0', () => { 90 | const basicEnum = z.enum(['A', 'B']); 91 | const complexSchema = z 92 | .string() 93 | .trim() 94 | .length(1) 95 | .transform((val) => val.toUpperCase()) 96 | .pipe(basicEnum); 97 | 98 | const schema = z.record( 99 | complexSchema, 100 | z.string().openapi({ ref: 'value' }), 101 | ); 102 | 103 | const expected: oas31.SchemaObject = { 104 | type: 'object', 105 | properties: { 106 | A: { $ref: '#/components/schemas/value' }, 107 | B: { $ref: '#/components/schemas/value' }, 108 | }, 109 | additionalProperties: false, 110 | }; 111 | 112 | const result = createSchema(schema, createOutputOpenapi3State(), [ 113 | 'record', 114 | ]); 115 | 116 | expect(result).toEqual(expected); 117 | }); 118 | 119 | it('supports registering key enum schemas in 3.0.0', () => { 120 | const basicEnum = z.enum(['A', 'B']); 121 | const complexSchema = z 122 | .string() 123 | .trim() 124 | .length(1) 125 | .transform((val) => val.toUpperCase()) 126 | .pipe(basicEnum) 127 | .openapi({ ref: 'key' }); 128 | 129 | const schema = z.record(complexSchema, z.string()); 130 | 131 | const expected: oas31.SchemaObject = { 132 | type: 'object', 133 | properties: { 134 | A: { 135 | type: 'string', 136 | }, 137 | B: { 138 | type: 'string', 139 | }, 140 | }, 141 | additionalProperties: false, 142 | }; 143 | 144 | const result = createSchema(schema, createOutputOpenapi3State(), [ 145 | 'record', 146 | ]); 147 | 148 | expect(result).toEqual(expected); 149 | }); 150 | 151 | it('supports registering key schemas in 3.1.0', () => { 152 | const expected: oas31.SchemaObject = { 153 | type: 'object', 154 | propertyNames: { 155 | $ref: '#/components/schemas/key', 156 | }, 157 | additionalProperties: { 158 | type: 'string', 159 | }, 160 | }; 161 | const complexSchema = z.string().regex(/^foo/).openapi({ ref: 'key' }); 162 | 163 | const schema = z.record(complexSchema, z.string()); 164 | 165 | const result = createSchema(schema, createOutputState(), ['record']); 166 | 167 | expect(result).toEqual(expected); 168 | }); 169 | 170 | it('supports lazy key schemas in 3.1.0', () => { 171 | const expected: oas31.SchemaObject = { 172 | type: 'object', 173 | propertyNames: { 174 | $ref: '#/components/schemas/key', 175 | }, 176 | additionalProperties: { 177 | type: 'string', 178 | }, 179 | }; 180 | const complexSchema = z.string().regex(/^foo/).openapi({ ref: 'key' }); 181 | 182 | const schema = z.record(complexSchema, z.string()); 183 | 184 | const result = createSchema(schema, createOutputState(), ['record']); 185 | 186 | expect(result).toEqual(expected); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /src/create/schema/tests/refine.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('refine', () => { 9 | it('returns a schema when creating an output schema with preprocess', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'string', 12 | }; 13 | const schema = z.string().refine((check) => typeof check === 'string'); 14 | 15 | const result = createSchema(schema, createOutputState(), ['refine']); 16 | 17 | expect(result).toEqual(expected); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/create/schema/tests/set.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('set', () => { 9 | it('creates simple arrays', () => { 10 | const expected: oas31.SchemaObject = { 11 | type: 'array', 12 | items: { 13 | type: 'string', 14 | }, 15 | uniqueItems: true, 16 | }; 17 | const schema = z.set(z.string()); 18 | 19 | const result = createSchema(schema, createOutputState(), ['set']); 20 | 21 | expect(result).toEqual(expected); 22 | }); 23 | 24 | it('creates min and max', () => { 25 | const expected: oas31.SchemaObject = { 26 | type: 'array', 27 | uniqueItems: true, 28 | items: { 29 | type: 'string', 30 | }, 31 | minItems: 0, 32 | maxItems: 10, 33 | }; 34 | const schema = z.set(z.string()).min(0).max(10); 35 | 36 | const result = createSchema(schema, createOutputState(), ['set']); 37 | 38 | expect(result).toEqual(expected); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/create/schema/tests/string.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { type ZodString, z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { 7 | createOutputOpenapi3State, 8 | createOutputState, 9 | } from '../../../testing/state'; 10 | 11 | describe('string', () => { 12 | it('creates a simple string schema', () => { 13 | const expected: oas31.SchemaObject = { 14 | type: 'string', 15 | }; 16 | 17 | const schema = z.string(); 18 | 19 | const result = createSchema(schema, createOutputState(), ['string']); 20 | 21 | expect(result).toStrictEqual(expected); 22 | }); 23 | 24 | it('creates a string schema with a regex pattern', () => { 25 | const expected: oas31.SchemaObject = { 26 | type: 'string', 27 | pattern: '^hello', 28 | }; 29 | const schema = z.string().regex(/^hello/); 30 | 31 | const result = createSchema(schema, createOutputState(), ['string']); 32 | 33 | expect(result).toStrictEqual(expected); 34 | }); 35 | 36 | it('creates a string schema with a startsWith pattern', () => { 37 | const expected: oas31.SchemaObject = { 38 | type: 'string', 39 | pattern: '^hello', 40 | }; 41 | const schema = z.string().startsWith('hello'); 42 | 43 | const result = createSchema(schema, createOutputState(), ['string']); 44 | 45 | expect(result).toStrictEqual(expected); 46 | }); 47 | 48 | it('creates a string schema with an endsWith pattern', () => { 49 | const expected: oas31.SchemaObject = { 50 | type: 'string', 51 | pattern: 'hello$', 52 | }; 53 | const schema = z.string().endsWith('hello'); 54 | 55 | const result = createSchema(schema, createOutputState(), ['string']); 56 | 57 | expect(result).toStrictEqual(expected); 58 | }); 59 | 60 | it('creates a string schema with an includes pattern', () => { 61 | const expected: oas31.SchemaObject = { 62 | type: 'string', 63 | pattern: 'hello', 64 | }; 65 | const schema = z.string().includes('hello'); 66 | 67 | const result = createSchema(schema, createOutputState(), ['string']); 68 | 69 | expect(result).toStrictEqual(expected); 70 | }); 71 | 72 | it('creates a string schema with an includes starting at index pattern', () => { 73 | const expected: oas31.SchemaObject = { 74 | type: 'string', 75 | pattern: '^.{5}hello', 76 | }; 77 | const schema = z.string().includes('hello', { position: 5 }); 78 | 79 | const result = createSchema(schema, createOutputState(), ['string']); 80 | 81 | expect(result).toStrictEqual(expected); 82 | }); 83 | 84 | it('creates a string schema with an includes starting at index 0', () => { 85 | const expected: oas31.SchemaObject = { 86 | type: 'string', 87 | pattern: '^hello', 88 | }; 89 | const schema = z.string().includes('hello', { position: 0 }); 90 | 91 | const result = createSchema(schema, createOutputState(), ['string']); 92 | 93 | expect(result).toStrictEqual(expected); 94 | }); 95 | 96 | it('creates a string schema with multiple patterns and length checks', () => { 97 | const expected: oas31.SchemaObject = { 98 | allOf: [ 99 | { 100 | type: 'string', 101 | pattern: '^foo', 102 | minLength: 10, 103 | }, 104 | { 105 | type: 'string', 106 | pattern: 'foo$', 107 | }, 108 | { 109 | type: 'string', 110 | pattern: '^hello', 111 | }, 112 | { 113 | type: 'string', 114 | pattern: 'hello', 115 | }, 116 | ], 117 | }; 118 | const schema = z 119 | .string() 120 | .min(10) 121 | .includes('hello') 122 | .startsWith('hello') 123 | .regex(/^foo/) 124 | .regex(/foo$/); 125 | 126 | const result = createSchema(schema, createOutputState(), ['string']); 127 | 128 | expect(result).toStrictEqual(expected); 129 | }); 130 | 131 | it('creates a string schema with min and max', () => { 132 | const expected: oas31.SchemaObject = { 133 | type: 'string', 134 | minLength: 0, 135 | maxLength: 1, 136 | }; 137 | const schema = z.string().min(0).max(1); 138 | 139 | const result = createSchema(schema, createOutputState(), ['string']); 140 | 141 | expect(result).toStrictEqual(expected); 142 | }); 143 | 144 | it('creates a string schema with nonempty', () => { 145 | const expected: oas31.SchemaObject = { 146 | type: 'string', 147 | minLength: 1, 148 | }; 149 | 150 | const schema = z.string().nonempty(); 151 | 152 | const result = createSchema(schema, createOutputState(), ['string']); 153 | 154 | expect(result).toStrictEqual(expected); 155 | }); 156 | 157 | it('creates a string schema with a set length', () => { 158 | const expected: oas31.SchemaObject = { 159 | type: 'string', 160 | minLength: 1, 161 | maxLength: 1, 162 | }; 163 | const schema = z.string().length(1); 164 | 165 | const result = createSchema(schema, createOutputState(), ['string']); 166 | 167 | expect(result).toStrictEqual(expected); 168 | }); 169 | 170 | it.each` 171 | zodString | format 172 | ${z.string().uuid()} | ${'uuid'} 173 | ${z.string().email()} | ${'email'} 174 | ${z.string().url()} | ${'uri'} 175 | ${z.string().datetime()} | ${'date-time'} 176 | ${z.string().date()} | ${'date'} 177 | ${z.string().time()} | ${'time'} 178 | ${z.string().duration()} | ${'duration'} 179 | ${z.string().ip({ version: 'v4' })} | ${'ipv4'} 180 | ${z.string().ip({ version: 'v6' })} | ${'ipv6'} 181 | ${z.string().cidr({ version: 'v4' })} | ${'ipv4'} 182 | ${z.string().cidr({ version: 'v6' })} | ${'ipv6'} 183 | `( 184 | 'creates a string schema with $format', 185 | ({ zodString, format }: { zodString: ZodString; format: string }) => { 186 | const expected: oas31.SchemaObject = { 187 | type: 'string', 188 | format, 189 | }; 190 | const result = createSchema(zodString, createOutputState(), ['string']); 191 | expect(result).toStrictEqual(expected); 192 | }, 193 | ); 194 | 195 | it('supports contentEncoding in 3.1.0', () => { 196 | const expected: oas31.SchemaObject = { 197 | type: 'string', 198 | contentEncoding: 'base64', 199 | }; 200 | 201 | const result = createSchema(z.string().base64(), createOutputState(), [ 202 | 'string', 203 | ]); 204 | 205 | expect(result).toStrictEqual(expected); 206 | }); 207 | 208 | it('does not support contentEncoding in 3.0.0', () => { 209 | const expected: oas31.SchemaObject = { 210 | type: 'string', 211 | }; 212 | 213 | const result = createSchema( 214 | z.string().base64(), 215 | createOutputOpenapi3State(), 216 | ['string'], 217 | ); 218 | 219 | expect(result).toStrictEqual(expected); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /src/create/schema/tests/transform.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createInputState, createOutputState } from '../../../testing/state'; 7 | 8 | describe('transform', () => { 9 | describe('input', () => { 10 | it('creates a schema from transform', () => { 11 | const schema = z.string().transform((str) => str.length); 12 | const expected: oas31.SchemaObject = { 13 | type: 'string', 14 | }; 15 | 16 | const result = createSchema(schema, createInputState(), ['transform']); 17 | 18 | expect(result).toStrictEqual(expected); 19 | }); 20 | 21 | it('produces an effect which is of type input', () => { 22 | const schema = z.string().transform((str) => str.length); 23 | const state = createInputState(); 24 | 25 | const expected: oas31.SchemaObject = { 26 | type: 'string', 27 | }; 28 | 29 | const result = createSchema(schema, state, ['transform']); 30 | 31 | expect(result).toEqual(expected); 32 | }); 33 | 34 | it('does not throw an error if the effectType is output and effectType is set in openapi', () => { 35 | const schema = z 36 | .string() 37 | .transform((str) => str.length) 38 | .openapi({ effectType: 'input' }); 39 | 40 | const state = createInputState(); 41 | 42 | createSchema(schema, state, ['transform']); 43 | }); 44 | 45 | it('renders the input schema if the effectType is same', () => { 46 | const schema = z 47 | .string() 48 | .transform((str) => str) 49 | .openapi({ effectType: 'same' }); 50 | 51 | const state = createInputState(); 52 | const expectedResult: oas31.SchemaObject = { 53 | type: 'string', 54 | }; 55 | 56 | const result = createSchema(schema, state, ['transform']); 57 | expect(result).toEqual(expectedResult); 58 | }); 59 | }); 60 | 61 | describe('output', () => { 62 | it('throws an error with a schema with transform', () => { 63 | const schema = z.string().transform((str) => str.length); 64 | const state = createOutputState(); 65 | state.path.push('somepath'); 66 | 67 | expect(() => 68 | createSchema(schema, state, ['transform']), 69 | ).toThrowErrorMatchingInlineSnapshot( 70 | `"Failed to determine a type for ZodEffects - transform at somepath > transform. Please change the 'effectType' to 'same' or 'input', wrap it in a ZodPipeline or assign it a manual 'type'."`, 71 | ); 72 | }); 73 | 74 | it('creates a schema with the manual type when a type is manually specified', () => { 75 | const expected: oas31.SchemaObject = { 76 | type: 'number', 77 | }; 78 | const schema = z 79 | .string() 80 | .transform((str) => str.length) 81 | .openapi({ type: 'number' }); 82 | 83 | const result = createSchema(schema, createOutputState(), ['transform']); 84 | 85 | expect(result).toEqual(expected); 86 | }); 87 | 88 | it('returns a schema when creating a schema with transform when openapi effectType is set', () => { 89 | const schema = z 90 | .string() 91 | .transform((str) => str) 92 | .openapi({ effectType: 'input' }); 93 | const expected: oas31.SchemaObject = { 94 | type: 'string', 95 | }; 96 | const result = createSchema(schema, createOutputState(), ['transform']); 97 | 98 | expect(result).toEqual(expected); 99 | }); 100 | 101 | it('renders the input schema if the effectType is same', () => { 102 | const schema = z 103 | .string() 104 | .transform((str) => str) 105 | .openapi({ effectType: 'same' }); 106 | 107 | const state = createOutputState(); 108 | const expectedResult: oas31.SchemaObject = { 109 | type: 'string', 110 | }; 111 | 112 | const result = createSchema(schema, state, ['transform']); 113 | expect(result).toEqual(expectedResult); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/create/schema/tests/tuple.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { 7 | createOutputOpenapi3State, 8 | createOutputState, 9 | } from '../../../testing/state'; 10 | 11 | describe('tuple', () => { 12 | it('creates an array schema', () => { 13 | const expected: oas31.SchemaObject = { 14 | type: 'array', 15 | prefixItems: [ 16 | { 17 | type: 'string', 18 | }, 19 | { 20 | type: 'number', 21 | }, 22 | ], 23 | minItems: 2, 24 | maxItems: 2, 25 | }; 26 | const schema = z.tuple([z.string(), z.number()]); 27 | 28 | const result = createSchema(schema, createOutputState(), ['tuple']); 29 | 30 | expect(result).toEqual(expected); 31 | }); 32 | 33 | it('creates an array schema with additionalProperties', () => { 34 | const expected: oas31.SchemaObject = { 35 | type: 'array', 36 | prefixItems: [ 37 | { 38 | type: 'string', 39 | }, 40 | { 41 | type: 'number', 42 | }, 43 | ], 44 | items: { 45 | type: 'boolean', 46 | }, 47 | }; 48 | const schema = z.tuple([z.string(), z.number()]).rest(z.boolean()); 49 | 50 | const result = createSchema(schema, createOutputState(), ['tuple']); 51 | 52 | expect(result).toEqual(expected); 53 | }); 54 | 55 | it('creates an empty array schema', () => { 56 | const expected: oas31.SchemaObject = { 57 | type: 'array', 58 | minItems: 0, 59 | maxItems: 0, 60 | }; 61 | const schema = z.tuple([]); 62 | 63 | const result = createSchema(schema, createOutputState(), ['tuple']); 64 | 65 | expect(result).toEqual(expected); 66 | }); 67 | 68 | it('creates an array schema with additionalProperties in openapi 3.0.0', () => { 69 | const expected: oas31.SchemaObject = { 70 | type: 'array', 71 | items: { 72 | oneOf: [ 73 | { 74 | type: 'string', 75 | }, 76 | { 77 | type: 'number', 78 | }, 79 | { 80 | type: 'boolean', 81 | }, 82 | ], 83 | }, 84 | }; 85 | const schema = z.tuple([z.string(), z.number()]).rest(z.boolean()); 86 | 87 | const result = createSchema(schema, createOutputOpenapi3State(), ['tuple']); 88 | 89 | expect(result).toEqual(expected); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/create/schema/tests/union.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('union', () => { 9 | it('creates an anyOf schema for a union', () => { 10 | const expected: oas31.SchemaObject = { 11 | anyOf: [ 12 | { 13 | type: 'string', 14 | }, 15 | { 16 | type: 'number', 17 | }, 18 | ], 19 | }; 20 | const schema = z.union([z.string(), z.number()]); 21 | 22 | const result = createSchema(schema, createOutputState(), ['union']); 23 | 24 | expect(result).toEqual(expected); 25 | }); 26 | 27 | it('creates an oneOf schema for a union if unionOneOf is true', () => { 28 | const expected: oas31.SchemaObject = { 29 | oneOf: [ 30 | { 31 | type: 'string', 32 | }, 33 | { 34 | type: 'number', 35 | }, 36 | ], 37 | }; 38 | const schema = z 39 | .union([z.string(), z.number()]) 40 | .openapi({ unionOneOf: true }); 41 | 42 | const result = createSchema(schema, createOutputState(), ['union']); 43 | 44 | expect(result).toEqual(expected); 45 | }); 46 | 47 | it('creates an oneOf schema for a union if the document options unionOneOf is true', () => { 48 | const expected: oas31.SchemaObject = { 49 | oneOf: [ 50 | { 51 | type: 'string', 52 | }, 53 | { 54 | type: 'number', 55 | }, 56 | ], 57 | }; 58 | const schema = z.union([z.string(), z.number()]); 59 | 60 | const result = createSchema( 61 | schema, 62 | createOutputState(undefined, { unionOneOf: true }), 63 | ['union'], 64 | ); 65 | 66 | expect(result).toEqual(expected); 67 | }); 68 | 69 | it('preferences individual unionOneOf over global setting', () => { 70 | const expected: oas31.SchemaObject = { 71 | anyOf: [ 72 | { 73 | type: 'string', 74 | }, 75 | { 76 | type: 'number', 77 | }, 78 | ], 79 | }; 80 | const schema = z 81 | .union([z.string(), z.number()]) 82 | .openapi({ unionOneOf: false }); 83 | 84 | const result = createSchema( 85 | schema, 86 | createOutputState(undefined, { unionOneOf: true }), 87 | ['union'], 88 | ); 89 | 90 | expect(result).toEqual(expected); 91 | }); 92 | 93 | it('ignores optional values in a union', () => { 94 | const schema = z.union([ 95 | z.string(), 96 | z.literal(undefined), 97 | z.undefined(), 98 | z.never(), 99 | ]); 100 | 101 | const result = createSchema(schema, createOutputState(), ['union']); 102 | 103 | expect(result).toEqual({ 104 | anyOf: [ 105 | { 106 | type: 'string', 107 | }, 108 | ], 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/create/schema/tests/unknown.test.ts: -------------------------------------------------------------------------------- 1 | import '../../../entries/extend'; 2 | import { z } from 'zod'; 3 | 4 | import { createSchema } from '..'; 5 | import type { oas31 } from '../../../openapi3-ts/dist'; 6 | import { createOutputState } from '../../../testing/state'; 7 | 8 | describe('unknown', () => { 9 | it('should create an empty schema for unknown', () => { 10 | const expected: oas31.SchemaObject = {}; 11 | const schema = z.unknown(); 12 | 13 | const result = createSchema(schema, createOutputState(), ['unknown']); 14 | 15 | expect(result).toStrictEqual(expected); 16 | }); 17 | 18 | it('should create an empty schema for any', () => { 19 | const expected: oas31.SchemaObject = {}; 20 | const schema = z.any(); 21 | 22 | const result = createSchema(schema, createOutputState(), ['unknown']); 23 | 24 | expect(result).toStrictEqual(expected); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/create/specificationExtension.test.ts: -------------------------------------------------------------------------------- 1 | import { isISpecificationExtension } from './specificationExtension'; 2 | 3 | describe('isISpecificationExtension', () => { 4 | it('returns true for strings starting with x-', () => { 5 | expect(isISpecificationExtension('x-someString')).toBe(true); 6 | }); 7 | 8 | it('returns false for strings not starting with x-', () => { 9 | expect(isISpecificationExtension('xsomeString')).toBe(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/create/specificationExtension.ts: -------------------------------------------------------------------------------- 1 | import type { oas31 } from '../openapi3-ts/dist'; 2 | 3 | export const isISpecificationExtension = ( 4 | key: string, 5 | ): key is oas31.IExtensionName => key.startsWith('x-'); 6 | -------------------------------------------------------------------------------- /src/entries/api.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createComponents, 3 | getDefaultComponents, 4 | type ComponentsObject, 5 | } from '../create/components'; 6 | export { createMediaTypeSchema } from '../create/content'; 7 | export { createParamOrRef, getZodObject } from '../create/parameters'; 8 | -------------------------------------------------------------------------------- /src/entries/extend.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { extendZodWithOpenApi } from '../extendZod'; 4 | 5 | extendZodWithOpenApi(z); 6 | 7 | // eslint-disable-next-line @typescript-eslint/consistent-type-exports, import-x/export 8 | export * from '../extendZodTypes'; // compatibility with < TS 5.0 as the export type * syntax is not supported 9 | -------------------------------------------------------------------------------- /src/extendZod.test.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { createSchema } from './create/schema/single'; 4 | import { extendZodWithOpenApi } from './extendZod'; 5 | import { currentSymbol, previousSymbol } from './extendZodSymbols'; 6 | 7 | extendZodWithOpenApi(z); 8 | 9 | describe('extendZodWithOpenApi', () => { 10 | it('allows .openapi() to be added to Zod Types', () => { 11 | const run = () => { 12 | z.string().openapi({ description: 'test' }); 13 | z.object({ a: z.string() }).openapi({ description: 'test2' }); 14 | }; 15 | 16 | expect(() => run()).not.toThrow(); 17 | }); 18 | 19 | it('allows .openapi() to be chained in Zod Types', () => { 20 | const a = z.string().openapi({ description: 'test' }); 21 | const b = a.openapi({ description: 'test2' }); 22 | 23 | expect(a._def.zodOpenApi?.openapi?.description).toBe('test'); 24 | expect(b._def.zodOpenApi?.openapi?.description).toBe('test2'); 25 | expect( 26 | b._def.zodOpenApi?.[previousSymbol]?._def.zodOpenApi?.openapi 27 | ?.description, 28 | ).toBe('test'); 29 | }); 30 | 31 | it('sets current metadata when a schema is used again', () => { 32 | const a = z.string().openapi({ ref: 'a' }); 33 | const b = a.uuid(); 34 | 35 | expect(a._def.zodOpenApi?.[currentSymbol]).toBe(a); 36 | expect(b._def.zodOpenApi?.[currentSymbol]).toBe(a); 37 | }); 38 | 39 | it('adds ._def.zodOpenApi.openapi fields to a zod type', () => { 40 | const a = z.string().openapi({ description: 'a' }); 41 | const b = z.number().openapi({ examples: [1] }); 42 | 43 | expect(a._def.zodOpenApi?.openapi?.description).toBe('a'); 44 | expect(b._def.zodOpenApi?.openapi?.examples).toStrictEqual([1]); 45 | }); 46 | 47 | it('adds persists .openapi() across some methods', () => { 48 | const a = z.string().openapi({ description: 'a' }).uuid(); 49 | 50 | expect(a._def.zodOpenApi?.openapi?.description).toBe('a'); 51 | }); 52 | 53 | it('adds extendsMetadata to an object when .extend is used', () => { 54 | const a = z.object({ a: z.string() }).openapi({ ref: 'a' }); 55 | const b = a.extend({ b: z.string() }); 56 | 57 | expect(a._def.zodOpenApi?.openapi?.ref).toBe('a'); 58 | expect(b._def.zodOpenApi?.[previousSymbol]).toStrictEqual(a); 59 | }); 60 | 61 | it('removes previous openapi ref for an object when .omit or .pick is used', () => { 62 | const a = z.object({ a: z.string() }).openapi({ ref: 'a' }); 63 | const b = a.extend({ b: z.string() }); 64 | const c = b.pick({ a: true }); 65 | const d = b.omit({ a: true }); 66 | const e = b.pick({ a: true }).openapi({ ref: 'e' }); 67 | const f = b.omit({ a: true }).openapi({ ref: 'f' }); 68 | 69 | const object = z.object({ 70 | a, 71 | b, 72 | c, 73 | d, 74 | e, 75 | f, 76 | }); 77 | 78 | expect(a._def.zodOpenApi?.openapi?.ref).toBe('a'); 79 | expect(b._def.zodOpenApi?.[previousSymbol]).toStrictEqual(a); 80 | expect(c._def.zodOpenApi?.openapi).toEqual({}); 81 | expect(d._def.zodOpenApi?.openapi).toEqual({}); 82 | 83 | const schema = createSchema(object); 84 | 85 | expect(schema).toEqual({ 86 | components: { 87 | a: { 88 | properties: { 89 | a: { 90 | type: 'string', 91 | }, 92 | }, 93 | required: ['a'], 94 | type: 'object', 95 | }, 96 | e: { 97 | properties: { 98 | a: { 99 | type: 'string', 100 | }, 101 | }, 102 | required: ['a'], 103 | type: 'object', 104 | }, 105 | f: { 106 | properties: { 107 | b: { 108 | type: 'string', 109 | }, 110 | }, 111 | required: ['b'], 112 | type: 'object', 113 | }, 114 | }, 115 | schema: { 116 | properties: { 117 | a: { 118 | $ref: '#/components/schemas/a', 119 | }, 120 | b: { 121 | allOf: [ 122 | { 123 | $ref: '#/components/schemas/a', 124 | }, 125 | ], 126 | properties: { 127 | b: { 128 | type: 'string', 129 | }, 130 | }, 131 | required: ['b'], 132 | }, 133 | c: { 134 | properties: { 135 | a: { 136 | type: 'string', 137 | }, 138 | }, 139 | required: ['a'], 140 | type: 'object', 141 | }, 142 | d: { 143 | properties: { 144 | b: { 145 | type: 'string', 146 | }, 147 | }, 148 | required: ['b'], 149 | type: 'object', 150 | }, 151 | e: { 152 | $ref: '#/components/schemas/e', 153 | }, 154 | f: { 155 | $ref: '#/components/schemas/f', 156 | }, 157 | }, 158 | required: ['a', 'b', 'c', 'd', 'e', 'f'], 159 | type: 'object', 160 | }, 161 | }); 162 | }); 163 | 164 | it("allows 'same' effectType when the input and output are equal", () => { 165 | z.string() 166 | .transform((string) => string.toUpperCase()) 167 | .openapi({ effectType: 'same' }); 168 | 169 | z.string() 170 | .transform((string) => string.length) 171 | // @ts-expect-error - This is to test the effectType 172 | .openapi({ effectType: 'same' }); 173 | }); 174 | 175 | it('preserves properties when using .openapi consecutively', () => { 176 | const fooString = z 177 | .string() 178 | .regex(/^foo/) 179 | .transform((string) => string.toUpperCase()) 180 | .openapi({ effectType: 'input' }); 181 | 182 | const barString = fooString.openapi({ description: 'foo' }); 183 | 184 | expect(barString._def.zodOpenApi?.openapi?.effectType).toBe('input'); 185 | }); 186 | 187 | it('makes a date example accept strings', () => { 188 | const fooString = z.union([z.date().optional(), z.string(), z.null()]); 189 | 190 | const barString = fooString.openapi({ 191 | examples: [null, '2021-01-01'], 192 | }); 193 | 194 | expect(barString._def.zodOpenApi?.openapi?.examples).toEqual([ 195 | null, 196 | '2021-01-01', 197 | ]); 198 | }); 199 | 200 | it('makes allows example to accept undefined but forbids undefined in examples', () => { 201 | const fooString = z.union([z.date().optional(), z.string(), z.null()]); 202 | 203 | const barString = fooString.openapi({ 204 | example: undefined, 205 | // @ts-expect-error - Testing types 206 | examples: [undefined], 207 | }); 208 | 209 | expect(barString._def.zodOpenApi?.openapi?.example).toBeUndefined(); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /src/extendZod.ts: -------------------------------------------------------------------------------- 1 | import type { ZodRawShape, ZodTypeDef, z } from 'zod'; 2 | 3 | import './extendZodTypes'; 4 | import { currentSymbol, previousSymbol } from './extendZodSymbols'; 5 | 6 | type ZodOpenApiMetadataDef = NonNullable; 7 | type ZodOpenApiMetadata = ZodOpenApiMetadataDef['openapi']; 8 | 9 | const mergeOpenApi = ( 10 | openapi: ZodOpenApiMetadata, 11 | { 12 | ref: _ref, 13 | refType: _refType, 14 | param: _param, 15 | header: _header, 16 | ...rest 17 | }: ZodOpenApiMetadata = {}, 18 | ) => ({ 19 | ...rest, 20 | ...openapi, 21 | }); 22 | 23 | export function extendZodWithOpenApi(zod: typeof z) { 24 | if (typeof zod.ZodType.prototype.openapi !== 'undefined') { 25 | return; 26 | } 27 | 28 | zod.ZodType.prototype.openapi = function (openapi) { 29 | const { zodOpenApi, ...rest } = this._def as { 30 | zodOpenApi?: ZodOpenApiMetadataDef; 31 | [key: string]: unknown; 32 | }; 33 | 34 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any 35 | const result = new (this as any).constructor({ 36 | ...rest, 37 | zodOpenApi: { 38 | openapi: mergeOpenApi( 39 | openapi as unknown as ZodOpenApiMetadata, 40 | zodOpenApi?.openapi, 41 | ), 42 | }, 43 | }); 44 | 45 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 46 | result._def.zodOpenApi[currentSymbol] = result; 47 | 48 | if (zodOpenApi) { 49 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 50 | result._def.zodOpenApi[previousSymbol] = this; 51 | } 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 54 | return result; 55 | }; 56 | 57 | // eslint-disable-next-line @typescript-eslint/unbound-method 58 | const zodDescribe = zod.ZodType.prototype.describe; 59 | 60 | zod.ZodType.prototype.describe = function (...args: [description: string]) { 61 | const result = zodDescribe.apply(this, args); 62 | const def = result._def as ZodTypeDef; 63 | 64 | if (def.zodOpenApi) { 65 | const cloned = { ...def.zodOpenApi }; 66 | cloned.openapi = mergeOpenApi({ description: args[0] }, cloned.openapi); 67 | cloned[previousSymbol] = this; 68 | cloned[currentSymbol] = result; 69 | def.zodOpenApi = cloned; 70 | } else { 71 | def.zodOpenApi = { 72 | openapi: { description: args[0] }, 73 | [currentSymbol]: result, 74 | }; 75 | } 76 | 77 | return result; 78 | }; 79 | 80 | // eslint-disable-next-line @typescript-eslint/unbound-method 81 | const zodObjectExtend = zod.ZodObject.prototype.extend; 82 | 83 | zod.ZodObject.prototype.extend = function ( 84 | ...args: [augmentation: ZodRawShape] 85 | ) { 86 | const extendResult = zodObjectExtend.apply(this, args); 87 | 88 | const zodOpenApi = extendResult._def.zodOpenApi; 89 | if (zodOpenApi) { 90 | const cloned = { ...zodOpenApi }; 91 | cloned.openapi = mergeOpenApi({}, cloned.openapi); 92 | cloned[previousSymbol] = this; 93 | extendResult._def.zodOpenApi = cloned; 94 | } else { 95 | extendResult._def.zodOpenApi = { 96 | [previousSymbol]: this, 97 | }; 98 | } 99 | 100 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any 101 | return extendResult as any; 102 | }; 103 | 104 | // eslint-disable-next-line @typescript-eslint/unbound-method 105 | const zodObjectOmit = zod.ZodObject.prototype.omit; 106 | 107 | zod.ZodObject.prototype.omit = function ( 108 | ...args: [mask: Record] 109 | ) { 110 | const omitResult = zodObjectOmit.apply(this, args); 111 | 112 | const zodOpenApi = omitResult._def.zodOpenApi; 113 | if (zodOpenApi) { 114 | const cloned = { ...zodOpenApi }; 115 | cloned.openapi = mergeOpenApi({}, cloned.openapi); 116 | delete cloned[previousSymbol]; 117 | delete cloned[currentSymbol]; 118 | omitResult._def.zodOpenApi = cloned; 119 | } 120 | 121 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any 122 | return omitResult as any; 123 | }; 124 | 125 | // eslint-disable-next-line @typescript-eslint/unbound-method 126 | const zodObjectPick = zod.ZodObject.prototype.pick; 127 | 128 | zod.ZodObject.prototype.pick = function ( 129 | ...args: [mask: Record] 130 | ) { 131 | const pickResult = zodObjectPick.apply(this, args); 132 | 133 | const zodOpenApi = pickResult._def.zodOpenApi; 134 | if (zodOpenApi) { 135 | const cloned = { ...zodOpenApi }; 136 | cloned.openapi = mergeOpenApi({}, cloned.openapi); 137 | delete cloned[previousSymbol]; 138 | delete cloned[currentSymbol]; 139 | pickResult._def.zodOpenApi = cloned; 140 | } 141 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any 142 | return pickResult as any; 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /src/extendZodSymbols.ts: -------------------------------------------------------------------------------- 1 | // TODO: Remove this file for TS 5.0 2 | export const currentSymbol = Symbol('current'); 3 | export const previousSymbol = Symbol('previous'); 4 | -------------------------------------------------------------------------------- /src/extendZodTypes.ts: -------------------------------------------------------------------------------- 1 | import type { ZodObject, ZodTypeAny, z } from 'zod'; 2 | 3 | import type { CreationType } from './create/components'; 4 | import type { currentSymbol, previousSymbol } from './extendZodSymbols'; 5 | import type { oas30, oas31 } from './openapi3-ts/dist'; 6 | 7 | type SchemaObject = oas30.SchemaObject & oas31.SchemaObject; 8 | 9 | type ReplaceDate = T extends Date ? Date | string : T; 10 | 11 | /** 12 | * zod-openapi metadata 13 | */ 14 | interface ZodOpenApiMetadata< 15 | T extends ZodTypeAny, 16 | TInferred = Exclude | z.output>, undefined>, 17 | > extends SchemaObject { 18 | example?: TInferred; 19 | examples?: [TInferred, ...TInferred[]]; 20 | default?: TInferred; 21 | /** 22 | * Used to set the output of a ZodUnion to be `oneOf` instead of `allOf` 23 | */ 24 | unionOneOf?: boolean; 25 | /** 26 | * Used to output this Zod Schema in the components schemas section. Any usage of this Zod Schema will then be transformed into a $ref. 27 | */ 28 | ref?: string; 29 | /** 30 | * Used when you are manually adding a Zod Schema to the components section. This controls whether this should be rendered as a request (`input`) or response (`output`). Defaults to `output` 31 | */ 32 | refType?: CreationType; 33 | /** 34 | * Used to set the created type of an effect. 35 | * If this was previously set to `same` and this is throwing an error, your effect is no longer returning the same type. 36 | */ 37 | effectType?: 38 | | CreationType 39 | | (z.input extends z.output 40 | ? z.output extends z.input 41 | ? 'same' 42 | : never 43 | : never); 44 | /** 45 | * Used to set metadata for a parameter, request header or cookie 46 | */ 47 | param?: Partial & { 48 | example?: TInferred; 49 | examples?: Record< 50 | string, 51 | (oas31.ExampleObject & { value: TInferred }) | oas31.ReferenceObject 52 | >; 53 | /** 54 | * Used to output this Zod Schema in the components parameters section. Any usage of this Zod Schema will then be transformed into a $ref. 55 | */ 56 | ref?: string; 57 | }; 58 | /** 59 | * Used to set data for a response header 60 | */ 61 | header?: Partial & { 62 | /** 63 | * Used to output this Zod Schema in the components headers section. Any usage of this Zod Schema will then be transformed into a $ref. 64 | */ 65 | ref?: string; 66 | }; 67 | /** 68 | * Used to override the generated type. If this is provided no metadata will be generated. 69 | */ 70 | type?: SchemaObject['type']; 71 | } 72 | 73 | interface ZodOpenApiMetadataDef { 74 | /** 75 | * Up to date OpenAPI metadata 76 | */ 77 | openapi?: ZodOpenApiMetadata; 78 | /** 79 | * Used to keep track of the Zod Schema had `.openapi` called on it 80 | */ 81 | [currentSymbol]?: ZodTypeAny; 82 | /** 83 | * Used to keep track of the previous Zod Schema that had `.openapi` called on it if another `.openapi` is called. 84 | * This can also be present when .extend is called on an object. 85 | */ 86 | [previousSymbol]?: ZodTypeAny; 87 | } 88 | 89 | interface ZodOpenApiExtendMetadata { 90 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 91 | extends: ZodObject; 92 | } 93 | 94 | declare module 'zod' { 95 | interface ZodType { 96 | /** 97 | * Add OpenAPI metadata to a Zod Type 98 | */ 99 | openapi(this: T, metadata: ZodOpenApiMetadata): T; 100 | } 101 | 102 | interface ZodTypeDef { 103 | zodOpenApi?: ZodOpenApiMetadataDef; 104 | } 105 | 106 | export interface ZodObjectDef { 107 | extendMetadata?: ZodOpenApiExtendMetadata; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create/document'; 2 | export * from './create/schema/single'; 3 | export * from './extendZod'; 4 | export * from './openapi3-ts/dist'; 5 | -------------------------------------------------------------------------------- /src/openapi.test.ts: -------------------------------------------------------------------------------- 1 | import { satisfiesVersion } from './openapi'; 2 | 3 | describe('satisfiesVersion', () => { 4 | it('returns true for matching versions', () => { 5 | expect(satisfiesVersion('3.1.0', '3.1.0')).toBe(true); 6 | }); 7 | 8 | it('returns true when version is higher than comparison', () => { 9 | expect(satisfiesVersion('3.1.0', '3.0.0')).toBe(true); 10 | }); 11 | 12 | it('returns false when version is lower than comparison', () => { 13 | expect(satisfiesVersion('3.0.0', '3.1.0')).toBe(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/openapi.ts: -------------------------------------------------------------------------------- 1 | import type { oas30, oas31 } from './openapi3-ts/dist'; 2 | 3 | export const openApiVersions = [ 4 | '3.0.0', 5 | '3.0.1', 6 | '3.0.2', 7 | '3.0.3', 8 | '3.1.0', 9 | '3.1.1', 10 | ] as const; 11 | 12 | export type OpenApiVersion = (typeof openApiVersions)[number]; 13 | 14 | export const satisfiesVersion = ( 15 | test: OpenApiVersion, 16 | against: OpenApiVersion, 17 | ) => openApiVersions.indexOf(test) >= openApiVersions.indexOf(against); 18 | 19 | export const isReferenceObject = ( 20 | schemaOrRef: 21 | | oas31.SchemaObject 22 | | oas31.ReferenceObject 23 | | oas30.SchemaObject 24 | | oas30.ReferenceObject, 25 | ): schemaOrRef is oas31.ReferenceObject | oas30.ReferenceObject => 26 | Boolean('$ref' in schemaOrRef && schemaOrRef.$ref); 27 | -------------------------------------------------------------------------------- /src/openapi3-ts/dist/dsl/openapi-builder30.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from 'yaml'; 2 | import * as oa from '../model/openapi30'; 3 | export declare class OpenApiBuilder { 4 | rootDoc: oa.OpenAPIObject; 5 | static create(doc?: oa.OpenAPIObject): OpenApiBuilder; 6 | constructor(doc?: oa.OpenAPIObject); 7 | getSpec(): oa.OpenAPIObject; 8 | getSpecAsJson(replacer?: (key: string, value: unknown) => unknown, space?: string | number): string; 9 | getSpecAsYaml(replacer?: Parameters[1], options?: Parameters[2]): string; 10 | private static isValidOpenApiVersion; 11 | addOpenApiVersion(openApiVersion: string): OpenApiBuilder; 12 | addInfo(info: oa.InfoObject): OpenApiBuilder; 13 | addContact(contact: oa.ContactObject): OpenApiBuilder; 14 | addLicense(license: oa.LicenseObject): OpenApiBuilder; 15 | addTitle(title: string): OpenApiBuilder; 16 | addDescription(description: string): OpenApiBuilder; 17 | addTermsOfService(termsOfService: string): OpenApiBuilder; 18 | addVersion(version: string): OpenApiBuilder; 19 | addPath(path: string, pathItem: oa.PathItemObject): OpenApiBuilder; 20 | addSchema(name: string, schema: oa.SchemaObject | oa.ReferenceObject): OpenApiBuilder; 21 | addResponse(name: string, response: oa.ResponseObject | oa.ReferenceObject): OpenApiBuilder; 22 | addParameter(name: string, parameter: oa.ParameterObject | oa.ReferenceObject): OpenApiBuilder; 23 | addExample(name: string, example: oa.ExampleObject | oa.ReferenceObject): OpenApiBuilder; 24 | addRequestBody(name: string, reqBody: oa.RequestBodyObject | oa.ReferenceObject): OpenApiBuilder; 25 | addHeader(name: string, header: oa.HeaderObject | oa.ReferenceObject): OpenApiBuilder; 26 | addSecurityScheme(name: string, secScheme: oa.SecuritySchemeObject | oa.ReferenceObject): OpenApiBuilder; 27 | addLink(name: string, link: oa.LinkObject | oa.ReferenceObject): OpenApiBuilder; 28 | addCallback(name: string, callback: oa.CallbackObject | oa.ReferenceObject): OpenApiBuilder; 29 | addServer(server: oa.ServerObject): OpenApiBuilder; 30 | addTag(tag: oa.TagObject): OpenApiBuilder; 31 | addExternalDocs(extDoc: oa.ExternalDocumentationObject): OpenApiBuilder; 32 | } 33 | -------------------------------------------------------------------------------- /src/openapi3-ts/dist/dsl/openapi-builder31.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from 'yaml'; 2 | import * as oa from '../model/openapi31'; 3 | export declare class OpenApiBuilder { 4 | rootDoc: oa.OpenAPIObject; 5 | static create(doc?: oa.OpenAPIObject): OpenApiBuilder; 6 | constructor(doc?: oa.OpenAPIObject); 7 | getSpec(): oa.OpenAPIObject; 8 | getSpecAsJson(replacer?: (key: string, value: unknown) => unknown, space?: string | number): string; 9 | getSpecAsYaml(replacer?: Parameters[1], options?: Parameters[2]): string; 10 | private static isValidOpenApiVersion; 11 | addOpenApiVersion(openApiVersion: string): OpenApiBuilder; 12 | addInfo(info: oa.InfoObject): OpenApiBuilder; 13 | addContact(contact: oa.ContactObject): OpenApiBuilder; 14 | addLicense(license: oa.LicenseObject): OpenApiBuilder; 15 | addTitle(title: string): OpenApiBuilder; 16 | addDescription(description: string): OpenApiBuilder; 17 | addTermsOfService(termsOfService: string): OpenApiBuilder; 18 | addVersion(version: string): OpenApiBuilder; 19 | addPath(path: string, pathItem: oa.PathItemObject): OpenApiBuilder; 20 | addSchema(name: string, schema: oa.SchemaObject | oa.ReferenceObject): OpenApiBuilder; 21 | addResponse(name: string, response: oa.ResponseObject | oa.ReferenceObject): OpenApiBuilder; 22 | addParameter(name: string, parameter: oa.ParameterObject | oa.ReferenceObject): OpenApiBuilder; 23 | addExample(name: string, example: oa.ExampleObject | oa.ReferenceObject): OpenApiBuilder; 24 | addRequestBody(name: string, reqBody: oa.RequestBodyObject | oa.ReferenceObject): OpenApiBuilder; 25 | addHeader(name: string, header: oa.HeaderObject | oa.ReferenceObject): OpenApiBuilder; 26 | addSecurityScheme(name: string, secScheme: oa.SecuritySchemeObject | oa.ReferenceObject): OpenApiBuilder; 27 | addLink(name: string, link: oa.LinkObject | oa.ReferenceObject): OpenApiBuilder; 28 | addCallback(name: string, callback: oa.CallbackObject | oa.ReferenceObject): OpenApiBuilder; 29 | addServer(server: oa.ServerObject): OpenApiBuilder; 30 | addTag(tag: oa.TagObject): OpenApiBuilder; 31 | addExternalDocs(extDoc: oa.ExternalDocumentationObject): OpenApiBuilder; 32 | addWebhook(webhook: string, webhookItem: oa.PathItemObject): OpenApiBuilder; 33 | } 34 | -------------------------------------------------------------------------------- /src/openapi3-ts/dist/index.ts: -------------------------------------------------------------------------------- 1 | export * as oas30 from "./oas30"; 2 | export * as oas31 from "./oas31"; 3 | 4 | -------------------------------------------------------------------------------- /src/openapi3-ts/dist/model/oas-common.ts: -------------------------------------------------------------------------------- 1 | import { ISpecificationExtension } from './specification-extension'; 2 | export interface ServerObject extends ISpecificationExtension { 3 | url: string; 4 | description?: string; 5 | variables?: { 6 | [v: string]: ServerVariableObject; 7 | }; 8 | } 9 | export interface ServerVariableObject extends ISpecificationExtension { 10 | enum?: string[] | boolean[] | number[]; 11 | default: string | boolean | number; 12 | description?: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/openapi3-ts/dist/model/server.ts: -------------------------------------------------------------------------------- 1 | import { ServerObject, ServerVariableObject } from './oas-common'; 2 | import { IExtensionName, IExtensionType } from './specification-extension'; 3 | export declare class Server implements ServerObject { 4 | url: string; 5 | description?: string; 6 | variables: { 7 | [v: string]: ServerVariable; 8 | }; 9 | [k: IExtensionName]: IExtensionType; 10 | constructor(url: string, desc?: string); 11 | addVariable(name: string, variable: ServerVariable): void; 12 | } 13 | export declare class ServerVariable implements ServerVariableObject { 14 | enum?: string[] | boolean[] | number[]; 15 | default: string | boolean | number; 16 | description?: string; 17 | [k: IExtensionName]: IExtensionType; 18 | constructor(defaultValue: string | boolean | number, enums?: string[] | boolean[] | number[], description?: string); 19 | } 20 | -------------------------------------------------------------------------------- /src/openapi3-ts/dist/model/specification-extension.ts: -------------------------------------------------------------------------------- 1 | export type IExtensionName = `x-${string}`; 2 | export type IExtensionType = any; 3 | export type ISpecificationExtension = { 4 | [extensionName: IExtensionName]: IExtensionType; 5 | }; 6 | export declare class SpecificationExtension implements ISpecificationExtension { 7 | [extensionName: IExtensionName]: any; 8 | static isValidExtension(extensionName: string): boolean; 9 | getExtension(extensionName: string): any; 10 | addExtension(extensionName: string, payload: any): void; 11 | listExtensions(): string[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/openapi3-ts/dist/oas30.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './model/openapi30'; 3 | 4 | export type { IExtensionName, IExtensionType, ISpecificationExtension } from './model/specification-extension'; 5 | -------------------------------------------------------------------------------- /src/openapi3-ts/dist/oas31.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './model/openapi31'; 3 | 4 | export type { IExtensionName, IExtensionType, ISpecificationExtension } from './model/specification-extension'; 5 | -------------------------------------------------------------------------------- /src/openapi3-ts/oas30.ts: -------------------------------------------------------------------------------- 1 | // To support envs which cannot interpret "exports" field in package.json. e.g. tsc with "moduleResolution": "node" 2 | export * from './dist/oas30' 3 | -------------------------------------------------------------------------------- /src/openapi3-ts/oas31.ts: -------------------------------------------------------------------------------- 1 | // To support envs which cannot interpret "exports" field in package.json. e.g. tsc with "moduleResolution": "node" 2 | export * from './dist/oas31' 3 | -------------------------------------------------------------------------------- /src/testing/state.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultComponents } from '../create/components'; 2 | import type { 3 | CreateDocumentOptions, 4 | ZodOpenApiComponentsObject, 5 | } from '../create/document'; 6 | import type { SchemaState } from '../create/schema'; 7 | 8 | export const createOutputState = ( 9 | componentsObject?: ZodOpenApiComponentsObject, 10 | documentOptions?: CreateDocumentOptions, 11 | ): SchemaState => ({ 12 | components: getDefaultComponents(componentsObject), 13 | type: 'output', 14 | path: [], 15 | visited: new Set(), 16 | documentOptions, 17 | }); 18 | 19 | export const createInputState = ( 20 | componentsObject?: ZodOpenApiComponentsObject, 21 | ): SchemaState => ({ 22 | components: getDefaultComponents(componentsObject), 23 | type: 'input', 24 | path: [], 25 | visited: new Set(), 26 | }); 27 | 28 | export const createOutputOpenapi3State = (): SchemaState => ({ 29 | components: { ...getDefaultComponents(), openapi: '3.0.0' }, 30 | type: 'output', 31 | path: [], 32 | visited: new Set(), 33 | }); 34 | -------------------------------------------------------------------------------- /src/zodType.ts: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/asteasolutions/zod-to-openapi/blob/master/src/lib/zod-is-type.ts 2 | 3 | import type { 4 | EnumLike, 5 | UnknownKeysParam, 6 | ZodAny, 7 | ZodArray, 8 | ZodBigInt, 9 | ZodBoolean, 10 | ZodBranded, 11 | ZodCatch, 12 | ZodDate, 13 | ZodDefault, 14 | ZodDiscriminatedUnion, 15 | ZodEffects, 16 | ZodEnum, 17 | ZodFirstPartyTypeKind, 18 | ZodFunction, 19 | ZodIntersection, 20 | ZodLazy, 21 | ZodLiteral, 22 | ZodMap, 23 | ZodNaN, 24 | ZodNativeEnum, 25 | ZodNever, 26 | ZodNull, 27 | ZodNullable, 28 | ZodNumber, 29 | ZodObject, 30 | ZodOptional, 31 | ZodPipeline, 32 | ZodPromise, 33 | ZodRawShape, 34 | ZodReadonly, 35 | ZodRecord, 36 | ZodSet, 37 | ZodString, 38 | ZodSymbol, 39 | ZodTuple, 40 | ZodType, 41 | ZodTypeAny, 42 | ZodTypeDef, 43 | ZodUndefined, 44 | ZodUnion, 45 | ZodUnionOptions, 46 | ZodUnknown, 47 | ZodVoid, 48 | } from 'zod'; 49 | 50 | type ZodTypeMap = { 51 | ZodAny: ZodAny; 52 | ZodArray: ZodArray; 53 | ZodBigInt: ZodBigInt; 54 | ZodBoolean: ZodBoolean; 55 | ZodBranded: ZodBranded; 56 | ZodCatch: ZodCatch; 57 | ZodDate: ZodDate; 58 | ZodDefault: ZodDefault; 59 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 60 | ZodDiscriminatedUnion: ZodDiscriminatedUnion; 61 | ZodEffects: ZodEffects; 62 | ZodEnum: ZodEnum<[string, ...string[]]>; 63 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 64 | ZodFunction: ZodFunction, ZodTypeAny>; 65 | ZodIntersection: ZodIntersection; 66 | ZodLazy: ZodLazy; 67 | ZodLiteral: ZodLiteral; 68 | ZodMap: ZodMap; 69 | ZodNaN: ZodNaN; 70 | ZodNativeEnum: ZodNativeEnum; 71 | ZodNever: ZodNever; 72 | ZodNull: ZodNull; 73 | ZodNullable: ZodNullable; 74 | ZodNumber: ZodNumber; 75 | ZodObject: ZodObject; 76 | ZodOptional: ZodOptional; 77 | ZodPipeline: ZodPipeline; 78 | ZodPromise: ZodPromise; 79 | ZodReadonly: ZodReadonly; 80 | ZodRecord: ZodRecord; 81 | ZodSet: ZodSet; 82 | ZodString: ZodString; 83 | ZodSymbol: ZodSymbol; 84 | ZodTuple: ZodTuple; 85 | ZodUndefined: ZodUndefined; 86 | ZodUnion: ZodUnion; 87 | ZodUnknown: ZodUnknown; 88 | ZodVoid: ZodVoid; 89 | }; 90 | 91 | export const isZodType = ( 92 | zodType: unknown, 93 | typeName: K, 94 | ): zodType is ZodTypeMap[K] => 95 | ( 96 | (zodType as ZodType)?._def as ZodTypeDef & { 97 | typeName: ZodFirstPartyTypeKind; // FIXME: https://github.com/colinhacks/zod/pull/2459 98 | } 99 | )?.typeName === typeName; 100 | 101 | export const isAnyZodType = (zodType: unknown): zodType is ZodType => 102 | Boolean( 103 | ( 104 | (zodType as ZodType)?._def as ZodTypeDef & { 105 | typeName: ZodFirstPartyTypeKind; 106 | } 107 | )?.typeName, 108 | ); 109 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["**/__mocks__/**/*", "**/*.test.ts", "src/testing/**/*"], 3 | "extends": "./tsconfig.json", 4 | "include": ["src/**/*"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2022"], 4 | "outDir": "lib", 5 | "removeComments": false, 6 | "target": "ES2022", 7 | "module": "Node16", 8 | "moduleResolution": "Node16" 9 | }, 10 | "exclude": ["lib*/**/*", "crackle.config.ts", "dist", "api", "extend"], 11 | "extends": "skuba/config/tsconfig.json" 12 | } 13 | --------------------------------------------------------------------------------