88 *
99 * ts-to-zod is a powerful tool but has limitations that require post-processing:
1010 *
11- * ### 1. Zod Import Path (`"zod"` → `"zod/v4"` )
11+ * ### 1. Zod Import Path (keep standard `"zod"` for version agnosticism )
1212 *
13- * **Problem**: ts-to-zod generates `import { z } from "zod"` but this project
14- * uses the Zod v4 subpath import `"zod/v4"` for explicit version targeting.
15- *
16- * **Solution**: Replace the import path in the generated output.
13+ * ts-to-zod generates `import { z } from "zod"` which works with both
14+ * Zod v3.25+ and v4. We keep this standard import to support both versions.
1715 *
1816 * ### 2. External Type References (`z.any()` → actual schemas)
1917 *
2422 *
2523 * **Solution**: Import the schemas from MCP SDK and remove the z.any() placeholders.
2624 *
27- * ### 3. Index Signatures (`z.record().and()` → `z.looseObject ()`)
25+ * ### 3. Index Signatures (`z.record().and()` → `z.object().passthrough ()`)
2826 *
2927 * **Problem**: TypeScript index signatures like `[key: string]: unknown` are
3028 * translated by ts-to-zod to `z.record(z.string(), z.unknown()).and(z.object({...}))`.
3129 * This creates a `ZodIntersection` type which doesn't support `.extend()` etc.
3230 *
33- * **Solution**: Replace the intersection pattern with `z.looseObject()`.
31+ * **Solution**: Replace with `z.object({...}).passthrough()` which works in both
32+ * Zod v3 and v4, allowing extra properties while validating known ones.
3433 *
3534 * ## Adding Schema Descriptions
3635 *
@@ -178,50 +177,46 @@ async function generateJsonSchema() {
178177 * Post-process generated schemas for project compatibility.
179178 */
180179function postProcess ( content : string ) : string {
181- // 1. Update import to use zod/v4
182- content = content . replace (
183- 'import { z } from "zod";' ,
184- 'import * as z from "zod/v4";' ,
185- ) ;
186-
187- // 2. Add MCP SDK schema imports
180+ // 1. Add MCP SDK schema imports (keep standard zod import for v3/v4 compatibility)
188181 const mcpImports = EXTERNAL_TYPE_SCHEMAS . join ( ",\n " ) ;
189182 content = content . replace (
190- 'import * as z from "zod/v4 ";' ,
191- `import * as z from "zod/v4 ";
183+ 'import { z } from "zod";' ,
184+ `import { z } from "zod";
192185import {
193186 ${ mcpImports } ,
194187} from "@modelcontextprotocol/sdk/types.js";` ,
195188 ) ;
196189
197- // 3 . Remove z.any() placeholders for external types (now imported from MCP SDK)
190+ // 2 . Remove z.any() placeholders for external types (now imported from MCP SDK)
198191 for ( const schema of EXTERNAL_TYPE_SCHEMAS ) {
199192 content = content . replace (
200193 new RegExp ( `(?:export )?const ${ schema } = z\\.any\\(\\);\\n?` , "g" ) ,
201194 "" ,
202195 ) ;
203196 }
204197
205- // 4 . Replace z.record().and(z.object({...})) with z.looseObject ({...})
198+ // 3 . Replace z.record().and(z.object({...})) with z.object ({...}).passthrough( )
206199 // Uses brace-counting to handle nested objects correctly.
207- content = replaceRecordAndWithLooseObject ( content ) ;
200+ // passthrough() works in both Zod v3 and v4, unlike looseObject() which is v4-only.
201+ content = replaceRecordAndWithPassthrough ( content ) ;
208202
209- // 5 . Add header comment
203+ // 4 . Add header comment
210204 content = content . replace (
211205 "// Generated by ts-to-zod" ,
212206 `// Generated by ts-to-zod
213- // Post-processed for Zod v4 and MCP SDK compatibility
207+ // Post-processed for Zod v3/ v4 compatibility and MCP SDK integration
214208// Run: npm run generate:schemas` ,
215209 ) ;
216210
217211 return content ;
218212}
219213
220214/**
221- * Replace z.record(z.string(), z.unknown()).and(z.object({...})) with z.looseObject ({...})
215+ * Replace z.record(z.string(), z.unknown()).and(z.object({...})) with z.object ({...}).passthrough( )
222216 * Uses brace-counting to handle nested objects correctly.
217+ * passthrough() works in both Zod v3 and v4, allowing extra properties.
223218 */
224- function replaceRecordAndWithLooseObject ( content : string ) : string {
219+ function replaceRecordAndWithPassthrough ( content : string ) : string {
225220 const pattern = "z.record(z.string(), z.unknown()).and(z.object({" ;
226221 let result = content ;
227222 let startIndex = 0 ;
@@ -245,7 +240,7 @@ function replaceRecordAndWithLooseObject(content: string): string {
245240 // Check if followed by ))
246241 if ( result . slice ( i , i + 2 ) === "))" ) {
247242 const objectContent = result . slice ( objectStart , i - 1 ) ;
248- const replacement = `z.looseObject ({${ objectContent } })` ;
243+ const replacement = `z.object ({${ objectContent } }).passthrough( )` ;
249244 result = result . slice ( 0 , matchStart ) + replacement + result . slice ( i + 2 ) ;
250245 startIndex = matchStart + replacement . length ;
251246 } else {
@@ -260,11 +255,7 @@ function replaceRecordAndWithLooseObject(content: string): string {
260255 * Post-process generated integration tests.
261256 */
262257function postProcessTests ( content : string ) : string {
263- content = content . replace (
264- 'import { z } from "zod";' ,
265- 'import * as z from "zod/v4";' ,
266- ) ;
267-
258+ // Keep standard zod import for v3/v4 compatibility
268259 content = content . replace (
269260 "// Generated by ts-to-zod" ,
270261 `// Generated by ts-to-zod
0 commit comments