From 888b4e8f7c44e4248f19f35393bb0ed2bc76832f Mon Sep 17 00:00:00 2001 From: Nenad Krneta Date: Wed, 22 Jul 2026 17:59:06 +0400 Subject: [PATCH] feat(validator-zod)!: upgrade zod to v4 BREAKING CHANGE: bump Zod peer dependency to ^4.0.0 and devDependency to ^4.4.3. Update source/tests for Zod v4 issue shapes and error messages. Replace ZodSchema with ZodType and .nonempty() with .min(1). Update root devDependency zod to ^4.4.3. --- .changeset/upgrade-zod-v4.md | 5 ++ package.json | 2 +- packages/validator-zod/README.md | 26 +++++----- packages/validator-zod/package.json | 4 +- packages/validator-zod/src/index.ts | 31 ++++++----- .../validator-zod/tests/validator.spec.ts | 51 +++++++++++-------- pnpm-lock.yaml | 18 +++---- 7 files changed, 76 insertions(+), 61 deletions(-) create mode 100644 .changeset/upgrade-zod-v4.md diff --git a/.changeset/upgrade-zod-v4.md b/.changeset/upgrade-zod-v4.md new file mode 100644 index 00000000..7c29027b --- /dev/null +++ b/.changeset/upgrade-zod-v4.md @@ -0,0 +1,5 @@ +--- +'@felte/validator-zod': major +--- + +BREAKING: upgrade Zod peer dependency to v4. Zod v3 schemas are no longer supported; update imports to use `ZodType` instead of `ZodSchema` and replace `.nonempty()` with `.min(1)`. diff --git a/package.json b/package.json index 9f089016..b5300ef1 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "vite-plugin-solid": "^2.7.0", "vitest": "^2.1.4", "yup": "^1.2.0", - "zod": "^1.11.13" + "zod": "^4.4.3" }, "volta": { "node": "22.10.0" diff --git a/packages/validator-zod/README.md b/packages/validator-zod/README.md index 4ddec1ab..7f004cf2 100644 --- a/packages/validator-zod/README.md +++ b/packages/validator-zod/README.md @@ -26,8 +26,8 @@ import { validator } from '@felte/validator-zod'; import { z } from 'zod'; const schema = z.object({ - email: z.string().email().nonempty(), - password: z.string().nonempty(), + email: z.string().email().min(1), + password: z.string().min(1), }); const { form } = createForm({ @@ -44,8 +44,8 @@ import { validateSchema } from '@felte/validator-zod'; import { z } from 'zod'; const schema = z.object({ - email: z.string().email().nonempty(), - password: z.string().nonempty(), + email: z.string().email().min(1), + password: z.string().min(1), }); const { form } = createForm({ @@ -64,17 +64,15 @@ import { validator } from '@felte/validator-zod'; import { z } from 'zod'; const schema = z.object({ - email: z.string().email().nonempty(), - password: z.string().nonempty(), + email: z.string().email().min(1), + password: z.string().min(1), }); // We only warn if the user has started typing a value -const warnSchema = zod.object({ - password: zod - .string() - .refine((value) => (value ? value.length > 8 : true), { - message: 'Password is not secure', - }), +const warnSchema = z.object({ + password: z.string().refine((value) => (value ? value.length > 8 : true), { + message: 'Password is not secure', + }), }); const { form } = createForm({ @@ -95,8 +93,8 @@ Zod allows you to infer the type of your schema using `z.infer`. This can be use import { z } from 'zod'; const schema = z.object({ - email: z.string().email().nonempty(), - password: z.string().nonempty(), + email: z.string().email().min(1), + password: z.string().min(1), }); const { form } = createForm>(/* ... */); diff --git a/packages/validator-zod/package.json b/packages/validator-zod/package.json index 80d10e51..7efaaede 100644 --- a/packages/validator-zod/package.json +++ b/packages/validator-zod/package.json @@ -44,10 +44,10 @@ "@felte/core": "workspace:*", "felte": "workspace:*", "svelte": "^3.46.4", - "zod": "^3.2.0" + "zod": "^4.4.3" }, "peerDependencies": { - "zod": "^3.2.0" + "zod": "^4.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/validator-zod/src/index.ts b/packages/validator-zod/src/index.ts index 8b236f19..1a7b2a0b 100644 --- a/packages/validator-zod/src/index.ts +++ b/packages/validator-zod/src/index.ts @@ -7,25 +7,30 @@ import type { Extender, } from '@felte/common'; import { _update } from '@felte/common'; -import type { ZodError, ZodSchema } from 'zod'; +import type { ZodError, ZodType } from 'zod'; -export type ValidatorConfig = { - schema: ZodSchema; +export type ValidatorConfig = { + schema: ZodType; level?: 'error' | 'warning'; }; export function validateSchema( - schema: ZodSchema + schema: ZodType, ): ValidationFunction { function walk( - error: ZodError, - err: AssignableErrors + issues: ZodError['issues'], + err: AssignableErrors, ): AssignableErrors { - for (const issue of error.issues) { - if (issue.code === 'invalid_union') { - for (const unionError of issue.unionErrors) { - err = walk(unionError, err); + for (const issue of issues) { + if (issue.code === 'invalid_union' && issue.errors.length) { + for (const optionIssues of issue.errors) { + err = walk(optionIssues, err); } + } else if ( + issue.code === 'invalid_key' || + issue.code === 'invalid_element' + ) { + err = walk(issue.issues, err); } else { if (!issue.path) continue; @@ -43,12 +48,12 @@ export function validateSchema( } return async function validate( - values: Data + values: Data, ): Promise | undefined> { const result = await schema.safeParseAsync(values); if (!result.success) { let err = {} as AssignableErrors; - err = walk(result.error, err); + err = walk(result.error.issues, err); return err; } }; @@ -59,7 +64,7 @@ export function validator({ level = 'error', }: ValidatorConfig): Extender { return function extender( - currentForm: CurrentForm + currentForm: CurrentForm, ): ExtenderHandler { if (currentForm.stage !== 'SETUP') return {}; const validateFn = validateSchema(schema); diff --git a/packages/validator-zod/tests/validator.spec.ts b/packages/validator-zod/tests/validator.spec.ts index daa7d3e2..9786e4f2 100644 --- a/packages/validator-zod/tests/validator.spec.ts +++ b/packages/validator-zod/tests/validator.spec.ts @@ -2,7 +2,7 @@ import '@testing-library/jest-dom/vitest'; import { expect, describe, test, vi } from 'vitest'; import { createForm } from './common'; import { validateSchema, validator } from '../src'; -import { z, ZodSchema } from 'zod'; +import { z, ZodType } from 'zod'; import { get } from 'svelte/store'; type Data = { @@ -10,6 +10,9 @@ type Data = { password: string; }; +const ZOD_EMAIL_ERROR = 'Invalid email address'; +const ZOD_MIN_ERROR = 'Too small: expected string to have >=1 characters'; + describe('Validator zod', () => { test('correctly validates', async () => { const schema = z.object({ @@ -30,8 +33,8 @@ describe('Validator zod', () => { expect(get(data)).to.deep.equal(mockData); expect(get(errors)).to.deep.equal({ - email: ['Invalid email', 'String must contain at least 1 character(s)'], - password: ['String must contain at least 1 character(s)'], + email: [ZOD_EMAIL_ERROR, ZOD_MIN_ERROR], + password: [ZOD_MIN_ERROR], }); data.set({ @@ -71,8 +74,8 @@ describe('Validator zod', () => { expect(get(data)).to.deep.equal(mockData); expect(get(errors)).to.deep.equal({ account: { - email: ['Invalid email', 'String must contain at least 1 character(s)'], - password: ['String must contain at least 1 character(s)'], + email: [ZOD_EMAIL_ERROR, ZOD_MIN_ERROR], + password: [ZOD_MIN_ERROR], }, }); @@ -122,8 +125,8 @@ describe('Validator zod', () => { expect(get(data)).to.deep.equal(mockData); expect(get(errors)).to.deep.equal({ - email: ['Invalid email', 'String must contain at least 1 character(s)'], - password: ['String must contain at least 1 character(s)'], + email: [ZOD_EMAIL_ERROR, ZOD_MIN_ERROR], + password: [ZOD_MIN_ERROR], }); expect(get(warnings)).to.deep.equal({ email: null, @@ -171,8 +174,8 @@ describe('Validator zod', () => { expect(get(data)).to.deep.equal(mockData); expect(get(errors)).to.deep.equal({ account: { - email: ['Invalid email', 'String must contain at least 1 character(s)'], - password: ['String must contain at least 1 character(s)'], + email: [ZOD_EMAIL_ERROR, ZOD_MIN_ERROR], + password: [ZOD_MIN_ERROR], }, }); @@ -222,12 +225,8 @@ describe('Validator zod', () => { expect(get(data)).to.deep.equal(mockData); expect(get(errors)).to.deep.equal({ account: { - email: [ - 'not an email', - 'Invalid email', - 'String must contain at least 1 character(s)', - ], - password: ['String must contain at least 1 character(s)'], + email: ['not an email', ZOD_EMAIL_ERROR, ZOD_MIN_ERROR], + password: [ZOD_MIN_ERROR], }, }); @@ -326,7 +325,7 @@ describe('Validator zod', () => { }); test('should surface union type errors', async () => { - async function t(schema: ZodSchema, initialValues: object) { + async function t(schema: ZodType, initialValues: object) { const { validate, errors } = createForm({ initialValues, extend: validator({ schema }), @@ -338,14 +337,20 @@ describe('Validator zod', () => { const schema = z.object({ foo: z.string().min(1) }); const data = { foo: '' }; - const unionErrors = await t(z.union([schema, schema]), data); const errors = await t(schema, data); + const unionErrors = await t( + z.union([schema, z.object({ foo: z.string().min(2) })]), + data, + ); - expect(unionErrors).to.deep.equal(errors); + expect(errors).to.deep.equal({ foo: [ZOD_MIN_ERROR] }); + expect(unionErrors).to.deep.equal({ + foo: [ZOD_MIN_ERROR, 'Too small: expected string to have >=2 characters'], + }); }); test('should surface discriminatedUnion type errors', async () => { - async function t(schema: ZodSchema, initialValues: object) { + async function t(schema: ZodType, initialValues: object) { const { validate, errors } = createForm({ initialValues, extend: validator({ schema }), @@ -356,11 +361,13 @@ describe('Validator zod', () => { const schema = z.discriminatedUnion('type', [ z.object({ type: z.literal('foo'), foo: z.string().min(1) }), - z.object({ type: z.literal('bar'), bar: z.string().min(1) }) - ], { errorMap: () => ({ message: 'Oops' }) }); + z.object({ type: z.literal('bar'), bar: z.string().min(1) }), + ]); const errors = await t(schema, { type: 'baz' }); - expect(errors).to.deep.equal({ type: ['Oops'] }); + expect(errors).to.deep.equal({ + type: ["Invalid discriminator value. Expected 'foo' | 'bar'"], + }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 489312a3..e68203ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,8 +199,8 @@ importers: specifier: ^1.2.0 version: 1.4.0 zod: - specifier: ^1.11.13 - version: 1.11.17 + specifier: ^4.4.3 + version: 4.4.3 examples/lit/basic: dependencies: @@ -907,8 +907,8 @@ importers: specifier: ^3.46.4 version: 3.59.2 zod: - specifier: ^3.2.0 - version: 3.23.8 + specifier: ^4.4.3 + version: 4.4.3 packages/vanilla: dependencies: @@ -7973,12 +7973,12 @@ packages: typescript: ^4.9.4 || ^5.0.2 zod: ^3 - zod@1.11.17: - resolution: {integrity: sha512-UzIwO92D0dSFwIRyyqAfRXICITLjF0IP8tRbEK/un7adirMssWZx8xF/1hZNE7t61knWZ+lhEuUvxlu2MO8qqA==} - zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -16026,8 +16026,8 @@ snapshots: typescript: 5.6.3 zod: 3.23.8 - zod@1.11.17: {} - zod@3.23.8: {} + zod@4.4.3: {} + zwitch@2.0.4: {}