zod-v3-to-v4 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -133,6 +133,9 @@ pnpm playground:interactive
133
133
  <td align="center" valign="top" width="14.28%"><a href="http://adam.doussan.info"><img src="https://avatars.githubusercontent.com/u/26745150?v=4?s=100" width="100px;" alt="acdoussan"/><br /><sub><b>acdoussan</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Aacdoussan" title="Bug reports">🐛</a></td>
134
134
  <td align="center" valign="top" width="14.28%"><a href="http://matsjfunke.com"><img src="https://avatars.githubusercontent.com/u/125814808?v=4?s=100" width="100px;" alt="Mats Julius Funke"/><br /><sub><b>Mats Julius Funke</b></sub></a><br /><a href="#ideas-matsjfunke" title="Ideas, Planning, & Feedback">🤔</a></td>
135
135
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/popfendi"><img src="https://avatars.githubusercontent.com/u/84185235?v=4?s=100" width="100px;" alt="popfendi"/><br /><sub><b>popfendi</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Apopfendi" title="Bug reports">🐛</a></td>
136
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/luis-azevedo-visma"><img src="https://avatars.githubusercontent.com/u/149402066?v=4?s=100" width="100px;" alt="Luís Azevedo"/><br /><sub><b>Luís Azevedo</b></sub></a><br /><a href="#ideas-luis-azevedo-visma" title="Ideas, Planning, & Feedback">🤔</a></td>
137
+ <td align="center" valign="top" width="14.28%"><a href="https://florian-lefebvre.dev"><img src="https://avatars.githubusercontent.com/u/69633530?v=4?s=100" width="100px;" alt="Florian Lefebvre"/><br /><sub><b>Florian Lefebvre</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Aflorian-lefebvre" title="Bug reports">🐛</a></td>
138
+ <td align="center" valign="top" width="14.28%"><a href="http://www.robertcooper.me"><img src="https://avatars.githubusercontent.com/u/16786990?v=4?s=100" width="100px;" alt="Robert Cooper"/><br /><sub><b>Robert Cooper</b></sub></a><br /><a href="https://github.com/nicoespeon/zod-v3-to-v4/issues?q=author%3Arobertcoopercode" title="Bug reports">🐛</a></td>
136
139
  </tr>
137
140
  </tbody>
138
141
  <tfoot>
@@ -25,16 +25,38 @@ export function getZodImport(importDeclaration) {
25
25
  ?.getNamedImports()
26
26
  .find((namedImport) => namedImport.getName() === "z");
27
27
  }
