Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b92eea2
chore: update zod and devDeps
brettz9 Aug 9, 2025
9c3805f
fix: make properties of file optional (and rmv unused code)
brettz9 Nov 19, 2025
7cbe960
test: check tests
brettz9 Nov 19, 2025
5c034c1
docs: copy README for npm; also link to repo and issues in package.json
brettz9 Nov 19, 2025
64e07bf
chore: use at least Node 20 for actions
brettz9 Nov 19, 2025
e2e0e5f
test: check new literal array argument
brettz9 Nov 20, 2025
fad52a5
fix: use preferred (equivalent) method for descriptive metadata
brettz9 Nov 20, 2025
3a9c8d8
feat: restore SzFunction with Zod 4 restoring ZodFunction behavior
brettz9 Nov 20, 2025
02b1c1b
feat: support metadata
brettz9 Nov 20, 2025
646eed4
feat: allow `any` to add custom checks and errors
brettz9 Nov 20, 2025
db587d3
docs: mention other Zodex-related projects
brettz9 Nov 21, 2025
c90503f
chore: updates
brettz9 Nov 24, 2025
2409850
chore: change from deprecated tseslint method
brettz9 Nov 28, 2025
0cf7311
feat: support xor and looseRecord for zod 4.2.1
brettz9 Dec 15, 2025
7cfc7ae
chore: update devDeps
brettz9 Dec 28, 2025
230ead2
feat: support zod 4.3 min/max on maps
brettz9 Dec 31, 2025
cc588d1
chore: update zod and devDeps
brettz9 Jan 17, 2026
c689b4e
chore: update to zod 4.4.3
brettz9 Apr 29, 2026
611e067
fix: add ZodPreprocess to ZodTypes
brettz9 Jul 28, 2026
9444767
chore: update CI actions
brettz9 Aug 5, 2026
9db5819
chore: simplify CI to avoid path error
brettz9 Aug 5, 2026
cb524b4
chore: fix CI Node version
brettz9 Aug 5, 2026
d4f5ac6
chore: update packageManager field
brettz9 Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 4 additions & 19 deletions .github/actions/node/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,14 @@ description: "Node.js & pnpm setup"
runs:
using: "composite"
steps:
- uses: actions/setup-node@v3
with:
node-version: "18.x"
registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
run_install: false

- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

- uses: actions/cache@v3
name: Setup pnpm cache
- uses: actions/setup-node@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
node-version: "22.x"
registry-url: "https://registry.npmjs.org"
cache: "pnpm"

- name: Install dependencies
shell: bash
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ jobs:
check-style:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/node
- run: pnpm check-style

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/node
- run: pnpm lint

Expand All @@ -28,7 +28,7 @@ jobs:
run:
working-directory: lib
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/node
- run: pnpm test

Expand All @@ -38,7 +38,7 @@ jobs:
run:
working-directory: lib
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/node
- run: pnpm build

Expand All @@ -50,10 +50,10 @@ jobs:
working-directory: lib
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: ./.github/actions/node
with:
node-version: 18.x
node-version: 22.x
- uses: go-semantic-release/action@v1
id: semrel
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ dist/
.eslintcache
coverage
.idea
lib/README.md
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,8 @@ or target the whole object or individual properties.
`refine()`, `custom()` and `instanceof` methods (and they will be
ignored), but these are really just implementations of `check()`
which is supported

## Other projects

- [json-schema-to-zodex](https://github.com/brettz9/json-schema-to-zodex)
- [zod-to-json-schema](https://github.com/StefanTerdell/zod-to-json-schema)
16 changes: 8 additions & 8 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"zod": "^3.25.64",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"zod": "^4.3.6",
"zodex": "workspace:*"
},
"devDependencies": {
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2",
"typescript": "^5.8.3",
"vite": "^6.3.5"
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"typescript": "^6.0.3",
"vite": "^8.0.10"
}
}
2 changes: 1 addition & 1 deletion demo/tsconfig.app.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts"],"version":"5.8.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts"],"version":"6.0.3"}
2 changes: 1 addition & 1 deletion demo/tsconfig.node.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./vite.config.ts"],"version":"5.8.3"}
{"root":["./vite.config.ts"],"version":"6.0.3"}
11 changes: 9 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";

