├── .editorconfig ├── .eslintrc.json ├── .github ├── funding.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── license ├── package.json ├── pnpm-lock.yaml ├── readme.md ├── src ├── __snapshots__ │ └── openai-strict-mode.test.ts.snap ├── _utils.ts ├── all-parsers.test.ts ├── index.ts ├── issues.test.ts ├── meta.test.ts ├── openai-strict-mode.test.ts ├── override.test.js ├── parse-def.test.ts ├── parsers │ ├── array.test.ts │ ├── bigint.test.ts │ ├── branded.test.ts │ ├── date.test.ts │ ├── default.test.ts │ ├── effects.test.ts │ ├── error-references.ts │ ├── intersection.test.ts │ ├── map.test.ts │ ├── native-enum.test.ts │ ├── nullable.test.ts │ ├── number.test.ts │ ├── object.test.ts │ ├── optional.test.ts │ ├── pipe.test.ts │ ├── promise.test.ts │ ├── record.test.ts │ ├── set.test.ts │ ├── string.test.ts │ ├── tuple.test.ts │ └── union.test.ts ├── readme.test.ts ├── refererences.test.ts ├── vendor │ └── zod-to-json-schema │ │ ├── LICENSE │ │ ├── Options.ts │ │ ├── README.md │ │ ├── Refs.ts │ │ ├── errorMessages.ts │ │ ├── index.ts │ │ ├── parseDef.ts │ │ ├── parsers │ │ ├── any.ts │ │ ├── array.ts │ │ ├── bigint.ts │ │ ├── boolean.ts │ │ ├── branded.ts │ │ ├── catch.ts │ │ ├── date.ts │ │ ├── default.ts │ │ ├── effects.ts │ │ ├── enum.ts │ │ ├── intersection.ts │ │ ├── literal.ts │ │ ├── map.ts │ │ ├── nativeEnum.ts │ │ ├── never.ts │ │ ├── null.ts │ │ ├── nullable.ts │ │ ├── number.ts │ │ ├── object.ts │ │ ├── optional.ts │ │ ├── pipeline.ts │ │ ├── promise.ts │ │ ├── readonly.ts │ │ ├── record.ts │ │ ├── set.ts │ │ ├── string.ts │ │ ├── tuple.ts │ │ ├── undefined.ts │ │ ├── union.ts │ │ └── unknown.ts │ │ ├── util.ts │ │ └── zodToJsonSchema.ts └── zod-to-json-schema.test.ts ├── tsconfig.json └── tsup.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@fisch0920/eslint-config/node"], 4 | "ignorePatterns": ["src/vendor"] 5 | } 6 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [transitive-bullshit] 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: true 11 | matrix: 12 | node-version: 13 | - 18 14 | - 20 15 | - 21 16 | - 22 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Install pnpm 23 | uses: pnpm/action-setup@v3 24 | id: pnpm-install 25 | with: 26 | version: 9.12.1 27 | run_install: false 28 | 29 | - name: Install Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: 'pnpm' 34 | 35 | - name: Install dependencies 36 | run: pnpm install --frozen-lockfile --strict-peer-dependencies 37 | 38 | - name: Run test 39 | run: pnpm test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next/ 13 | 14 | # production 15 | build/ 16 | dist/ 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # turbo 32 | .turbo 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | .env 42 | 43 | old/ 44 | out/ 45 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | package-manager-strict=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/vendor 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "bracketSpacing": true, 8 | "bracketSameLine": false, 9 | "arrowParens": "always", 10 | "trailingComma": "none" 11 | } 12 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Travis Fischer 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openai-zod-to-json-schema", 3 | "version": "1.0.3", 4 | "description": "Convert Zod schemas to JSON schemas which are optionally compatible with OpenAI's structured outputs.", 5 | "author": "Travis Fischer ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/transitive-bullshit/openai-zod-to-json-schema.git" 10 | }, 11 | "packageManager": "pnpm@9.12.1", 12 | "engines": { 13 | "node": ">=18" 14 | }, 15 | "type": "module", 16 | "main": "./dist/index.js", 17 | "source": "./src/index.ts", 18 | "types": "./dist/index.d.ts", 19 | "sideEffects": false, 20 | "exports": { 21 | ".": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/index.js", 24 | "require": "./dist/index.cjs" 25 | } 26 | }, 27 | "files": [ 28 | "dist" 29 | ], 30 | "scripts": { 31 | "build": "tsup", 32 | "dev": "tsup --watch", 33 | "clean": "del clean", 34 | "prebuild": "run-s clean vendor", 35 | "predev": "run-s clean", 36 | "pretest": "run-s build", 37 | "test": "run-s test:*", 38 | "test:format": "prettier --check \"**/*.{js,ts,tsx}\"", 39 | "test:lint": "eslint .", 40 | "test:typecheck": "tsc --noEmit", 41 | "test:unit": "vitest run", 42 | "prevendor": "del src/vendor", 43 | "vendor": "mkdir -p src/vendor/zod-to-json-schema && cp -r node_modules/openai/src/_vendor/zod-to-json-schema src/vendor", 44 | "preinstall": "npx only-allow pnpm" 45 | }, 46 | "devDependencies": { 47 | "@fisch0920/eslint-config": "^1.4.0", 48 | "@types/json-schema": "^7.0.15", 49 | "@types/node": "^22.7.5", 50 | "ajv": "^8.17.1", 51 | "ajv-formats": "^3.0.1", 52 | "del-cli": "^6.0.0", 53 | "eslint": "^8.57.1", 54 | "json-schema": "^0.4.0", 55 | "local-ref-resolver": "^0.2.0", 56 | "npm-run-all2": "^6.2.3", 57 | "only-allow": "^1.2.1", 58 | "openai": "^4.67.3", 59 | "prettier": "^3.3.3", 60 | "tsup": "^8.3.0", 61 | "typescript": "^5.6.3", 62 | "vitest": "2.1.3", 63 | "zod": "^3.23.8" 64 | }, 65 | "peerDependencies": { 66 | "zod": "^3.23.8" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # openai-zod-to-json-schema 2 | 3 | > Convert [Zod schemas](https://zod.dev) to [JSON schemas](https://json-schema.org) which are optionally compatible with [OpenAI's structured outputs](https://platform.openai.com/docs/guides/structured-outputs). 4 | 5 |

6 | Build Status 7 | NPM 8 | MIT License 9 | Prettier Code Formatting 10 |

11 | 12 | - [Intro](#intro) 13 | - [Install](#install) 14 | - [Usage](#usage) 15 | - [Why?](#why) 16 | - [License](#license) 17 | 18 | ## Intro 19 | 20 | This package exports OpenAI's [vendored version of zod-to-json-schema](https://github.com/openai/openai-node/tree/master/src/_vendor/zod-to-json-schema) as a standalone module (the source code is copied directly to guarantee a 1:1 match). 21 | 22 | It re-adds all of the unit tests from the original [zod-to-json-schema](https://github.com/StefanTerdell/zod-to-json-schema) by [Stefan Terdell](https://github.com/StefanTerdell). 23 | 24 | It also adds some additional unit tests for OpenAI's `strict` mode. See [OpenAI's docs on structured outputs](https://platform.openai.com/docs/guides/structured-outputs/supported-schemas) for more details on the subset of JSON Schemas that are supported by OpenAI's structured outputs. 25 | 26 | This package will be kept in sync with any changes to OpenAI's vendored version. 27 | 28 | ## Install 29 | 30 | > [!NOTE] 31 | > This package requires `Node.js >= 18` or an equivalent environment (Bun, Deno, CF workers, etc). 32 | 33 | ```sh 34 | npm install openai-zod-to-json-schema zod 35 | ``` 36 | 37 | ## Usage 38 | 39 | All usage is the same as the original [zod-to-json-schema](https://github.com/StefanTerdell/zod-to-json-schema), with the addition of a single optional boolean option: `openaiStrictMode`. 40 | 41 | ```ts 42 | import { zodToJsonSchema } from 'openai-zod-to-json-schema' 43 | import { z } from 'zod' 44 | 45 | const schema = zodToJsonSchema(z.any(), { openaiStrictMode: true }) 46 | ``` 47 | 48 | ## Why? 49 | 50 | - We should be able to access OpenAI's version of `zod-to-json-schema` without depending on the entire `openai` package. 51 | - OpenAI's vendored version of `zod-to-json-schema` removed all unit tests for some reason, which could cause undesired regressions. 52 | - We wanted a minimal, OpenAI-compatible version of `zod-to-json-schema` for [openai-fetch](https://github.com/dexaai/openai-fetch), [dexter](https://github.com/dexaai/dexter), and [agentic](https://github.com/transitive-bullshit/agentic). 53 | 54 | ## License 55 | 56 | MIT © [Travis Fischer](https://x.com/transitive_bs) 57 | 58 | Also see the original [zod-to-json-schema license](https://github.com/StefanTerdell/zod-to-json-schema). 59 | -------------------------------------------------------------------------------- /src/__snapshots__/openai-strict-mode.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`openaiStrictMode > Optional properties should be set as required 1`] = ` 4 | { 5 | "$schema": "http://json-schema.org/draft-07/schema#", 6 | "additionalProperties": false, 7 | "properties": { 8 | "bar": { 9 | "type": "number", 10 | }, 11 | "baz": { 12 | "additionalProperties": false, 13 | "properties": { 14 | "nala": { 15 | "type": "string", 16 | }, 17 | }, 18 | "required": [ 19 | "nala", 20 | ], 21 | "type": "object", 22 | }, 23 | "foo": { 24 | "type": "string", 25 | }, 26 | }, 27 | "required": [ 28 | "foo", 29 | "bar", 30 | "baz", 31 | ], 32 | "type": "object", 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /src/_utils.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest' 2 | 3 | export function assert(inputA: T, inputB: E) { 4 | expect(inputA).toEqual(inputB) 5 | } 6 | -------------------------------------------------------------------------------- /src/all-parsers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { zodToJsonSchema } from '.' 5 | import { assert } from './_utils' 6 | 7 | enum NativeEnum { 8 | 'a', 9 | 'b', 10 | 'c' 11 | } 12 | 13 | export const allParsersSchema = z 14 | .object({ 15 | any: z.any(), 16 | array: z.array(z.any()), 17 | arrayMin: z.array(z.any()).min(1), 18 | arrayMax: z.array(z.any()).max(1), 19 | arrayMinMax: z.array(z.any()).min(1).max(1), 20 | bigInt: z.bigint(), 21 | boolean: z.boolean(), 22 | date: z.date(), 23 | default: z.any().default(42), 24 | effectRefine: z.string().refine((x) => x + x), 25 | effectTransform: z.string().transform((x) => !!x), 26 | effectPreprocess: z.preprocess((x) => { 27 | try { 28 | return JSON.stringify(x) 29 | } catch { 30 | return 'wahh' 31 | } 32 | }, z.string()), 33 | enum: z.enum(['hej', 'svejs']), 34 | intersection: z.intersection(z.string().min(1), z.string().max(4)), 35 | literal: z.literal('hej'), 36 | map: z.map(z.string().uuid(), z.boolean()), 37 | nativeEnum: z.nativeEnum(NativeEnum), 38 | never: z.never() as any, 39 | null: z.null(), 40 | nullablePrimitive: z.string().nullable(), 41 | nullableObject: z.object({ hello: z.string() }).nullable(), 42 | number: z.number(), 43 | numberGt: z.number().gt(1), 44 | numberLt: z.number().lt(1), 45 | numberGtLt: z.number().gt(1).lt(1), 46 | numberGte: z.number().gte(1), 47 | numberLte: z.number().lte(1), 48 | numberGteLte: z.number().gte(1).lte(1), 49 | numberMultipleOf: z.number().multipleOf(2), 50 | numberInt: z.number().int(), 51 | objectPasstrough: z 52 | .object({ foo: z.string(), bar: z.number().optional() }) 53 | .passthrough(), 54 | objectCatchall: z 55 | .object({ foo: z.string(), bar: z.number().optional() }) 56 | .catchall(z.boolean()), 57 | objectStrict: z 58 | .object({ foo: z.string(), bar: z.number().optional() }) 59 | .strict(), 60 | objectStrip: z 61 | .object({ foo: z.string(), bar: z.number().optional() }) 62 | .strip(), 63 | promise: z.promise(z.string()), 64 | recordStringBoolean: z.record(z.string(), z.boolean()), 65 | recordUuidBoolean: z.record(z.string().uuid(), z.boolean()), 66 | recordBooleanBoolean: z.record(z.boolean(), z.boolean()), 67 | set: z.set(z.string()), 68 | string: z.string(), 69 | stringMin: z.string().min(1), 70 | stringMax: z.string().max(1), 71 | stringEmail: z.string().email(), 72 | stringEmoji: z.string().emoji(), 73 | stringUrl: z.string().url(), 74 | stringUuid: z.string().uuid(), 75 | stringRegEx: z.string().regex(new RegExp('abc')), 76 | stringCuid: z.string().cuid(), 77 | tuple: z.tuple([z.string(), z.number(), z.boolean()]), 78 | undefined: z.undefined(), 79 | unionPrimitives: z.union([ 80 | z.string(), 81 | z.number(), 82 | z.boolean(), 83 | z.bigint(), 84 | z.null() 85 | ]), 86 | unionPrimitiveLiterals: z.union([ 87 | z.literal(123), 88 | z.literal('abc'), 89 | z.literal(null), 90 | z.literal(true) 91 | // z.literal(1n), // target es2020 92 | ]), 93 | unionNonPrimitives: z.union([ 94 | z.string(), 95 | z.object({ 96 | foo: z.string(), 97 | bar: z.number().optional() 98 | }) 99 | ]), 100 | unknown: z.unknown() 101 | }) 102 | .partial() 103 | .default({ string: 'hello' }) 104 | .describe('watup') 105 | 106 | describe('All Parsers tests', () => { 107 | test('With JSON schema target, should produce valid json schema (7)', () => { 108 | const jsonSchema = zodToJsonSchema(allParsersSchema, { 109 | target: 'jsonSchema7' 110 | }) 111 | 112 | const expectedOutput = { 113 | $schema: 'http://json-schema.org/draft-07/schema#', 114 | type: 'object', 115 | properties: { 116 | any: {}, 117 | array: { 118 | type: 'array' 119 | }, 120 | arrayMin: { 121 | type: 'array', 122 | minItems: 1 123 | }, 124 | arrayMax: { 125 | type: 'array', 126 | maxItems: 1 127 | }, 128 | arrayMinMax: { 129 | type: 'array', 130 | minItems: 1, 131 | maxItems: 1 132 | }, 133 | bigInt: { 134 | type: 'integer', 135 | format: 'int64' 136 | }, 137 | boolean: { 138 | type: 'boolean' 139 | }, 140 | date: { 141 | type: 'string', 142 | format: 'date-time' 143 | }, 144 | default: { 145 | default: 42 146 | }, 147 | effectRefine: { 148 | type: 'string' 149 | }, 150 | effectTransform: { 151 | type: 'string' 152 | }, 153 | effectPreprocess: { 154 | type: 'string' 155 | }, 156 | enum: { 157 | type: 'string', 158 | enum: ['hej', 'svejs'] 159 | }, 160 | intersection: { 161 | allOf: [ 162 | { 163 | type: 'string', 164 | minLength: 1 165 | }, 166 | { 167 | type: 'string', 168 | maxLength: 4 169 | } 170 | ] 171 | }, 172 | literal: { 173 | type: 'string', 174 | const: 'hej' 175 | }, 176 | map: { 177 | type: 'array', 178 | maxItems: 125, 179 | items: { 180 | type: 'array', 181 | items: [ 182 | { 183 | type: 'string', 184 | format: 'uuid' 185 | }, 186 | { 187 | type: 'boolean' 188 | } 189 | ], 190 | minItems: 2, 191 | maxItems: 2 192 | } 193 | }, 194 | nativeEnum: { 195 | type: 'number', 196 | enum: [0, 1, 2] 197 | }, 198 | never: { 199 | not: {} 200 | }, 201 | null: { 202 | type: 'null' 203 | }, 204 | nullablePrimitive: { 205 | type: ['string', 'null'] 206 | }, 207 | nullableObject: { 208 | anyOf: [ 209 | { 210 | type: 'object', 211 | properties: { 212 | hello: { 213 | type: 'string' 214 | } 215 | }, 216 | required: ['hello'], 217 | additionalProperties: false 218 | }, 219 | { 220 | type: 'null' 221 | } 222 | ] 223 | }, 224 | number: { 225 | type: 'number' 226 | }, 227 | numberGt: { 228 | type: 'number', 229 | exclusiveMinimum: 1 230 | }, 231 | numberLt: { 232 | type: 'number', 233 | exclusiveMaximum: 1 234 | }, 235 | numberGtLt: { 236 | type: 'number', 237 | exclusiveMinimum: 1, 238 | exclusiveMaximum: 1 239 | }, 240 | numberGte: { 241 | type: 'number', 242 | minimum: 1 243 | }, 244 | numberLte: { 245 | type: 'number', 246 | maximum: 1 247 | }, 248 | numberGteLte: { 249 | type: 'number', 250 | minimum: 1, 251 | maximum: 1 252 | }, 253 | numberMultipleOf: { 254 | type: 'number', 255 | multipleOf: 2 256 | }, 257 | numberInt: { 258 | type: 'integer' 259 | }, 260 | objectPasstrough: { 261 | type: 'object', 262 | properties: { 263 | foo: { 264 | type: 'string' 265 | }, 266 | bar: { 267 | type: 'number' 268 | } 269 | }, 270 | required: ['foo'], 271 | additionalProperties: true 272 | }, 273 | objectCatchall: { 274 | type: 'object', 275 | properties: { 276 | foo: { 277 | type: 'string' 278 | }, 279 | bar: { 280 | type: 'number' 281 | } 282 | }, 283 | required: ['foo'], 284 | additionalProperties: { 285 | type: 'boolean' 286 | } 287 | }, 288 | objectStrict: { 289 | type: 'object', 290 | properties: { 291 | foo: { 292 | type: 'string' 293 | }, 294 | bar: { 295 | type: 'number' 296 | } 297 | }, 298 | required: ['foo'], 299 | additionalProperties: false 300 | }, 301 | objectStrip: { 302 | type: 'object', 303 | properties: { 304 | foo: { 305 | type: 'string' 306 | }, 307 | bar: { 308 | type: 'number' 309 | } 310 | }, 311 | required: ['foo'], 312 | additionalProperties: false 313 | }, 314 | promise: { 315 | type: 'string' 316 | }, 317 | recordStringBoolean: { 318 | type: 'object', 319 | additionalProperties: { 320 | type: 'boolean' 321 | } 322 | }, 323 | recordUuidBoolean: { 324 | type: 'object', 325 | additionalProperties: { 326 | type: 'boolean' 327 | }, 328 | propertyNames: { 329 | format: 'uuid' 330 | } 331 | }, 332 | recordBooleanBoolean: { 333 | type: 'object', 334 | additionalProperties: { 335 | type: 'boolean' 336 | } 337 | }, 338 | set: { 339 | type: 'array', 340 | uniqueItems: true, 341 | items: { 342 | type: 'string' 343 | } 344 | }, 345 | string: { 346 | type: 'string' 347 | }, 348 | stringMin: { 349 | type: 'string', 350 | minLength: 1 351 | }, 352 | stringMax: { 353 | type: 'string', 354 | maxLength: 1 355 | }, 356 | stringEmail: { 357 | type: 'string', 358 | format: 'email' 359 | }, 360 | stringEmoji: { 361 | type: 'string', 362 | pattern: '^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$' 363 | }, 364 | stringUrl: { 365 | type: 'string', 366 | format: 'uri' 367 | }, 368 | stringUuid: { 369 | type: 'string', 370 | format: 'uuid' 371 | }, 372 | stringRegEx: { 373 | type: 'string', 374 | pattern: 'abc' 375 | }, 376 | stringCuid: { 377 | type: 'string', 378 | pattern: '^[cC][^\\s-]{8,}$' 379 | }, 380 | tuple: { 381 | type: 'array', 382 | minItems: 3, 383 | maxItems: 3, 384 | items: [ 385 | { 386 | type: 'string' 387 | }, 388 | { 389 | type: 'number' 390 | }, 391 | { 392 | type: 'boolean' 393 | } 394 | ] 395 | }, 396 | undefined: { 397 | not: {} 398 | }, 399 | unionPrimitives: { 400 | type: ['string', 'number', 'boolean', 'integer', 'null'] 401 | }, 402 | unionPrimitiveLiterals: { 403 | type: ['number', 'string', 'null', 'boolean'], 404 | enum: [123, 'abc', null, true] 405 | }, 406 | unionNonPrimitives: { 407 | anyOf: [ 408 | { 409 | type: 'string' 410 | }, 411 | { 412 | type: 'object', 413 | properties: { 414 | foo: { 415 | type: 'string' 416 | }, 417 | bar: { 418 | type: 'number' 419 | } 420 | }, 421 | required: ['foo'], 422 | additionalProperties: false 423 | } 424 | ] 425 | }, 426 | unknown: {} 427 | }, 428 | additionalProperties: false, 429 | default: { 430 | string: 'hello' 431 | }, 432 | description: 'watup' 433 | } 434 | assert(jsonSchema, expectedOutput) 435 | }) 436 | 437 | test('With OpenAPI schema target, should produce valid Open API schema', () => { 438 | const jsonSchema = zodToJsonSchema(allParsersSchema, { 439 | target: 'openApi3' 440 | }) 441 | const expectedOutput = { 442 | type: 'object', 443 | properties: { 444 | any: {}, 445 | array: { 446 | type: 'array' 447 | }, 448 | arrayMin: { 449 | type: 'array', 450 | minItems: 1 451 | }, 452 | arrayMax: { 453 | type: 'array', 454 | maxItems: 1 455 | }, 456 | arrayMinMax: { 457 | type: 'array', 458 | minItems: 1, 459 | maxItems: 1 460 | }, 461 | bigInt: { 462 | type: 'integer', 463 | format: 'int64' 464 | }, 465 | boolean: { 466 | type: 'boolean' 467 | }, 468 | date: { 469 | type: 'string', 470 | format: 'date-time' 471 | }, 472 | default: { 473 | default: 42 474 | }, 475 | effectRefine: { 476 | type: 'string' 477 | }, 478 | effectTransform: { 479 | type: 'string' 480 | }, 481 | effectPreprocess: { 482 | type: 'string' 483 | }, 484 | enum: { 485 | type: 'string', 486 | enum: ['hej', 'svejs'] 487 | }, 488 | intersection: { 489 | allOf: [ 490 | { 491 | type: 'string', 492 | minLength: 1 493 | }, 494 | { 495 | type: 'string', 496 | maxLength: 4 497 | } 498 | ] 499 | }, 500 | literal: { 501 | type: 'string', 502 | enum: ['hej'] 503 | }, 504 | map: { 505 | type: 'array', 506 | maxItems: 125, 507 | items: { 508 | type: 'array', 509 | items: [ 510 | { 511 | type: 'string', 512 | format: 'uuid' 513 | }, 514 | { 515 | type: 'boolean' 516 | } 517 | ], 518 | minItems: 2, 519 | maxItems: 2 520 | } 521 | }, 522 | nativeEnum: { 523 | type: 'number', 524 | enum: [0, 1, 2] 525 | }, 526 | never: { 527 | not: {} 528 | }, 529 | null: { 530 | enum: ['null'], 531 | nullable: true 532 | }, 533 | nullablePrimitive: { 534 | type: 'string', 535 | nullable: true 536 | }, 537 | nullableObject: { 538 | type: 'object', 539 | properties: { 540 | hello: { 541 | type: 'string' 542 | } 543 | }, 544 | required: ['hello'], 545 | additionalProperties: false, 546 | nullable: true 547 | }, 548 | number: { 549 | type: 'number' 550 | }, 551 | numberGt: { 552 | type: 'number', 553 | exclusiveMinimum: true, 554 | minimum: 1 555 | }, 556 | numberLt: { 557 | type: 'number', 558 | exclusiveMaximum: true, 559 | maximum: 1 560 | }, 561 | numberGtLt: { 562 | type: 'number', 563 | exclusiveMinimum: true, 564 | minimum: 1, 565 | exclusiveMaximum: true, 566 | maximum: 1 567 | }, 568 | numberGte: { 569 | type: 'number', 570 | minimum: 1 571 | }, 572 | numberLte: { 573 | type: 'number', 574 | maximum: 1 575 | }, 576 | numberGteLte: { 577 | type: 'number', 578 | minimum: 1, 579 | maximum: 1 580 | }, 581 | numberMultipleOf: { 582 | type: 'number', 583 | multipleOf: 2 584 | }, 585 | numberInt: { 586 | type: 'integer' 587 | }, 588 | objectPasstrough: { 589 | type: 'object', 590 | properties: { 591 | foo: { 592 | type: 'string' 593 | }, 594 | bar: { 595 | type: 'number' 596 | } 597 | }, 598 | required: ['foo'], 599 | additionalProperties: true 600 | }, 601 | objectCatchall: { 602 | type: 'object', 603 | properties: { 604 | foo: { 605 | type: 'string' 606 | }, 607 | bar: { 608 | type: 'number' 609 | } 610 | }, 611 | required: ['foo'], 612 | additionalProperties: { 613 | type: 'boolean' 614 | } 615 | }, 616 | objectStrict: { 617 | type: 'object', 618 | properties: { 619 | foo: { 620 | type: 'string' 621 | }, 622 | bar: { 623 | type: 'number' 624 | } 625 | }, 626 | required: ['foo'], 627 | additionalProperties: false 628 | }, 629 | objectStrip: { 630 | type: 'object', 631 | properties: { 632 | foo: { 633 | type: 'string' 634 | }, 635 | bar: { 636 | type: 'number' 637 | } 638 | }, 639 | required: ['foo'], 640 | additionalProperties: false 641 | }, 642 | promise: { 643 | type: 'string' 644 | }, 645 | recordStringBoolean: { 646 | type: 'object', 647 | additionalProperties: { 648 | type: 'boolean' 649 | } 650 | }, 651 | recordUuidBoolean: { 652 | type: 'object', 653 | additionalProperties: { 654 | type: 'boolean' 655 | } 656 | }, 657 | recordBooleanBoolean: { 658 | type: 'object', 659 | additionalProperties: { 660 | type: 'boolean' 661 | } 662 | }, 663 | set: { 664 | type: 'array', 665 | uniqueItems: true, 666 | items: { 667 | type: 'string' 668 | } 669 | }, 670 | string: { 671 | type: 'string' 672 | }, 673 | stringMin: { 674 | type: 'string', 675 | minLength: 1 676 | }, 677 | stringMax: { 678 | type: 'string', 679 | maxLength: 1 680 | }, 681 | stringEmail: { 682 | type: 'string', 683 | format: 'email' 684 | }, 685 | stringEmoji: { 686 | type: 'string', 687 | pattern: '^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$' 688 | }, 689 | stringUrl: { 690 | type: 'string', 691 | format: 'uri' 692 | }, 693 | stringUuid: { 694 | type: 'string', 695 | format: 'uuid' 696 | }, 697 | stringRegEx: { 698 | type: 'string', 699 | pattern: 'abc' 700 | }, 701 | stringCuid: { 702 | type: 'string', 703 | pattern: '^[cC][^\\s-]{8,}$' 704 | }, 705 | tuple: { 706 | type: 'array', 707 | minItems: 3, 708 | maxItems: 3, 709 | items: [ 710 | { 711 | type: 'string' 712 | }, 713 | { 714 | type: 'number' 715 | }, 716 | { 717 | type: 'boolean' 718 | } 719 | ] 720 | }, 721 | undefined: { 722 | not: {} 723 | }, 724 | unionPrimitives: { 725 | anyOf: [ 726 | { 727 | type: 'string' 728 | }, 729 | { 730 | type: 'number' 731 | }, 732 | { 733 | type: 'boolean' 734 | }, 735 | { 736 | type: 'integer', 737 | format: 'int64' 738 | }, 739 | { 740 | enum: ['null'], 741 | nullable: true 742 | } 743 | ] 744 | }, 745 | unionPrimitiveLiterals: { 746 | anyOf: [ 747 | { 748 | type: 'number', 749 | enum: [123] 750 | }, 751 | { 752 | type: 'string', 753 | enum: ['abc'] 754 | }, 755 | { 756 | type: 'object' 757 | }, 758 | { 759 | type: 'boolean', 760 | enum: [true] 761 | } 762 | ] 763 | }, 764 | unionNonPrimitives: { 765 | anyOf: [ 766 | { 767 | type: 'string' 768 | }, 769 | { 770 | type: 'object', 771 | properties: { 772 | foo: { 773 | type: 'string' 774 | }, 775 | bar: { 776 | type: 'number' 777 | } 778 | }, 779 | required: ['foo'], 780 | additionalProperties: false 781 | } 782 | ] 783 | }, 784 | unknown: {} 785 | }, 786 | additionalProperties: false, 787 | default: { 788 | string: 'hello' 789 | }, 790 | description: 'watup' 791 | } 792 | assert(jsonSchema, expectedOutput) 793 | }) 794 | }) 795 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './vendor/zod-to-json-schema' 2 | -------------------------------------------------------------------------------- /src/issues.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { zodToJsonSchema } from '.' 5 | import { assert } from './_utils' 6 | 7 | describe('Issue tests', () => { 8 | test('@94', () => { 9 | const topicSchema = z.object({ 10 | topics: z 11 | .array( 12 | z.object({ 13 | topic: z.string().describe('The topic of the position') 14 | }) 15 | ) 16 | .describe('An array of topics') 17 | }) 18 | 19 | const res = zodToJsonSchema(topicSchema) 20 | 21 | assert(res, { 22 | $schema: 'http://json-schema.org/draft-07/schema#', 23 | type: 'object', 24 | required: ['topics'], 25 | properties: { 26 | topics: { 27 | type: 'array', 28 | items: { 29 | type: 'object', 30 | required: ['topic'], 31 | properties: { 32 | topic: { 33 | type: 'string', 34 | description: 'The topic of the position' 35 | } 36 | }, 37 | additionalProperties: false 38 | }, 39 | description: 'An array of topics' 40 | } 41 | }, 42 | additionalProperties: false 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/meta.test.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { zodToJsonSchema } from '.' 6 | import { assert } from './_utils' 7 | 8 | describe('Metadata', () => { 9 | test('should be possible to use description', () => { 10 | const $z = z.string().describe('My neat string') 11 | const $j = zodToJsonSchema($z) 12 | const $e: JSONSchema7Type = { 13 | $schema: 'http://json-schema.org/draft-07/schema#', 14 | type: 'string', 15 | description: 'My neat string' 16 | } 17 | 18 | assert($j, $e) 19 | }) 20 | 21 | test('should be possible to add a markdownDescription', () => { 22 | const $z = z.string().describe('My neat string') 23 | const $j = zodToJsonSchema($z, { markdownDescription: true }) 24 | const $e = { 25 | $schema: 'http://json-schema.org/draft-07/schema#', 26 | type: 'string', 27 | description: 'My neat string', 28 | markdownDescription: 'My neat string' 29 | } 30 | 31 | assert($j, $e) 32 | }) 33 | 34 | test('should handle optional schemas with different descriptions', () => { 35 | const recurringSchema = z.object({}) 36 | const zodSchema = z 37 | .object({ 38 | p1: recurringSchema.optional().describe('aaaaaaaaa'), 39 | p2: recurringSchema.optional().describe('bbbbbbbbb'), 40 | p3: recurringSchema.optional().describe('ccccccccc') 41 | }) 42 | .describe('sssssssss') 43 | 44 | const jsonSchema = zodToJsonSchema(zodSchema, { 45 | target: 'openApi3', 46 | $refStrategy: 'none' 47 | }) 48 | 49 | assert(jsonSchema, { 50 | additionalProperties: false, 51 | description: 'sssssssss', 52 | properties: { 53 | p1: { 54 | additionalProperties: false, 55 | description: 'aaaaaaaaa', 56 | properties: {}, 57 | type: 'object' 58 | }, 59 | p2: { 60 | additionalProperties: false, 61 | description: 'bbbbbbbbb', 62 | properties: {}, 63 | type: 'object' 64 | }, 65 | p3: { 66 | additionalProperties: false, 67 | description: 'ccccccccc', 68 | properties: {}, 69 | type: 'object' 70 | } 71 | }, 72 | type: 'object' 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/openai-strict-mode.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { zodToJsonSchema } from '.' 5 | 6 | describe('openaiStrictMode', () => { 7 | test('Optional properties should be set as required', () => { 8 | expect( 9 | zodToJsonSchema( 10 | z.object({ 11 | foo: z.string(), 12 | bar: z.number().optional(), 13 | baz: z 14 | .object({ 15 | nala: z.string().optional() 16 | }) 17 | .optional() 18 | }), 19 | { openaiStrictMode: true } 20 | ) 21 | ).toMatchSnapshot() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/override.test.js: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { describe, test } from 'vitest' 3 | import { zodToJsonSchema, ignoreOverride } from '.' 4 | import { assert } from './_utils' 5 | 6 | describe('override', () => { 7 | test('the readme example', () => { 8 | assert( 9 | zodToJsonSchema( 10 | z.object({ 11 | ignoreThis: z.string(), 12 | overrideThis: z.string(), 13 | removeThis: z.string() 14 | }), 15 | { 16 | override: (def, refs) => { 17 | const path = refs.currentPath.join('/') 18 | 19 | if (path === '#/properties/overrideThis') { 20 | return { 21 | type: 'integer' 22 | } 23 | } 24 | 25 | if (path === '#/properties/removeThis') { 26 | return undefined 27 | } 28 | 29 | // Important! Do not return `undefined` or void unless you want to remove the property from the resulting schema completely. 30 | return ignoreOverride 31 | } 32 | } 33 | ), 34 | { 35 | $schema: 'http://json-schema.org/draft-07/schema#', 36 | type: 'object', 37 | required: ['ignoreThis', 'overrideThis'], 38 | properties: { 39 | ignoreThis: { 40 | type: 'string' 41 | }, 42 | overrideThis: { 43 | type: 'integer' 44 | } 45 | }, 46 | additionalProperties: false 47 | } 48 | ) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/parse-def.test.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema7Type } from 'json-schema' 2 | import Ajv from 'ajv' 3 | import { describe, expect, test } from 'vitest' 4 | import { z } from 'zod' 5 | 6 | import { getRefs, parseDef } from '.' 7 | 8 | const ajv = new Ajv() 9 | 10 | describe('Basic parsing', () => { 11 | test('should return a proper json schema with some common types without validation', () => { 12 | const zodSchema = z.object({ 13 | requiredString: z.string(), 14 | optionalString: z.string().optional(), 15 | literalString: z.literal('literalStringValue'), 16 | stringArray: z.array(z.string()), 17 | stringEnum: z.enum(['stringEnumOptionA', 'stringEnumOptionB']), 18 | tuple: z.tuple([z.string(), z.number(), z.boolean()]), 19 | record: z.record(z.boolean()), 20 | requiredNumber: z.number(), 21 | optionalNumber: z.number().optional(), 22 | numberOrNull: z.number().nullable(), 23 | numberUnion: z.union([z.literal(1), z.literal(2), z.literal(3)]), 24 | mixedUnion: z.union([ 25 | z.literal('abc'), 26 | z.literal(123), 27 | z.object({ nowItGetsAnnoying: z.literal(true) }) 28 | ]), 29 | objectOrNull: z.object({ myString: z.string() }).nullable(), 30 | passthrough: z.object({ myString: z.string() }).passthrough() 31 | }) 32 | const expectedJsonSchema: JSONSchema7Type = { 33 | type: 'object', 34 | properties: { 35 | requiredString: { 36 | type: 'string' 37 | }, 38 | optionalString: { 39 | type: 'string' 40 | }, 41 | literalString: { 42 | type: 'string', 43 | const: 'literalStringValue' 44 | }, 45 | stringArray: { 46 | type: 'array', 47 | items: { 48 | type: 'string' 49 | } 50 | }, 51 | stringEnum: { 52 | type: 'string', 53 | enum: ['stringEnumOptionA', 'stringEnumOptionB'] 54 | }, 55 | tuple: { 56 | type: 'array', 57 | minItems: 3, 58 | items: [ 59 | { 60 | type: 'string' 61 | }, 62 | { 63 | type: 'number' 64 | }, 65 | { 66 | type: 'boolean' 67 | } 68 | ], 69 | maxItems: 3 70 | }, 71 | record: { 72 | type: 'object', 73 | additionalProperties: { 74 | type: 'boolean' 75 | } 76 | }, 77 | requiredNumber: { 78 | type: 'number' 79 | }, 80 | optionalNumber: { 81 | type: 'number' 82 | }, 83 | numberOrNull: { 84 | type: ['number', 'null'] 85 | }, 86 | numberUnion: { 87 | type: 'number', 88 | enum: [1, 2, 3] 89 | }, 90 | mixedUnion: { 91 | anyOf: [ 92 | { 93 | type: 'string', 94 | const: 'abc' 95 | }, 96 | { 97 | type: 'number', 98 | const: 123 99 | }, 100 | { 101 | type: 'object', 102 | properties: { 103 | nowItGetsAnnoying: { 104 | type: 'boolean', 105 | const: true 106 | } 107 | }, 108 | required: ['nowItGetsAnnoying'], 109 | additionalProperties: false 110 | } 111 | ] 112 | }, 113 | objectOrNull: { 114 | anyOf: [ 115 | { 116 | type: 'object', 117 | properties: { 118 | myString: { 119 | type: 'string' 120 | } 121 | }, 122 | required: ['myString'], 123 | additionalProperties: false 124 | }, 125 | { 126 | type: 'null' 127 | } 128 | ] 129 | }, 130 | passthrough: { 131 | type: 'object', 132 | properties: { 133 | myString: { 134 | type: 'string' 135 | } 136 | }, 137 | required: ['myString'], 138 | additionalProperties: true 139 | } 140 | }, 141 | required: [ 142 | 'requiredString', 143 | 'literalString', 144 | 'stringArray', 145 | 'stringEnum', 146 | 'tuple', 147 | 'record', 148 | 'requiredNumber', 149 | 'numberOrNull', 150 | 'numberUnion', 151 | 'mixedUnion', 152 | 'objectOrNull', 153 | 'passthrough' 154 | ], 155 | additionalProperties: false 156 | } 157 | const parsedSchema = parseDef(zodSchema._def, getRefs()) 158 | expect(parsedSchema).toEqual(expectedJsonSchema) 159 | expect(ajv.validateSchema(parsedSchema!)).toEqual(true) 160 | }) 161 | 162 | test('should handle a nullable string properly', () => { 163 | const shorthand = z.string().nullable() 164 | const union = z.union([z.string(), z.null()]) 165 | 166 | const expected = { type: ['string', 'null'] } 167 | 168 | expect(parseDef(shorthand._def, getRefs())).toEqual(expected) 169 | expect(parseDef(union._def, getRefs())).toEqual(expected) 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /src/parsers/array.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import deref from 'local-ref-resolver' 3 | import { describe, test } from 'vitest' 4 | import { z } from 'zod' 5 | 6 | import { getRefs, parseArrayDef } from '..' 7 | import { assert } from '../_utils' 8 | import { errorReferences } from './error-references' 9 | 10 | describe('Arrays and array validations', () => { 11 | test('should be possible to describe a simple array', () => { 12 | const parsedSchema = parseArrayDef(z.array(z.string())._def, getRefs()) 13 | const jsonSchema: JSONSchema7Type = { 14 | type: 'array', 15 | items: { 16 | type: 'string' 17 | } 18 | } 19 | assert(parsedSchema, jsonSchema) 20 | }) 21 | test('should be possible to describe a simple array with any item', () => { 22 | const parsedSchema = parseArrayDef(z.array(z.any())._def, getRefs()) 23 | const jsonSchema: JSONSchema7Type = { 24 | type: 'array' 25 | } 26 | assert(parsedSchema, jsonSchema) 27 | }) 28 | test('should be possible to describe a string array with a minimum and maximum length', () => { 29 | const parsedSchema = parseArrayDef( 30 | z.array(z.string()).min(2).max(4)._def, 31 | getRefs() 32 | ) 33 | const jsonSchema: JSONSchema7Type = { 34 | type: 'array', 35 | items: { 36 | type: 'string' 37 | }, 38 | minItems: 2, 39 | maxItems: 4 40 | } 41 | assert(parsedSchema, jsonSchema) 42 | }) 43 | test('should be possible to describe a string array with an exect length', () => { 44 | const parsedSchema = parseArrayDef( 45 | z.array(z.string()).length(5)._def, 46 | getRefs() 47 | ) 48 | const jsonSchema: JSONSchema7Type = { 49 | type: 'array', 50 | items: { 51 | type: 'string' 52 | }, 53 | minItems: 5, 54 | maxItems: 5 55 | } 56 | assert(parsedSchema, jsonSchema) 57 | }) 58 | test('should be possible to describe a string array with a minimum length of 1 by using nonempty', () => { 59 | const parsedSchema = parseArrayDef( 60 | z.array(z.any()).nonempty()._def, 61 | getRefs() 62 | ) 63 | const jsonSchema: JSONSchema7Type = { 64 | type: 'array', 65 | minItems: 1 66 | } 67 | assert(parsedSchema, jsonSchema) 68 | }) 69 | 70 | test('should be possible do properly reference array items', () => { 71 | const willHaveBeenSeen = z.object({ hello: z.string() }) 72 | const unionSchema = z.union([willHaveBeenSeen, willHaveBeenSeen]) 73 | const arraySchema = z.array(unionSchema) 74 | const jsonSchema = parseArrayDef(arraySchema._def, getRefs()) 75 | //TODO: Remove 'any'-cast when json schema type package supports it. 'anyOf' in 'items' should be completely according to spec though. 76 | assert((jsonSchema.items as any).anyOf[1].$ref, '#/items/anyOf/0') 77 | 78 | const resolvedSchema = deref(jsonSchema) 79 | assert(resolvedSchema.items.anyOf[1], resolvedSchema.items.anyOf[0]) 80 | }) 81 | 82 | test('should include custom error messages for minLength and maxLength', () => { 83 | const minLengthMessage = 'Must have at least 5 items.' 84 | const maxLengthMessage = 'Can have at most 10 items.' 85 | const jsonSchema: JSONSchema7Type = { 86 | type: 'array', 87 | minItems: 5, 88 | maxItems: 10, 89 | errorMessage: { 90 | minItems: minLengthMessage, 91 | maxItems: maxLengthMessage 92 | } 93 | } 94 | const zodArraySchema = z 95 | .array(z.any()) 96 | .min(5, minLengthMessage) 97 | .max(10, maxLengthMessage) 98 | const jsonParsedSchema = parseArrayDef( 99 | zodArraySchema._def, 100 | errorReferences() 101 | ) 102 | assert(jsonSchema, jsonParsedSchema) 103 | }) 104 | test('should include custom error messages for exactLength', () => { 105 | const exactLengthMessage = 'Must have exactly 5 items.' 106 | const jsonSchema: JSONSchema7Type = { 107 | type: 'array', 108 | minItems: 5, 109 | maxItems: 5, 110 | errorMessage: { 111 | minItems: exactLengthMessage, 112 | maxItems: exactLengthMessage 113 | } 114 | } 115 | const zodArraySchema = z.array(z.any()).length(5, exactLengthMessage) 116 | const jsonParsedSchema = parseArrayDef( 117 | zodArraySchema._def, 118 | errorReferences() 119 | ) 120 | assert(jsonSchema, jsonParsedSchema) 121 | }) 122 | 123 | test('should not include errorMessages property if none are passed', () => { 124 | const jsonSchema: JSONSchema7Type = { 125 | type: 'array', 126 | minItems: 5, 127 | maxItems: 10 128 | } 129 | const zodArraySchema = z.array(z.any()).min(5).max(10) 130 | const jsonParsedSchema = parseArrayDef( 131 | zodArraySchema._def, 132 | errorReferences() 133 | ) 134 | assert(jsonSchema, jsonParsedSchema) 135 | }) 136 | test("should not include error messages if it isn't explicitly set to true in References constructor", () => { 137 | const zodSchemas = [ 138 | z.array(z.any()).min(1, 'bad'), 139 | z.array(z.any()).max(1, 'bad') 140 | ] 141 | for (const schema of zodSchemas) { 142 | const jsonParsedSchema = parseArrayDef(schema._def, getRefs()) 143 | assert(jsonParsedSchema.errorMessages, undefined) 144 | } 145 | }) 146 | }) 147 | -------------------------------------------------------------------------------- /src/parsers/bigint.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { getRefs, parseBigintDef } from '..' 6 | import { assert } from '../_utils' 7 | 8 | describe('bigint', () => { 9 | test('should be possible to use bigint', () => { 10 | const parsedSchema = parseBigintDef(z.bigint()._def, getRefs()) 11 | const jsonSchema: JSONSchema7Type = { 12 | type: 'integer', 13 | format: 'int64' 14 | } 15 | assert(parsedSchema, jsonSchema) 16 | }) 17 | 18 | // Jest doesn't like bigints. 🤷 19 | test('should be possible to define gt/lt', () => { 20 | const parsedSchema = parseBigintDef( 21 | z.bigint().gte(BigInt(10)).lte(BigInt(20))._def, 22 | getRefs() 23 | ) 24 | const jsonSchema = { 25 | type: 'integer', 26 | format: 'int64', 27 | minimum: BigInt(10), 28 | maximum: BigInt(20) 29 | } 30 | assert(parsedSchema, jsonSchema) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/parsers/branded.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { getRefs, parseBrandedDef } from '..' 5 | import { assert } from '../_utils' 6 | 7 | describe('objects', () => { 8 | test('should be possible to use branded string', () => { 9 | const schema = z.string().brand<'x'>() 10 | const parsedSchema = parseBrandedDef(schema._def, getRefs()) 11 | 12 | const expectedSchema = { 13 | type: 'string' 14 | } 15 | assert(parsedSchema, expectedSchema) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/parsers/date.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { getRefs, parseDateDef } from '..' 6 | import { assert } from '../_utils' 7 | import { errorReferences } from './error-references.js' 8 | 9 | describe('Date validations', () => { 10 | test('should be possible to date as a string type', () => { 11 | const zodDateSchema = z.date() 12 | const parsedSchemaWithOption = parseDateDef( 13 | zodDateSchema._def, 14 | getRefs({ dateStrategy: 'string' }) 15 | ) 16 | const parsedSchemaFromDefault = parseDateDef(zodDateSchema._def, getRefs()) 17 | 18 | const jsonSchema: JSONSchema7Type = { 19 | type: 'string', 20 | format: 'date-time' 21 | } 22 | 23 | assert(parsedSchemaWithOption, jsonSchema) 24 | assert(parsedSchemaFromDefault, jsonSchema) 25 | }) 26 | 27 | test('should be possible to describe minimum date', () => { 28 | const zodDateSchema = z 29 | .date() 30 | .min(new Date('1970-01-02'), { message: 'Too old' }) 31 | const parsedSchema = parseDateDef( 32 | zodDateSchema._def, 33 | getRefs({ dateStrategy: 'integer' }) 34 | ) 35 | 36 | const jsonSchema: JSONSchema7Type = { 37 | type: 'integer', 38 | format: 'unix-time', 39 | minimum: 86_400_000 40 | } 41 | 42 | assert(parsedSchema, jsonSchema) 43 | }) 44 | 45 | test('should be possible to describe maximum date', () => { 46 | const zodDateSchema = z.date().max(new Date('1970-01-02')) 47 | const parsedSchema = parseDateDef( 48 | zodDateSchema._def, 49 | getRefs({ dateStrategy: 'integer' }) 50 | ) 51 | 52 | const jsonSchema: JSONSchema7Type = { 53 | type: 'integer', 54 | format: 'unix-time', 55 | maximum: 86_400_000 56 | } 57 | 58 | assert(parsedSchema, jsonSchema) 59 | }) 60 | 61 | test('should be possible to describe both maximum and minimum date', () => { 62 | const zodDateSchema = z 63 | .date() 64 | .min(new Date('1970-01-02')) 65 | .max(new Date('1972-01-02')) 66 | const parsedSchema = parseDateDef( 67 | zodDateSchema._def, 68 | getRefs({ dateStrategy: 'integer' }) 69 | ) 70 | 71 | const jsonSchema: JSONSchema7Type = { 72 | type: 'integer', 73 | format: 'unix-time', 74 | minimum: 86_400_000, 75 | maximum: 63_158_400_000 76 | } 77 | 78 | assert(parsedSchema, jsonSchema) 79 | }) 80 | 81 | test("should include custom error message for both maximum and minimum if they're passed", () => { 82 | const minimumErrorMessage = 'To young' 83 | const maximumErrorMessage = 'To old' 84 | const zodDateSchema = z 85 | .date() 86 | .min(new Date('1970-01-02'), minimumErrorMessage) 87 | .max(new Date('1972-01-02'), maximumErrorMessage) 88 | 89 | const parsedSchema = parseDateDef( 90 | zodDateSchema._def, 91 | errorReferences({ dateStrategy: 'integer' }) 92 | ) 93 | 94 | const jsonSchema: JSONSchema7Type = { 95 | type: 'integer', 96 | format: 'unix-time', 97 | minimum: 86_400_000, 98 | maximum: 63_158_400_000, 99 | errorMessage: { 100 | minimum: minimumErrorMessage, 101 | maximum: maximumErrorMessage 102 | } 103 | } 104 | 105 | assert(parsedSchema, jsonSchema) 106 | }) 107 | 108 | test('multiple choices of strategy should result in anyOf', () => { 109 | const zodDateSchema = z.date() 110 | const parsedSchema = parseDateDef( 111 | zodDateSchema._def, 112 | getRefs({ dateStrategy: ['format:date-time', 'format:date', 'integer'] }) 113 | ) 114 | 115 | const jsonSchema: JSONSchema7Type = { 116 | anyOf: [ 117 | { 118 | type: 'string', 119 | format: 'date-time' 120 | }, 121 | { 122 | type: 'string', 123 | format: 'date' 124 | }, 125 | { 126 | type: 'integer', 127 | format: 'unix-time' 128 | } 129 | ] 130 | } 131 | 132 | assert(parsedSchema, jsonSchema) 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /src/parsers/default.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { getRefs, parseDefaultDef } from '..' 6 | import { assert } from '../_utils' 7 | 8 | describe('promise', () => { 9 | test('should be possible to use default on objects', () => { 10 | const parsedSchema = parseDefaultDef( 11 | z.object({ foo: z.boolean() }).default({ foo: true })._def, 12 | getRefs() 13 | ) 14 | const jsonSchema: JSONSchema7Type = { 15 | type: 'object', 16 | additionalProperties: false, 17 | required: ['foo'], 18 | properties: { 19 | foo: { 20 | type: 'boolean' 21 | } 22 | }, 23 | default: { 24 | foo: true 25 | } 26 | } 27 | assert(parsedSchema, jsonSchema) 28 | }) 29 | 30 | test('should be possible to use default on primitives', () => { 31 | const parsedSchema = parseDefaultDef( 32 | z.string().default('default')._def, 33 | getRefs() 34 | ) 35 | const jsonSchema: JSONSchema7Type = { 36 | type: 'string', 37 | default: 'default' 38 | } 39 | assert(parsedSchema, jsonSchema) 40 | }) 41 | 42 | test('default with transform', () => { 43 | const stringWithDefault = z 44 | .string() 45 | .transform((val) => val.toUpperCase()) 46 | .default('default') 47 | 48 | const parsedSchema = parseDefaultDef(stringWithDefault._def, getRefs()) 49 | const jsonSchema: JSONSchema7Type = { 50 | type: 'string', 51 | default: 'default' 52 | } 53 | 54 | assert(parsedSchema, jsonSchema) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/parsers/effects.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { getRefs, parseEffectsDef } from '..' 6 | import { assert } from '../_utils' 7 | 8 | describe('effects', () => { 9 | test('should be possible to use refine', () => { 10 | const parsedSchema = parseEffectsDef( 11 | z.number().refine((x) => x + 1)._def, 12 | getRefs(), 13 | false 14 | ) 15 | const jsonSchema: JSONSchema7Type = { 16 | type: 'number' 17 | } 18 | assert(parsedSchema, jsonSchema) 19 | }) 20 | 21 | test('should default to the input type', () => { 22 | const schema = z.string().transform((arg) => Number.parseInt(arg)) 23 | 24 | const jsonSchema = parseEffectsDef(schema._def, getRefs(), false) 25 | 26 | assert(jsonSchema, { 27 | type: 'string' 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/parsers/error-references.ts: -------------------------------------------------------------------------------- 1 | import { getRefs, type Options, type Refs, type Targets } from '..' 2 | 3 | export function errorReferences( 4 | options?: string | Partial> 5 | ): Refs { 6 | const r = getRefs(options) 7 | r.errorMessages = true 8 | return r 9 | } 10 | -------------------------------------------------------------------------------- /src/parsers/intersection.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { getRefs, parseIntersectionDef } from '..' 5 | import { assert } from '../_utils' 6 | 7 | describe('intersections', () => { 8 | test('should be possible to use intersections', () => { 9 | const intersection = z.intersection(z.string().min(1), z.string().max(3)) 10 | 11 | const jsonSchema = parseIntersectionDef(intersection._def, getRefs()) 12 | 13 | assert(jsonSchema, { 14 | allOf: [ 15 | { 16 | type: 'string', 17 | minLength: 1 18 | }, 19 | { 20 | type: 'string', 21 | maxLength: 3 22 | } 23 | ] 24 | }) 25 | }) 26 | 27 | test('should be possible to deref intersections', () => { 28 | const schema = z.string() 29 | const intersection = z.intersection(schema, schema) 30 | const jsonSchema = parseIntersectionDef(intersection._def, getRefs()) 31 | 32 | assert(jsonSchema, { 33 | allOf: [ 34 | { 35 | type: 'string' 36 | }, 37 | { 38 | $ref: '#/allOf/0' 39 | } 40 | ] 41 | }) 42 | }) 43 | 44 | test('should intersect complex objects correctly', () => { 45 | const schema1 = z.object({ 46 | foo: z.string() 47 | }) 48 | const schema2 = z.object({ 49 | bar: z.string() 50 | }) 51 | const intersection = z.intersection(schema1, schema2) 52 | const jsonSchema = parseIntersectionDef( 53 | intersection._def, 54 | getRefs({ target: 'jsonSchema2019-09' }) 55 | ) 56 | 57 | assert(jsonSchema, { 58 | allOf: [ 59 | { 60 | properties: { 61 | foo: { 62 | type: 'string' 63 | } 64 | }, 65 | required: ['foo'], 66 | type: 'object' 67 | }, 68 | { 69 | properties: { 70 | bar: { 71 | type: 'string' 72 | } 73 | }, 74 | required: ['bar'], 75 | type: 'object' 76 | } 77 | ], 78 | unevaluatedProperties: false 79 | }) 80 | }) 81 | 82 | test('should return `unevaluatedProperties` only if all sub-schemas has additionalProperties set to false', () => { 83 | const schema1 = z.object({ 84 | foo: z.string() 85 | }) 86 | const schema2 = z 87 | .object({ 88 | bar: z.string() 89 | }) 90 | .passthrough() 91 | const intersection = z.intersection(schema1, schema2) 92 | const jsonSchema = parseIntersectionDef( 93 | intersection._def, 94 | getRefs({ target: 'jsonSchema2019-09' }) 95 | ) 96 | 97 | assert(jsonSchema, { 98 | allOf: [ 99 | { 100 | properties: { 101 | foo: { 102 | type: 'string' 103 | } 104 | }, 105 | required: ['foo'], 106 | type: 'object' 107 | }, 108 | { 109 | properties: { 110 | bar: { 111 | type: 'string' 112 | } 113 | }, 114 | required: ['bar'], 115 | type: 'object', 116 | additionalProperties: true 117 | } 118 | ] 119 | }) 120 | }) 121 | 122 | test('should intersect multiple complex objects correctly', () => { 123 | const schema1 = z.object({ 124 | foo: z.string() 125 | }) 126 | const schema2 = z.object({ 127 | bar: z.string() 128 | }) 129 | const schema3 = z.object({ 130 | baz: z.string() 131 | }) 132 | const intersection = schema1.and(schema2).and(schema3) 133 | const jsonSchema = parseIntersectionDef( 134 | intersection._def, 135 | getRefs({ target: 'jsonSchema2019-09' }) 136 | ) 137 | 138 | assert(jsonSchema, { 139 | allOf: [ 140 | { 141 | properties: { 142 | foo: { 143 | type: 'string' 144 | } 145 | }, 146 | required: ['foo'], 147 | type: 'object' 148 | }, 149 | { 150 | properties: { 151 | bar: { 152 | type: 'string' 153 | } 154 | }, 155 | required: ['bar'], 156 | type: 'object' 157 | }, 158 | { 159 | properties: { 160 | baz: { 161 | type: 'string' 162 | } 163 | }, 164 | required: ['baz'], 165 | type: 'object' 166 | } 167 | ], 168 | unevaluatedProperties: false 169 | }) 170 | }) 171 | 172 | test('should return `unevaluatedProperties` only if all of the multiple sub-schemas has additionalProperties set to false', () => { 173 | const schema1 = z.object({ 174 | foo: z.string() 175 | }) 176 | const schema2 = z.object({ 177 | bar: z.string() 178 | }) 179 | const schema3 = z 180 | .object({ 181 | baz: z.string() 182 | }) 183 | .passthrough() 184 | const intersection = schema1.and(schema2).and(schema3) 185 | const jsonSchema = parseIntersectionDef( 186 | intersection._def, 187 | getRefs({ target: 'jsonSchema2019-09' }) 188 | ) 189 | 190 | assert(jsonSchema, { 191 | allOf: [ 192 | { 193 | properties: { 194 | foo: { 195 | type: 'string' 196 | } 197 | }, 198 | required: ['foo'], 199 | type: 'object' 200 | }, 201 | { 202 | properties: { 203 | bar: { 204 | type: 'string' 205 | } 206 | }, 207 | required: ['bar'], 208 | type: 'object' 209 | }, 210 | { 211 | additionalProperties: true, 212 | properties: { 213 | baz: { 214 | type: 'string' 215 | } 216 | }, 217 | required: ['baz'], 218 | type: 'object' 219 | } 220 | ] 221 | }) 222 | }) 223 | }) 224 | -------------------------------------------------------------------------------- /src/parsers/map.test.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | import { type JSONSchema7Type } from 'json-schema' 3 | import { describe, test } from 'vitest' 4 | import { z } from 'zod' 5 | 6 | import { getRefs, parseMapDef } from '..' 7 | import { assert } from '../_utils' 8 | 9 | const ajv = new Ajv() 10 | describe('map', () => { 11 | test('should be possible to use Map', () => { 12 | const mapSchema = z.map(z.string(), z.number()) 13 | 14 | const parsedSchema = parseMapDef(mapSchema._def, getRefs()) 15 | 16 | const jsonSchema: JSONSchema7Type = { 17 | type: 'array', 18 | maxItems: 125, 19 | items: { 20 | type: 'array', 21 | items: [ 22 | { 23 | type: 'string' 24 | }, 25 | { 26 | type: 'number' 27 | } 28 | ], 29 | minItems: 2, 30 | maxItems: 2 31 | } 32 | } 33 | 34 | assert(parsedSchema, jsonSchema) 35 | 36 | const myMap: z.infer = new Map() 37 | myMap.set('hello', 123) 38 | 39 | ajv.validate(jsonSchema, [...myMap]) 40 | const ajvResult = !ajv.errors 41 | 42 | const zodResult = mapSchema.safeParse(myMap).success 43 | 44 | assert(zodResult, true) 45 | assert(ajvResult, true) 46 | }) 47 | 48 | test('should be possible to use additionalProperties-pattern (record)', () => { 49 | assert( 50 | parseMapDef( 51 | z.map(z.string().min(1), z.number())._def, 52 | getRefs({ mapStrategy: 'record' }) 53 | ), 54 | { 55 | type: 'object', 56 | additionalProperties: { 57 | type: 'number' 58 | }, 59 | propertyNames: { 60 | minLength: 1 61 | } 62 | } 63 | ) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/parsers/native-enum.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { parseNativeEnumDef } from '..' 6 | import { assert } from '../_utils' 7 | 8 | describe('Native enums', () => { 9 | test('should be possible to convert a basic native number enum', () => { 10 | enum MyEnum { 11 | val1, 12 | val2, 13 | val3 14 | } 15 | 16 | const parsedSchema = parseNativeEnumDef(z.nativeEnum(MyEnum)._def) 17 | const jsonSchema: JSONSchema7Type = { 18 | type: 'number', 19 | enum: [0, 1, 2] 20 | } 21 | assert(parsedSchema, jsonSchema) 22 | }) 23 | 24 | test('should be possible to convert a native string enum', () => { 25 | enum MyEnum { 26 | val1 = 'a', 27 | val2 = 'b', 28 | val3 = 'c' 29 | } 30 | 31 | const parsedSchema = parseNativeEnumDef(z.nativeEnum(MyEnum)._def) 32 | const jsonSchema: JSONSchema7Type = { 33 | type: 'string', 34 | enum: ['a', 'b', 'c'] 35 | } 36 | assert(parsedSchema, jsonSchema) 37 | }) 38 | 39 | test('should be possible to convert a mixed value native enum', () => { 40 | enum MyEnum { 41 | val1 = 'a', 42 | val2 = 1, 43 | val3 = 'c' 44 | } 45 | 46 | const parsedSchema = parseNativeEnumDef(z.nativeEnum(MyEnum)._def) 47 | const jsonSchema: JSONSchema7Type = { 48 | type: ['string', 'number'], 49 | enum: ['a', 1, 'c'] 50 | } 51 | assert(parsedSchema, jsonSchema) 52 | }) 53 | 54 | test('should be possible to convert a native const assertion object', () => { 55 | const MyConstAssertionObject = { 56 | val1: 0, 57 | val2: 1, 58 | val3: 2 59 | } as const 60 | 61 | const parsedSchema = parseNativeEnumDef( 62 | z.nativeEnum(MyConstAssertionObject)._def 63 | ) 64 | const jsonSchema: JSONSchema7Type = { 65 | type: 'number', 66 | enum: [0, 1, 2] 67 | } 68 | assert(parsedSchema, jsonSchema) 69 | }) 70 | 71 | test('should be possible to convert a native const assertion string object', () => { 72 | const MyConstAssertionObject = { 73 | val1: 'a', 74 | val2: 'b', 75 | val3: 'c' 76 | } as const 77 | 78 | const parsedSchema = parseNativeEnumDef( 79 | z.nativeEnum(MyConstAssertionObject)._def 80 | ) 81 | const jsonSchema: JSONSchema7Type = { 82 | type: 'string', 83 | enum: ['a', 'b', 'c'] 84 | } 85 | assert(parsedSchema, jsonSchema) 86 | }) 87 | 88 | test('should be possible to convert a mixed value native const assertion string object', () => { 89 | const MyConstAssertionObject = { 90 | val1: 'a', 91 | val2: 1, 92 | val3: 'c' 93 | } as const 94 | 95 | const parsedSchema = parseNativeEnumDef( 96 | z.nativeEnum(MyConstAssertionObject)._def 97 | ) 98 | const jsonSchema: JSONSchema7Type = { 99 | type: ['string', 'number'], 100 | enum: ['a', 1, 'c'] 101 | } 102 | assert(parsedSchema, jsonSchema) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /src/parsers/nullable.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { getRefs, parseObjectDef } from '..' 5 | import { assert } from '../_utils' 6 | 7 | describe('nullable', () => { 8 | test('should be possible to properly reference nested nullable primitives', () => { 9 | const nullablePrimitive = z.string().nullable() 10 | 11 | const schema = z.object({ 12 | one: nullablePrimitive, 13 | two: nullablePrimitive 14 | }) 15 | 16 | const jsonSchema: any = parseObjectDef(schema._def, getRefs()) 17 | 18 | assert(jsonSchema.properties.one.type, ['string', 'null']) 19 | assert(jsonSchema.properties.two.$ref, '#/properties/one') 20 | }) 21 | 22 | test('should be possible to properly reference nested nullable primitives', () => { 23 | const three = z.string() 24 | 25 | const nullableObject = z 26 | .object({ 27 | three 28 | }) 29 | .nullable() 30 | 31 | const schema = z.object({ 32 | one: nullableObject, 33 | two: nullableObject, 34 | three 35 | }) 36 | 37 | const jsonSchema: any = parseObjectDef(schema._def, getRefs()) 38 | 39 | assert(jsonSchema.properties.one, { 40 | anyOf: [ 41 | { 42 | type: 'object', 43 | additionalProperties: false, 44 | required: ['three'], 45 | properties: { 46 | three: { 47 | type: 'string' 48 | } 49 | } 50 | }, 51 | { 52 | type: 'null' 53 | } 54 | ] 55 | }) 56 | assert(jsonSchema.properties.two.$ref, '#/properties/one') 57 | assert( 58 | jsonSchema.properties.three.$ref, 59 | '#/properties/one/anyOf/0/properties/three' 60 | ) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/parsers/number.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { getRefs, parseNumberDef } from '..' 6 | import { assert } from '../_utils' 7 | import { errorReferences } from './error-references.js' 8 | 9 | describe('Number validations', () => { 10 | test('should be possible to describe minimum number', () => { 11 | const parsedSchema = parseNumberDef(z.number().min(5)._def, getRefs()) 12 | const jsonSchema: JSONSchema7Type = { 13 | type: 'number', 14 | minimum: 5 15 | } 16 | assert(parsedSchema, jsonSchema) 17 | }) 18 | test('should be possible to describe maximum number', () => { 19 | const parsedSchema = parseNumberDef(z.number().max(5)._def, getRefs()) 20 | const jsonSchema: JSONSchema7Type = { 21 | type: 'number', 22 | maximum: 5 23 | } 24 | assert(parsedSchema, jsonSchema) 25 | }) 26 | test('should be possible to describe both minimum and maximum number', () => { 27 | const parsedSchema = parseNumberDef( 28 | z.number().min(5).max(5)._def, 29 | getRefs() 30 | ) 31 | const jsonSchema: JSONSchema7Type = { 32 | type: 'number', 33 | minimum: 5, 34 | maximum: 5 35 | } 36 | assert(parsedSchema, jsonSchema) 37 | }) 38 | test('should be possible to describe an integer', () => { 39 | const parsedSchema = parseNumberDef(z.number().int()._def, getRefs()) 40 | const jsonSchema: JSONSchema7Type = { 41 | type: 'integer' 42 | } 43 | assert(parsedSchema, jsonSchema) 44 | }) 45 | test('should be possible to describe multiples of n', () => { 46 | const parsedSchema = parseNumberDef( 47 | z.number().multipleOf(2)._def, 48 | getRefs() 49 | ) 50 | const jsonSchema: JSONSchema7Type = { 51 | type: 'number', 52 | multipleOf: 2 53 | } 54 | assert(parsedSchema, jsonSchema) 55 | }) 56 | test('should be possible to describe positive, negative, nonpositive and nonnegative numbers', () => { 57 | const parsedSchema = parseNumberDef( 58 | z.number().positive().negative().nonpositive().nonnegative()._def, 59 | getRefs() 60 | ) 61 | const jsonSchema: JSONSchema7Type = { 62 | type: 'number', 63 | minimum: 0, 64 | maximum: 0, 65 | exclusiveMaximum: 0, 66 | exclusiveMinimum: 0 67 | } 68 | assert(parsedSchema, jsonSchema) 69 | }) 70 | test("should include custom error messages for inclusive checks if they're passed", () => { 71 | const minErrorMessage = 'Number must be at least 5' 72 | const maxErrorMessage = 'Number must be at most 10' 73 | const zodNumberSchema = z 74 | .number() 75 | .gte(5, minErrorMessage) 76 | .lte(10, maxErrorMessage) 77 | const jsonSchema: JSONSchema7Type = { 78 | type: 'number', 79 | minimum: 5, 80 | maximum: 10, 81 | errorMessage: { 82 | minimum: minErrorMessage, 83 | maximum: maxErrorMessage 84 | } 85 | } 86 | const jsonParsedSchema = parseNumberDef( 87 | zodNumberSchema._def, 88 | errorReferences() 89 | ) 90 | assert(jsonParsedSchema, jsonSchema) 91 | }) 92 | test("should include custom error messages for exclusive checks if they're passed", () => { 93 | const minErrorMessage = 'Number must be greater than 5' 94 | const maxErrorMessage = 'Number must less than 10' 95 | const zodNumberSchema = z 96 | .number() 97 | .gt(5, minErrorMessage) 98 | .lt(10, maxErrorMessage) 99 | const jsonSchema: JSONSchema7Type = { 100 | type: 'number', 101 | exclusiveMinimum: 5, 102 | exclusiveMaximum: 10, 103 | errorMessage: { 104 | exclusiveMinimum: minErrorMessage, 105 | exclusiveMaximum: maxErrorMessage 106 | } 107 | } 108 | const jsonParsedSchema = parseNumberDef( 109 | zodNumberSchema._def, 110 | errorReferences() 111 | ) 112 | assert(jsonParsedSchema, jsonSchema) 113 | }) 114 | test("should include custom error messages for multipleOf and int if they're passed", () => { 115 | const intErrorMessage = 'Must be an integer' 116 | const multipleOfErrorMessage = 'Must be a multiple of 5' 117 | const jsonSchema: JSONSchema7Type = { 118 | type: 'integer', 119 | multipleOf: 5, 120 | errorMessage: { 121 | type: intErrorMessage, 122 | multipleOf: multipleOfErrorMessage 123 | } 124 | } 125 | const zodNumberSchema = z 126 | .number() 127 | .multipleOf(5, multipleOfErrorMessage) 128 | .int(intErrorMessage) 129 | const jsonParsedSchema = parseNumberDef( 130 | zodNumberSchema._def, 131 | errorReferences() 132 | ) 133 | assert(jsonParsedSchema, jsonSchema) 134 | }) 135 | test("should not include errorMessage property if they're not passed", () => { 136 | const zodNumberSchemas = [ 137 | z.number().lt(5), 138 | z.number().gt(5), 139 | z.number().gte(5), 140 | z.number().lte(5), 141 | z.number().multipleOf(5), 142 | z.number().int(), 143 | z.number().int().multipleOf(5).lt(5).gt(3).lte(4).gte(3) 144 | ] 145 | const jsonParsedSchemas = zodNumberSchemas.map((schema) => 146 | parseNumberDef(schema._def, errorReferences()) 147 | ) 148 | for (const jsonParsedSchema of jsonParsedSchemas) { 149 | assert(jsonParsedSchema.errorMessage, undefined) 150 | } 151 | }) 152 | test("should not include error messages if error message isn't explicitly set to true in References constructor", () => { 153 | const zodNumberSchemas = [ 154 | z.number().lt(5), 155 | z.number().gt(5), 156 | z.number().gte(5), 157 | z.number().lte(5), 158 | z.number().multipleOf(5), 159 | z.number().int() 160 | ] 161 | for (const schema of zodNumberSchemas) { 162 | const jsonParsedSchema = parseNumberDef(schema._def, getRefs()) 163 | assert(jsonParsedSchema.errorMessage, undefined) 164 | } 165 | }) 166 | }) 167 | -------------------------------------------------------------------------------- /src/parsers/object.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { getRefs, parseObjectDef } from '..' 5 | import { assert } from '../_utils' 6 | 7 | describe('objects', () => { 8 | test('should be possible to describe catchAll schema', () => { 9 | const schema = z 10 | .object({ normalProperty: z.string() }) 11 | .catchall(z.boolean()) 12 | 13 | const parsedSchema = parseObjectDef(schema._def, getRefs()) 14 | const expectedSchema = { 15 | type: 'object', 16 | properties: { 17 | normalProperty: { type: 'string' } 18 | }, 19 | required: ['normalProperty'], 20 | additionalProperties: { 21 | type: 'boolean' 22 | } 23 | } 24 | assert(parsedSchema, expectedSchema) 25 | }) 26 | 27 | test('should be possible to use selective partial', () => { 28 | const schema = z 29 | .object({ foo: z.boolean(), bar: z.number() }) 30 | .partial({ foo: true }) 31 | 32 | const parsedSchema = parseObjectDef(schema._def, getRefs()) 33 | const expectedSchema = { 34 | type: 'object', 35 | properties: { 36 | foo: { type: 'boolean' }, 37 | bar: { type: 'number' } 38 | }, 39 | required: ['bar'], 40 | additionalProperties: false 41 | } 42 | assert(parsedSchema, expectedSchema) 43 | }) 44 | 45 | test('should allow additional properties unless strict when removeAdditionalStrategy is strict', () => { 46 | const schema = z.object({ foo: z.boolean(), bar: z.number() }) 47 | 48 | const parsedSchema = parseObjectDef( 49 | schema._def, 50 | getRefs({ removeAdditionalStrategy: 'strict' }) 51 | ) 52 | const expectedSchema = { 53 | type: 'object', 54 | properties: { 55 | foo: { type: 'boolean' }, 56 | bar: { type: 'number' } 57 | }, 58 | required: ['foo', 'bar'], 59 | additionalProperties: true 60 | } 61 | assert(parsedSchema, expectedSchema) 62 | 63 | const strictSchema = z 64 | .object({ foo: z.boolean(), bar: z.number() }) 65 | .strict() 66 | 67 | const parsedStrictSchema = parseObjectDef( 68 | strictSchema._def, 69 | getRefs({ removeAdditionalStrategy: 'strict' }) 70 | ) 71 | const expectedStrictSchema = { 72 | type: 'object', 73 | properties: { 74 | foo: { type: 'boolean' }, 75 | bar: { type: 'number' } 76 | }, 77 | required: ['foo', 'bar'], 78 | additionalProperties: false 79 | } 80 | assert(parsedStrictSchema, expectedStrictSchema) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/parsers/optional.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { getRefs, parseDef } from '..' 6 | import { assert } from '../_utils' 7 | 8 | describe('Standalone optionals', () => { 9 | test('should work as unions with undefined', () => { 10 | const parsedSchema = parseDef(z.string().optional()._def, getRefs()) 11 | 12 | const jsonSchema: JSONSchema7Type = { 13 | anyOf: [ 14 | { 15 | not: {} 16 | }, 17 | { 18 | type: 'string' 19 | } 20 | ] 21 | } 22 | 23 | assert(parsedSchema, jsonSchema) 24 | }) 25 | 26 | test('should not affect object properties', () => { 27 | const parsedSchema = parseDef( 28 | z.object({ myProperty: z.string().optional() })._def, 29 | getRefs() 30 | ) 31 | 32 | const jsonSchema: JSONSchema7Type = { 33 | type: 'object', 34 | properties: { 35 | myProperty: { 36 | type: 'string' 37 | } 38 | }, 39 | additionalProperties: false 40 | } 41 | 42 | assert(parsedSchema, jsonSchema) 43 | }) 44 | 45 | test('should work with nested properties', () => { 46 | const parsedSchema = parseDef( 47 | z.object({ myProperty: z.string().optional().array() })._def, 48 | getRefs() 49 | ) 50 | 51 | const jsonSchema: JSONSchema7Type = { 52 | type: 'object', 53 | properties: { 54 | myProperty: { 55 | type: 'array', 56 | items: { 57 | anyOf: [{ not: {} }, { type: 'string' }] 58 | } 59 | } 60 | }, 61 | required: ['myProperty'], 62 | additionalProperties: false 63 | } 64 | 65 | assert(parsedSchema, jsonSchema) 66 | }) 67 | 68 | test('should work with nested properties as object properties', () => { 69 | const parsedSchema = parseDef( 70 | z.object({ 71 | myProperty: z.object({ myInnerProperty: z.string().optional() }) 72 | })._def, 73 | getRefs() 74 | ) 75 | 76 | const jsonSchema: JSONSchema7Type = { 77 | type: 'object', 78 | properties: { 79 | myProperty: { 80 | type: 'object', 81 | properties: { 82 | myInnerProperty: { 83 | type: 'string' 84 | } 85 | }, 86 | additionalProperties: false 87 | } 88 | }, 89 | required: ['myProperty'], 90 | additionalProperties: false 91 | } 92 | 93 | assert(parsedSchema, jsonSchema) 94 | }) 95 | 96 | test('should work with nested properties with nested object property parents', () => { 97 | const parsedSchema = parseDef( 98 | z.object({ 99 | myProperty: z.object({ 100 | myInnerProperty: z.string().optional().array() 101 | }) 102 | })._def, 103 | getRefs() 104 | ) 105 | 106 | const jsonSchema: JSONSchema7Type = { 107 | type: 'object', 108 | properties: { 109 | myProperty: { 110 | type: 'object', 111 | properties: { 112 | myInnerProperty: { 113 | type: 'array', 114 | items: { 115 | anyOf: [ 116 | { not: {} }, 117 | { 118 | type: 'string' 119 | } 120 | ] 121 | } 122 | } 123 | }, 124 | required: ['myInnerProperty'], 125 | additionalProperties: false 126 | } 127 | }, 128 | required: ['myProperty'], 129 | additionalProperties: false 130 | } 131 | 132 | assert(parsedSchema, jsonSchema) 133 | }) 134 | 135 | test('should work with ref pathing', () => { 136 | const recurring = z.string() 137 | 138 | const schema = z.tuple([recurring.optional(), recurring]) 139 | 140 | const parsedSchema = parseDef(schema._def, getRefs()) 141 | 142 | const jsonSchema: JSONSchema7Type = { 143 | type: 'array', 144 | minItems: 2, 145 | maxItems: 2, 146 | items: [ 147 | { anyOf: [{ not: {} }, { type: 'string' }] }, 148 | { $ref: '#/items/0/anyOf/1' } 149 | ] 150 | } 151 | 152 | assert(parsedSchema, jsonSchema) 153 | }) 154 | }) 155 | -------------------------------------------------------------------------------- /src/parsers/pipe.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { getRefs, parsePipelineDef } from '..' 5 | import { assert } from '../_utils' 6 | 7 | describe('pipe', () => { 8 | test('Should create an allOf schema with all its inner schemas represented', () => { 9 | const schema = z.number().pipe(z.number().int()) 10 | 11 | assert(parsePipelineDef(schema._def, getRefs()), { 12 | allOf: [{ type: 'number' }, { type: 'integer' }] 13 | }) 14 | }) 15 | 16 | test('Should parse the input schema if that strategy is selected', () => { 17 | const schema = z.number().pipe(z.number().int()) 18 | 19 | assert(parsePipelineDef(schema._def, getRefs({ pipeStrategy: 'input' })), { 20 | type: 'number' 21 | }) 22 | }) 23 | 24 | test('Should parse the output schema (last schema in pipe) if that strategy is selected', () => { 25 | const schema = z.string().pipe(z.date()).pipe(z.number().int()) 26 | 27 | assert(parsePipelineDef(schema._def, getRefs({ pipeStrategy: 'output' })), { 28 | type: 'integer' 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/parsers/promise.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { getRefs, parsePromiseDef } from '..' 6 | import { assert } from '../_utils' 7 | 8 | describe('promise', () => { 9 | test('should be possible to use promise', () => { 10 | const parsedSchema = parsePromiseDef(z.promise(z.string())._def, getRefs()) 11 | const jsonSchema: JSONSchema7Type = { 12 | type: 'string' 13 | } 14 | assert(parsedSchema, jsonSchema) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/parsers/record.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { getRefs, parseRecordDef } from '..' 5 | import { assert } from '../_utils' 6 | 7 | describe('records', () => { 8 | test('should be possible to describe a simple record', () => { 9 | const schema = z.record(z.number()) 10 | 11 | const parsedSchema = parseRecordDef(schema._def, getRefs()) 12 | const expectedSchema = { 13 | type: 'object', 14 | additionalProperties: { 15 | type: 'number' 16 | } 17 | } 18 | assert(parsedSchema, expectedSchema) 19 | }) 20 | 21 | test('should be possible to describe a complex record with checks', () => { 22 | const schema = z.record( 23 | z.object({ foo: z.number().min(2) }).catchall(z.string().cuid()) 24 | ) 25 | 26 | const parsedSchema = parseRecordDef(schema._def, getRefs()) 27 | const expectedSchema = { 28 | type: 'object', 29 | additionalProperties: { 30 | type: 'object', 31 | properties: { 32 | foo: { 33 | type: 'number', 34 | minimum: 2 35 | } 36 | }, 37 | required: ['foo'], 38 | additionalProperties: { 39 | type: 'string', 40 | pattern: '^[cC][^\\s-]{8,}$' 41 | } 42 | } 43 | } 44 | assert(parsedSchema, expectedSchema) 45 | }) 46 | 47 | test('should be possible to describe a key schema', () => { 48 | const schema = z.record(z.string().uuid(), z.number()) 49 | 50 | const parsedSchema = parseRecordDef(schema._def, getRefs()) 51 | const expectedSchema = { 52 | type: 'object', 53 | additionalProperties: { 54 | type: 'number' 55 | }, 56 | propertyNames: { 57 | format: 'uuid' 58 | } 59 | } 60 | assert(parsedSchema, expectedSchema) 61 | }) 62 | 63 | test('should be possible to describe a key with an enum', () => { 64 | const schema = z.record(z.enum(['foo', 'bar']), z.number()) 65 | const parsedSchema = parseRecordDef(schema._def, getRefs()) 66 | const expectedSchema = { 67 | type: 'object', 68 | additionalProperties: { 69 | type: 'number' 70 | }, 71 | propertyNames: { 72 | enum: ['foo', 'bar'] 73 | } 74 | } 75 | assert(parsedSchema, expectedSchema) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /src/parsers/set.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import { describe, test } from 'vitest' 3 | import { z } from 'zod' 4 | 5 | import { getRefs, parseSetDef } from '..' 6 | import { assert } from '../_utils' 7 | import { errorReferences } from './error-references.js' 8 | 9 | describe('set', () => { 10 | test("should include min and max size error messages if they're passed.", () => { 11 | const minSizeError = 'Set must have at least 5 elements' 12 | const maxSizeError = "Set can't have more than 10 elements" 13 | const errs = { 14 | minItems: minSizeError, 15 | maxItems: maxSizeError 16 | } 17 | const jsonSchema: JSONSchema7Type = { 18 | type: 'array', 19 | minItems: 5, 20 | maxItems: 10, 21 | errorMessage: errs, 22 | uniqueItems: true, 23 | items: {} 24 | } 25 | const zodSchema = z.set(z.any()).min(5, minSizeError).max(10, maxSizeError) 26 | const jsonParsedSchema = parseSetDef(zodSchema._def, errorReferences()) 27 | assert(jsonParsedSchema, jsonSchema) 28 | }) 29 | test('should not include error messages if none are passed', () => { 30 | const jsonSchema: JSONSchema7Type = { 31 | type: 'array', 32 | minItems: 5, 33 | maxItems: 10, 34 | uniqueItems: true, 35 | items: {} 36 | } 37 | const zodSchema = z.set(z.any()).min(5).max(10) 38 | const jsonParsedSchema = parseSetDef(zodSchema._def, errorReferences()) 39 | assert(jsonParsedSchema, jsonSchema) 40 | }) 41 | test("should not include error messages if it's not explicitly set to true in the References constructor", () => { 42 | const zodSchema = z.set(z.any()).min(1, 'bad').max(5, 'vbad') 43 | const jsonParsedSchema = parseSetDef(zodSchema._def, getRefs()) 44 | assert(jsonParsedSchema.errorMessage, undefined) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/parsers/string.test.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | import addFormats from 'ajv-formats' 3 | import { type JSONSchema7Type } from 'json-schema' 4 | import { describe, test } from 'vitest' 5 | import { z } from 'zod' 6 | 7 | import { 8 | type ErrorMessages, 9 | getRefs, 10 | type JsonSchema7StringType, 11 | parseStringDef, 12 | zodPatterns 13 | } from '..' 14 | import { assert } from '../_utils' 15 | import { errorReferences } from './error-references.js' 16 | 17 | const ajv = addFormats(new Ajv()) 18 | 19 | describe('String validations', () => { 20 | test('should be possible to describe minimum length of a string', () => { 21 | const parsedSchema = parseStringDef(z.string().min(5)._def, getRefs()) 22 | const jsonSchema: JSONSchema7Type = { 23 | type: 'string', 24 | minLength: 5 25 | } 26 | assert(parsedSchema, jsonSchema) 27 | 28 | ajv.validate(parsedSchema, '1234') 29 | assert(ajv.errors, [ 30 | { 31 | keyword: 'minLength', 32 | instancePath: '', 33 | schemaPath: '#/minLength', 34 | params: { limit: 5 }, 35 | message: 'must NOT have fewer than 5 characters' 36 | } 37 | ]) 38 | }) 39 | test('should be possible to describe maximum length of a string', () => { 40 | const parsedSchema = parseStringDef(z.string().max(5)._def, getRefs()) 41 | const jsonSchema: JSONSchema7Type = { 42 | type: 'string', 43 | maxLength: 5 44 | } 45 | assert(parsedSchema, jsonSchema) 46 | ajv.validate(parsedSchema, '123456') 47 | assert(ajv.errors, [ 48 | { 49 | keyword: 'maxLength', 50 | instancePath: '', 51 | schemaPath: '#/maxLength', 52 | params: { limit: 5 }, 53 | message: 'must NOT have more than 5 characters' 54 | } 55 | ]) 56 | }) 57 | test('should be possible to describe both minimum and maximum length of a string', () => { 58 | const parsedSchema = parseStringDef( 59 | z.string().min(5).max(5)._def, 60 | getRefs() 61 | ) 62 | const jsonSchema: JSONSchema7Type = { 63 | type: 'string', 64 | minLength: 5, 65 | maxLength: 5 66 | } 67 | assert(parsedSchema, jsonSchema) 68 | }) 69 | test('should be possible to use email constraint', () => { 70 | const parsedSchema = parseStringDef(z.string().email()._def, getRefs()) 71 | const jsonSchema: JSONSchema7Type = { 72 | type: 'string', 73 | format: 'email' 74 | } 75 | assert(parsedSchema, jsonSchema) 76 | ajv.validate(parsedSchema, 'herpderp') 77 | assert(ajv.errors, [ 78 | { 79 | instancePath: '', 80 | schemaPath: '#/format', 81 | keyword: 'format', 82 | params: { format: 'email' }, 83 | message: 'must match format "email"' 84 | } 85 | ]) 86 | assert(ajv.validate(parsedSchema, 'hej@hej.com'), true) 87 | }) 88 | test('should be possible to use uuid constraint', () => { 89 | const parsedSchema = parseStringDef(z.string().uuid()._def, getRefs()) 90 | const jsonSchema: JSONSchema7Type = { 91 | type: 'string', 92 | format: 'uuid' 93 | } 94 | assert(parsedSchema, jsonSchema) 95 | ajv.validate(parsedSchema, 'herpderp') 96 | assert(ajv.errors, [ 97 | { 98 | instancePath: '', 99 | schemaPath: '#/format', 100 | keyword: 'format', 101 | params: { format: 'uuid' }, 102 | message: 'must match format "uuid"' 103 | } 104 | ]) 105 | assert( 106 | ajv.validate(parsedSchema, '2ad7b2ce-e571-44b8-bee3-84fb3ac80d6b'), 107 | true 108 | ) 109 | }) 110 | test('should be possible to use url constraint', () => { 111 | const parsedSchema = parseStringDef(z.string().url()._def, getRefs()) 112 | const jsonSchema: JSONSchema7Type = { 113 | type: 'string', 114 | format: 'uri' 115 | } 116 | assert(parsedSchema, jsonSchema) 117 | ajv.validate(parsedSchema, 'herpderp') 118 | assert(ajv.errors, [ 119 | { 120 | instancePath: '', 121 | schemaPath: '#/format', 122 | keyword: 'format', 123 | params: { format: 'uri' }, 124 | message: 'must match format "uri"' 125 | } 126 | ]) 127 | assert(ajv.validate(parsedSchema, 'http://hello.com'), true) 128 | }) 129 | 130 | test('should be possible to use regex constraint', () => { 131 | const parsedSchema = parseStringDef( 132 | z.string().regex(/[A-C]/)._def, 133 | getRefs() 134 | ) 135 | const jsonSchema: JSONSchema7Type = { 136 | type: 'string', 137 | pattern: '[A-C]' 138 | } 139 | assert(parsedSchema, jsonSchema) 140 | ajv.validate(parsedSchema, 'herpderp') 141 | assert(ajv.errors, [ 142 | { 143 | instancePath: '', 144 | schemaPath: '#/pattern', 145 | keyword: 'pattern', 146 | params: { pattern: '[A-C]' }, 147 | message: 'must match pattern "[A-C]"' 148 | } 149 | ]) 150 | assert(ajv.validate(parsedSchema, 'B'), true) 151 | }) 152 | 153 | test('should be possible to use CUID constraint', () => { 154 | const parsedSchema = parseStringDef(z.string().cuid()._def, getRefs()) 155 | const jsonSchema: JSONSchema7Type = { 156 | type: 'string', 157 | pattern: '^[cC][^\\s-]{8,}$' 158 | } 159 | assert(parsedSchema, jsonSchema) 160 | ajv.validate(parsedSchema, 'herpderp') 161 | assert(ajv.errors, [ 162 | { 163 | instancePath: '', 164 | schemaPath: '#/pattern', 165 | keyword: 'pattern', 166 | params: { pattern: '^[cC][^\\s-]{8,}$' }, 167 | message: 'must match pattern "^[cC][^\\s-]{8,}$"' 168 | } 169 | ]) 170 | assert(ajv.validate(parsedSchema, 'ckopqwooh000001la8mbi2im9'), true) 171 | }) 172 | 173 | test('should gracefully ignore the .trim() "check"', () => { 174 | const parsedSchema = parseStringDef(z.string().trim()._def, getRefs()) 175 | const jsonSchema = { type: 'string' } 176 | assert(parsedSchema, jsonSchema) 177 | }) 178 | 179 | test('should work with the startsWith check', () => { 180 | assert( 181 | parseStringDef(z.string().startsWith('aBcD123{}[]')._def, getRefs()), 182 | { 183 | type: 'string', 184 | pattern: '^aBcD123\\{\\}\\[\\]' 185 | } 186 | ) 187 | }) 188 | 189 | test('should work with the endsWith check', () => { 190 | assert(parseStringDef(z.string().endsWith('aBcD123{}[]')._def, getRefs()), { 191 | type: 'string', 192 | pattern: 'aBcD123\\{\\}\\[\\]$' 193 | }) 194 | }) 195 | 196 | test('should bundle multiple pattern type checks in an allOf container', () => { 197 | assert( 198 | parseStringDef( 199 | z.string().startsWith('alpha').endsWith('omega')._def, 200 | getRefs() 201 | ), 202 | { 203 | type: 'string', 204 | allOf: [ 205 | { 206 | pattern: '^alpha' 207 | }, 208 | { 209 | pattern: 'omega$' 210 | } 211 | ] 212 | } 213 | ) 214 | }) 215 | 216 | test('should pick correct value if multiple min/max are present', () => { 217 | assert( 218 | parseStringDef(z.string().min(1).min(2).max(3).max(4)._def, getRefs()), 219 | { 220 | type: 'string', 221 | maxLength: 3, 222 | minLength: 2 223 | } 224 | ) 225 | }) 226 | 227 | test("should include custom error messages for each string check if they're included", () => { 228 | const regex = /cool/ 229 | const errorMessages = { 230 | min: 'Not long enough', 231 | max: 'Too long', 232 | emailStrategy: 'not email', 233 | url: 'not url', 234 | uuid: 'not uuid', 235 | regex: "didn't match regex " + regex.source, 236 | startsWith: "didn't start with " + regex.source, 237 | endsWith: "didn't end with " + regex.source 238 | } 239 | const testCases: { 240 | schema: z.ZodString 241 | errorMessage: ErrorMessages 242 | }[] = [ 243 | { 244 | schema: z.string().min(1, errorMessages.min), 245 | errorMessage: { 246 | minLength: errorMessages.min 247 | } 248 | }, 249 | { 250 | schema: z.string().max(1, errorMessages.max), 251 | errorMessage: { 252 | maxLength: errorMessages.max 253 | } 254 | }, 255 | { 256 | schema: z.string().uuid(errorMessages.uuid), 257 | errorMessage: { 258 | format: errorMessages.uuid 259 | } 260 | }, 261 | { 262 | schema: z.string().email(errorMessages.emailStrategy), 263 | errorMessage: { 264 | format: errorMessages.emailStrategy 265 | } 266 | }, 267 | { 268 | schema: z.string().url(errorMessages.url), 269 | errorMessage: { 270 | format: errorMessages.url 271 | } 272 | }, 273 | { 274 | schema: z.string().startsWith(regex.source, errorMessages.startsWith), 275 | errorMessage: { 276 | pattern: errorMessages.startsWith 277 | } 278 | }, 279 | { 280 | schema: z.string().endsWith(regex.source, errorMessages.endsWith), 281 | errorMessage: { 282 | pattern: errorMessages.endsWith 283 | } 284 | }, 285 | { 286 | schema: z.string().regex(regex, errorMessages.regex), 287 | errorMessage: { 288 | pattern: errorMessages.regex 289 | } 290 | } 291 | ] 292 | for (const testCase of testCases) { 293 | const { schema, errorMessage } = testCase 294 | const jsonSchema: JSONSchema7Type = { 295 | type: 'string', 296 | errorMessage 297 | } 298 | const jsonSchemaParsed = parseStringDef(schema._def, errorReferences()) 299 | assert(jsonSchemaParsed.errorMessage, jsonSchema.errorMessage) 300 | } 301 | }) 302 | test("should not include a custom error message for any string when they aren't passed for single checks", () => { 303 | const regex = /cool/ 304 | const testCases: z.ZodString[] = [ 305 | z.string().min(1), 306 | z.string().max(1), 307 | z.string().uuid(), 308 | z.string().email(), 309 | z.string().url(), 310 | z.string().startsWith(regex.source), 311 | z.string().endsWith(regex.source), 312 | z.string().regex(regex), 313 | z.string().regex(regex).regex(regex) 314 | ] 315 | for (const schema of testCases) { 316 | const jsonSchemaParsed = parseStringDef(schema._def, errorReferences()) 317 | assert(jsonSchemaParsed.errorMessage, undefined) 318 | } 319 | }) 320 | test("should include custom error messages in 'allOf' if they're passed for a given pattern, and include other errors at the top level.", () => { 321 | const regex = /cool/ 322 | const pattern = regex.source 323 | const errorMessages = { 324 | one: `Pattern one doesn't match.`, 325 | two: `Pattern two doesn't match`, 326 | format: 'Pretty terrible format', 327 | minLength: 'too short', 328 | maxLength: 'too long' 329 | } 330 | const jsonSchema: JSONSchema7Type = { 331 | type: 'string', 332 | minLength: 5, 333 | maxLength: 10, 334 | format: 'uuid', 335 | allOf: [ 336 | { 337 | pattern, 338 | errorMessage: { 339 | pattern: errorMessages.one 340 | } 341 | }, 342 | { 343 | pattern, 344 | errorMessage: { 345 | pattern: errorMessages.two 346 | } 347 | }, 348 | { 349 | pattern 350 | } 351 | ], 352 | errorMessage: { 353 | format: errorMessages.format, 354 | minLength: errorMessages.minLength 355 | } 356 | } 357 | const zodSchema = z 358 | .string() 359 | .max(10) 360 | .uuid(errorMessages.format) 361 | .min(5, errorMessages.minLength) 362 | .regex(regex, errorMessages.one) 363 | .regex(regex, errorMessages.two) 364 | .regex(regex) 365 | const jsonSchemaParse = parseStringDef(zodSchema._def, errorReferences()) 366 | assert(jsonSchemaParse, jsonSchema) 367 | }) 368 | 369 | test('should include include the pattern error message in the top level with other messages if there is only one pattern', () => { 370 | const formatMessage = 'not a uuid' 371 | const regex = /cool/ 372 | const regexErrorMessage = "doesn't match regex " + regex.source 373 | const jsonSchema: JSONSchema7Type = { 374 | type: 'string', 375 | format: 'uuid', 376 | pattern: regex.source, 377 | errorMessage: { 378 | format: formatMessage, 379 | pattern: regexErrorMessage 380 | } 381 | } 382 | const zodSchema = z 383 | .string() 384 | .uuid(formatMessage) 385 | .regex(regex, regexErrorMessage) 386 | const jsonParsedSchema = parseStringDef(zodSchema._def, errorReferences()) 387 | assert(jsonParsedSchema, jsonSchema) 388 | }) 389 | 390 | test("should not include error messages if the error message option isn't explicitly passed to References constructor", () => { 391 | const zodSchema = [ 392 | z.string().min(5, 'bad'), 393 | z.string().max(5, 'bad'), 394 | z.string().regex(/cool/, 'bad'), 395 | z.string().uuid('bad'), 396 | z.string().email('bad'), 397 | z.string().url('bad'), 398 | z.string().regex(/cool/, 'bad').regex(/cool/, 'bad').url('bad') 399 | ] 400 | for (const schema of zodSchema) { 401 | const jsonParsedSchema = parseStringDef(schema._def, getRefs()) 402 | assert(jsonParsedSchema.errorMessage, undefined) 403 | if (!jsonParsedSchema.allOf?.length) continue 404 | for (const oneOf of jsonParsedSchema.allOf) { 405 | assert(oneOf.errorMessage, undefined) 406 | } 407 | } 408 | }) 409 | 410 | test('should bundle multiple formats into anyOf', () => { 411 | const zodSchema = z.string().ip().email() 412 | const jsonSchema: JSONSchema7Type = { 413 | type: 'string', 414 | anyOf: [ 415 | { 416 | format: 'ipv4' 417 | }, 418 | { 419 | format: 'ipv6' 420 | }, 421 | { 422 | format: 'email' 423 | } 424 | ] 425 | } 426 | const jsonParsedSchema = parseStringDef(zodSchema._def, errorReferences()) 427 | assert(jsonParsedSchema, jsonSchema) 428 | }) 429 | 430 | test('should default to contentEncoding for base64, but format and pattern should also work', () => { 431 | const def = z.string().base64()._def 432 | 433 | assert(parseStringDef(def, getRefs()), { 434 | type: 'string', 435 | contentEncoding: 'base64' 436 | }) 437 | 438 | assert( 439 | parseStringDef( 440 | def, 441 | getRefs({ base64Strategy: 'contentEncoding:base64' }) 442 | ), 443 | { 444 | type: 'string', 445 | contentEncoding: 'base64' 446 | } 447 | ) 448 | 449 | assert(parseStringDef(def, getRefs({ base64Strategy: 'format:binary' })), { 450 | type: 'string', 451 | format: 'binary' 452 | }) 453 | 454 | assert(parseStringDef(def, getRefs({ base64Strategy: 'pattern:zod' })), { 455 | type: 'string', 456 | pattern: zodPatterns.base64.source 457 | }) 458 | }) 459 | 460 | test('should be possible to pick format:email, format:idn-email or pattern:zod', () => { 461 | assert(parseStringDef(z.string().email()._def, getRefs()), { 462 | type: 'string', 463 | format: 'email' 464 | }) 465 | 466 | assert( 467 | parseStringDef( 468 | z.string().email()._def, 469 | getRefs({ emailStrategy: 'format:email' }) 470 | ), 471 | { 472 | type: 'string', 473 | format: 'email' 474 | } 475 | ) 476 | 477 | assert( 478 | parseStringDef( 479 | z.string().email()._def, 480 | getRefs({ emailStrategy: 'format:idn-email' }) 481 | ), 482 | { 483 | type: 'string', 484 | format: 'idn-email' 485 | } 486 | ) 487 | 488 | assert( 489 | parseStringDef( 490 | z.string().email()._def, 491 | getRefs({ emailStrategy: 'pattern:zod' }) 492 | ), 493 | { 494 | type: 'string', 495 | pattern: zodPatterns.email.source 496 | } 497 | ) 498 | }) 499 | 500 | test('should correctly handle reasonable non-contrived regexes with flags', () => { 501 | assert( 502 | parseStringDef( 503 | z.string().regex(/(^|\^foo)Ba[r-z]+./)._def, 504 | getRefs({ applyRegexFlags: true }) 505 | ), 506 | { 507 | type: 'string', 508 | pattern: '(^|\\^foo)Ba[r-z]+.' 509 | } 510 | ) 511 | 512 | assert( 513 | parseStringDef( 514 | z.string().regex(/(^|\^foo)ba[r-z]+./i)._def, 515 | getRefs({ applyRegexFlags: true }) 516 | ), 517 | { 518 | type: 'string', 519 | pattern: '(^|\\^[fF][oO][oO])[bB][aA][r-zR-Z]+.' 520 | } 521 | ) 522 | 523 | assert( 524 | parseStringDef( 525 | z.string().regex(/(^|\^foo)Ba[r-z]+./ms)._def, 526 | getRefs({ applyRegexFlags: true }) 527 | ), 528 | { 529 | type: 'string', 530 | pattern: '((^|(?<=[\r\n]))|\\^foo)Ba[r-z]+[.\r\n]' 531 | } 532 | ) 533 | 534 | assert( 535 | parseStringDef( 536 | z.string().regex(/(^|\^foo)ba[r-z]+./ims)._def, 537 | getRefs({ applyRegexFlags: true }) 538 | ), 539 | { 540 | type: 'string', 541 | pattern: '((^|(?<=[\r\n]))|\\^[fF][oO][oO])[bB][aA][r-zR-Z]+[.\r\n]' 542 | } 543 | ) 544 | }) 545 | }) 546 | -------------------------------------------------------------------------------- /src/parsers/tuple.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { getRefs, parseTupleDef } from '..' 5 | import { assert } from '../_utils' 6 | 7 | describe('objects', () => { 8 | test('should be possible to describe a simple tuple schema', () => { 9 | const schema = z.tuple([z.string(), z.number()]) 10 | 11 | const parsedSchema = parseTupleDef(schema._def, getRefs()) 12 | const expectedSchema = { 13 | type: 'array', 14 | items: [{ type: 'string' }, { type: 'number' }], 15 | minItems: 2, 16 | maxItems: 2 17 | } 18 | assert(parsedSchema, expectedSchema) 19 | }) 20 | 21 | test('should be possible to describe a tuple schema with rest()', () => { 22 | const schema = z.tuple([z.string(), z.number()]).rest(z.boolean()) 23 | 24 | const parsedSchema = parseTupleDef(schema._def, getRefs()) 25 | const expectedSchema = { 26 | type: 'array', 27 | items: [{ type: 'string' }, { type: 'number' }], 28 | minItems: 2, 29 | additionalItems: { 30 | type: 'boolean' 31 | } 32 | } 33 | assert(parsedSchema, expectedSchema) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/parsers/union.test.ts: -------------------------------------------------------------------------------- 1 | import { type JSONSchema7Type } from 'json-schema' 2 | import deref from 'local-ref-resolver' 3 | import { describe, test } from 'vitest' 4 | import { z } from 'zod' 5 | 6 | import { getRefs, parseUnionDef } from '..' 7 | import { assert } from '../_utils' 8 | 9 | describe('Unions', () => { 10 | test('Should be possible to get a simple type array from a union of only unvalidated primitives', () => { 11 | const parsedSchema = parseUnionDef( 12 | z.union([z.string(), z.number(), z.boolean(), z.null()])._def, 13 | getRefs() 14 | ) 15 | const jsonSchema: JSONSchema7Type = { 16 | type: ['string', 'number', 'boolean', 'null'] 17 | } 18 | assert(parsedSchema, jsonSchema) 19 | }) 20 | 21 | test('Should be possible to get a simple type array with enum values from a union of literals', () => { 22 | const parsedSchema = parseUnionDef( 23 | z.union([ 24 | z.literal('string'), 25 | z.literal(123), 26 | z.literal(true), 27 | z.literal(null) 28 | ])._def, 29 | getRefs() 30 | ) 31 | const jsonSchema: JSONSchema7Type = { 32 | type: ['string', 'number', 'boolean', 'null'], 33 | enum: ['string', 123, true, null] 34 | } 35 | assert(parsedSchema, jsonSchema) 36 | }) 37 | 38 | test('Should be possible to create a union with objects, arrays and validated primitives as an anyOf', () => { 39 | const parsedSchema = parseUnionDef( 40 | z.union([ 41 | z.object({ herp: z.string(), derp: z.boolean() }), 42 | z.array(z.number()), 43 | z.string().min(3), 44 | z.number() 45 | ])._def, 46 | getRefs() 47 | ) 48 | const jsonSchema: JSONSchema7Type = { 49 | anyOf: [ 50 | { 51 | type: 'object', 52 | properties: { 53 | herp: { 54 | type: 'string' 55 | }, 56 | derp: { 57 | type: 'boolean' 58 | } 59 | }, 60 | required: ['herp', 'derp'], 61 | additionalProperties: false 62 | }, 63 | { 64 | type: 'array', 65 | items: { 66 | type: 'number' 67 | } 68 | }, 69 | { 70 | type: 'string', 71 | minLength: 3 72 | }, 73 | { 74 | type: 'number' 75 | } 76 | ] 77 | } 78 | assert(parsedSchema, jsonSchema) 79 | }) 80 | 81 | test('should be possible to deref union schemas', () => { 82 | const recurring = z.object({ foo: z.boolean() }) 83 | 84 | const union = z.union([recurring, recurring, recurring]) 85 | 86 | const jsonSchema = parseUnionDef(union._def, getRefs()) 87 | 88 | assert(jsonSchema, { 89 | anyOf: [ 90 | { 91 | type: 'object', 92 | properties: { 93 | foo: { 94 | type: 'boolean' 95 | } 96 | }, 97 | required: ['foo'], 98 | additionalProperties: false 99 | }, 100 | { 101 | $ref: '#/anyOf/0' 102 | }, 103 | { 104 | $ref: '#/anyOf/0' 105 | } 106 | ] 107 | }) 108 | 109 | const resolvedSchema = deref(jsonSchema) 110 | assert(resolvedSchema.anyOf[0], resolvedSchema.anyOf[1]) 111 | assert(resolvedSchema.anyOf[1], resolvedSchema.anyOf[2]) 112 | }) 113 | 114 | test('nullable primitives should come out fine', () => { 115 | const union = z.union([z.string(), z.null()]) 116 | 117 | const jsonSchema = parseUnionDef(union._def, getRefs()) 118 | 119 | assert(jsonSchema, { 120 | type: ['string', 'null'] 121 | }) 122 | }) 123 | 124 | test('should join a union of Zod enums into a single enum', () => { 125 | const union = z.union([z.enum(['a', 'b', 'c']), z.enum(['c', 'd', 'e'])]) 126 | 127 | const jsonSchema = parseUnionDef(union._def, getRefs()) 128 | 129 | assert(jsonSchema, { 130 | type: 'string', 131 | enum: ['a', 'b', 'c', 'd', 'e'] 132 | }) 133 | }) 134 | 135 | test('should work with discriminated union type', () => { 136 | const discUnion = z.discriminatedUnion('kek', [ 137 | z.object({ kek: z.literal('A'), lel: z.boolean() }), 138 | z.object({ kek: z.literal('B'), lel: z.number() }) 139 | ]) 140 | 141 | const jsonSchema = parseUnionDef(discUnion._def, getRefs()) 142 | 143 | assert(jsonSchema, { 144 | anyOf: [ 145 | { 146 | type: 'object', 147 | properties: { 148 | kek: { 149 | type: 'string', 150 | const: 'A' 151 | }, 152 | lel: { 153 | type: 'boolean' 154 | } 155 | }, 156 | required: ['kek', 'lel'], 157 | additionalProperties: false 158 | }, 159 | { 160 | type: 'object', 161 | properties: { 162 | kek: { 163 | type: 'string', 164 | const: 'B' 165 | }, 166 | lel: { 167 | type: 'number' 168 | } 169 | }, 170 | required: ['kek', 'lel'], 171 | additionalProperties: false 172 | } 173 | ] 174 | }) 175 | }) 176 | 177 | test('should not ignore descriptions in literal unions', () => { 178 | assert( 179 | [ 180 | parseUnionDef( 181 | z.union([z.literal(true), z.literal('herp'), z.literal(3)])._def, 182 | getRefs() 183 | ), 184 | parseUnionDef( 185 | z.union([ 186 | z.literal(true), 187 | z.literal('herp').describe('derp'), 188 | z.literal(3) 189 | ])._def, 190 | getRefs() 191 | ) 192 | ], 193 | [ 194 | { type: ['boolean', 'string', 'number'], enum: [true, 'herp', 3] }, 195 | { 196 | anyOf: [ 197 | { type: 'boolean', const: true }, 198 | { type: 'string', const: 'herp', description: 'derp' }, 199 | { type: 'number', const: 3 } 200 | ] 201 | } 202 | ] 203 | ) 204 | }) 205 | }) 206 | -------------------------------------------------------------------------------- /src/readme.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { zodToJsonSchema } from '.' 5 | 6 | describe('The readme example', () => { 7 | test('should be valid', () => { 8 | const mySchema = z 9 | .object({ 10 | myString: z.string().min(5), 11 | myUnion: z.union([z.number(), z.boolean()]) 12 | }) 13 | .describe('My neat object schema') 14 | 15 | const jsonSchema = zodToJsonSchema(mySchema, 'mySchema') 16 | 17 | expect(jsonSchema).toEqual({ 18 | $schema: 'http://json-schema.org/draft-07/schema#', 19 | $ref: '#/definitions/mySchema', 20 | definitions: { 21 | mySchema: { 22 | description: 'My neat object schema', 23 | type: 'object', 24 | properties: { 25 | myString: { 26 | type: 'string', 27 | minLength: 5 28 | }, 29 | myUnion: { 30 | type: ['number', 'boolean'] 31 | } 32 | }, 33 | additionalProperties: false, 34 | required: ['myString', 'myUnion'] 35 | } 36 | } 37 | }) 38 | }) 39 | 40 | test('should have a valid error message example', () => { 41 | const EmailSchema = z.string().email('Invalid email').min(5, 'Too short') 42 | const expected = { 43 | $schema: 'http://json-schema.org/draft-07/schema#', 44 | type: 'string', 45 | format: 'email', 46 | minLength: 5, 47 | errorMessage: { 48 | format: 'Invalid email', 49 | minLength: 'Too short' 50 | } 51 | } 52 | const parsedJsonSchema = zodToJsonSchema(EmailSchema, { 53 | errorMessages: true 54 | }) 55 | expect(parsedJsonSchema).toEqual(expected) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/refererences.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import type { JSONSchema7Type } from 'json-schema' 3 | import Ajv from 'ajv' 4 | import deref from 'local-ref-resolver' 5 | import { describe, test } from 'vitest' 6 | import { z } from 'zod' 7 | 8 | import { zodToJsonSchema } from '.' 9 | import { assert } from './_utils' 10 | 11 | const ajv = new Ajv() 12 | 13 | describe('Pathing', () => { 14 | test('should handle recurring properties with paths', () => { 15 | const addressSchema = z.object({ 16 | street: z.string(), 17 | number: z.number(), 18 | city: z.string() 19 | }) 20 | const someAddresses = z.object({ 21 | address1: addressSchema, 22 | address2: addressSchema, 23 | lotsOfAddresses: z.array(addressSchema) 24 | }) 25 | const jsonSchema = { 26 | $schema: 'http://json-schema.org/draft-07/schema#', 27 | type: 'object', 28 | properties: { 29 | address1: { 30 | type: 'object', 31 | properties: { 32 | street: { type: 'string' }, 33 | number: { type: 'number' }, 34 | city: { type: 'string' } 35 | }, 36 | additionalProperties: false, 37 | required: ['street', 'number', 'city'] 38 | }, 39 | address2: { $ref: '#/properties/address1' }, 40 | lotsOfAddresses: { 41 | type: 'array', 42 | items: { $ref: '#/properties/address1' } 43 | } 44 | }, 45 | additionalProperties: false, 46 | required: ['address1', 'address2', 'lotsOfAddresses'] 47 | } 48 | 49 | const parsedSchema = zodToJsonSchema(someAddresses) 50 | assert(parsedSchema, jsonSchema) 51 | assert(ajv.validateSchema(parsedSchema!), true) 52 | }) 53 | 54 | test('Should properly reference union participants', () => { 55 | const participant = z.object({ str: z.string() }) 56 | 57 | const schema = z.object({ 58 | union: z.union([participant, z.string()]), 59 | part: participant 60 | }) 61 | 62 | const expectedJsonSchema = { 63 | $schema: 'http://json-schema.org/draft-07/schema#', 64 | type: 'object', 65 | properties: { 66 | union: { 67 | anyOf: [ 68 | { 69 | type: 'object', 70 | properties: { 71 | str: { 72 | type: 'string' 73 | } 74 | }, 75 | additionalProperties: false, 76 | required: ['str'] 77 | }, 78 | { 79 | type: 'string' 80 | } 81 | ] 82 | }, 83 | part: { 84 | $ref: '#/properties/union/anyOf/0' 85 | } 86 | }, 87 | additionalProperties: false, 88 | required: ['union', 'part'] 89 | } 90 | 91 | const parsedSchema = zodToJsonSchema(schema) 92 | assert(parsedSchema, expectedJsonSchema) 93 | assert(ajv.validateSchema(parsedSchema!), true) 94 | 95 | const resolvedSchema = deref(expectedJsonSchema) 96 | assert( 97 | resolvedSchema.properties.part, 98 | resolvedSchema.properties.union.anyOf[0] 99 | ) 100 | }) 101 | 102 | test('Should be able to handle recursive schemas', () => { 103 | type Category = { 104 | name: string 105 | subcategories: Category[] 106 | } 107 | 108 | // cast to z.ZodSchema 109 | // @ts-ignore 110 | const categorySchema: z.ZodSchema = z.lazy(() => 111 | z.object({ 112 | name: z.string(), 113 | subcategories: z.array(categorySchema) 114 | }) 115 | ) 116 | 117 | const parsedSchema = zodToJsonSchema(categorySchema) 118 | 119 | const expectedJsonSchema = { 120 | $schema: 'http://json-schema.org/draft-07/schema#', 121 | type: 'object', 122 | properties: { 123 | name: { 124 | type: 'string' 125 | }, 126 | subcategories: { 127 | type: 'array', 128 | items: { 129 | $ref: '#' 130 | } 131 | } 132 | }, 133 | required: ['name', 'subcategories'], 134 | additionalProperties: false 135 | } 136 | 137 | assert(parsedSchema, expectedJsonSchema) 138 | assert(ajv.validateSchema(parsedSchema!), true) 139 | 140 | const resolvedSchema = deref(parsedSchema) 141 | assert(resolvedSchema.properties.subcategories.items, resolvedSchema) 142 | }) 143 | 144 | test('Should be able to handle complex & nested recursive schemas', () => { 145 | type Category = { 146 | name: string 147 | inner: { 148 | subcategories?: Record | null 149 | } 150 | } 151 | 152 | // cast to z.ZodSchema 153 | // @ts-ignore 154 | const categorySchema: z.ZodSchema = z.lazy(() => 155 | z.object({ 156 | name: z.string(), 157 | inner: z.object({ 158 | subcategories: z.record(categorySchema).nullable().optional() 159 | }) 160 | }) 161 | ) 162 | 163 | const inObjectSchema = z.object({ 164 | category: categorySchema 165 | }) 166 | 167 | const parsedSchema = zodToJsonSchema(inObjectSchema) 168 | 169 | const expectedJsonSchema = { 170 | $schema: 'http://json-schema.org/draft-07/schema#', 171 | type: 'object', 172 | additionalProperties: false, 173 | required: ['category'], 174 | properties: { 175 | category: { 176 | type: 'object', 177 | properties: { 178 | name: { 179 | type: 'string' 180 | }, 181 | inner: { 182 | type: 'object', 183 | additionalProperties: false, 184 | properties: { 185 | subcategories: { 186 | anyOf: [ 187 | { 188 | type: 'object', 189 | additionalProperties: { 190 | $ref: '#/properties/category' 191 | } 192 | }, 193 | { 194 | type: 'null' 195 | } 196 | ] 197 | } 198 | } 199 | } 200 | }, 201 | required: ['name', 'inner'], 202 | additionalProperties: false 203 | } 204 | } 205 | } 206 | 207 | assert(parsedSchema, expectedJsonSchema) 208 | assert(ajv.validateSchema(parsedSchema!), true) 209 | }) 210 | 211 | test('should work with relative references', () => { 212 | const recurringSchema = z.string() 213 | const objectSchema = z.object({ 214 | foo: recurringSchema, 215 | bar: recurringSchema 216 | }) 217 | 218 | const jsonSchema = zodToJsonSchema(objectSchema, { 219 | $refStrategy: 'relative' 220 | }) 221 | 222 | const exptectedResult: JSONSchema7Type = { 223 | $schema: 'http://json-schema.org/draft-07/schema#', 224 | type: 'object', 225 | properties: { 226 | foo: { 227 | type: 'string' 228 | }, 229 | bar: { 230 | $ref: '1/foo' 231 | } 232 | }, 233 | required: ['foo', 'bar'], 234 | additionalProperties: false 235 | } 236 | 237 | assert(jsonSchema, exptectedResult) 238 | }) 239 | 240 | test('should be possible to override the base path', () => { 241 | const recurringSchema = z.string() 242 | const objectSchema = z.object({ 243 | foo: recurringSchema, 244 | bar: recurringSchema 245 | }) 246 | 247 | const jsonSchema = zodToJsonSchema(objectSchema, { 248 | basePath: ['#', 'lol', 'xD'] 249 | }) 250 | 251 | const exptectedResult: JSONSchema7Type = { 252 | $schema: 'http://json-schema.org/draft-07/schema#', 253 | type: 'object', 254 | properties: { 255 | foo: { 256 | type: 'string' 257 | }, 258 | bar: { 259 | $ref: '#/lol/xD/properties/foo' 260 | } 261 | }, 262 | required: ['foo', 'bar'], 263 | additionalProperties: false 264 | } 265 | 266 | assert(jsonSchema, exptectedResult) 267 | }) 268 | 269 | test('should be possible to override the base path with name', () => { 270 | const recurringSchema = z.string() 271 | const objectSchema = z.object({ 272 | foo: recurringSchema, 273 | bar: recurringSchema 274 | }) 275 | 276 | const jsonSchema = zodToJsonSchema(objectSchema, { 277 | basePath: ['#', 'lol', 'xD'], 278 | name: 'kex' 279 | }) 280 | 281 | const exptectedResult: JSONSchema7Type = { 282 | $schema: 'http://json-schema.org/draft-07/schema#', 283 | $ref: '#/lol/xD/definitions/kex', 284 | definitions: { 285 | kex: { 286 | type: 'object', 287 | properties: { 288 | foo: { 289 | type: 'string' 290 | }, 291 | bar: { 292 | $ref: '#/lol/xD/definitions/kex/properties/foo' 293 | } 294 | }, 295 | required: ['foo', 'bar'], 296 | additionalProperties: false 297 | } 298 | } 299 | } 300 | 301 | assert(jsonSchema, exptectedResult) 302 | }) 303 | 304 | test('should be possible to opt out of $ref building', () => { 305 | const recurringSchema = z.string() 306 | const objectSchema = z.object({ 307 | foo: recurringSchema, 308 | bar: recurringSchema 309 | }) 310 | 311 | const jsonSchema = zodToJsonSchema(objectSchema, { 312 | $refStrategy: 'none' 313 | }) 314 | 315 | const exptectedResult: JSONSchema7Type = { 316 | $schema: 'http://json-schema.org/draft-07/schema#', 317 | type: 'object', 318 | properties: { 319 | foo: { 320 | type: 'string' 321 | }, 322 | bar: { 323 | type: 'string' 324 | } 325 | }, 326 | required: ['foo', 'bar'], 327 | additionalProperties: false 328 | } 329 | 330 | assert(jsonSchema, exptectedResult) 331 | }) 332 | 333 | test('When opting out of ref building and using recursive schemas, should warn and default to any', () => { 334 | const was = console.warn 335 | let warning = '' 336 | console.warn = (x: any) => (warning = x) 337 | 338 | type Category = { 339 | name: string 340 | subcategories: Category[] 341 | } 342 | 343 | // cast to z.ZodSchema 344 | // @ts-ignore 345 | const categorySchema: z.ZodSchema = z.lazy(() => 346 | z.object({ 347 | name: z.string(), 348 | subcategories: z.array(categorySchema) 349 | }) 350 | ) 351 | 352 | const parsedSchema = zodToJsonSchema(categorySchema, { 353 | $refStrategy: 'none' 354 | }) 355 | 356 | const expectedJsonSchema = { 357 | $schema: 'http://json-schema.org/draft-07/schema#', 358 | type: 'object', 359 | properties: { 360 | name: { 361 | type: 'string' 362 | }, 363 | subcategories: { 364 | type: 'array', 365 | items: {} 366 | } 367 | }, 368 | required: ['name', 'subcategories'], 369 | additionalProperties: false 370 | } 371 | 372 | assert(parsedSchema, expectedJsonSchema) 373 | assert( 374 | warning, 375 | 'Recursive reference detected at #/properties/subcategories/items! Defaulting to any' 376 | ) 377 | 378 | console.warn = was 379 | }) 380 | 381 | test('should be possible to override get proper references even when picking optional definitions path $defs', () => { 382 | const recurringSchema = z.string() 383 | const objectSchema = z.object({ 384 | foo: recurringSchema, 385 | bar: recurringSchema 386 | }) 387 | 388 | const jsonSchema = zodToJsonSchema(objectSchema, { 389 | name: 'hello', 390 | definitionPath: '$defs' 391 | }) 392 | 393 | const exptectedResult = { 394 | $schema: 'http://json-schema.org/draft-07/schema#', 395 | $ref: '#/$defs/hello', 396 | $defs: { 397 | hello: { 398 | type: 'object', 399 | properties: { 400 | foo: { 401 | type: 'string' 402 | }, 403 | bar: { 404 | $ref: '#/$defs/hello/properties/foo' 405 | } 406 | }, 407 | required: ['foo', 'bar'], 408 | additionalProperties: false 409 | } 410 | } 411 | } 412 | 413 | assert(jsonSchema, exptectedResult) 414 | }) 415 | 416 | test('should be possible to override get proper references even when picking optional definitions path definitions', () => { 417 | const recurringSchema = z.string() 418 | const objectSchema = z.object({ 419 | foo: recurringSchema, 420 | bar: recurringSchema 421 | }) 422 | 423 | const jsonSchema = zodToJsonSchema(objectSchema, { 424 | name: 'hello', 425 | definitionPath: 'definitions' 426 | }) 427 | 428 | const exptectedResult = { 429 | $schema: 'http://json-schema.org/draft-07/schema#', 430 | $ref: '#/definitions/hello', 431 | definitions: { 432 | hello: { 433 | type: 'object', 434 | properties: { 435 | foo: { 436 | type: 'string' 437 | }, 438 | bar: { 439 | $ref: '#/definitions/hello/properties/foo' 440 | } 441 | }, 442 | required: ['foo', 'bar'], 443 | additionalProperties: false 444 | } 445 | } 446 | } 447 | 448 | assert(jsonSchema, exptectedResult) 449 | }) 450 | 451 | test('should preserve correct $ref when overriding name with string', () => { 452 | const recurringSchema = z.string() 453 | const objectSchema = z.object({ 454 | foo: recurringSchema, 455 | bar: recurringSchema 456 | }) 457 | 458 | const jsonSchema = zodToJsonSchema(objectSchema, 'hello') 459 | 460 | const exptectedResult = { 461 | $schema: 'http://json-schema.org/draft-07/schema#', 462 | $ref: '#/definitions/hello', 463 | definitions: { 464 | hello: { 465 | type: 'object', 466 | properties: { 467 | foo: { 468 | type: 'string' 469 | }, 470 | bar: { 471 | $ref: '#/definitions/hello/properties/foo' 472 | } 473 | }, 474 | required: ['foo', 'bar'], 475 | additionalProperties: false 476 | } 477 | } 478 | } 479 | 480 | assert(jsonSchema, exptectedResult) 481 | }) 482 | 483 | test('should preserve correct $ref when overriding name with object property', () => { 484 | const recurringSchema = z.string() 485 | const objectSchema = z.object({ 486 | foo: recurringSchema, 487 | bar: recurringSchema 488 | }) 489 | 490 | const jsonSchema = zodToJsonSchema(objectSchema, { name: 'hello' }) 491 | 492 | const exptectedResult = { 493 | $schema: 'http://json-schema.org/draft-07/schema#', 494 | $ref: '#/definitions/hello', 495 | definitions: { 496 | hello: { 497 | type: 'object', 498 | properties: { 499 | foo: { 500 | type: 'string' 501 | }, 502 | bar: { 503 | $ref: '#/definitions/hello/properties/foo' 504 | } 505 | }, 506 | required: ['foo', 'bar'], 507 | additionalProperties: false 508 | } 509 | } 510 | } 511 | 512 | assert(jsonSchema, exptectedResult) 513 | }) 514 | 515 | test('should be possible to preload a single definition', () => { 516 | const myRecurringSchema = z.string() 517 | const myObjectSchema = z.object({ 518 | a: myRecurringSchema, 519 | b: myRecurringSchema 520 | }) 521 | 522 | const myJsonSchema = zodToJsonSchema(myObjectSchema, { 523 | definitions: { myRecurringSchema } 524 | }) 525 | 526 | assert(myJsonSchema, { 527 | $schema: 'http://json-schema.org/draft-07/schema#', 528 | type: 'object', 529 | required: ['a', 'b'], 530 | properties: { 531 | a: { 532 | $ref: '#/definitions/myRecurringSchema' 533 | }, 534 | b: { 535 | $ref: '#/definitions/myRecurringSchema' 536 | } 537 | }, 538 | additionalProperties: false, 539 | definitions: { 540 | myRecurringSchema: { 541 | type: 'string' 542 | } 543 | } 544 | }) 545 | }) 546 | 547 | test('should be possible to preload multiple definitions', () => { 548 | const myRecurringSchema = z.string() 549 | const mySecondRecurringSchema = z.object({ 550 | x: myRecurringSchema 551 | }) 552 | const myObjectSchema = z.object({ 553 | a: myRecurringSchema, 554 | b: mySecondRecurringSchema, 555 | c: mySecondRecurringSchema 556 | }) 557 | 558 | const myJsonSchema = zodToJsonSchema(myObjectSchema, { 559 | definitions: { myRecurringSchema, mySecondRecurringSchema } 560 | }) 561 | 562 | assert(myJsonSchema, { 563 | $schema: 'http://json-schema.org/draft-07/schema#', 564 | type: 'object', 565 | required: ['a', 'b', 'c'], 566 | properties: { 567 | a: { 568 | $ref: '#/definitions/myRecurringSchema' 569 | }, 570 | b: { 571 | $ref: '#/definitions/mySecondRecurringSchema' 572 | }, 573 | c: { 574 | $ref: '#/definitions/mySecondRecurringSchema' 575 | } 576 | }, 577 | additionalProperties: false, 578 | definitions: { 579 | myRecurringSchema: { 580 | type: 'string' 581 | }, 582 | mySecondRecurringSchema: { 583 | type: 'object', 584 | required: ['x'], 585 | properties: { 586 | x: { 587 | $ref: '#/definitions/myRecurringSchema' 588 | } 589 | }, 590 | additionalProperties: false 591 | } 592 | } 593 | }) 594 | }) 595 | 596 | test('should be possible to preload multiple definitions and have a named schema', () => { 597 | const myRecurringSchema = z.string() 598 | const mySecondRecurringSchema = z.object({ 599 | x: myRecurringSchema 600 | }) 601 | const myObjectSchema = z.object({ 602 | a: myRecurringSchema, 603 | b: mySecondRecurringSchema, 604 | c: mySecondRecurringSchema 605 | }) 606 | 607 | const myJsonSchema = zodToJsonSchema(myObjectSchema, { 608 | definitions: { myRecurringSchema, mySecondRecurringSchema }, 609 | name: 'mySchemaName' 610 | }) 611 | 612 | assert(myJsonSchema, { 613 | $schema: 'http://json-schema.org/draft-07/schema#', 614 | $ref: '#/definitions/mySchemaName', 615 | definitions: { 616 | mySchemaName: { 617 | type: 'object', 618 | required: ['a', 'b', 'c'], 619 | properties: { 620 | a: { 621 | $ref: '#/definitions/myRecurringSchema' 622 | }, 623 | b: { 624 | $ref: '#/definitions/mySecondRecurringSchema' 625 | }, 626 | c: { 627 | $ref: '#/definitions/mySecondRecurringSchema' 628 | } 629 | }, 630 | additionalProperties: false 631 | }, 632 | myRecurringSchema: { 633 | type: 'string' 634 | }, 635 | mySecondRecurringSchema: { 636 | type: 'object', 637 | required: ['x'], 638 | properties: { 639 | x: { 640 | $ref: '#/definitions/myRecurringSchema' 641 | } 642 | }, 643 | additionalProperties: false 644 | } 645 | } 646 | }) 647 | }) 648 | 649 | test('should be possible to preload multiple definitions and have a named schema and set the definitions path', () => { 650 | const myRecurringSchema = z.string() 651 | const mySecondRecurringSchema = z.object({ 652 | x: myRecurringSchema 653 | }) 654 | const myObjectSchema = z.object({ 655 | a: myRecurringSchema, 656 | b: mySecondRecurringSchema, 657 | c: mySecondRecurringSchema 658 | }) 659 | 660 | const myJsonSchema = zodToJsonSchema(myObjectSchema, { 661 | definitions: { myRecurringSchema, mySecondRecurringSchema }, 662 | name: 'mySchemaName', 663 | definitionPath: '$defs' 664 | }) 665 | 666 | assert(myJsonSchema, { 667 | $schema: 'http://json-schema.org/draft-07/schema#', 668 | $ref: '#/$defs/mySchemaName', 669 | $defs: { 670 | mySchemaName: { 671 | type: 'object', 672 | required: ['a', 'b', 'c'], 673 | properties: { 674 | a: { 675 | $ref: '#/$defs/myRecurringSchema' 676 | }, 677 | b: { 678 | $ref: '#/$defs/mySecondRecurringSchema' 679 | }, 680 | c: { 681 | $ref: '#/$defs/mySecondRecurringSchema' 682 | } 683 | }, 684 | additionalProperties: false 685 | }, 686 | myRecurringSchema: { 687 | type: 'string' 688 | }, 689 | mySecondRecurringSchema: { 690 | type: 'object', 691 | required: ['x'], 692 | properties: { 693 | x: { 694 | $ref: '#/$defs/myRecurringSchema' 695 | } 696 | }, 697 | additionalProperties: false 698 | } 699 | } 700 | }) 701 | }) 702 | 703 | test('should be possible to preload a single definition with custom basePath', () => { 704 | const myRecurringSchema = z.string() 705 | const myObjectSchema = z.object({ 706 | a: myRecurringSchema, 707 | b: myRecurringSchema 708 | }) 709 | 710 | const myJsonSchema = zodToJsonSchema(myObjectSchema, { 711 | definitions: { myRecurringSchema }, 712 | basePath: ['hello'] 713 | }) 714 | 715 | assert(myJsonSchema, { 716 | $schema: 'http://json-schema.org/draft-07/schema#', 717 | type: 'object', 718 | required: ['a', 'b'], 719 | properties: { 720 | a: { 721 | $ref: 'hello/definitions/myRecurringSchema' 722 | }, 723 | b: { 724 | $ref: 'hello/definitions/myRecurringSchema' 725 | } 726 | }, 727 | additionalProperties: false, 728 | definitions: { 729 | myRecurringSchema: { 730 | type: 'string' 731 | } 732 | } 733 | }) 734 | }) 735 | 736 | test('should be possible to preload a single definition with custom basePath and name', () => { 737 | const myRecurringSchema = z.string() 738 | const myObjectSchema = z.object({ 739 | a: myRecurringSchema, 740 | b: myRecurringSchema 741 | }) 742 | 743 | const myJsonSchema = zodToJsonSchema(myObjectSchema, { 744 | definitions: { myRecurringSchema }, 745 | basePath: ['hello'], 746 | name: 'kex' 747 | }) 748 | 749 | assert(myJsonSchema, { 750 | $schema: 'http://json-schema.org/draft-07/schema#', 751 | $ref: 'hello/definitions/kex', 752 | definitions: { 753 | kex: { 754 | type: 'object', 755 | required: ['a', 'b'], 756 | properties: { 757 | a: { 758 | $ref: 'hello/definitions/myRecurringSchema' 759 | }, 760 | b: { 761 | $ref: 'hello/definitions/myRecurringSchema' 762 | } 763 | }, 764 | additionalProperties: false 765 | }, 766 | myRecurringSchema: { 767 | type: 'string' 768 | } 769 | } 770 | }) 771 | }) 772 | 773 | test('should be possible for a preloaded definition to circularly reference itself', () => { 774 | const myRecurringSchema: any = z.object({ 775 | circular: z.lazy(() => myRecurringSchema) 776 | }) 777 | 778 | const myObjectSchema = z.object({ 779 | a: myRecurringSchema, 780 | b: myRecurringSchema 781 | }) 782 | 783 | const myJsonSchema = zodToJsonSchema(myObjectSchema, { 784 | definitions: { myRecurringSchema }, 785 | basePath: ['hello'], 786 | name: 'kex' 787 | }) 788 | 789 | assert(myJsonSchema, { 790 | $schema: 'http://json-schema.org/draft-07/schema#', 791 | $ref: 'hello/definitions/kex', 792 | definitions: { 793 | kex: { 794 | type: 'object', 795 | required: ['a', 'b'], 796 | properties: { 797 | a: { 798 | $ref: 'hello/definitions/myRecurringSchema' 799 | }, 800 | b: { 801 | $ref: 'hello/definitions/myRecurringSchema' 802 | } 803 | }, 804 | additionalProperties: false 805 | }, 806 | myRecurringSchema: { 807 | type: 'object', 808 | required: ['circular'], 809 | properties: { 810 | circular: { 811 | $ref: 'hello/definitions/myRecurringSchema' 812 | } 813 | }, 814 | additionalProperties: false 815 | } 816 | } 817 | }) 818 | }) 819 | 820 | test('should handle the user example', () => { 821 | interface User { 822 | id: string 823 | headUser?: User 824 | } 825 | 826 | const userSchema: z.ZodType = z.lazy(() => 827 | z.object({ 828 | id: z.string(), 829 | headUser: userSchema.optional() 830 | }) 831 | ) 832 | 833 | const schema = z.object({ user: userSchema }) 834 | 835 | assert( 836 | zodToJsonSchema(schema, { 837 | definitions: { userSchema } 838 | }), 839 | { 840 | $schema: 'http://json-schema.org/draft-07/schema#', 841 | type: 'object', 842 | properties: { 843 | user: { 844 | $ref: '#/definitions/userSchema' 845 | } 846 | }, 847 | required: ['user'], 848 | additionalProperties: false, 849 | definitions: { 850 | userSchema: { 851 | type: 'object', 852 | properties: { 853 | id: { 854 | type: 'string' 855 | }, 856 | headUser: { 857 | $ref: '#/definitions/userSchema' 858 | } 859 | }, 860 | required: ['id'], 861 | additionalProperties: false 862 | } 863 | } 864 | } 865 | ) 866 | }) 867 | 868 | test('should handle mutual recursion', () => { 869 | const leafSchema = z.object({ 870 | prop: z.string() 871 | }) 872 | 873 | // eslint-disable-next-line prefer-const 874 | let nodeChildSchema: z.ZodType 875 | 876 | const nodeSchema = z.object({ 877 | children: z.lazy(() => z.array(nodeChildSchema)) 878 | }) 879 | 880 | nodeChildSchema = z.union([leafSchema, nodeSchema]) 881 | 882 | const treeSchema = z.object({ 883 | nodes: nodeSchema 884 | }) 885 | 886 | assert( 887 | zodToJsonSchema(treeSchema, { 888 | name: 'Tree', 889 | definitions: { 890 | Leaf: leafSchema, 891 | NodeChild: nodeChildSchema, 892 | Node: nodeSchema 893 | } 894 | }), 895 | { 896 | $ref: '#/definitions/Tree', 897 | definitions: { 898 | Leaf: { 899 | type: 'object', 900 | properties: { 901 | prop: { 902 | type: 'string' 903 | } 904 | }, 905 | required: ['prop'], 906 | additionalProperties: false 907 | }, 908 | Node: { 909 | type: 'object', 910 | properties: { 911 | children: { 912 | type: 'array', 913 | items: { 914 | $ref: '#/definitions/NodeChild' 915 | } 916 | } 917 | }, 918 | required: ['children'], 919 | additionalProperties: false 920 | }, 921 | NodeChild: { 922 | anyOf: [ 923 | { 924 | $ref: '#/definitions/Leaf' 925 | }, 926 | { 927 | $ref: '#/definitions/Node' 928 | } 929 | ] 930 | }, 931 | Tree: { 932 | type: 'object', 933 | properties: { 934 | nodes: { 935 | $ref: '#/definitions/Node' 936 | } 937 | }, 938 | required: ['nodes'], 939 | additionalProperties: false 940 | } 941 | }, 942 | $schema: 'http://json-schema.org/draft-07/schema#' 943 | } 944 | ) 945 | }) 946 | 947 | test('should not fail when definition is lazy', () => { 948 | const lazyString = z.lazy(() => z.string()) 949 | 950 | const lazyObject = z.lazy(() => z.object({ lazyProp: lazyString })) 951 | 952 | const jsonSchema = zodToJsonSchema(lazyObject, { 953 | definitions: { lazyString } 954 | }) 955 | 956 | const expected = { 957 | type: 'object', 958 | properties: { lazyProp: { $ref: '#/definitions/lazyString' } }, 959 | required: ['lazyProp'], 960 | additionalProperties: false, 961 | definitions: { lazyString: { type: 'string' } }, 962 | $schema: 'http://json-schema.org/draft-07/schema#' 963 | } 964 | 965 | assert(jsonSchema, expected) 966 | }) 967 | }) 968 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Stefan Terdell 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/Options.ts: -------------------------------------------------------------------------------- 1 | import { ZodSchema, ZodTypeDef } from 'zod'; 2 | import { Refs, Seen } from './Refs'; 3 | import { JsonSchema7Type } from './parseDef'; 4 | 5 | export type Targets = 'jsonSchema7' | 'jsonSchema2019-09' | 'openApi3'; 6 | 7 | export type DateStrategy = 'format:date-time' | 'format:date' | 'string' | 'integer'; 8 | 9 | export const ignoreOverride = Symbol('Let zodToJsonSchema decide on which parser to use'); 10 | 11 | export type Options = { 12 | name: string | undefined; 13 | $refStrategy: 'root' | 'relative' | 'none' | 'seen' | 'extract-to-root'; 14 | basePath: string[]; 15 | effectStrategy: 'input' | 'any'; 16 | pipeStrategy: 'input' | 'output' | 'all'; 17 | dateStrategy: DateStrategy | DateStrategy[]; 18 | mapStrategy: 'entries' | 'record'; 19 | removeAdditionalStrategy: 'passthrough' | 'strict'; 20 | nullableStrategy: 'from-target' | 'property'; 21 | target: Target; 22 | strictUnions: boolean; 23 | definitionPath: string; 24 | definitions: Record; 25 | errorMessages: boolean; 26 | markdownDescription: boolean; 27 | patternStrategy: 'escape' | 'preserve'; 28 | applyRegexFlags: boolean; 29 | emailStrategy: 'format:email' | 'format:idn-email' | 'pattern:zod'; 30 | base64Strategy: 'format:binary' | 'contentEncoding:base64' | 'pattern:zod'; 31 | nameStrategy: 'ref' | 'duplicate-ref' | 'title'; 32 | override?: ( 33 | def: ZodTypeDef, 34 | refs: Refs, 35 | seen: Seen | undefined, 36 | forceResolution?: boolean, 37 | ) => JsonSchema7Type | undefined | typeof ignoreOverride; 38 | openaiStrictMode?: boolean; 39 | }; 40 | 41 | const defaultOptions: Omit = { 42 | name: undefined, 43 | $refStrategy: 'root', 44 | effectStrategy: 'input', 45 | pipeStrategy: 'all', 46 | dateStrategy: 'format:date-time', 47 | mapStrategy: 'entries', 48 | nullableStrategy: 'from-target', 49 | removeAdditionalStrategy: 'passthrough', 50 | definitionPath: 'definitions', 51 | target: 'jsonSchema7', 52 | strictUnions: false, 53 | errorMessages: false, 54 | markdownDescription: false, 55 | patternStrategy: 'escape', 56 | applyRegexFlags: false, 57 | emailStrategy: 'format:email', 58 | base64Strategy: 'contentEncoding:base64', 59 | nameStrategy: 'ref', 60 | }; 61 | 62 | export const getDefaultOptions = ( 63 | options: Partial> | string | undefined, 64 | ) => { 65 | // We need to add `definitions` here as we may mutate it 66 | return ( 67 | typeof options === 'string' ? 68 | { 69 | ...defaultOptions, 70 | basePath: ['#'], 71 | definitions: {}, 72 | name: options, 73 | } 74 | : { 75 | ...defaultOptions, 76 | basePath: ['#'], 77 | definitions: {}, 78 | ...options, 79 | }) as Options; 80 | }; 81 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/README.md: -------------------------------------------------------------------------------- 1 | # Zod to Json Schema 2 | 3 | Vendored version of https://github.com/StefanTerdell/zod-to-json-schema that has been updated to generate JSON Schemas that are compatible with OpenAI's [strict mode](https://platform.openai.com/docs/guides/structured-outputs/supported-schemas) 4 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/Refs.ts: -------------------------------------------------------------------------------- 1 | import type { ZodTypeDef } from 'zod'; 2 | import { getDefaultOptions, Options, Targets } from './Options'; 3 | import { JsonSchema7Type } from './parseDef'; 4 | import { zodDef } from './util'; 5 | 6 | export type Refs = { 7 | seen: Map; 8 | /** 9 | * Set of all the `$ref`s we created, e.g. `Set(['#/$defs/ui'])` 10 | * this notable does not include any `definitions` that were 11 | * explicitly given as an option. 12 | */ 13 | seenRefs: Set; 14 | currentPath: string[]; 15 | propertyPath: string[] | undefined; 16 | } & Options; 17 | 18 | export type Seen = { 19 | def: ZodTypeDef; 20 | path: string[]; 21 | jsonSchema: JsonSchema7Type | undefined; 22 | }; 23 | 24 | export const getRefs = (options?: string | Partial>): Refs => { 25 | const _options = getDefaultOptions(options); 26 | const currentPath = 27 | _options.name !== undefined ? 28 | [..._options.basePath, _options.definitionPath, _options.name] 29 | : _options.basePath; 30 | return { 31 | ..._options, 32 | currentPath: currentPath, 33 | propertyPath: undefined, 34 | seenRefs: new Set(), 35 | seen: new Map( 36 | Object.entries(_options.definitions).map(([name, def]) => [ 37 | zodDef(def), 38 | { 39 | def: zodDef(def), 40 | path: [..._options.basePath, _options.definitionPath, name], 41 | // Resolution of references will be forced even though seen, so it's ok that the schema is undefined here for now. 42 | jsonSchema: undefined, 43 | }, 44 | ]), 45 | ), 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/errorMessages.ts: -------------------------------------------------------------------------------- 1 | import { JsonSchema7TypeUnion } from './parseDef'; 2 | import { Refs } from './Refs'; 3 | 4 | export type ErrorMessages = Partial< 5 | Omit<{ [key in keyof T]: string }, OmitProperties | 'type' | 'errorMessages'> 6 | >; 7 | 8 | export function addErrorMessage }>( 9 | res: T, 10 | key: keyof T, 11 | errorMessage: string | undefined, 12 | refs: Refs, 13 | ) { 14 | if (!refs?.errorMessages) return; 15 | if (errorMessage) { 16 | res.errorMessage = { 17 | ...res.errorMessage, 18 | [key]: errorMessage, 19 | }; 20 | } 21 | } 22 | 23 | export function setResponseValueAndErrors< 24 | Json7Type extends JsonSchema7TypeUnion & { 25 | errorMessage?: ErrorMessages; 26 | }, 27 | Key extends keyof Omit, 28 | >(res: Json7Type, key: Key, value: Json7Type[Key], errorMessage: string | undefined, refs: Refs) { 29 | res[key] = value; 30 | addErrorMessage(res, key, errorMessage, refs); 31 | } 32 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Options'; 2 | export * from './Refs'; 3 | export * from './errorMessages'; 4 | export * from './parseDef'; 5 | export * from './parsers/any'; 6 | export * from './parsers/array'; 7 | export * from './parsers/bigint'; 8 | export * from './parsers/boolean'; 9 | export * from './parsers/branded'; 10 | export * from './parsers/catch'; 11 | export * from './parsers/date'; 12 | export * from './parsers/default'; 13 | export * from './parsers/effects'; 14 | export * from './parsers/enum'; 15 | export * from './parsers/intersection'; 16 | export * from './parsers/literal'; 17 | export * from './parsers/map'; 18 | export * from './parsers/nativeEnum'; 19 | export * from './parsers/never'; 20 | export * from './parsers/null'; 21 | export * from './parsers/nullable'; 22 | export * from './parsers/number'; 23 | export * from './parsers/object'; 24 | export * from './parsers/optional'; 25 | export * from './parsers/pipeline'; 26 | export * from './parsers/promise'; 27 | export * from './parsers/readonly'; 28 | export * from './parsers/record'; 29 | export * from './parsers/set'; 30 | export * from './parsers/string'; 31 | export * from './parsers/tuple'; 32 | export * from './parsers/undefined'; 33 | export * from './parsers/union'; 34 | export * from './parsers/unknown'; 35 | export * from './zodToJsonSchema'; 36 | import { zodToJsonSchema } from './zodToJsonSchema'; 37 | export default zodToJsonSchema; 38 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parseDef.ts: -------------------------------------------------------------------------------- 1 | import { ZodFirstPartyTypeKind, ZodTypeDef } from 'zod'; 2 | import { JsonSchema7AnyType, parseAnyDef } from './parsers/any'; 3 | import { JsonSchema7ArrayType, parseArrayDef } from './parsers/array'; 4 | import { JsonSchema7BigintType, parseBigintDef } from './parsers/bigint'; 5 | import { JsonSchema7BooleanType, parseBooleanDef } from './parsers/boolean'; 6 | import { parseBrandedDef } from './parsers/branded'; 7 | import { parseCatchDef } from './parsers/catch'; 8 | import { JsonSchema7DateType, parseDateDef } from './parsers/date'; 9 | import { parseDefaultDef } from './parsers/default'; 10 | import { parseEffectsDef } from './parsers/effects'; 11 | import { JsonSchema7EnumType, parseEnumDef } from './parsers/enum'; 12 | import { JsonSchema7AllOfType, parseIntersectionDef } from './parsers/intersection'; 13 | import { JsonSchema7LiteralType, parseLiteralDef } from './parsers/literal'; 14 | import { JsonSchema7MapType, parseMapDef } from './parsers/map'; 15 | import { JsonSchema7NativeEnumType, parseNativeEnumDef } from './parsers/nativeEnum'; 16 | import { JsonSchema7NeverType, parseNeverDef } from './parsers/never'; 17 | import { JsonSchema7NullType, parseNullDef } from './parsers/null'; 18 | import { JsonSchema7NullableType, parseNullableDef } from './parsers/nullable'; 19 | import { JsonSchema7NumberType, parseNumberDef } from './parsers/number'; 20 | import { JsonSchema7ObjectType, parseObjectDef } from './parsers/object'; 21 | import { parseOptionalDef } from './parsers/optional'; 22 | import { parsePipelineDef } from './parsers/pipeline'; 23 | import { parsePromiseDef } from './parsers/promise'; 24 | import { JsonSchema7RecordType, parseRecordDef } from './parsers/record'; 25 | import { JsonSchema7SetType, parseSetDef } from './parsers/set'; 26 | import { JsonSchema7StringType, parseStringDef } from './parsers/string'; 27 | import { JsonSchema7TupleType, parseTupleDef } from './parsers/tuple'; 28 | import { JsonSchema7UndefinedType, parseUndefinedDef } from './parsers/undefined'; 29 | import { JsonSchema7UnionType, parseUnionDef } from './parsers/union'; 30 | import { JsonSchema7UnknownType, parseUnknownDef } from './parsers/unknown'; 31 | import { Refs, Seen } from './Refs'; 32 | import { parseReadonlyDef } from './parsers/readonly'; 33 | import { ignoreOverride } from './Options'; 34 | 35 | type JsonSchema7RefType = { $ref: string }; 36 | type JsonSchema7Meta = { 37 | title?: string; 38 | default?: any; 39 | description?: string; 40 | markdownDescription?: string; 41 | }; 42 | 43 | export type JsonSchema7TypeUnion = 44 | | JsonSchema7StringType 45 | | JsonSchema7ArrayType 46 | | JsonSchema7NumberType 47 | | JsonSchema7BigintType 48 | | JsonSchema7BooleanType 49 | | JsonSchema7DateType 50 | | JsonSchema7EnumType 51 | | JsonSchema7LiteralType 52 | | JsonSchema7NativeEnumType 53 | | JsonSchema7NullType 54 | | JsonSchema7NumberType 55 | | JsonSchema7ObjectType 56 | | JsonSchema7RecordType 57 | | JsonSchema7TupleType 58 | | JsonSchema7UnionType 59 | | JsonSchema7UndefinedType 60 | | JsonSchema7RefType 61 | | JsonSchema7NeverType 62 | | JsonSchema7MapType 63 | | JsonSchema7AnyType 64 | | JsonSchema7NullableType 65 | | JsonSchema7AllOfType 66 | | JsonSchema7UnknownType 67 | | JsonSchema7SetType; 68 | 69 | export type JsonSchema7Type = JsonSchema7TypeUnion & JsonSchema7Meta; 70 | 71 | export function parseDef( 72 | def: ZodTypeDef, 73 | refs: Refs, 74 | forceResolution = false, // Forces a new schema to be instantiated even though its def has been seen. Used for improving refs in definitions. See https://github.com/StefanTerdell/zod-to-json-schema/pull/61. 75 | ): JsonSchema7Type | undefined { 76 | const seenItem = refs.seen.get(def); 77 | 78 | if (refs.override) { 79 | const overrideResult = refs.override?.(def, refs, seenItem, forceResolution); 80 | 81 | if (overrideResult !== ignoreOverride) { 82 | return overrideResult; 83 | } 84 | } 85 | 86 | if (seenItem && !forceResolution) { 87 | const seenSchema = get$ref(seenItem, refs); 88 | 89 | if (seenSchema !== undefined) { 90 | if ('$ref' in seenSchema) { 91 | refs.seenRefs.add(seenSchema.$ref); 92 | } 93 | 94 | return seenSchema; 95 | } 96 | } 97 | 98 | const newItem: Seen = { def, path: refs.currentPath, jsonSchema: undefined }; 99 | 100 | refs.seen.set(def, newItem); 101 | 102 | const jsonSchema = selectParser(def, (def as any).typeName, refs, forceResolution); 103 | 104 | if (jsonSchema) { 105 | addMeta(def, refs, jsonSchema); 106 | } 107 | 108 | newItem.jsonSchema = jsonSchema; 109 | 110 | return jsonSchema; 111 | } 112 | 113 | const get$ref = ( 114 | item: Seen, 115 | refs: Refs, 116 | ): 117 | | { 118 | $ref: string; 119 | } 120 | | {} 121 | | undefined => { 122 | switch (refs.$refStrategy) { 123 | case 'root': 124 | return { $ref: item.path.join('/') }; 125 | // this case is needed as OpenAI strict mode doesn't support top-level `$ref`s, i.e. 126 | // the top-level schema *must* be `{"type": "object", "properties": {...}}` but if we ever 127 | // need to define a `$ref`, relative `$ref`s aren't supported, so we need to extract 128 | // the schema to `#/definitions/` and reference that. 129 | // 130 | // e.g. if we need to reference a schema at 131 | // `["#","definitions","contactPerson","properties","person1","properties","name"]` 132 | // then we'll extract it out to `contactPerson_properties_person1_properties_name` 133 | case 'extract-to-root': 134 | const name = item.path.slice(refs.basePath.length + 1).join('_'); 135 | 136 | // we don't need to extract the root schema in this case, as it's already 137 | // been added to the definitions 138 | if (name !== refs.name && refs.nameStrategy === 'duplicate-ref') { 139 | refs.definitions[name] = item.def; 140 | } 141 | 142 | return { $ref: [...refs.basePath, refs.definitionPath, name].join('/') }; 143 | case 'relative': 144 | return { $ref: getRelativePath(refs.currentPath, item.path) }; 145 | case 'none': 146 | case 'seen': { 147 | if ( 148 | item.path.length < refs.currentPath.length && 149 | item.path.every((value, index) => refs.currentPath[index] === value) 150 | ) { 151 | console.warn(`Recursive reference detected at ${refs.currentPath.join('/')}! Defaulting to any`); 152 | 153 | return {}; 154 | } 155 | 156 | return refs.$refStrategy === 'seen' ? {} : undefined; 157 | } 158 | } 159 | }; 160 | 161 | const getRelativePath = (pathA: string[], pathB: string[]) => { 162 | let i = 0; 163 | for (; i < pathA.length && i < pathB.length; i++) { 164 | if (pathA[i] !== pathB[i]) break; 165 | } 166 | return [(pathA.length - i).toString(), ...pathB.slice(i)].join('/'); 167 | }; 168 | 169 | const selectParser = ( 170 | def: any, 171 | typeName: ZodFirstPartyTypeKind, 172 | refs: Refs, 173 | forceResolution: boolean, 174 | ): JsonSchema7Type | undefined => { 175 | switch (typeName) { 176 | case ZodFirstPartyTypeKind.ZodString: 177 | return parseStringDef(def, refs); 178 | case ZodFirstPartyTypeKind.ZodNumber: 179 | return parseNumberDef(def, refs); 180 | case ZodFirstPartyTypeKind.ZodObject: 181 | return parseObjectDef(def, refs); 182 | case ZodFirstPartyTypeKind.ZodBigInt: 183 | return parseBigintDef(def, refs); 184 | case ZodFirstPartyTypeKind.ZodBoolean: 185 | return parseBooleanDef(); 186 | case ZodFirstPartyTypeKind.ZodDate: 187 | return parseDateDef(def, refs); 188 | case ZodFirstPartyTypeKind.ZodUndefined: 189 | return parseUndefinedDef(); 190 | case ZodFirstPartyTypeKind.ZodNull: 191 | return parseNullDef(refs); 192 | case ZodFirstPartyTypeKind.ZodArray: 193 | return parseArrayDef(def, refs); 194 | case ZodFirstPartyTypeKind.ZodUnion: 195 | case ZodFirstPartyTypeKind.ZodDiscriminatedUnion: 196 | return parseUnionDef(def, refs); 197 | case ZodFirstPartyTypeKind.ZodIntersection: 198 | return parseIntersectionDef(def, refs); 199 | case ZodFirstPartyTypeKind.ZodTuple: 200 | return parseTupleDef(def, refs); 201 | case ZodFirstPartyTypeKind.ZodRecord: 202 | return parseRecordDef(def, refs); 203 | case ZodFirstPartyTypeKind.ZodLiteral: 204 | return parseLiteralDef(def, refs); 205 | case ZodFirstPartyTypeKind.ZodEnum: 206 | return parseEnumDef(def); 207 | case ZodFirstPartyTypeKind.ZodNativeEnum: 208 | return parseNativeEnumDef(def); 209 | case ZodFirstPartyTypeKind.ZodNullable: 210 | return parseNullableDef(def, refs); 211 | case ZodFirstPartyTypeKind.ZodOptional: 212 | return parseOptionalDef(def, refs); 213 | case ZodFirstPartyTypeKind.ZodMap: 214 | return parseMapDef(def, refs); 215 | case ZodFirstPartyTypeKind.ZodSet: 216 | return parseSetDef(def, refs); 217 | case ZodFirstPartyTypeKind.ZodLazy: 218 | return parseDef(def.getter()._def, refs); 219 | case ZodFirstPartyTypeKind.ZodPromise: 220 | return parsePromiseDef(def, refs); 221 | case ZodFirstPartyTypeKind.ZodNaN: 222 | case ZodFirstPartyTypeKind.ZodNever: 223 | return parseNeverDef(); 224 | case ZodFirstPartyTypeKind.ZodEffects: 225 | return parseEffectsDef(def, refs, forceResolution); 226 | case ZodFirstPartyTypeKind.ZodAny: 227 | return parseAnyDef(); 228 | case ZodFirstPartyTypeKind.ZodUnknown: 229 | return parseUnknownDef(); 230 | case ZodFirstPartyTypeKind.ZodDefault: 231 | return parseDefaultDef(def, refs); 232 | case ZodFirstPartyTypeKind.ZodBranded: 233 | return parseBrandedDef(def, refs); 234 | case ZodFirstPartyTypeKind.ZodReadonly: 235 | return parseReadonlyDef(def, refs); 236 | case ZodFirstPartyTypeKind.ZodCatch: 237 | return parseCatchDef(def, refs); 238 | case ZodFirstPartyTypeKind.ZodPipeline: 239 | return parsePipelineDef(def, refs); 240 | case ZodFirstPartyTypeKind.ZodFunction: 241 | case ZodFirstPartyTypeKind.ZodVoid: 242 | case ZodFirstPartyTypeKind.ZodSymbol: 243 | return undefined; 244 | default: 245 | return ((_: never) => undefined)(typeName); 246 | } 247 | }; 248 | 249 | const addMeta = (def: ZodTypeDef, refs: Refs, jsonSchema: JsonSchema7Type): JsonSchema7Type => { 250 | if (def.description) { 251 | jsonSchema.description = def.description; 252 | 253 | if (refs.markdownDescription) { 254 | jsonSchema.markdownDescription = def.description; 255 | } 256 | } 257 | return jsonSchema; 258 | }; 259 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/any.ts: -------------------------------------------------------------------------------- 1 | export type JsonSchema7AnyType = {}; 2 | 3 | export function parseAnyDef(): JsonSchema7AnyType { 4 | return {}; 5 | } 6 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/array.ts: -------------------------------------------------------------------------------- 1 | import { ZodArrayDef, ZodFirstPartyTypeKind } from 'zod'; 2 | import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages'; 3 | import { JsonSchema7Type, parseDef } from '../parseDef'; 4 | import { Refs } from '../Refs'; 5 | 6 | export type JsonSchema7ArrayType = { 7 | type: 'array'; 8 | items?: JsonSchema7Type | undefined; 9 | minItems?: number; 10 | maxItems?: number; 11 | errorMessages?: ErrorMessages; 12 | }; 13 | 14 | export function parseArrayDef(def: ZodArrayDef, refs: Refs) { 15 | const res: JsonSchema7ArrayType = { 16 | type: 'array', 17 | }; 18 | if (def.type?._def?.typeName !== ZodFirstPartyTypeKind.ZodAny) { 19 | res.items = parseDef(def.type._def, { 20 | ...refs, 21 | currentPath: [...refs.currentPath, 'items'], 22 | }); 23 | } 24 | 25 | if (def.minLength) { 26 | setResponseValueAndErrors(res, 'minItems', def.minLength.value, def.minLength.message, refs); 27 | } 28 | if (def.maxLength) { 29 | setResponseValueAndErrors(res, 'maxItems', def.maxLength.value, def.maxLength.message, refs); 30 | } 31 | if (def.exactLength) { 32 | setResponseValueAndErrors(res, 'minItems', def.exactLength.value, def.exactLength.message, refs); 33 | setResponseValueAndErrors(res, 'maxItems', def.exactLength.value, def.exactLength.message, refs); 34 | } 35 | return res; 36 | } 37 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/bigint.ts: -------------------------------------------------------------------------------- 1 | import { ZodBigIntDef } from 'zod'; 2 | import { Refs } from '../Refs'; 3 | import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages'; 4 | 5 | export type JsonSchema7BigintType = { 6 | type: 'integer'; 7 | format: 'int64'; 8 | minimum?: BigInt; 9 | exclusiveMinimum?: BigInt; 10 | maximum?: BigInt; 11 | exclusiveMaximum?: BigInt; 12 | multipleOf?: BigInt; 13 | errorMessage?: ErrorMessages; 14 | }; 15 | 16 | export function parseBigintDef(def: ZodBigIntDef, refs: Refs): JsonSchema7BigintType { 17 | const res: JsonSchema7BigintType = { 18 | type: 'integer', 19 | format: 'int64', 20 | }; 21 | 22 | if (!def.checks) return res; 23 | 24 | for (const check of def.checks) { 25 | switch (check.kind) { 26 | case 'min': 27 | if (refs.target === 'jsonSchema7') { 28 | if (check.inclusive) { 29 | setResponseValueAndErrors(res, 'minimum', check.value, check.message, refs); 30 | } else { 31 | setResponseValueAndErrors(res, 'exclusiveMinimum', check.value, check.message, refs); 32 | } 33 | } else { 34 | if (!check.inclusive) { 35 | res.exclusiveMinimum = true as any; 36 | } 37 | setResponseValueAndErrors(res, 'minimum', check.value, check.message, refs); 38 | } 39 | break; 40 | case 'max': 41 | if (refs.target === 'jsonSchema7') { 42 | if (check.inclusive) { 43 | setResponseValueAndErrors(res, 'maximum', check.value, check.message, refs); 44 | } else { 45 | setResponseValueAndErrors(res, 'exclusiveMaximum', check.value, check.message, refs); 46 | } 47 | } else { 48 | if (!check.inclusive) { 49 | res.exclusiveMaximum = true as any; 50 | } 51 | setResponseValueAndErrors(res, 'maximum', check.value, check.message, refs); 52 | } 53 | break; 54 | case 'multipleOf': 55 | setResponseValueAndErrors(res, 'multipleOf', check.value, check.message, refs); 56 | break; 57 | } 58 | } 59 | return res; 60 | } 61 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/boolean.ts: -------------------------------------------------------------------------------- 1 | export type JsonSchema7BooleanType = { 2 | type: 'boolean'; 3 | }; 4 | 5 | export function parseBooleanDef(): JsonSchema7BooleanType { 6 | return { 7 | type: 'boolean', 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/branded.ts: -------------------------------------------------------------------------------- 1 | import { ZodBrandedDef } from 'zod'; 2 | import { parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export function parseBrandedDef(_def: ZodBrandedDef, refs: Refs) { 6 | return parseDef(_def.type._def, refs); 7 | } 8 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/catch.ts: -------------------------------------------------------------------------------- 1 | import { ZodCatchDef } from 'zod'; 2 | import { parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export const parseCatchDef = (def: ZodCatchDef, refs: Refs) => { 6 | return parseDef(def.innerType._def, refs); 7 | }; 8 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/date.ts: -------------------------------------------------------------------------------- 1 | import { ZodDateDef } from 'zod'; 2 | import { Refs } from '../Refs'; 3 | import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages'; 4 | import { JsonSchema7NumberType } from './number'; 5 | import { DateStrategy } from '../Options'; 6 | 7 | export type JsonSchema7DateType = 8 | | { 9 | type: 'integer' | 'string'; 10 | format: 'unix-time' | 'date-time' | 'date'; 11 | minimum?: number; 12 | maximum?: number; 13 | errorMessage?: ErrorMessages; 14 | } 15 | | { 16 | anyOf: JsonSchema7DateType[]; 17 | }; 18 | 19 | export function parseDateDef( 20 | def: ZodDateDef, 21 | refs: Refs, 22 | overrideDateStrategy?: DateStrategy, 23 | ): JsonSchema7DateType { 24 | const strategy = overrideDateStrategy ?? refs.dateStrategy; 25 | 26 | if (Array.isArray(strategy)) { 27 | return { 28 | anyOf: strategy.map((item, i) => parseDateDef(def, refs, item)), 29 | }; 30 | } 31 | 32 | switch (strategy) { 33 | case 'string': 34 | case 'format:date-time': 35 | return { 36 | type: 'string', 37 | format: 'date-time', 38 | }; 39 | case 'format:date': 40 | return { 41 | type: 'string', 42 | format: 'date', 43 | }; 44 | case 'integer': 45 | return integerDateParser(def, refs); 46 | } 47 | } 48 | 49 | const integerDateParser = (def: ZodDateDef, refs: Refs) => { 50 | const res: JsonSchema7DateType = { 51 | type: 'integer', 52 | format: 'unix-time', 53 | }; 54 | 55 | if (refs.target === 'openApi3') { 56 | return res; 57 | } 58 | 59 | for (const check of def.checks) { 60 | switch (check.kind) { 61 | case 'min': 62 | setResponseValueAndErrors( 63 | res, 64 | 'minimum', 65 | check.value, // This is in milliseconds 66 | check.message, 67 | refs, 68 | ); 69 | break; 70 | case 'max': 71 | setResponseValueAndErrors( 72 | res, 73 | 'maximum', 74 | check.value, // This is in milliseconds 75 | check.message, 76 | refs, 77 | ); 78 | break; 79 | } 80 | } 81 | 82 | return res; 83 | }; 84 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/default.ts: -------------------------------------------------------------------------------- 1 | import { ZodDefaultDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export function parseDefaultDef(_def: ZodDefaultDef, refs: Refs): JsonSchema7Type & { default: any } { 6 | return { 7 | ...parseDef(_def.innerType._def, refs), 8 | default: _def.defaultValue(), 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/effects.ts: -------------------------------------------------------------------------------- 1 | import { ZodEffectsDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export function parseEffectsDef( 6 | _def: ZodEffectsDef, 7 | refs: Refs, 8 | forceResolution: boolean, 9 | ): JsonSchema7Type | undefined { 10 | return refs.effectStrategy === 'input' ? parseDef(_def.schema._def, refs, forceResolution) : {}; 11 | } 12 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/enum.ts: -------------------------------------------------------------------------------- 1 | import { ZodEnumDef } from 'zod'; 2 | 3 | export type JsonSchema7EnumType = { 4 | type: 'string'; 5 | enum: string[]; 6 | }; 7 | 8 | export function parseEnumDef(def: ZodEnumDef): JsonSchema7EnumType { 9 | return { 10 | type: 'string', 11 | enum: [...def.values], 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/intersection.ts: -------------------------------------------------------------------------------- 1 | import { ZodIntersectionDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | import { JsonSchema7StringType } from './string'; 5 | 6 | export type JsonSchema7AllOfType = { 7 | allOf: JsonSchema7Type[]; 8 | unevaluatedProperties?: boolean; 9 | }; 10 | 11 | const isJsonSchema7AllOfType = ( 12 | type: JsonSchema7Type | JsonSchema7StringType, 13 | ): type is JsonSchema7AllOfType => { 14 | if ('type' in type && type.type === 'string') return false; 15 | return 'allOf' in type; 16 | }; 17 | 18 | export function parseIntersectionDef( 19 | def: ZodIntersectionDef, 20 | refs: Refs, 21 | ): JsonSchema7AllOfType | JsonSchema7Type | undefined { 22 | const allOf = [ 23 | parseDef(def.left._def, { 24 | ...refs, 25 | currentPath: [...refs.currentPath, 'allOf', '0'], 26 | }), 27 | parseDef(def.right._def, { 28 | ...refs, 29 | currentPath: [...refs.currentPath, 'allOf', '1'], 30 | }), 31 | ].filter((x): x is JsonSchema7Type => !!x); 32 | 33 | let unevaluatedProperties: Pick | undefined = 34 | refs.target === 'jsonSchema2019-09' ? { unevaluatedProperties: false } : undefined; 35 | 36 | const mergedAllOf: JsonSchema7Type[] = []; 37 | // If either of the schemas is an allOf, merge them into a single allOf 38 | allOf.forEach((schema) => { 39 | if (isJsonSchema7AllOfType(schema)) { 40 | mergedAllOf.push(...schema.allOf); 41 | if (schema.unevaluatedProperties === undefined) { 42 | // If one of the schemas has no unevaluatedProperties set, 43 | // the merged schema should also have no unevaluatedProperties set 44 | unevaluatedProperties = undefined; 45 | } 46 | } else { 47 | let nestedSchema: JsonSchema7Type = schema; 48 | if ('additionalProperties' in schema && schema.additionalProperties === false) { 49 | const { additionalProperties, ...rest } = schema; 50 | nestedSchema = rest; 51 | } else { 52 | // As soon as one of the schemas has additionalProperties set not to false, we allow unevaluatedProperties 53 | unevaluatedProperties = undefined; 54 | } 55 | mergedAllOf.push(nestedSchema); 56 | } 57 | }); 58 | return mergedAllOf.length ? 59 | { 60 | allOf: mergedAllOf, 61 | ...unevaluatedProperties, 62 | } 63 | : undefined; 64 | } 65 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/literal.ts: -------------------------------------------------------------------------------- 1 | import { ZodLiteralDef } from 'zod'; 2 | import { Refs } from '../Refs'; 3 | 4 | export type JsonSchema7LiteralType = 5 | | { 6 | type: 'string' | 'number' | 'integer' | 'boolean'; 7 | const: string | number | boolean; 8 | } 9 | | { 10 | type: 'object' | 'array'; 11 | }; 12 | 13 | export function parseLiteralDef(def: ZodLiteralDef, refs: Refs): JsonSchema7LiteralType { 14 | const parsedType = typeof def.value; 15 | if ( 16 | parsedType !== 'bigint' && 17 | parsedType !== 'number' && 18 | parsedType !== 'boolean' && 19 | parsedType !== 'string' 20 | ) { 21 | return { 22 | type: Array.isArray(def.value) ? 'array' : 'object', 23 | }; 24 | } 25 | 26 | if (refs.target === 'openApi3') { 27 | return { 28 | type: parsedType === 'bigint' ? 'integer' : parsedType, 29 | enum: [def.value], 30 | } as any; 31 | } 32 | 33 | return { 34 | type: parsedType === 'bigint' ? 'integer' : parsedType, 35 | const: def.value, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/map.ts: -------------------------------------------------------------------------------- 1 | import { ZodMapDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | import { JsonSchema7RecordType, parseRecordDef } from './record'; 5 | 6 | export type JsonSchema7MapType = { 7 | type: 'array'; 8 | maxItems: 125; 9 | items: { 10 | type: 'array'; 11 | items: [JsonSchema7Type, JsonSchema7Type]; 12 | minItems: 2; 13 | maxItems: 2; 14 | }; 15 | }; 16 | 17 | export function parseMapDef(def: ZodMapDef, refs: Refs): JsonSchema7MapType | JsonSchema7RecordType { 18 | if (refs.mapStrategy === 'record') { 19 | return parseRecordDef(def, refs); 20 | } 21 | 22 | const keys = 23 | parseDef(def.keyType._def, { 24 | ...refs, 25 | currentPath: [...refs.currentPath, 'items', 'items', '0'], 26 | }) || {}; 27 | const values = 28 | parseDef(def.valueType._def, { 29 | ...refs, 30 | currentPath: [...refs.currentPath, 'items', 'items', '1'], 31 | }) || {}; 32 | return { 33 | type: 'array', 34 | maxItems: 125, 35 | items: { 36 | type: 'array', 37 | items: [keys, values], 38 | minItems: 2, 39 | maxItems: 2, 40 | }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/nativeEnum.ts: -------------------------------------------------------------------------------- 1 | import { ZodNativeEnumDef } from 'zod'; 2 | 3 | export type JsonSchema7NativeEnumType = { 4 | type: 'string' | 'number' | ['string', 'number']; 5 | enum: (string | number)[]; 6 | }; 7 | 8 | export function parseNativeEnumDef(def: ZodNativeEnumDef): JsonSchema7NativeEnumType { 9 | const object = def.values; 10 | const actualKeys = Object.keys(def.values).filter((key: string) => { 11 | return typeof object[object[key]!] !== 'number'; 12 | }); 13 | 14 | const actualValues = actualKeys.map((key: string) => object[key]!); 15 | 16 | const parsedTypes = Array.from(new Set(actualValues.map((values: string | number) => typeof values))); 17 | 18 | return { 19 | type: 20 | parsedTypes.length === 1 ? 21 | parsedTypes[0] === 'string' ? 22 | 'string' 23 | : 'number' 24 | : ['string', 'number'], 25 | enum: actualValues, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/never.ts: -------------------------------------------------------------------------------- 1 | export type JsonSchema7NeverType = { 2 | not: {}; 3 | }; 4 | 5 | export function parseNeverDef(): JsonSchema7NeverType { 6 | return { 7 | not: {}, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/null.ts: -------------------------------------------------------------------------------- 1 | import { Refs } from '../Refs'; 2 | 3 | export type JsonSchema7NullType = { 4 | type: 'null'; 5 | }; 6 | 7 | export function parseNullDef(refs: Refs): JsonSchema7NullType { 8 | return refs.target === 'openApi3' ? 9 | ({ 10 | enum: ['null'], 11 | nullable: true, 12 | } as any) 13 | : { 14 | type: 'null', 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/nullable.ts: -------------------------------------------------------------------------------- 1 | import { ZodNullableDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | import { JsonSchema7NullType } from './null'; 5 | import { primitiveMappings } from './union'; 6 | 7 | export type JsonSchema7NullableType = 8 | | { 9 | anyOf: [JsonSchema7Type, JsonSchema7NullType]; 10 | } 11 | | { 12 | type: [string, 'null']; 13 | }; 14 | 15 | export function parseNullableDef(def: ZodNullableDef, refs: Refs): JsonSchema7NullableType | undefined { 16 | if ( 17 | ['ZodString', 'ZodNumber', 'ZodBigInt', 'ZodBoolean', 'ZodNull'].includes(def.innerType._def.typeName) && 18 | (!def.innerType._def.checks || !def.innerType._def.checks.length) 19 | ) { 20 | if (refs.target === 'openApi3' || refs.nullableStrategy === 'property') { 21 | return { 22 | type: primitiveMappings[def.innerType._def.typeName as keyof typeof primitiveMappings], 23 | nullable: true, 24 | } as any; 25 | } 26 | 27 | return { 28 | type: [primitiveMappings[def.innerType._def.typeName as keyof typeof primitiveMappings], 'null'], 29 | }; 30 | } 31 | 32 | if (refs.target === 'openApi3') { 33 | const base = parseDef(def.innerType._def, { 34 | ...refs, 35 | currentPath: [...refs.currentPath], 36 | }); 37 | 38 | if (base && '$ref' in base) return { allOf: [base], nullable: true } as any; 39 | 40 | return base && ({ ...base, nullable: true } as any); 41 | } 42 | 43 | const base = parseDef(def.innerType._def, { 44 | ...refs, 45 | currentPath: [...refs.currentPath, 'anyOf', '0'], 46 | }); 47 | 48 | return base && { anyOf: [base, { type: 'null' }] }; 49 | } 50 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/number.ts: -------------------------------------------------------------------------------- 1 | import { ZodNumberDef } from 'zod'; 2 | import { addErrorMessage, ErrorMessages, setResponseValueAndErrors } from '../errorMessages'; 3 | import { Refs } from '../Refs'; 4 | 5 | export type JsonSchema7NumberType = { 6 | type: 'number' | 'integer'; 7 | minimum?: number; 8 | exclusiveMinimum?: number; 9 | maximum?: number; 10 | exclusiveMaximum?: number; 11 | multipleOf?: number; 12 | errorMessage?: ErrorMessages; 13 | }; 14 | 15 | export function parseNumberDef(def: ZodNumberDef, refs: Refs): JsonSchema7NumberType { 16 | const res: JsonSchema7NumberType = { 17 | type: 'number', 18 | }; 19 | 20 | if (!def.checks) return res; 21 | 22 | for (const check of def.checks) { 23 | switch (check.kind) { 24 | case 'int': 25 | res.type = 'integer'; 26 | addErrorMessage(res, 'type', check.message, refs); 27 | break; 28 | case 'min': 29 | if (refs.target === 'jsonSchema7') { 30 | if (check.inclusive) { 31 | setResponseValueAndErrors(res, 'minimum', check.value, check.message, refs); 32 | } else { 33 | setResponseValueAndErrors(res, 'exclusiveMinimum', check.value, check.message, refs); 34 | } 35 | } else { 36 | if (!check.inclusive) { 37 | res.exclusiveMinimum = true as any; 38 | } 39 | setResponseValueAndErrors(res, 'minimum', check.value, check.message, refs); 40 | } 41 | break; 42 | case 'max': 43 | if (refs.target === 'jsonSchema7') { 44 | if (check.inclusive) { 45 | setResponseValueAndErrors(res, 'maximum', check.value, check.message, refs); 46 | } else { 47 | setResponseValueAndErrors(res, 'exclusiveMaximum', check.value, check.message, refs); 48 | } 49 | } else { 50 | if (!check.inclusive) { 51 | res.exclusiveMaximum = true as any; 52 | } 53 | setResponseValueAndErrors(res, 'maximum', check.value, check.message, refs); 54 | } 55 | break; 56 | case 'multipleOf': 57 | setResponseValueAndErrors(res, 'multipleOf', check.value, check.message, refs); 58 | break; 59 | } 60 | } 61 | return res; 62 | } 63 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/object.ts: -------------------------------------------------------------------------------- 1 | import { ZodObjectDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | function decideAdditionalProperties(def: ZodObjectDef, refs: Refs) { 6 | if (refs.removeAdditionalStrategy === 'strict') { 7 | return def.catchall._def.typeName === 'ZodNever' ? 8 | def.unknownKeys !== 'strict' 9 | : parseDef(def.catchall._def, { 10 | ...refs, 11 | currentPath: [...refs.currentPath, 'additionalProperties'], 12 | }) ?? true; 13 | } else { 14 | return def.catchall._def.typeName === 'ZodNever' ? 15 | def.unknownKeys === 'passthrough' 16 | : parseDef(def.catchall._def, { 17 | ...refs, 18 | currentPath: [...refs.currentPath, 'additionalProperties'], 19 | }) ?? true; 20 | } 21 | } 22 | 23 | export type JsonSchema7ObjectType = { 24 | type: 'object'; 25 | properties: Record; 26 | additionalProperties: boolean | JsonSchema7Type; 27 | required?: string[]; 28 | }; 29 | 30 | export function parseObjectDef(def: ZodObjectDef, refs: Refs) { 31 | const result: JsonSchema7ObjectType = { 32 | type: 'object', 33 | ...Object.entries(def.shape()).reduce( 34 | ( 35 | acc: { 36 | properties: Record; 37 | required: string[]; 38 | }, 39 | [propName, propDef], 40 | ) => { 41 | if (propDef === undefined || propDef._def === undefined) return acc; 42 | const parsedDef = parseDef(propDef._def, { 43 | ...refs, 44 | currentPath: [...refs.currentPath, 'properties', propName], 45 | propertyPath: [...refs.currentPath, 'properties', propName], 46 | }); 47 | if (parsedDef === undefined) return acc; 48 | return { 49 | properties: { 50 | ...acc.properties, 51 | [propName]: parsedDef, 52 | }, 53 | required: 54 | propDef.isOptional() && !refs.openaiStrictMode ? acc.required : [...acc.required, propName], 55 | }; 56 | }, 57 | { properties: {}, required: [] }, 58 | ), 59 | additionalProperties: decideAdditionalProperties(def, refs), 60 | }; 61 | if (!result.required!.length) delete result.required; 62 | return result; 63 | } 64 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/optional.ts: -------------------------------------------------------------------------------- 1 | import { ZodOptionalDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export const parseOptionalDef = (def: ZodOptionalDef, refs: Refs): JsonSchema7Type | undefined => { 6 | if (refs.currentPath.toString() === refs.propertyPath?.toString()) { 7 | return parseDef(def.innerType._def, refs); 8 | } 9 | 10 | const innerSchema = parseDef(def.innerType._def, { 11 | ...refs, 12 | currentPath: [...refs.currentPath, 'anyOf', '1'], 13 | }); 14 | 15 | return innerSchema ? 16 | { 17 | anyOf: [ 18 | { 19 | not: {}, 20 | }, 21 | innerSchema, 22 | ], 23 | } 24 | : {}; 25 | }; 26 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/pipeline.ts: -------------------------------------------------------------------------------- 1 | import { ZodPipelineDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | import { JsonSchema7AllOfType } from './intersection'; 5 | 6 | export const parsePipelineDef = ( 7 | def: ZodPipelineDef, 8 | refs: Refs, 9 | ): JsonSchema7AllOfType | JsonSchema7Type | undefined => { 10 | if (refs.pipeStrategy === 'input') { 11 | return parseDef(def.in._def, refs); 12 | } else if (refs.pipeStrategy === 'output') { 13 | return parseDef(def.out._def, refs); 14 | } 15 | 16 | const a = parseDef(def.in._def, { 17 | ...refs, 18 | currentPath: [...refs.currentPath, 'allOf', '0'], 19 | }); 20 | const b = parseDef(def.out._def, { 21 | ...refs, 22 | currentPath: [...refs.currentPath, 'allOf', a ? '1' : '0'], 23 | }); 24 | 25 | return { 26 | allOf: [a, b].filter((x): x is JsonSchema7Type => x !== undefined), 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/promise.ts: -------------------------------------------------------------------------------- 1 | import { ZodPromiseDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export function parsePromiseDef(def: ZodPromiseDef, refs: Refs): JsonSchema7Type | undefined { 6 | return parseDef(def.type._def, refs); 7 | } 8 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/readonly.ts: -------------------------------------------------------------------------------- 1 | import { ZodReadonlyDef } from 'zod'; 2 | import { parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export const parseReadonlyDef = (def: ZodReadonlyDef, refs: Refs) => { 6 | return parseDef(def.innerType._def, refs); 7 | }; 8 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/record.ts: -------------------------------------------------------------------------------- 1 | import { ZodFirstPartyTypeKind, ZodMapDef, ZodRecordDef, ZodTypeAny } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | import { JsonSchema7EnumType } from './enum'; 5 | import { JsonSchema7ObjectType } from './object'; 6 | import { JsonSchema7StringType, parseStringDef } from './string'; 7 | 8 | type JsonSchema7RecordPropertyNamesType = 9 | | Omit 10 | | Omit; 11 | 12 | export type JsonSchema7RecordType = { 13 | type: 'object'; 14 | additionalProperties: JsonSchema7Type; 15 | propertyNames?: JsonSchema7RecordPropertyNamesType; 16 | }; 17 | 18 | export function parseRecordDef( 19 | def: ZodRecordDef | ZodMapDef, 20 | refs: Refs, 21 | ): JsonSchema7RecordType { 22 | if (refs.target === 'openApi3' && def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) { 23 | return { 24 | type: 'object', 25 | required: def.keyType._def.values, 26 | properties: def.keyType._def.values.reduce( 27 | (acc: Record, key: string) => ({ 28 | ...acc, 29 | [key]: 30 | parseDef(def.valueType._def, { 31 | ...refs, 32 | currentPath: [...refs.currentPath, 'properties', key], 33 | }) ?? {}, 34 | }), 35 | {}, 36 | ), 37 | additionalProperties: false, 38 | } satisfies JsonSchema7ObjectType as any; 39 | } 40 | 41 | const schema: JsonSchema7RecordType = { 42 | type: 'object', 43 | additionalProperties: 44 | parseDef(def.valueType._def, { 45 | ...refs, 46 | currentPath: [...refs.currentPath, 'additionalProperties'], 47 | }) ?? {}, 48 | }; 49 | 50 | if (refs.target === 'openApi3') { 51 | return schema; 52 | } 53 | 54 | if (def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodString && def.keyType._def.checks?.length) { 55 | const keyType: JsonSchema7RecordPropertyNamesType = Object.entries( 56 | parseStringDef(def.keyType._def, refs), 57 | ).reduce((acc, [key, value]) => (key === 'type' ? acc : { ...acc, [key]: value }), {}); 58 | 59 | return { 60 | ...schema, 61 | propertyNames: keyType, 62 | }; 63 | } else if (def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) { 64 | return { 65 | ...schema, 66 | propertyNames: { 67 | enum: def.keyType._def.values, 68 | }, 69 | }; 70 | } 71 | 72 | return schema; 73 | } 74 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/set.ts: -------------------------------------------------------------------------------- 1 | import { ZodSetDef } from 'zod'; 2 | import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages'; 3 | import { JsonSchema7Type, parseDef } from '../parseDef'; 4 | import { Refs } from '../Refs'; 5 | 6 | export type JsonSchema7SetType = { 7 | type: 'array'; 8 | uniqueItems: true; 9 | items?: JsonSchema7Type | undefined; 10 | minItems?: number; 11 | maxItems?: number; 12 | errorMessage?: ErrorMessages; 13 | }; 14 | 15 | export function parseSetDef(def: ZodSetDef, refs: Refs): JsonSchema7SetType { 16 | const items = parseDef(def.valueType._def, { 17 | ...refs, 18 | currentPath: [...refs.currentPath, 'items'], 19 | }); 20 | 21 | const schema: JsonSchema7SetType = { 22 | type: 'array', 23 | uniqueItems: true, 24 | items, 25 | }; 26 | 27 | if (def.minSize) { 28 | setResponseValueAndErrors(schema, 'minItems', def.minSize.value, def.minSize.message, refs); 29 | } 30 | 31 | if (def.maxSize) { 32 | setResponseValueAndErrors(schema, 'maxItems', def.maxSize.value, def.maxSize.message, refs); 33 | } 34 | 35 | return schema; 36 | } 37 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/string.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { ZodStringDef } from 'zod'; 3 | import { ErrorMessages, setResponseValueAndErrors } from '../errorMessages'; 4 | import { Refs } from '../Refs'; 5 | 6 | let emojiRegex: RegExp | undefined; 7 | 8 | /** 9 | * Generated from the regular expressions found here as of 2024-05-22: 10 | * https://github.com/colinhacks/zod/blob/master/src/types.ts. 11 | * 12 | * Expressions with /i flag have been changed accordingly. 13 | */ 14 | export const zodPatterns = { 15 | /** 16 | * `c` was changed to `[cC]` to replicate /i flag 17 | */ 18 | cuid: /^[cC][^\s-]{8,}$/, 19 | cuid2: /^[0-9a-z]+$/, 20 | ulid: /^[0-9A-HJKMNP-TV-Z]{26}$/, 21 | /** 22 | * `a-z` was added to replicate /i flag 23 | */ 24 | email: /^(?!\.)(?!.*\.\.)([a-zA-Z0-9_'+\-\.]*)[a-zA-Z0-9_+-]@([a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,}$/, 25 | /** 26 | * Constructed a valid Unicode RegExp 27 | * 28 | * Lazily instantiate since this type of regex isn't supported 29 | * in all envs (e.g. React Native). 30 | * 31 | * See: 32 | * https://github.com/colinhacks/zod/issues/2433 33 | * Fix in Zod: 34 | * https://github.com/colinhacks/zod/commit/9340fd51e48576a75adc919bff65dbc4a5d4c99b 35 | */ 36 | emoji: () => { 37 | if (emojiRegex === undefined) { 38 | emojiRegex = RegExp('^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$', 'u'); 39 | } 40 | return emojiRegex; 41 | }, 42 | /** 43 | * Unused 44 | */ 45 | uuid: /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/, 46 | /** 47 | * Unused 48 | */ 49 | ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/, 50 | /** 51 | * Unused 52 | */ 53 | ipv6: /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/, 54 | base64: /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/, 55 | nanoid: /^[a-zA-Z0-9_-]{21}$/, 56 | } as const; 57 | 58 | export type JsonSchema7StringType = { 59 | type: 'string'; 60 | minLength?: number; 61 | maxLength?: number; 62 | format?: 63 | | 'email' 64 | | 'idn-email' 65 | | 'uri' 66 | | 'uuid' 67 | | 'date-time' 68 | | 'ipv4' 69 | | 'ipv6' 70 | | 'date' 71 | | 'time' 72 | | 'duration'; 73 | pattern?: string; 74 | allOf?: { 75 | pattern: string; 76 | errorMessage?: ErrorMessages<{ pattern: string }>; 77 | }[]; 78 | anyOf?: { 79 | format: string; 80 | errorMessage?: ErrorMessages<{ format: string }>; 81 | }[]; 82 | errorMessage?: ErrorMessages; 83 | contentEncoding?: string; 84 | }; 85 | 86 | export function parseStringDef(def: ZodStringDef, refs: Refs): JsonSchema7StringType { 87 | const res: JsonSchema7StringType = { 88 | type: 'string', 89 | }; 90 | 91 | function processPattern(value: string): string { 92 | return refs.patternStrategy === 'escape' ? escapeNonAlphaNumeric(value) : value; 93 | } 94 | 95 | if (def.checks) { 96 | for (const check of def.checks) { 97 | switch (check.kind) { 98 | case 'min': 99 | setResponseValueAndErrors( 100 | res, 101 | 'minLength', 102 | typeof res.minLength === 'number' ? Math.max(res.minLength, check.value) : check.value, 103 | check.message, 104 | refs, 105 | ); 106 | break; 107 | case 'max': 108 | setResponseValueAndErrors( 109 | res, 110 | 'maxLength', 111 | typeof res.maxLength === 'number' ? Math.min(res.maxLength, check.value) : check.value, 112 | check.message, 113 | refs, 114 | ); 115 | 116 | break; 117 | case 'email': 118 | switch (refs.emailStrategy) { 119 | case 'format:email': 120 | addFormat(res, 'email', check.message, refs); 121 | break; 122 | case 'format:idn-email': 123 | addFormat(res, 'idn-email', check.message, refs); 124 | break; 125 | case 'pattern:zod': 126 | addPattern(res, zodPatterns.email, check.message, refs); 127 | break; 128 | } 129 | 130 | break; 131 | case 'url': 132 | addFormat(res, 'uri', check.message, refs); 133 | break; 134 | case 'uuid': 135 | addFormat(res, 'uuid', check.message, refs); 136 | break; 137 | case 'regex': 138 | addPattern(res, check.regex, check.message, refs); 139 | break; 140 | case 'cuid': 141 | addPattern(res, zodPatterns.cuid, check.message, refs); 142 | break; 143 | case 'cuid2': 144 | addPattern(res, zodPatterns.cuid2, check.message, refs); 145 | break; 146 | case 'startsWith': 147 | addPattern(res, RegExp(`^${processPattern(check.value)}`), check.message, refs); 148 | break; 149 | case 'endsWith': 150 | addPattern(res, RegExp(`${processPattern(check.value)}$`), check.message, refs); 151 | break; 152 | 153 | case 'datetime': 154 | addFormat(res, 'date-time', check.message, refs); 155 | break; 156 | case 'date': 157 | addFormat(res, 'date', check.message, refs); 158 | break; 159 | case 'time': 160 | addFormat(res, 'time', check.message, refs); 161 | break; 162 | case 'duration': 163 | addFormat(res, 'duration', check.message, refs); 164 | break; 165 | case 'length': 166 | setResponseValueAndErrors( 167 | res, 168 | 'minLength', 169 | typeof res.minLength === 'number' ? Math.max(res.minLength, check.value) : check.value, 170 | check.message, 171 | refs, 172 | ); 173 | setResponseValueAndErrors( 174 | res, 175 | 'maxLength', 176 | typeof res.maxLength === 'number' ? Math.min(res.maxLength, check.value) : check.value, 177 | check.message, 178 | refs, 179 | ); 180 | break; 181 | case 'includes': { 182 | addPattern(res, RegExp(processPattern(check.value)), check.message, refs); 183 | break; 184 | } 185 | case 'ip': { 186 | if (check.version !== 'v6') { 187 | addFormat(res, 'ipv4', check.message, refs); 188 | } 189 | if (check.version !== 'v4') { 190 | addFormat(res, 'ipv6', check.message, refs); 191 | } 192 | break; 193 | } 194 | case 'emoji': 195 | addPattern(res, zodPatterns.emoji, check.message, refs); 196 | break; 197 | case 'ulid': { 198 | addPattern(res, zodPatterns.ulid, check.message, refs); 199 | break; 200 | } 201 | case 'base64': { 202 | switch (refs.base64Strategy) { 203 | case 'format:binary': { 204 | addFormat(res, 'binary' as any, check.message, refs); 205 | break; 206 | } 207 | 208 | case 'contentEncoding:base64': { 209 | setResponseValueAndErrors(res, 'contentEncoding', 'base64', check.message, refs); 210 | break; 211 | } 212 | 213 | case 'pattern:zod': { 214 | addPattern(res, zodPatterns.base64, check.message, refs); 215 | break; 216 | } 217 | } 218 | break; 219 | } 220 | case 'nanoid': { 221 | addPattern(res, zodPatterns.nanoid, check.message, refs); 222 | } 223 | case 'toLowerCase': 224 | case 'toUpperCase': 225 | case 'trim': 226 | break; 227 | default: 228 | ((_: never) => {})(check); 229 | } 230 | } 231 | } 232 | 233 | return res; 234 | } 235 | 236 | const escapeNonAlphaNumeric = (value: string) => 237 | Array.from(value) 238 | .map((c) => (/[a-zA-Z0-9]/.test(c) ? c : `\\${c}`)) 239 | .join(''); 240 | 241 | const addFormat = ( 242 | schema: JsonSchema7StringType, 243 | value: Required['format'], 244 | message: string | undefined, 245 | refs: Refs, 246 | ) => { 247 | if (schema.format || schema.anyOf?.some((x) => x.format)) { 248 | if (!schema.anyOf) { 249 | schema.anyOf = []; 250 | } 251 | 252 | if (schema.format) { 253 | schema.anyOf!.push({ 254 | format: schema.format, 255 | ...(schema.errorMessage && 256 | refs.errorMessages && { 257 | errorMessage: { format: schema.errorMessage.format }, 258 | }), 259 | }); 260 | delete schema.format; 261 | if (schema.errorMessage) { 262 | delete schema.errorMessage.format; 263 | if (Object.keys(schema.errorMessage).length === 0) { 264 | delete schema.errorMessage; 265 | } 266 | } 267 | } 268 | 269 | schema.anyOf!.push({ 270 | format: value, 271 | ...(message && refs.errorMessages && { errorMessage: { format: message } }), 272 | }); 273 | } else { 274 | setResponseValueAndErrors(schema, 'format', value, message, refs); 275 | } 276 | }; 277 | 278 | const addPattern = ( 279 | schema: JsonSchema7StringType, 280 | regex: RegExp | (() => RegExp), 281 | message: string | undefined, 282 | refs: Refs, 283 | ) => { 284 | if (schema.pattern || schema.allOf?.some((x) => x.pattern)) { 285 | if (!schema.allOf) { 286 | schema.allOf = []; 287 | } 288 | 289 | if (schema.pattern) { 290 | schema.allOf!.push({ 291 | pattern: schema.pattern, 292 | ...(schema.errorMessage && 293 | refs.errorMessages && { 294 | errorMessage: { pattern: schema.errorMessage.pattern }, 295 | }), 296 | }); 297 | delete schema.pattern; 298 | if (schema.errorMessage) { 299 | delete schema.errorMessage.pattern; 300 | if (Object.keys(schema.errorMessage).length === 0) { 301 | delete schema.errorMessage; 302 | } 303 | } 304 | } 305 | 306 | schema.allOf!.push({ 307 | pattern: processRegExp(regex, refs), 308 | ...(message && refs.errorMessages && { errorMessage: { pattern: message } }), 309 | }); 310 | } else { 311 | setResponseValueAndErrors(schema, 'pattern', processRegExp(regex, refs), message, refs); 312 | } 313 | }; 314 | 315 | // Mutate z.string.regex() in a best attempt to accommodate for regex flags when applyRegexFlags is true 316 | const processRegExp = (regexOrFunction: RegExp | (() => RegExp), refs: Refs): string => { 317 | const regex = typeof regexOrFunction === 'function' ? regexOrFunction() : regexOrFunction; 318 | if (!refs.applyRegexFlags || !regex.flags) return regex.source; 319 | 320 | // Currently handled flags 321 | const flags = { 322 | i: regex.flags.includes('i'), // Case-insensitive 323 | m: regex.flags.includes('m'), // `^` and `$` matches adjacent to newline characters 324 | s: regex.flags.includes('s'), // `.` matches newlines 325 | }; 326 | 327 | // The general principle here is to step through each character, one at a time, applying mutations as flags require. We keep track when the current character is escaped, and when it's inside a group /like [this]/ or (also) a range like /[a-z]/. The following is fairly brittle imperative code; edit at your peril! 328 | 329 | const source = flags.i ? regex.source.toLowerCase() : regex.source; 330 | let pattern = ''; 331 | let isEscaped = false; 332 | let inCharGroup = false; 333 | let inCharRange = false; 334 | 335 | for (let i = 0; i < source.length; i++) { 336 | if (isEscaped) { 337 | pattern += source[i]; 338 | isEscaped = false; 339 | continue; 340 | } 341 | 342 | if (flags.i) { 343 | if (inCharGroup) { 344 | if (source[i].match(/[a-z]/)) { 345 | if (inCharRange) { 346 | pattern += source[i]; 347 | pattern += `${source[i - 2]}-${source[i]}`.toUpperCase(); 348 | inCharRange = false; 349 | } else if (source[i + 1] === '-' && source[i + 2]?.match(/[a-z]/)) { 350 | pattern += source[i]; 351 | inCharRange = true; 352 | } else { 353 | pattern += `${source[i]}${source[i].toUpperCase()}`; 354 | } 355 | continue; 356 | } 357 | } else if (source[i].match(/[a-z]/)) { 358 | pattern += `[${source[i]}${source[i].toUpperCase()}]`; 359 | continue; 360 | } 361 | } 362 | 363 | if (flags.m) { 364 | if (source[i] === '^') { 365 | pattern += `(^|(?<=[\r\n]))`; 366 | continue; 367 | } else if (source[i] === '$') { 368 | pattern += `($|(?=[\r\n]))`; 369 | continue; 370 | } 371 | } 372 | 373 | if (flags.s && source[i] === '.') { 374 | pattern += inCharGroup ? `${source[i]}\r\n` : `[${source[i]}\r\n]`; 375 | continue; 376 | } 377 | 378 | pattern += source[i]; 379 | if (source[i] === '\\') { 380 | isEscaped = true; 381 | } else if (inCharGroup && source[i] === ']') { 382 | inCharGroup = false; 383 | } else if (!inCharGroup && source[i] === '[') { 384 | inCharGroup = true; 385 | } 386 | } 387 | 388 | try { 389 | const regexTest = new RegExp(pattern); 390 | } catch { 391 | console.warn( 392 | `Could not convert regex pattern at ${refs.currentPath.join( 393 | '/', 394 | )} to a flag-independent form! Falling back to the flag-ignorant source`, 395 | ); 396 | return regex.source; 397 | } 398 | 399 | return pattern; 400 | }; 401 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/tuple.ts: -------------------------------------------------------------------------------- 1 | import { ZodTupleDef, ZodTupleItems, ZodTypeAny } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export type JsonSchema7TupleType = { 6 | type: 'array'; 7 | minItems: number; 8 | items: JsonSchema7Type[]; 9 | } & ( 10 | | { 11 | maxItems: number; 12 | } 13 | | { 14 | additionalItems?: JsonSchema7Type | undefined; 15 | } 16 | ); 17 | 18 | export function parseTupleDef( 19 | def: ZodTupleDef, 20 | refs: Refs, 21 | ): JsonSchema7TupleType { 22 | if (def.rest) { 23 | return { 24 | type: 'array', 25 | minItems: def.items.length, 26 | items: def.items 27 | .map((x, i) => 28 | parseDef(x._def, { 29 | ...refs, 30 | currentPath: [...refs.currentPath, 'items', `${i}`], 31 | }), 32 | ) 33 | .reduce((acc: JsonSchema7Type[], x) => (x === undefined ? acc : [...acc, x]), []), 34 | additionalItems: parseDef(def.rest._def, { 35 | ...refs, 36 | currentPath: [...refs.currentPath, 'additionalItems'], 37 | }), 38 | }; 39 | } else { 40 | return { 41 | type: 'array', 42 | minItems: def.items.length, 43 | maxItems: def.items.length, 44 | items: def.items 45 | .map((x, i) => 46 | parseDef(x._def, { 47 | ...refs, 48 | currentPath: [...refs.currentPath, 'items', `${i}`], 49 | }), 50 | ) 51 | .reduce((acc: JsonSchema7Type[], x) => (x === undefined ? acc : [...acc, x]), []), 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/undefined.ts: -------------------------------------------------------------------------------- 1 | export type JsonSchema7UndefinedType = { 2 | not: {}; 3 | }; 4 | 5 | export function parseUndefinedDef(): JsonSchema7UndefinedType { 6 | return { 7 | not: {}, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/union.ts: -------------------------------------------------------------------------------- 1 | import { ZodDiscriminatedUnionDef, ZodLiteralDef, ZodTypeAny, ZodUnionDef } from 'zod'; 2 | import { JsonSchema7Type, parseDef } from '../parseDef'; 3 | import { Refs } from '../Refs'; 4 | 5 | export const primitiveMappings = { 6 | ZodString: 'string', 7 | ZodNumber: 'number', 8 | ZodBigInt: 'integer', 9 | ZodBoolean: 'boolean', 10 | ZodNull: 'null', 11 | } as const; 12 | type ZodPrimitive = keyof typeof primitiveMappings; 13 | type JsonSchema7Primitive = (typeof primitiveMappings)[keyof typeof primitiveMappings]; 14 | 15 | export type JsonSchema7UnionType = JsonSchema7PrimitiveUnionType | JsonSchema7AnyOfType; 16 | 17 | type JsonSchema7PrimitiveUnionType = 18 | | { 19 | type: JsonSchema7Primitive | JsonSchema7Primitive[]; 20 | } 21 | | { 22 | type: JsonSchema7Primitive | JsonSchema7Primitive[]; 23 | enum: (string | number | bigint | boolean | null)[]; 24 | }; 25 | 26 | type JsonSchema7AnyOfType = { 27 | anyOf: JsonSchema7Type[]; 28 | }; 29 | 30 | export function parseUnionDef( 31 | def: ZodUnionDef | ZodDiscriminatedUnionDef, 32 | refs: Refs, 33 | ): JsonSchema7PrimitiveUnionType | JsonSchema7AnyOfType | undefined { 34 | if (refs.target === 'openApi3') return asAnyOf(def, refs); 35 | 36 | const options: readonly ZodTypeAny[] = 37 | def.options instanceof Map ? Array.from(def.options.values()) : def.options; 38 | 39 | // This blocks tries to look ahead a bit to produce nicer looking schemas with type array instead of anyOf. 40 | if ( 41 | options.every((x) => x._def.typeName in primitiveMappings && (!x._def.checks || !x._def.checks.length)) 42 | ) { 43 | // all types in union are primitive and lack checks, so might as well squash into {type: [...]} 44 | 45 | const types = options.reduce((types: JsonSchema7Primitive[], x) => { 46 | const type = primitiveMappings[x._def.typeName as ZodPrimitive]; //Can be safely casted due to row 43 47 | return type && !types.includes(type) ? [...types, type] : types; 48 | }, []); 49 | 50 | return { 51 | type: types.length > 1 ? types : types[0]!, 52 | }; 53 | } else if (options.every((x) => x._def.typeName === 'ZodLiteral' && !x.description)) { 54 | // all options literals 55 | 56 | const types = options.reduce((acc: JsonSchema7Primitive[], x: { _def: ZodLiteralDef }) => { 57 | const type = typeof x._def.value; 58 | switch (type) { 59 | case 'string': 60 | case 'number': 61 | case 'boolean': 62 | return [...acc, type]; 63 | case 'bigint': 64 | return [...acc, 'integer' as const]; 65 | case 'object': 66 | if (x._def.value === null) return [...acc, 'null' as const]; 67 | case 'symbol': 68 | case 'undefined': 69 | case 'function': 70 | default: 71 | return acc; 72 | } 73 | }, []); 74 | 75 | if (types.length === options.length) { 76 | // all the literals are primitive, as far as null can be considered primitive 77 | 78 | const uniqueTypes = types.filter((x, i, a) => a.indexOf(x) === i); 79 | return { 80 | type: uniqueTypes.length > 1 ? uniqueTypes : uniqueTypes[0]!, 81 | enum: options.reduce( 82 | (acc, x) => { 83 | return acc.includes(x._def.value) ? acc : [...acc, x._def.value]; 84 | }, 85 | [] as (string | number | bigint | boolean | null)[], 86 | ), 87 | }; 88 | } 89 | } else if (options.every((x) => x._def.typeName === 'ZodEnum')) { 90 | return { 91 | type: 'string', 92 | enum: options.reduce( 93 | (acc: string[], x) => [...acc, ...x._def.values.filter((x: string) => !acc.includes(x))], 94 | [], 95 | ), 96 | }; 97 | } 98 | 99 | return asAnyOf(def, refs); 100 | } 101 | 102 | const asAnyOf = ( 103 | def: ZodUnionDef | ZodDiscriminatedUnionDef, 104 | refs: Refs, 105 | ): JsonSchema7PrimitiveUnionType | JsonSchema7AnyOfType | undefined => { 106 | const anyOf = ((def.options instanceof Map ? Array.from(def.options.values()) : def.options) as any[]) 107 | .map((x, i) => 108 | parseDef(x._def, { 109 | ...refs, 110 | currentPath: [...refs.currentPath, 'anyOf', `${i}`], 111 | }), 112 | ) 113 | .filter( 114 | (x): x is JsonSchema7Type => 115 | !!x && (!refs.strictUnions || (typeof x === 'object' && Object.keys(x).length > 0)), 116 | ); 117 | 118 | return anyOf.length ? { anyOf } : undefined; 119 | }; 120 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/parsers/unknown.ts: -------------------------------------------------------------------------------- 1 | export type JsonSchema7UnknownType = {}; 2 | 3 | export function parseUnknownDef(): JsonSchema7UnknownType { 4 | return {}; 5 | } 6 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/util.ts: -------------------------------------------------------------------------------- 1 | import type { ZodSchema, ZodTypeDef } from 'zod'; 2 | 3 | export const zodDef = (zodSchema: ZodSchema | ZodTypeDef): ZodTypeDef => { 4 | return '_def' in zodSchema ? zodSchema._def : zodSchema; 5 | }; 6 | 7 | export function isEmptyObj(obj: Object | null | undefined): boolean { 8 | if (!obj) return true; 9 | for (const _k in obj) return false; 10 | return true; 11 | } 12 | -------------------------------------------------------------------------------- /src/vendor/zod-to-json-schema/zodToJsonSchema.ts: -------------------------------------------------------------------------------- 1 | import { ZodSchema } from 'zod'; 2 | import { Options, Targets } from './Options'; 3 | import { JsonSchema7Type, parseDef } from './parseDef'; 4 | import { getRefs } from './Refs'; 5 | import { zodDef, isEmptyObj } from './util'; 6 | 7 | const zodToJsonSchema = ( 8 | schema: ZodSchema, 9 | options?: Partial> | string, 10 | ): (Target extends 'jsonSchema7' ? JsonSchema7Type : object) & { 11 | $schema?: string; 12 | definitions?: { 13 | [key: string]: Target extends 'jsonSchema7' ? JsonSchema7Type 14 | : Target extends 'jsonSchema2019-09' ? JsonSchema7Type 15 | : object; 16 | }; 17 | } => { 18 | const refs = getRefs(options); 19 | 20 | const name = 21 | typeof options === 'string' ? options 22 | : options?.nameStrategy === 'title' ? undefined 23 | : options?.name; 24 | 25 | const main = 26 | parseDef( 27 | schema._def, 28 | name === undefined ? refs : ( 29 | { 30 | ...refs, 31 | currentPath: [...refs.basePath, refs.definitionPath, name], 32 | } 33 | ), 34 | false, 35 | ) ?? {}; 36 | 37 | const title = 38 | typeof options === 'object' && options.name !== undefined && options.nameStrategy === 'title' ? 39 | options.name 40 | : undefined; 41 | 42 | if (title !== undefined) { 43 | main.title = title; 44 | } 45 | 46 | const definitions = (() => { 47 | if (isEmptyObj(refs.definitions)) { 48 | return undefined; 49 | } 50 | 51 | const definitions: Record = {}; 52 | const processedDefinitions = new Set(); 53 | 54 | // the call to `parseDef()` here might itself add more entries to `.definitions` 55 | // so we need to continually evaluate definitions until we've resolved all of them 56 | // 57 | // we have a generous iteration limit here to avoid blowing up the stack if there 58 | // are any bugs that would otherwise result in us iterating indefinitely 59 | for (let i = 0; i < 500; i++) { 60 | const newDefinitions = Object.entries(refs.definitions).filter( 61 | ([key]) => !processedDefinitions.has(key), 62 | ); 63 | if (newDefinitions.length === 0) break; 64 | 65 | for (const [key, schema] of newDefinitions) { 66 | definitions[key] = 67 | parseDef( 68 | zodDef(schema), 69 | { ...refs, currentPath: [...refs.basePath, refs.definitionPath, key] }, 70 | true, 71 | ) ?? {}; 72 | processedDefinitions.add(key); 73 | } 74 | } 75 | 76 | return definitions; 77 | })(); 78 | 79 | const combined: ReturnType> = 80 | name === undefined ? 81 | definitions ? 82 | { 83 | ...main, 84 | [refs.definitionPath]: definitions, 85 | } 86 | : main 87 | : refs.nameStrategy === 'duplicate-ref' ? 88 | { 89 | ...main, 90 | ...(definitions || refs.seenRefs.size ? 91 | { 92 | [refs.definitionPath]: { 93 | ...definitions, 94 | // only actually duplicate the schema definition if it was ever referenced 95 | // otherwise the duplication is completely pointless 96 | ...(refs.seenRefs.size ? { [name]: main } : undefined), 97 | }, 98 | } 99 | : undefined), 100 | } 101 | : { 102 | $ref: [...(refs.$refStrategy === 'relative' ? [] : refs.basePath), refs.definitionPath, name].join( 103 | '/', 104 | ), 105 | [refs.definitionPath]: { 106 | ...definitions, 107 | [name]: main, 108 | }, 109 | }; 110 | 111 | if (refs.target === 'jsonSchema7') { 112 | combined.$schema = 'http://json-schema.org/draft-07/schema#'; 113 | } else if (refs.target === 'jsonSchema2019-09') { 114 | combined.$schema = 'https://json-schema.org/draft/2019-09/schema#'; 115 | } 116 | 117 | return combined; 118 | }; 119 | 120 | export { zodToJsonSchema }; 121 | -------------------------------------------------------------------------------- /src/zod-to-json-schema.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { z } from 'zod' 3 | 4 | import { zodToJsonSchema } from '.' 5 | 6 | describe('Root schema result after parsing', () => { 7 | test('should return the schema directly in the root if no name is passed', () => { 8 | expect(zodToJsonSchema(z.any())).toEqual({ 9 | $schema: 'http://json-schema.org/draft-07/schema#' 10 | }) 11 | }) 12 | 13 | test('should return the schema inside a named property in "definitions" if a name is passed', () => { 14 | expect(zodToJsonSchema(z.any(), 'MySchema')).toEqual({ 15 | $schema: 'http://json-schema.org/draft-07/schema#', 16 | $ref: `#/definitions/MySchema`, 17 | definitions: { 18 | MySchema: {} 19 | } 20 | }) 21 | }) 22 | 23 | test('should return the schema inside a named property in "$defs" if a name and definitionPath is passed in options', () => { 24 | expect( 25 | zodToJsonSchema(z.any(), { name: 'MySchema', definitionPath: '$defs' }) 26 | ).toEqual({ 27 | $schema: 'http://json-schema.org/draft-07/schema#', 28 | $ref: `#/$defs/MySchema`, 29 | $defs: { 30 | MySchema: {} 31 | } 32 | }) 33 | }) 34 | 35 | test("should not scrub 'any'-schemas from unions when strictUnions=false", () => { 36 | expect( 37 | zodToJsonSchema( 38 | z.union([z.any(), z.instanceof(String), z.string(), z.number()]), 39 | { strictUnions: false } 40 | ) 41 | ).toEqual({ 42 | $schema: 'http://json-schema.org/draft-07/schema#', 43 | anyOf: [{}, {}, { type: 'string' }, { type: 'number' }] 44 | }) 45 | }) 46 | 47 | test("should scrub 'any'-schemas from unions when strictUnions=true", () => { 48 | expect( 49 | zodToJsonSchema( 50 | z.union([z.any(), z.instanceof(String), z.string(), z.number()]), 51 | { strictUnions: true } 52 | ) 53 | ).toEqual({ 54 | $schema: 'http://json-schema.org/draft-07/schema#', 55 | anyOf: [{ type: 'string' }, { type: 'number' }] 56 | }) 57 | }) 58 | 59 | test("should scrub 'any'-schemas from unions when strictUnions=true in objects", () => { 60 | expect( 61 | zodToJsonSchema( 62 | z.object({ 63 | field: z.union([ 64 | z.any(), 65 | z.instanceof(String), 66 | z.string(), 67 | z.number() 68 | ]) 69 | }), 70 | { strictUnions: true } 71 | ) 72 | ).toEqual({ 73 | $schema: 'http://json-schema.org/draft-07/schema#', 74 | additionalProperties: false, 75 | properties: { 76 | field: { anyOf: [{ type: 'string' }, { type: 'number' }] } 77 | }, 78 | type: 'object' 79 | }) 80 | }) 81 | 82 | test('Definitions play nice with named schemas', () => { 83 | const MySpecialStringSchema = z.string() 84 | const MyArraySchema = z.array(MySpecialStringSchema) 85 | 86 | const result = zodToJsonSchema(MyArraySchema, { 87 | definitions: { 88 | MySpecialStringSchema, 89 | MyArraySchema 90 | } 91 | }) 92 | 93 | expect(result).toEqual({ 94 | $schema: 'http://json-schema.org/draft-07/schema#', 95 | $ref: '#/definitions/MyArraySchema', 96 | definitions: { 97 | MySpecialStringSchema: { type: 'string' }, 98 | MyArraySchema: { 99 | type: 'array', 100 | items: { 101 | $ref: '#/definitions/MySpecialStringSchema' 102 | } 103 | } 104 | } 105 | }) 106 | }) 107 | 108 | test('should be possible to add name as title instead of as ref', () => { 109 | expect( 110 | zodToJsonSchema(z.string(), { name: 'hello', nameStrategy: 'title' }) 111 | ).toEqual({ 112 | $schema: 'http://json-schema.org/draft-07/schema#', 113 | type: 'string', 114 | title: 'hello' 115 | }) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "lib": ["esnext", "dom.iterable"], 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "noEmit": true, 9 | "target": "es2020", 10 | "outDir": "dist", 11 | 12 | "allowImportingTsExtensions": false, 13 | "allowJs": true, 14 | "esModuleInterop": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "incremental": false, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "noUncheckedIndexedAccess": true, 20 | "resolveJsonModule": true, 21 | "skipLibCheck": true, 22 | "sourceMap": true, 23 | "strict": true, 24 | "useDefineForClassFields": true 25 | // "verbatimModuleSyntax": true 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['src/index.ts'], 6 | outDir: 'dist', 7 | target: 'node18', 8 | platform: 'node', 9 | format: ['esm', 'cjs'], 10 | splitting: false, 11 | sourcemap: true, 12 | minify: false, 13 | shims: true, 14 | dts: true 15 | } 16 | ]) 17 | --------------------------------------------------------------------------------