Skip to content

Feature/zod v4#288

Merged
H-Weisner merged 12 commits into
mainfrom
feature/zod-v4
Apr 7, 2026
Merged

Feature/zod v4#288
H-Weisner merged 12 commits into
mainfrom
feature/zod-v4

Conversation

@H-Weisner

Copy link
Copy Markdown
Contributor

What's new?

This pull request upgrades the zod validation library from version 3 to version 4 and updates the codebase to ensure compatibility with the new version. The changes include dependency updates, type adjustments, and modifications to how validation errors are handled and logged.

Dependency upgrade:

  • Upgraded zod from version 3.x to 4.x in both dependencies and devDependencies in package.json, and updated the lockfile accordingly. [1] [2] [3] [4] [5]

Code compatibility updates:

  • Updated all type imports and usages from zod to use the new type names and structures (e.g., replaced ZodTypeAny with ZodType, adjusted object/array schema typing, and removed deprecated types). [1] [2] [3] [4] [5]
  • Updated the getMyZodErrors function and related validation logic to use the new error structure (issues instead of errors) and updated type signatures. [1] [2]

Validation and logging improvements:

  • Enhanced logging in useForm to use z.prettifyError for clearer error output, and updated the logic to match the new structure of validation results in Zod v4.

General code cleanup:

  • Removed unused or deprecated imports and adjusted function signatures for improved type safety and clarity. [1] [2]

These changes ensure the codebase is compatible with Zod v4 and take advantage of improved type safety and error reporting.

Ticket number(s) in JIRA (if internal)

ARM-??

board

Checklist

  • [✓] does this work have all the relevant tests?
  • [✓] are your changes in Storybook?
  • [✓] does everything have jsdoc?
  • [✓] is everything exported from index.ts?

Updates the zod peer dependency from v3 to v4 and adapts all form validation utilities to the revised API. Key changes: `ZodTypeAny` replaced by `ZodType`, internal `._def` sentinel replaced by `._zod`, parse result errors accessed via `.issues` instead of `.errors`, and `ZodEffects` / `ZodNativeEnum` removed from public type unions which no longer exist in v4's type system.

BREAKING CHANGE: peerDependency `zod` bumped from `3.*` to `4.*`; consumers must upgrade to Zod v4
…rove validation error logging

Zod v4 deprecated ZodRawShape in favour of the internal $ZodShape type from zod/v4/core. Updates all type casts and interface definitions to use the new type. Also improves the logSchemaErrors output to use z.prettifyError for human-readable error formatting instead of dumping raw validation internals.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR upgrades the form validation layer to be compatible with Zod v4, updating dependency versions and adjusting schema/error handling where Zod’s APIs and type surface have changed.

Changes:

  • Bumps Zod from v3 to v4 in the package manifest and lockfile.
  • Updates schema typing/helpers to use Zod v4 types and internal shape/issue structures.
  • Updates useForm validation flow to read error.issues and improves schema error logging via z.prettifyError.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
module/src/form/utils/validation.ts Updates schema-to-zod conversion typing and switches error mapping to Zod v4 issues.
module/src/form/types.ts Updates exported form/zod helper types for Zod v4 compatibility.
module/src/form/hooks/useForm.ts Switches to error.issues and improves optional validation error logging output.
module/pnpm-lock.yaml Locks updated Zod version and adds @types/node.
module/package.json Updates Zod version ranges and adds @types/node dev dependency.
Files not reviewed (1)
  • module/pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 103 to 106
if (typeof incomingToZod === 'object') {
return z.object(unpackObject(incomingToZod as z.ZodRawShape));
return z.object(unpackObject(incomingToZod as z.core.$ZodShape));
}
return unpackValueToZod(incomingToZod);

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unpackValueToZod can hit an infinite recursion for non-object, non-zod inputs (the final return unpackValueToZod(incomingToZod); never changes the value). Also, typeof incomingToZod === 'object' includes null, which would throw when unpackObject calls Object.keys. Consider handling null/primitives explicitly (e.g. throw a clear error for invalid schema values) instead of recursing.

Copilot uses AI. Check for mistakes.
Comment on lines +124 to 132
export const getMyZodErrors = (errors: z.core.$ZodIssue[], keyChainString?: string) => {
return errors
.filter(e => (keyChainString ? isMyKeyChainItem(keyStringFromKeyChain(e.path, 'dots'), keyChainString) : true))
.filter(e =>
keyChainString ? isMyKeyChainItem(keyStringFromKeyChain(e.path as KeyChain, 'dots'), keyChainString) : true
)
.map(e => ({
key: keyStringFromKeyChain(e.path, 'dots'),
key: keyStringFromKeyChain(e.path as KeyChain, 'dots'),
message: e.message,
}));

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getMyZodErrors was updated for Zod v4 issues, but there are no unit tests covering its filtering/mapping behavior (especially important after switching from error.errors to error.issues and adjusting path handling). Add targeted tests that build a failing schema, call safeParse, and assert the returned {key,message} list for both root and nested keyChain filters.

Copilot uses AI. Check for mistakes.
Comment thread module/src/form/types.ts Outdated
| ZodOptional<ZodNullable<ZodTypeAny>>
| ZodEffects<ZodTypeAny>;
ob: ZodObject<TProp & $ZodShape>
) => ZodObject<$ZodShape> | ZodNullable<ZodType> | ZodOptional<ZodType> | ZodOptional<ZodNullable<ZodType>>;

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IObjectOfZod.opts returns ZodObject<$ZodShape>, which drops the original object shape type (TProp) and can degrade type inference for callers chaining object validators. Consider preserving the generic (e.g. returning ZodObject<TProp & $ZodShape> or a generic ZodObject<...> tied to the input) so opts doesn't erase the object's typed shape.