export default tseslint.config(
export default defineConfig(
{
ignores: ["lib/schema.zodex.json", "**/dist/", ".idea", "coverage"],
ignores: [
"lib/schema.zodex.json",
"**/dist/",
".idea",
"coverage",
"**/*.cjs",
],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
Expand Down
14 changes: 14 additions & 0 deletions lib/.ncurc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use strict";

module.exports = {
reject: [
// Until package is ESM-only
"type-fest",

// Until typescript-eslint supports @7
"typescript",

// Preference to avoid updating?
"prettier",
],
};
124 changes: 95 additions & 29 deletions lib/dezerialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
SzUnion,
SzDiscriminatedUnion,
SzIntersection,
SzXor,
SzTuple,
SzRecord,
SzLooseRecord,
SzMap,
SzSet,
SzEnum,
Expand Down Expand Up @@ -147,48 +149,56 @@ export type Dezerialize<T extends SzType | SzRef> = T extends SzRef
Dezerialize<Key>,
Dezerialize<Value>
>
: T extends SzMap<
: T extends SzLooseRecord<
infer Key,
infer Value
>
? z.ZodMap<
? z.ZodRecord<
Dezerialize<Key>,
Dezerialize<Value>
> // Enum
: T extends SzEnum<
infer Values
>
: T extends SzMap<
infer Key,
infer Value
>
? z.ZodEnum<Values> // Union/Intersection
: T extends SzUnion<
infer _Options
? z.ZodMap<
Dezerialize<Key>,
Dezerialize<Value>
> // Enum
: T extends SzEnum<
infer Values
>
? z.ZodUnion<any>
: T extends SzDiscriminatedUnion<
infer Discriminator,
? z.ZodEnum<Values> // Union/Intersection
: T extends SzUnion<
infer _Options
>
? z.ZodDiscriminatedUnion<any>
: T extends SzIntersection<
infer L,
infer R
? z.ZodUnion<any>
: T extends SzDiscriminatedUnion<
infer Discriminator,
infer _Options
>
? z.ZodIntersection<
Dezerialize<L>,
Dezerialize<R>
> // Specials
: T extends SzPromise<
infer Value
>
? z.ZodPromise<
Dezerialize<Value>
? z.ZodDiscriminatedUnion<any>
: T extends SzIntersection<
infer L,
infer R
>
: T extends SzCatch<
? z.ZodIntersection<
Dezerialize<L>,
Dezerialize<R>
> // Specials
: T extends SzPromise<
infer Value
>
? z.ZodCatch<
? z.ZodPromise<
Dezerialize<Value>
>
: any; // unknown;
: T extends SzCatch<
infer Value
>
? z.ZodCatch<
Dezerialize<Value>
>
: any; // unknown;

type DezerializersMap = {
[T in SzType["type"]]: (
Expand Down Expand Up @@ -230,6 +240,14 @@ const getError = (shape: SzType, opts: DezerializerOptions) => {
const d = dezerializeRefs;

const dezerializers = {
function: (shape, opts) => {
const i = z.function({
input: d(shape.input, opts) as any,
output: d(shape.output, opts),
});

return getCustomChecks(i, shape, opts);
},
number: (shape, opts) => {
const method =
shape.format && NUMBER_FORMATS.has(shape.format)
Expand Down Expand Up @@ -527,8 +545,25 @@ const dezerializers = {
opts.pathToSchema.set(opts.path, i);
return getCustomChecks(i, shape, opts);
}) as any,
looseRecord: ((shape: SzLooseRecord, opts: DezerializerOptions) => {
const i = z.looseRecord(
checkRef(shape.key, opts) ||
(d(shape.key, {
...opts,
path: opts.path + "/key",
}) as z.ZodString | z.ZodNumber | z.ZodSymbol),
checkRef(shape.value, opts) ||
d(shape.value, {
...opts,
path: opts.path + "/value",
}),
getError(shape, opts),
);
opts.pathToSchema.set(opts.path, i);
return getCustomChecks(i, shape, opts);
}) as any,
map: ((shape: SzMap<any, any>, opts: DezerializerOptions) => {
const i = z.map(
let i = z.map(
checkRef(shape.key, opts) ||
d(shape.key, {
...opts,
Expand All @@ -542,6 +577,14 @@ const dezerializers = {
getError(shape, opts),
);

if ("min" in shape && shape.min !== undefined) {
i = i.min(shape.min);
}

if ("max" in shape && shape.max !== undefined) {
i = i.max(shape.max);
}

opts.pathToSchema.set(opts.path, i);
return getCustomChecks(i, shape, opts);
}) as any,
Expand Down Expand Up @@ -583,6 +626,21 @@ const dezerializers = {
opts.pathToSchema.set(opts.path, i);
return getCustomChecks(i, shape, opts);
}) as any,
xor: ((shape: SzXor, opts: DezerializerOptions) => {
const i = z.xor(
shape.options.map(
(opt, idx) =>
checkRef(opt, opts) ||
d(opt, {
...opts,
path: opts.path + "/options/" + idx,
}),
) as any,
getError(shape, opts),
);
opts.pathToSchema.set(opts.path, i);
return getCustomChecks(i, shape, opts);
}) as any,
intersection: ((shape: SzIntersection, opts: DezerializerOptions) => {
const i = z.intersection(
checkRef(shape.left, opts) ||
Expand Down Expand Up @@ -699,7 +757,15 @@ export function dezerializeRefs(
if ("description" in shape && typeof shape.description === "string") {
const { description, ...rest } = shape;
const inner = d(rest, opts);
const result = inner.describe(description);
const result = inner.meta({ description });
opts.pathToSchema.set(opts.path, result);
return result;
}

if ("meta" in shape) {
const { meta, ...rest } = shape;
const inner = d(rest, opts);
const result = meta ? inner.meta(meta) : inner;
opts.pathToSchema.set(opts.path, result);
return result;
}
Expand Down
Loading
Loading