28
- export function collectZodReferences(importDeclarations) {
29
- return importDeclarations.flatMap((importDeclaration) => importDeclaration.getNamedImports().flatMap((namedImport) => {
30
- const namedNode = namedImport.getAliasNode() ?? namedImport.getNameNode();
31
- if (!namedNode.isKind(SyntaxKind.Identifier)) {
32
- return [];
28
+ export function collectZodReferences(sourceFile, importDeclarations) {
29
+ const identifierMap = buildIdentifierMap(sourceFile);
30
+ return {
31
+ zodReferences: importDeclarations.flatMap((importDeclaration) => importDeclaration.getNamedImports().flatMap((namedImport) => {
32
+ const namedNode = namedImport.getAliasNode() ?? namedImport.getNameNode();
33
+ if (!namedNode.isKind(SyntaxKind.Identifier)) {
34
+ return [];
35
+ }
36
+ const name = namedNode.getText();
37
+ const allReferences = identifierMap.get(name) ?? [];
38
+ return allReferences.filter((node) => node !== namedNode);
39
+ })),
40
+ identifierMap,
41
+ };
42
+ }
43
+ /**
44
+ * Build a map of identifier name -> all nodes with that name.
45
+ * This allows O(1) lookups instead of expensive findReferencesAsNodes() calls.
46
+ */
47
+ function buildIdentifierMap(sourceFile) {
48
+ const identifierMap = new Map();
49
+ for (const identifier of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
50
+ const name = identifier.getText();
51
+ const existing = identifierMap.get(name);
52
+ if (existing) {
53
+ existing.push(identifier);
54
+ }
55
+ else {
56
+ identifierMap.set(name, [identifier]);
33
57
  }
34
- return namedNode
35
- .findReferencesAsNodes()
36
- .filter((node) => node !== namedNode);
37
- }));
58
+ }
59
+ return identifierMap;
38
60
  }
39
61
  /**
40
62
  * Collect references to variables that are derived from Zod schemas.
@@ -47,7 +69,7 @@ export function collectZodReferences(importDeclarations) {
47
69
  * This function will return references to `baseSchema` (like the one used
48
70
  * in `constrainedSchema`), so we can also migrate those.
49
71
  */
50
- export function collectDerivedZodSchemaReferences(zodReferences) {
72
+ export function collectDerivedZodSchemaReferences(zodReferences, identifierMap) {
51
73
  const derivedReferences = [];
52
74
  const visited = new Set();
53
75
  function collectFromReferences(refs) {
@@ -67,11 +89,9 @@ export function collectDerivedZodSchemaReferences(zodReferences) {
67
89
  if (!nameNode.isKind(SyntaxKind.Identifier)) {
68
90
  continue;
69
91
  }
70
- // Find all references to this variable (excluding the declaration itself)
71
- const variableReferences = nameNode
72
- .findReferencesAsNodes()
73
- .filter((node) => node !== nameNode)
74
- .filter((node) => node.isKind(SyntaxKind.Identifier));
92
+ // Find all references to this variable
93
+ const allReferences = identifierMap.get(variableName) ?? [];
94
+ const variableReferences = allReferences.filter((node) => node !== nameNode);
75
95
  derivedReferences.push(...variableReferences);
76
96
  // Recursively collect references from these derived schemas
77
97
  collectFromReferences(variableReferences);
@@ -85,6 +85,8 @@ export function convertZCoercePatternsToTopLevelApi(node, zodName) {
85
85
  });
86
86
  }
87
87
  export function convertZObjectPatternsToTopLevelApi(node, zodName) {
88
+ // First, convert z.object() methods like .strict() or .passthrough()
89
+ convertObjectMethodOnSchemaReference(node, zodName);
88
90
  convertNameToTopLevelApi(node, {
89
91
  zodName,
90
92
  oldName: "object",
@@ -98,6 +100,56 @@ export function convertZObjectPatternsToTopLevelApi(node, zodName) {
98
100
  });
99
101
  convertZObjectMergeToExtend(node);
100
102
  }
103
+ /**
104
+ * @example
105
+ * UserSchema.pick({ id: true }).strict()
106
+ * // becomes
107
+ * z.strictObject(UserSchema.pick({ id: true }).shape)
108
+ */
109
+ function convertObjectMethodOnSchemaReference(node, zodName) {
110
+ const methodMappings = {
111
+ strict: "strictObject",
112
+ passthrough: "looseObject",
113
+ strip: "object",
114
+ nonstrict: "object",
115
+ };
116
+ const methodNames = Object.keys(methodMappings);
117
+ node
118
+ .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
119
+ // Start from the deepest to handle nested cases
120
+ .reverse()
121
+ .filter((e) => methodNames.includes(e.getName()))
122
+ .forEach((propertyAccess) => {
123
+ const methodName = propertyAccess.getName();
124
+ const parent = propertyAccess.getFirstAncestorByKind(SyntaxKind.CallExpression);
125
+ if (!parent || parent.getExpression() !== propertyAccess) {
126
+ return;
127
+ }
128
+ const baseExpression = propertyAccess.getExpression();
129
+ const baseText = baseExpression.getText();
130
+ const hasZObjectInChain = baseExpression
131
+ .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
132
+ .some((e) => e.getName() === "object" && e.getExpression().getText() === zodName);
133
+ if (hasZObjectInChain) {
134
+ return;
135
+ }
136
+ if (baseExpression.isKind(SyntaxKind.CallExpression) &&
137
+ baseExpression
138
+ .getExpression()
139
+ .isKind(SyntaxKind.PropertyAccessExpression)) {
140
+ const propAccess = baseExpression.getExpression();
141
+ if (propAccess.isKind(SyntaxKind.PropertyAccessExpression) &&
142
+ propAccess.getName() === "object" &&
143
+ propAccess.getExpression().getText() === zodName) {
144
+ return;
145
+ }
146
+ }
147
+ // This is a schema reference with .strict(), etc.
148
+ // Transform it to `z.strictObject(schemaRef.shape)`
149
+ const newName = methodMappings[methodName];
150
+ parent.replaceWithText(`${zodName}.${newName}(${baseText}.shape)`);
151
+ });
152
+ }
101
153
  function convertZObjectMergeToExtend(node) {
102
154
  node
103
155
  .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
@@ -1,5 +1,41 @@
1
1
  import { SyntaxKind, } from "ts-morph";
2
2
  import { getDirectDescendantsOfKind, isZodReference, } from "./zod-node.js";
3
+ export function convertSetErrorMapToConfig(sourceFile, zodName) {
4
+ sourceFile
5
+ .getDescendantsOfKind(SyntaxKind.CallExpression)
6
+ .filter((expression) => {
7
+ const callee = expression.getExpression();
8
+ if (!callee.isKind(SyntaxKind.PropertyAccessExpression)) {
9
+ return false;
10
+ }
11
+ const objectName = callee.getExpression().getText();
12
+ const methodName = callee.getName();
13
+ return objectName === zodName && methodName === "setErrorMap";
14
+ })
15
+ .forEach((expression) => {
16
+ const argument = expression.getArguments()[0];
17
+ if (!argument) {
18
+ return;
19
+ }
20
+ expression.replaceWithText(`${zodName}.config({ customError: ${argument.getText()} })`);
21
+ });
22
+ }
23
+ export function convertZodErrorMapType(sourceFile, zodName) {
24
+ sourceFile
25
+ .getDescendantsOfKind(SyntaxKind.TypeReference)
26
+ .filter((typeRef) => {
27
+ const typeName = typeRef.getTypeName();
28
+ if (!typeName.isKind(SyntaxKind.QualifiedName)) {
29
+ return false;
30
+ }
31
+ const left = typeName.getLeft().getText();
32
+ const right = typeName.getRight().getText();
33
+ return left === zodName && right === "ZodErrorMap";
34
+ })
35
+ .forEach((typeRef) => {
36
+ typeRef.replaceWithText(`${zodName}.core.$ZodErrorMap`);
37
+ });
38
+ }
3
39
  export function convertErrorMapToErrorFunction(node) {
4
40
  // Find all errorMap properties
5
41
  getDirectDescendantsOfKind(node, SyntaxKind.Identifier)
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { Project } from "ts-morph";
8
- import z from "zod";
8
+ import { z } from "zod";
9
9
  import { migrateZodV3ToV4 } from "./migrate.js";
10
10
  const execAsync = promisify(exec);
11
11
  intro(`🏗️ Let's migrate Zod from v3 to v4`);
package/dist/migrate.js CHANGED
@@ -3,7 +3,7 @@ import { findRootExpression } from "./ast.js";
3
3
  import { AstroZodModuleSpecifiers, collectDerivedZodSchemaReferences, collectZodImportDeclarations, collectZodReferences, getZodName, } from "./collect-imports.js";
4
4
  import { convertAstroDeprecatedZodImports } from "./convert-astro-imports.js";
5
5
  import { convertZArrayPatternsToTopLevelApi, convertZCoercePatternsToTopLevelApi, convertZFunctionPatternsToTopLevelApi, convertZNumberPatternsToZInt, convertZObjectPatternsToTopLevelApi, convertZRecordPatternsToTopLevelApi, convertZStringPatternsToTopLevelApi, } from "./convert-name-to-top-level-api.js";
6
- import { convertDeprecatedErrorKeysToErrorFunction, convertErrorMapToErrorFunction, convertMessageKeyToError, convertZodErrorAddIssueToDirectPushes, convertZodErrorToTreeifyError, } from "./convert-zod-errors.js";
6
+ import { convertDeprecatedErrorKeysToErrorFunction, convertErrorMapToErrorFunction, convertMessageKeyToError, convertSetErrorMapToConfig, convertZodErrorAddIssueToDirectPushes, convertZodErrorMapType, convertZodErrorToTreeifyError, } from "./convert-zod-errors.js";
7
7
  import { replaceDeletedTypes } from "./replace-deleted-types.js";
8
8
  import { isZodNode } from "./zod-node.js";
9
9
  export function migrateZodV3ToV4(sourceFile, options = {}) {
@@ -25,8 +25,8 @@ export function migrateZodV3ToV4(sourceFile, options = {}) {
25
25
  }
26
26
  });
27
27
  // Collect references before modifying imports
28
- const zodReferences = collectZodReferences(importDeclarations);
29
- const derivedReferences = collectDerivedZodSchemaReferences(zodReferences);
28
+ const { zodReferences, identifierMap } = collectZodReferences(sourceFile, importDeclarations);
29
+ const derivedReferences = collectDerivedZodSchemaReferences(zodReferences, identifierMap);
30
30
  replaceDeletedTypes(importDeclarations, zodReferences);
31
31
  [...zodReferences, ...derivedReferences].forEach((node) => {
32
32
  if (node.wasForgotten()) {
@@ -56,6 +56,8 @@ export function migrateZodV3ToV4(sourceFile, options = {}) {
56
56
  });
57
57
  convertZodErrorToTreeifyError(sourceFile, zodName);
58
58
  convertZodErrorAddIssueToDirectPushes(sourceFile, zodName);
59
+ convertSetErrorMapToConfig(sourceFile, zodName);
60
+ convertZodErrorMapType(sourceFile, zodName);
59
61
  renameZSchemaEnumToLowercase(sourceFile, zodName);
60
62
  convertAstroDeprecatedZodImports(importDeclarations);
61
63
  return sourceFile.getFullText();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod-v3-to-v4",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Migrate Zod from v3 to v4",
5
5
  "keywords": [
6
6
  "zod",
@@ -44,22 +44,22 @@
44
44
  "*": "prettier --ignore-unknown --write"
45
45
  },
46
46
  "dependencies": {
47
- "@clack/prompts": "1.0.0-alpha.1",
48
- "ts-morph": "26.0.0"
47
+ "@clack/prompts": "1.0.0-alpha.9",
48
+ "ts-morph": "26.0.0",
49
+ "zod": "3.25.12"
49
50
  },
50
51
  "devDependencies": {
51
- "@types/node": "20.9.0",
52
+ "@types/node": "25.0.3",
52
53
  "husky": "9.1.7",
53
- "lint-staged": "16.0.0",
54
+ "lint-staged": "16.2.7",
54
55
  "prettier": "3.5.3",
55
56
  "prettier-plugin-curly": "0.3.1",
56
57
  "prettier-plugin-organize-imports": "4.3.0",
57
58
  "prettier-plugin-packagejson": "2.5.10",
58
- "prettier-plugin-sh": "0.15.0",
59
+ "prettier-plugin-sh": "0.17.4",
59
60
  "rimraf": "6.0.1",
60
- "typescript": "5.8.3",
61
- "vitest": "3.1.4",
62
- "zod": "3.25.12"
61
+ "typescript": "5.9.3",
62
+ "vitest": "3.1.4"
63
63
  },
64
64
  "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
65
65
  }