Suggested change
) => ZodObject<$ZodShape> | ZodNullable<ZodType> | ZodOptional<ZodType> | ZodOptional<ZodNullable<ZodType>>;
) => ZodObject<TProp & $ZodShape> | ZodNullable<ZodType> | ZodOptional<ZodType> | ZodOptional<ZodNullable<ZodType>>;

Copilot uses AI. Check for mistakes.
Comment thread module/package.json Outdated
"react": "19.x",
"react-dom": "19.x",
"zod": "3.*"
"zod": "4.*"

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zod version ranges differ between peerDependencies (4.*) and devDependencies (^4.3.6). This can allow consumers to resolve older 4.x minors than the version you develop/test against, increasing the risk of runtime/type mismatches. Consider aligning the ranges (e.g. set the peer range to the same minimum you require) and keeping a single source of truth for the supported Zod v4 baseline.

Suggested change
"zod": "4.*"
"zod": "^4.3.6"

Copilot uses AI. Check for mistakes.
Comment thread module/package.json
Comment on lines 22 to 27
"peerDependencies": {
"date-fns": "2.x",
"react": "19.x",
"react-dom": "19.x",
"zod": "3.*"
"zod": "4.*"
},

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says Zod was upgraded in "dependencies" and "devDependencies", but in package.json it's in peerDependencies (and devDependencies). If this is a published library change, consider updating the PR description (or the dependency section, if intended) to match how Zod is actually declared.

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • module/pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

module/src/form/types.ts:26

  • There’s a stray ß character in this doc comment (type.ß), which looks like an accidental typo and will leak into generated docs / IDE tooltips. Please remove it.
/**
 * Works out whether some data is an object, and array, or another type.ß
 * Used by `formProp` to type the next argument in the array.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 103 to 107
if (typeof incomingToZod === 'object') {
return z.object(unpackObject(incomingToZod as z.ZodRawShape));
return z.object(unpackObject(incomingToZod as z.core.$ZodShape));
}
return unpackValueToZod(incomingToZod);
};

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unpackValueToZod ends with return unpackValueToZod(incomingToZod);, which will recurse forever for any non-object value that reaches this point (e.g. undefined) and eventually stack overflow. Consider making the fallback a hard error (throw) or returning incomingToZod (or z.any()) so the function has a terminating base case.

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +100
// eslint-disable-next-line no-underscore-dangle -- these are zod values
if ((incomingToZod as z.ZodAny)._zod) {

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check if ((incomingToZod as z.ZodAny)._zod) relies on a private/underscored internal property to detect Zod schemas. This is brittle across Zod releases and can break validation schema conversion if the internal marker changes. Prefer a public/stable detection approach (e.g. incomingToZod instanceof z.ZodType or another officially supported predicate) to avoid coupling to Zod internals.

Suggested change
// eslint-disable-next-line no-underscore-dangle -- these are zod values
if ((incomingToZod as z.ZodAny)._zod) {
if (incomingToZod instanceof z.ZodType) {

Copilot uses AI. Check for mistakes.
Comment on lines 95 to 97
if (isObjectOfZod(incomingToZod)) {
const obInner = unpackValueToZod(incomingToZod.schema) as z.ZodObject<z.ZodRawShape>;
const obInner = unpackValueToZod(incomingToZod.schema) as z.ZodObject<z.core.$ZodShape>;
return incomingToZod.opts ? incomingToZod.opts(obInner) : obInner;

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file now uses internal Zod v4 core types via z.core.$ZodShape / z.core.$ZodIssue. These $Zod* types are part of Zod’s internal core surface and increase the chance of breakage on patch upgrades. If possible, prefer public types exported from zod (or consistently import types from zod/v4/core in one place) rather than mixing z.core.* references in type assertions/signatures.

Copilot uses AI. Check for mistakes.
# Conflicts:
#	module/src/form/hooks/useForm.ts
Tightens the zod peer dependency from the broad `4.*` wildcard to `^4.3.6` to ensure a minimum compatible version is enforced while still allowing compatible minor/patch updates.
Base automatically changed from feature/typescript-v6 to main April 7, 2026 13:55
@H-Weisner H-Weisner requested a review from Copilot April 7, 2026 14:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • module/pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread module/src/form/types.ts Outdated
Comment on lines 22 to 23
import type { $ZodShape } from 'zod/v4/core';

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing $ZodShape from the deep path zod/v4/core couples this library's public type surface to a non-public Zod module path. Because types.ts is part of the emitted .d.ts, this can break consumers if the subpath isn't exported (or changes) in their installed Zod v4. Prefer using only public types exported from zod (or define your own shape type like Record<string, ZodType>), so consumers don't need zod/v4/* to exist.

Suggested change
import type { $ZodShape } from 'zod/v4/core';
type ZodShape = Record<string, ZodType>;

Copilot uses AI. Check for mistakes.
…add getMyZodErrors tests

The $ZodShape type was previously imported directly from 'zod/v4/core', but should be accessed via z.core.$ZodShape to align with Zod v4's public API surface and avoid relying on an internal subpath export. Also adds comprehensive test coverage for the getMyZodErrors utility, covering root-level key filtering, nested key chain filtering, and empty input handling.
@H-Weisner H-Weisner merged commit 99c2a5c into main Apr 7, 2026
7 checks passed
@H-Weisner H-Weisner deleted the feature/zod-v4 branch April 7, 2026 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants