diff --git a/README.md b/README.md index 2b71170..e42fb51 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,15 @@ npm install zod-from-json-schema ## Zod 3 vs 4 -- If you need Zod 4, use the latest version of this package. -- If you need Zod 3, use the latest version that's less than 0.4.0 (at the of writing that's 0.0.5). It supports a smaller subsets of JSON Schema. +Zod 4 is available both as the package version 4, but also as part of the version 3 packages. We support both, as well as Zod 3. Here's which version of this package to use: + +|Zod|zod-from-json-schema| +|---|--------------------| +| v4 proper | latest | +| v4 via 3.x | ^0.4.2 | +| v3 | ^0.0.5 | + +Note that the older package for Zod 3 supports a smaller subset of JSON schema than the latest. New features will only be added to the latest. ## Usage diff --git a/package-lock.json b/package-lock.json index 9712ea7..131bc6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "zod-from-json-schema", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zod-from-json-schema", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { - "zod": "^3.25.25" + "zod": "^4.0.17" }, "devDependencies": { + "@types/node": "^24.3.0", "@vitest/coverage-v8": "^3.0.9", "esbuild": "^0.25.2", "typescript": "^5.8.3", @@ -896,6 +897,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, "node_modules/@vitest/coverage-v8": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.9.tgz", @@ -1976,6 +1987,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", @@ -2275,9 +2293,9 @@ } }, "node_modules/zod": { - "version": "3.25.25", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.25.tgz", - "integrity": "sha512-ZIe8jLjydG09F/5joIm10fSKa3pQ64YS1OfhQzNVwbHEd0MeAF3A9CbcKl5/1Pr6Wr37TMJtEWki8X2Y/2MvGQ==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index d0adbbf..3edc471 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zod-from-json-schema", - "version": "0.4.2", + "version": "0.5.0", "description": "Creates Zod types from JSON Schema at runtime", "main": "dist/index.js", "module": "dist/index.mjs", @@ -29,9 +29,10 @@ "url": "git+https://github.com/glideapps/zod-from-json-schema.git" }, "dependencies": { - "zod": "^3.25.25" + "zod": "^4.0.17" }, "devDependencies": { + "@types/node": "^24.3.0", "@vitest/coverage-v8": "^3.0.9", "esbuild": "^0.25.2", "typescript": "^5.8.3", diff --git a/src/examples.json b/src/examples.json index 573f84e..df4a8d7 100644 --- a/src/examples.json +++ b/src/examples.json @@ -2,7 +2,6 @@ { "type": "object", "properties": {}, - "required": [], "additionalProperties": false, "$schema": "https://json-schema.org/draft/2020-12/schema" }, @@ -32,9 +31,11 @@ "type": "object", "properties": { "mode": { - "enum": ["Dashboard", "Data", "Layout", "Workflows", "Settings"] + "enum": ["Dashboard", "Data", "Layout", "Workflows", "Settings"], + "type": "string" } }, + "required": ["mode"], "additionalProperties": false, "$schema": "https://json-schema.org/draft/2020-12/schema" @@ -47,7 +48,6 @@ "description": "Filter the apps by name or description. Looks for substrings." } }, - "required": [], "additionalProperties": false, "$schema": "https://json-schema.org/draft/2020-12/schema" }, @@ -56,7 +56,8 @@ "properties": { "kind": { "const": "ai-custom-chat-component", - "description": "The kind of the component. Must be exactly 'ai-custom-chat-component'" + "description": "The kind of the component. Must be exactly 'ai-custom-chat-component'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -67,7 +68,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -81,7 +83,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -100,7 +103,8 @@ "properties": { "kind": { "const": "hero", - "description": "The kind of the component. Must be exactly 'hero'" + "description": "The kind of the component. Must be exactly 'hero'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -111,7 +115,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -125,7 +130,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -139,7 +145,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -153,10 +160,12 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { - "const": "https://picsum.photos/seed/picsum/200/300" + "const": "https://picsum.photos/seed/picsum/200/300", + "type": "string" } }, "required": ["kind", "value"], @@ -167,10 +176,12 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { - "const": "https://picsum.photos/seed/picsum/200/300" + "const": "https://picsum.photos/seed/picsum/200/300", + "type": "string" } }, "required": ["kind", "value"], @@ -186,7 +197,8 @@ "properties": { "kind": { "const": "fields", - "description": "The kind of the component. Must be exactly 'fields'" + "description": "The kind of the component. Must be exactly 'fields'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -197,7 +209,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -216,7 +229,8 @@ "properties": { "kind": { "const": "page-location", - "description": "The kind of the component. Must be exactly 'page-location'" + "description": "The kind of the component. Must be exactly 'page-location'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -227,7 +241,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -241,7 +256,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -260,7 +276,8 @@ "properties": { "kind": { "const": "page-image", - "description": "The kind of the component. Must be exactly 'page-image'" + "description": "The kind of the component. Must be exactly 'page-image'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -271,7 +288,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -285,10 +303,12 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { - "const": "https://picsum.photos/seed/picsum/200/300" + "const": "https://picsum.photos/seed/picsum/200/300", + "type": "string" } }, "required": ["kind", "value"], @@ -299,7 +319,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -313,7 +334,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -332,7 +354,8 @@ "properties": { "kind": { "const": "page-simple-image", - "description": "The kind of the component. Must be exactly 'page-simple-image'" + "description": "The kind of the component. Must be exactly 'page-simple-image'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -343,10 +366,12 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { - "const": "https://picsum.photos/seed/picsum/200/300" + "const": "https://picsum.photos/seed/picsum/200/300", + "type": "string" } }, "required": ["kind", "value"], @@ -357,7 +382,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -376,7 +402,8 @@ "properties": { "kind": { "const": "page-video", - "description": "The kind of the component. Must be exactly 'page-video'" + "description": "The kind of the component. Must be exactly 'page-video'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -387,7 +414,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -401,7 +429,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -420,7 +449,8 @@ "properties": { "kind": { "const": "page-simple-video", - "description": "The kind of the component. Must be exactly 'page-simple-video'" + "description": "The kind of the component. Must be exactly 'page-simple-video'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -436,7 +466,8 @@ "properties": { "kind": { "const": "big-numbers", - "description": "The kind of the component. Must be exactly 'big-numbers'" + "description": "The kind of the component. Must be exactly 'big-numbers'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -447,7 +478,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -466,7 +498,8 @@ "properties": { "kind": { "const": "page-web-view", - "description": "The kind of the component. Must be exactly 'page-web-view'" + "description": "The kind of the component. Must be exactly 'page-web-view'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -482,7 +515,8 @@ "properties": { "kind": { "const": "page-progress", - "description": "The kind of the component. Must be exactly 'page-progress'" + "description": "The kind of the component. Must be exactly 'page-progress'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -493,7 +527,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -507,7 +542,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -526,7 +562,8 @@ "properties": { "kind": { "const": "page-audio", - "description": "The kind of the component. Must be exactly 'page-audio'" + "description": "The kind of the component. Must be exactly 'page-audio'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -542,7 +579,8 @@ "properties": { "kind": { "const": "page-audio-recorder", - "description": "The kind of the component. Must be exactly 'page-audio-recorder'" + "description": "The kind of the component. Must be exactly 'page-audio-recorder'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -553,7 +591,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -567,7 +606,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -581,7 +621,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -595,7 +636,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -614,7 +656,8 @@ "properties": { "kind": { "const": "container", - "description": "The kind of the component. Must be exactly 'container'" + "description": "The kind of the component. Must be exactly 'container'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -625,10 +668,12 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { - "const": "https://picsum.photos/seed/picsum/200/300" + "const": "https://picsum.photos/seed/picsum/200/300", + "type": "string" } }, "required": ["kind", "value"], @@ -644,7 +689,8 @@ "properties": { "kind": { "const": "page-separator", - "description": "The kind of the component. Must be exactly 'page-separator'" + "description": "The kind of the component. Must be exactly 'page-separator'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -660,7 +706,8 @@ "properties": { "kind": { "const": "internal-separator", - "description": "The kind of the component. Must be exactly 'internal-separator'" + "description": "The kind of the component. Must be exactly 'internal-separator'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -676,7 +723,8 @@ "properties": { "kind": { "const": "text", - "description": "The kind of the component. Must be exactly 'text'" + "description": "The kind of the component. Must be exactly 'text'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -687,7 +735,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -706,7 +755,8 @@ "properties": { "kind": { "const": "page-note", - "description": "The kind of the component. Must be exactly 'page-note'" + "description": "The kind of the component. Must be exactly 'page-note'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -717,7 +767,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -731,7 +782,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -750,7 +802,8 @@ "properties": { "kind": { "const": "page-rich-text", - "description": "The kind of the component. Must be exactly 'page-rich-text'" + "description": "The kind of the component. Must be exactly 'page-rich-text'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -761,7 +814,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -780,7 +834,8 @@ "properties": { "kind": { "const": "page-hint", - "description": "The kind of the component. Must be exactly 'page-hint'" + "description": "The kind of the component. Must be exactly 'page-hint'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -791,7 +846,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -805,7 +861,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -824,7 +881,8 @@ "properties": { "kind": { "const": "buttons-block", - "description": "The kind of the component. Must be exactly 'buttons-block'" + "description": "The kind of the component. Must be exactly 'buttons-block'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -840,7 +898,8 @@ "properties": { "kind": { "const": "dynamic-button-bar", - "description": "The kind of the component. Must be exactly 'dynamic-button-bar'" + "description": "The kind of the component. Must be exactly 'dynamic-button-bar'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -851,7 +910,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -865,7 +925,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -884,7 +945,8 @@ "properties": { "kind": { "const": "page-button", - "description": "The kind of the component. Must be exactly 'page-button'" + "description": "The kind of the component. Must be exactly 'page-button'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -895,7 +957,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -914,7 +977,8 @@ "properties": { "kind": { "const": "page-link", - "description": "The kind of the component. Must be exactly 'page-link'" + "description": "The kind of the component. Must be exactly 'page-link'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -925,7 +989,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -939,7 +1004,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -958,7 +1024,8 @@ "properties": { "kind": { "const": "page-action-row", - "description": "The kind of the component. Must be exactly 'page-action-row'" + "description": "The kind of the component. Must be exactly 'page-action-row'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -969,7 +1036,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -983,7 +1051,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -997,10 +1066,12 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { - "const": "https://picsum.photos/seed/picsum/200/300" + "const": "https://picsum.photos/seed/picsum/200/300", + "type": "string" } }, "required": ["kind", "value"], @@ -1016,7 +1087,8 @@ "properties": { "kind": { "const": "page-rating", - "description": "The kind of the component. Must be exactly 'page-rating'" + "description": "The kind of the component. Must be exactly 'page-rating'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1027,7 +1099,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -1041,7 +1114,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -1055,10 +1129,12 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { - "const": "https://picsum.photos/seed/picsum/200/300" + "const": "https://picsum.photos/seed/picsum/200/300", + "type": "string" } }, "required": ["kind", "value"], @@ -1074,7 +1150,8 @@ "properties": { "kind": { "const": "contact-form", - "description": "The kind of the component. Must be exactly 'contact-form'" + "description": "The kind of the component. Must be exactly 'contact-form'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1085,7 +1162,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -1099,7 +1177,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -1118,7 +1197,8 @@ "properties": { "kind": { "const": "form-container", - "description": "The kind of the component. Must be exactly 'form-container'" + "description": "The kind of the component. Must be exactly 'form-container'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1134,7 +1214,8 @@ "properties": { "kind": { "const": "breadcrumbs", - "description": "The kind of the component. Must be exactly 'breadcrumbs'" + "description": "The kind of the component. Must be exactly 'breadcrumbs'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1150,7 +1231,8 @@ "properties": { "kind": { "const": "inline-scanner", - "description": "The kind of the component. Must be exactly 'inline-scanner'" + "description": "The kind of the component. Must be exactly 'inline-scanner'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1161,7 +1243,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -1180,7 +1263,8 @@ "properties": { "kind": { "const": "page-signature-field", - "description": "The kind of the component. Must be exactly 'page-signature-field'" + "description": "The kind of the component. Must be exactly 'page-signature-field'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1191,10 +1275,12 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { - "const": "https://picsum.photos/seed/picsum/200/300" + "const": "https://picsum.photos/seed/picsum/200/300", + "type": "string" } }, "required": ["kind", "value"], @@ -1205,7 +1291,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -1224,7 +1311,8 @@ "properties": { "kind": { "const": "page-stopwatch", - "description": "The kind of the component. Must be exactly 'page-stopwatch'" + "description": "The kind of the component. Must be exactly 'page-stopwatch'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1240,7 +1328,8 @@ "properties": { "kind": { "const": "page-spinner", - "description": "The kind of the component. Must be exactly 'page-spinner'" + "description": "The kind of the component. Must be exactly 'page-spinner'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1256,7 +1345,8 @@ "properties": { "kind": { "const": "ai-custom-component", - "description": "The kind of the component. Must be exactly 'ai-custom-component'" + "description": "The kind of the component. Must be exactly 'ai-custom-component'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1267,7 +1357,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -1281,7 +1372,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" @@ -1300,7 +1392,8 @@ "properties": { "kind": { "const": "page-renderable-content", - "description": "The kind of the component. Must be exactly 'page-renderable-content'" + "description": "The kind of the component. Must be exactly 'page-renderable-content'", + "type": "string" }, "builderDisplayName": { "type": "string", @@ -1311,7 +1404,8 @@ "properties": { "kind": { "const": "string", - "description": "Must be exactly 'string'" + "description": "Must be exactly 'string'", + "type": "string" }, "value": { "type": "string" diff --git a/src/examples.test.ts b/src/examples.test.ts index 0fcb58d..b61b622 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -1,7 +1,10 @@ import { describe, it, expect } from "vitest"; -import { convertJsonSchemaToZod, JSONSchema } from "./index"; +import { convertJsonSchemaToZod } from "./index"; import { z } from "zod/v4"; -import examples from "./examples.json"; +import type { JSONSchema } from "zod/v4/core"; +import jsonExampleData from "./examples.json" assert { type: "json"}; +const examples = jsonExampleData as JSONSchema.BaseSchema[]; + // Helper function to find differences between objects function findDifferences(original: any, result: any, path = ""): string[] { diff --git a/src/handlers/primitive/number.ts b/src/handlers/primitive/number.ts index 1512384..77a57c8 100644 --- a/src/handlers/primitive/number.ts +++ b/src/handlers/primitive/number.ts @@ -38,7 +38,11 @@ export class ExclusiveMinimumHandler implements PrimitiveHandler { if (types.number !== false) { const currentNumber = types.number || z.number(); if (currentNumber instanceof z.ZodNumber) { - types.number = currentNumber.gt(numberSchema.exclusiveMinimum); + if (typeof numberSchema.exclusiveMinimum === "number") { + types.number = currentNumber.gt(numberSchema.exclusiveMinimum); + } else { + types.number = false; + } } } } @@ -52,7 +56,11 @@ export class ExclusiveMaximumHandler implements PrimitiveHandler { if (types.number !== false) { const currentNumber = types.number || z.number(); if (currentNumber instanceof z.ZodNumber) { - types.number = currentNumber.lt(numberSchema.exclusiveMaximum); + if (typeof numberSchema.exclusiveMaximum === "number") { + types.number = currentNumber.lt(numberSchema.exclusiveMaximum); + } else { + types.number = false; + } } } } diff --git a/src/index.test.ts b/src/index.test.ts index bf2f26d..2bac341 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,11 +1,12 @@ import { describe, it, expect } from "vitest"; import { convertJsonSchemaToZod, jsonSchemaObjectToZodRawShape } from "./index"; import { z } from "zod/v4"; +import {JSONSchema} from "zod/v4/core"; describe("convertJsonSchemaToZod", () => { it("should correctly convert a schema with additionalProperties: {}", () => { // Define a simple JSON schema - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { @@ -29,7 +30,7 @@ describe("convertJsonSchemaToZod", () => { it("should correctly convert a schema with additionalProperties: false", () => { // Define a JSON schema with additionalProperties: false - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { @@ -51,7 +52,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly convert a schema with array type", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: { @@ -66,7 +67,7 @@ describe("convertJsonSchemaToZod", () => { describe("Enum handling", () => { it("should correctly convert a schema with string enum (no type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", enum: ["red", "green", "blue"], }; @@ -79,7 +80,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly convert a schema with number enum (no type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", enum: [1, 2, 3], }; @@ -92,11 +93,11 @@ describe("convertJsonSchemaToZod", () => { const resultSchema = z.toJSONSchema(zodSchema); // Zod v4 converts unions to anyOf instead of enum - expect(resultSchema.anyOf).toEqual([{ const: 1 }, { const: 2 }, { const: 3 }]); + expect(resultSchema.anyOf).toEqual([{ type: "number", const: 1 }, { type: "number", const: 2 }, { type: "number", const: 3 }]); }); it("should correctly convert a schema with boolean enum (no type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", enum: [true, false], }; @@ -110,11 +111,11 @@ describe("convertJsonSchemaToZod", () => { const resultSchema = z.toJSONSchema(zodSchema); // Zod v4 converts unions to anyOf instead of enum - expect(resultSchema.anyOf).toEqual([{ const: true }, { const: false }]); + expect(resultSchema.anyOf).toEqual([{ type: "boolean", const: true }, { type: "boolean", const: false }]); }); it("should correctly convert a schema with mixed enum (no type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", enum: ["red", 1, true, null], }; @@ -131,11 +132,11 @@ describe("convertJsonSchemaToZod", () => { const resultSchema = z.toJSONSchema(zodSchema); // Zod v4 converts unions to anyOf instead of enum - expect(resultSchema.anyOf).toEqual([{ const: "red" }, { const: 1 }, { const: true }, { type: "null" }]); + expect(resultSchema.anyOf).toEqual([{ type: "string", const: "red" }, { type: "number", const: 1 }, { type: "boolean", const: true }, { type: "null" }]); }); it("should correctly convert a schema with single item mixed enum (no type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", enum: [42], // Single non-string value }; @@ -153,7 +154,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle empty enum case (no type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", enum: [], // Empty enum }; @@ -168,7 +169,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly convert a schema with string type and enum", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "string", enum: ["red", "green", "blue"], @@ -186,7 +187,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle empty string enum (with type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "string", enum: [], // Empty enum @@ -202,7 +203,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly convert a schema with number type and enum", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "number", enum: [1, 2, 3], @@ -220,7 +221,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly convert a schema with number type and single-item enum", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "number", enum: [42], @@ -237,7 +238,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly convert a schema with boolean type and single-item enum", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "boolean", enum: [true], @@ -258,7 +259,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly convert a schema with boolean type and multiple-item enum", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "boolean", enum: [true, false], @@ -276,7 +277,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle empty number enum (with type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "number", enum: [], // Empty enum @@ -292,7 +293,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle empty boolean enum (with type)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "boolean", enum: [], // Empty enum @@ -311,7 +312,7 @@ describe("convertJsonSchemaToZod", () => { describe("Const value handling", () => { it("should correctly handle string const values", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", const: "fixed value", }; @@ -326,7 +327,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly handle number const values", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", const: 42, }; @@ -341,7 +342,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly handle boolean const values", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", const: true, }; @@ -356,7 +357,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly handle null const values", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", const: null, }; @@ -372,8 +373,9 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle object const values", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", + // @ts-expect-error: @TODO: Resolve, for some reason, the spec and the schema do not align on this const: { key: "value" }, }; @@ -392,8 +394,9 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle array const values", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", + // @ts-expect-error: @TODO: Resolve, for some reason, the spec and the schema do not align on this const: [1, 2, 3], }; @@ -414,7 +417,7 @@ describe("convertJsonSchemaToZod", () => { describe("Combination schemas", () => { it("should correctly handle anyOf", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", anyOf: [{ type: "string" }, { type: "number" }], }; @@ -428,7 +431,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly handle allOf", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", allOf: [ { @@ -457,7 +460,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should correctly handle oneOf", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", oneOf: [{ type: "string" }, { type: "number" }], }; @@ -473,7 +476,7 @@ describe("convertJsonSchemaToZod", () => { describe("Edge cases", () => { it("should handle null type schema", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "null", }; @@ -489,7 +492,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle empty object schema with no properties", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", }; @@ -498,7 +501,7 @@ describe("convertJsonSchemaToZod", () => { expect(() => zodSchema.parse({})).not.toThrow(); expect(() => zodSchema.parse({ extra: "prop" })).not.toThrow(); // By default additionalProperties is true - + // Arrays should be rejected for explicit object type expect(() => zodSchema.parse([])).toThrow(); @@ -507,7 +510,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle array schema with no items", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", }; @@ -522,7 +525,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should return z.any() for empty schema", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", }; @@ -540,7 +543,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should add description to schema", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "string", description: "A test description", @@ -556,7 +559,7 @@ describe("convertJsonSchemaToZod", () => { // Tests for unimplemented but supported features describe("String validation", () => { it("should support minLength and maxLength constraints", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "string", minLength: 3, @@ -564,16 +567,16 @@ describe("convertJsonSchemaToZod", () => { }; const zodSchema = convertJsonSchemaToZod(jsonSchema); - + // Test that the validation works correctly expect(zodSchema.safeParse("hello").success).toBe(true); // 5 chars, within range expect(zodSchema.safeParse("hi").success).toBe(false); // 2 chars, too short expect(zodSchema.safeParse("this is too long").success).toBe(false); // too long - + // Test Unicode support expect(zodSchema.safeParse("💩💩💩").success).toBe(true); // 3 graphemes expect(zodSchema.safeParse("💩").success).toBe(false); // 1 grapheme, too short - + // Note: length constraints implemented with .refine() don't round-trip // back to JSON Schema, so we only test the validation behavior const resultSchema = z.toJSONSchema(zodSchema); @@ -581,10 +584,9 @@ describe("convertJsonSchemaToZod", () => { }); it("should support pattern constraint", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "string", - format: "regex", pattern: "^[a-zA-Z0-9]+$", }; @@ -596,7 +598,7 @@ describe("convertJsonSchemaToZod", () => { describe("Number validation", () => { it("should support minimum and maximum constraints", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "number", minimum: 0, @@ -609,7 +611,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should support exclusiveMinimum and exclusiveMaximum constraints", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "number", exclusiveMinimum: 0, @@ -622,19 +624,19 @@ describe("convertJsonSchemaToZod", () => { }); it("should support multipleOf constraint", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "number", multipleOf: 5, }; const zodSchema = convertJsonSchemaToZod(jsonSchema); - + // Test that the validation works correctly expect(zodSchema.safeParse(10).success).toBe(true); expect(zodSchema.safeParse(15).success).toBe(true); expect(zodSchema.safeParse(7).success).toBe(false); - + // Note: multipleOf constraints implemented with .refine() don't round-trip // back to JSON Schema, so we only test the validation behavior const resultSchema = z.toJSONSchema(zodSchema); @@ -644,7 +646,7 @@ describe("convertJsonSchemaToZod", () => { describe("Array validation", () => { it("should support minItems and maxItems constraints", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: { @@ -660,7 +662,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should support uniqueItems constraint", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: { @@ -683,7 +685,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should support uniqueItems constraint with object items", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: { @@ -717,7 +719,7 @@ describe("convertJsonSchemaToZod", () => { describe("Tuple arrays (items as array)", () => { it("should handle tuple array with different types", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: [{ type: "string" }, { type: "number" }, { type: "boolean" }], @@ -738,7 +740,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle tuple array with single item type", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: [{ type: "string" }], @@ -753,7 +755,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle empty tuple array", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: [], @@ -766,7 +768,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle tuple array with complex item types", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: [ @@ -801,7 +803,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should convert tuple to proper JSON schema", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", items: [{ type: "string" }, { type: "number" }], @@ -818,7 +820,7 @@ describe("convertJsonSchemaToZod", () => { describe("prefixItems (Draft 2020-12 tuples)", () => { it("should handle prefixItems with different types", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", prefixItems: [{ type: "string" }, { type: "number" }, { type: "boolean" }], @@ -843,7 +845,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle prefixItems with single item type", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", prefixItems: [{ type: "string" }], @@ -858,7 +860,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle empty prefixItems array", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", prefixItems: [], @@ -872,7 +874,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle prefixItems with complex nested types", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", prefixItems: [ @@ -926,7 +928,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should validate prefixItems behavior correctly", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", prefixItems: [{ type: "string" }, { type: "number" }], @@ -943,7 +945,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle prefixItems with constraints", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", prefixItems: [ @@ -962,7 +964,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle prefixItems with items: false (strict tuple)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", prefixItems: [{ type: "string" }, { type: "number" }], @@ -978,7 +980,7 @@ describe("convertJsonSchemaToZod", () => { }); it("should handle prefixItems with items schema (constrained additional items)", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "array", prefixItems: [{ type: "string" }, { type: "number" }], @@ -1000,17 +1002,19 @@ describe("convertJsonSchemaToZod", () => { describe("jsonSchemaObjectToZodRawShape", () => { it("should extract properties from a JSON schema", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { type: "object", properties: { name: { type: "string" }, age: { type: "integer" }, isActive: { type: "boolean" }, + // @ts-expect-error: invalid schema definition to test behaviour missing: undefined, }, required: ["name", "age"], }; + // @ts-expect-error: invalid schema definition to test behaviour const rawShape = jsonSchemaObjectToZodRawShape(jsonSchema); // Properties should exist in the raw shape @@ -1025,28 +1029,30 @@ describe("jsonSchemaObjectToZodRawShape", () => { }); it("should handle empty properties", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { type: "object", properties: {}, }; + // @ts-expect-error: invalid schema definition to test behaviour const rawShape = jsonSchemaObjectToZodRawShape(jsonSchema); expect(Object.keys(rawShape).length).toBe(0); expect(rawShape).toEqual({}); }); it("should handle missing properties field", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { type: "object", }; + // @ts-expect-error: invalid schema definition to test behaviour const rawShape = jsonSchemaObjectToZodRawShape(jsonSchema); expect(Object.keys(rawShape).length).toBe(0); expect(rawShape).toEqual({}); }); it("should correctly convert nested object properties", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { type: "object", properties: { user: { @@ -1054,6 +1060,7 @@ describe("jsonSchemaObjectToZodRawShape", () => { properties: { name: { type: "string" }, email: { type: "string" }, + // @ts-expect-error: invalid schema definition to test behaviour missing: undefined, }, required: ["name"], @@ -1061,6 +1068,7 @@ describe("jsonSchemaObjectToZodRawShape", () => { }, }; + // @ts-expect-error: invalid schema definition to test behaviour const rawShape = jsonSchemaObjectToZodRawShape(jsonSchema); expect(rawShape).toHaveProperty("user"); @@ -1085,7 +1093,8 @@ describe("jsonSchemaObjectToZodRawShape", () => { }); it("should be usable to build custom schemas", () => { - const jsonSchema = { + const jsonSchema: JSONSchema.BaseSchema = { + type: "object", properties: { name: { type: "string" }, age: { type: "integer" }, @@ -1093,6 +1102,7 @@ describe("jsonSchemaObjectToZodRawShape", () => { }; // Get the raw shape + // @ts-expect-error: invalid schema definition to test behaviour const rawShape = jsonSchemaObjectToZodRawShape(jsonSchema); // Make fields optional manually and add custom fields @@ -1122,5 +1132,8 @@ describe("jsonSchemaObjectToZodRawShape", () => { // Test refinement with invalid age expect(() => customSchema.parse({ age: 16 })).toThrow(); + + // Refinement with correct age, using same format as above test to confirm error was throw for correct reason + expect(() => customSchema.parse({ age: 19 })).not.toThrow(); }); }); diff --git a/src/index.ts b/src/index.ts index b8c1344..f08052a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,13 +8,42 @@ export { convertJsonSchemaToZod }; // Export utilities that tests might depend on export { createUniqueItemsValidator, isValidWithSchema } from "./core/utils"; +// Helper type to infer Zod types from JSON Schema properties +type InferZodTypeFromJsonSchema = + T extends { type: "string" } ? z.ZodString : + T extends { type: "number" } ? z.ZodNumber : + T extends { type: "integer" } ? z.ZodNumber : + T extends { type: "boolean" } ? z.ZodBoolean : + T extends { type: "null" } ? z.ZodNull : + T extends { type: "array" } ? z.ZodArray : + T extends { type: "object" } ? z.ZodObject : + T extends { const: any } ? z.ZodLiteral : + T extends { enum: readonly any[] } ? z.ZodEnum : + z.ZodTypeAny; + +// Helper type to map JSON Schema properties to Zod raw shape +type InferZodRawShape> = { + [K in keyof T]: InferZodTypeFromJsonSchema +}; + +/** + * Converts a JSON Schema object to a Zod raw shape with proper typing + * @param schema The JSON Schema object to convert + * @returns A Zod raw shape for use with z.object() with inferred types + */ +export function jsonSchemaObjectToZodRawShape< + T extends JSONSchema.Schema & { properties: Record } +>(schema: T): InferZodRawShape; + /** * Converts a JSON Schema object to a Zod raw shape * @param schema The JSON Schema object to convert * @returns A Zod raw shape for use with z.object() */ -export function jsonSchemaObjectToZodRawShape(schema: JSONSchema.Schema): z.ZodRawShape { - const raw: Record = {}; +export function jsonSchemaObjectToZodRawShape(schema: JSONSchema.Schema): Record; + +export function jsonSchemaObjectToZodRawShape(schema: JSONSchema.Schema): Record { + const raw: Record = {}; for (const [key, value] of Object.entries(schema.properties ?? {})) { if (value === undefined) continue;