orval
orval copied to clipboard
Support for Zod 4
Hello (again)! There have been some re-structuring that will likely affect your previous work re: Zod 4 support. Apologies for the inconvenience!!
--
Here's a dedicated guide for library authors that answers some common questions: https://v4.zod.dev/library-authors
- Best practices around peer dependencies
- How to support Zod v3 and Zod v4 simultaneously
- How to support Zod & Zod Mini without extra work
Note that the @zod/core package has been abandoned in favor of a subpath: zod/v4/core. This makes it much easier for libraries with build on top of Zod with a single peer dependency on "zod". The reasons for this change are explained in more detail in the beta announcement: https://v4.zod.dev/v4
Thanks for letting me know, I looked at the linked page and confirmed that no additional development is required.
@soartec-lab
Can zod be imported from zod/v4 in the generated files?
extend is not compatible with what's generated, example generated:
import {
z as zod
} from 'zod';
// ....
export const eventAddContributionBody = zod.object({
"guestName": zod.string().min(1).max(eventAddContributionBodyGuestNameMax),
"message": zod.string().min(eventAddContributionBodyMessageMin).max(eventAddContributionBodyMessageMax).nullable(),
"uploadedMedia": zod.array(zod.object({
"etag": zod.string(),
"key": zod.string()
})).max(eventAddContributionBodyUploadedMediaMax).nullable()
})
Trying to extend it:
import { z } from "zod/v4"
/// ...
const uploadSchema = eventAddContributionBody
.extend({
message: z.preprocess(
(val) => (val === "" ? null : val), // Preprocess an empty string into null, so if the user leaves the field with an empty string, it won't show a validation error.
eventAddContributionBody.shape.message, // After preprocessing, validate against the *original* message schema rules
),
})
Type 'ZodPipe<ZodTransform<unknown, unknown>, $ZodType<unknown, unknown>>' is missing the following properties from type 'ZodType<any, any, any>': _type, _parse, _getType, _getOrReturnCtx, and 7 more.ts(2740)
@DeluxeOwl
Orval will support compatibility with zod v4 from the next version.
https://github.com/orval-labs/orval/milestone/26?closed=1
@soartec-lab I see the next version is already available, but looking at the implementation, it doesn't seem to be the correct strategy, since zod v4 is gonna be available only on v3 for a while, but it is available since 3.25.0 on a different import path.
It's almost as if you released support for a lib that cannot be used until the next major is released, and we have no idea when that's going to happen. I was pretty sure the support was going to be available via a configuration override in the orval settings
Hi orval team,
I’m really looking forward to Orval supporting Zod v4 officially. However, from what I’ve seen so far, the support doesn’t seem ideal yet. In the meantime, I created a simple post-processing script that does some basic string replacements to make things work for me temporarily.
If the Orval team adds proper Zod v4 support in the future, I’ll switch back right away. For now, I’m sharing this script here in case anyone else finds it useful. Just a heads-up: this script was generated by AI, and I haven’t put effort into code style, security, or robustness — it’s purely for my own local use.
Thanks for your great work!
import fs from 'fs';
import path from 'path';
/**
* Replacement rules: RegExp + replacement string or function
*/
const replacementPatterns: [RegExp, string | ((match: string) => string)][] = [
// ✅ Replace zod imports with zod/v4
[/(from\s+['"])zod(['"])/g, '$1zod/v4$2'],
// Deprecated zod.string().xxx() -> zod.xxx()
[/\bzod\.string\(\)\.email\(\)/g, 'zod.email()'],
[/\bzod\.string\(\)\.url\(\)/g, 'zod.url()'],
[/\bzod\.string\(\)\.jwt\(\)/g, 'zod.jwt()'],
[/\bzod\.string\(\)\.emoji\(\)/g, 'zod.emoji()'],
[/\bzod\.string\(\)\.guid\(\)/g, 'zod.guid()'],
[/\bzod\.string\(\)\.uuid\(\)/g, 'zod.uuid()'],
[/\bzod\.string\(\)\.uuidv4\(\)/g, 'zod.uuid()'],
[/\bzod\.string\(\)\.uuidv6\(\)/g, 'zod.uuid()'],
[/\bzod\.string\(\)\.uuidv7\(\)/g, 'zod.uuid()'],
[/\bzod\.string\(\)\.nanoid\(\)/g, 'zod.nanoid()'],
[/\bzod\.string\(\)\.cuid\(\)/g, 'zod.cuid()'],
[/\bzod\.string\(\)\.cuid2\(\)/g, 'zod.cuid2()'],
[/\bzod\.string\(\)\.ulid\(\)/g, 'zod.ulid()'],
[/\bzod\.string\(\)\.base64\(\)/g, 'zod.base64()'],
[/\bzod\.string\(\)\.base64url\(\)/g, 'zod.base64url()'],
[/\bzod\.string\(\)\.xid\(\)/g, 'zod.xid()'],
[/\bzod\.string\(\)\.ksuid\(\)/g, 'zod.ksuid()'],
[/\bzod\.string\(\)\.ipv4\(\)/g, 'zod.ipv4()'],
[/\bzod\.string\(\)\.ipv6\(\)/g, 'zod.ipv6()'],
[/\bzod\.string\(\)\.cidrv4\(\)/g, 'zod.cidrv4()'],
[/\bzod\.string\(\)\.cidrv6\(\)/g, 'zod.cidrv6()'],
[/\bzod\.string\(\)\.e164\(\)/g, 'zod.e164()'],
[/\bzod\.string\(\)\.datetime\(\)/g, 'zod.iso.datetime()'],
[/\bzod\.string\(\)\.date\(\)/g, 'zod.iso.date()'],
[/\bzod\.string\(\)\.time\(\)/g, 'zod.iso.time()'],
[/\bzod\.string\(\)\.duration\(\)/g, 'zod.iso.duration()'],
// Optional: existing refactors
[/\bzod\.string\(\)\.min\(1\)/g, 'zod.string().nonempty()'],
];
/**
* Recursively collect all .ts/.js/.tsx/.jsx files in the target directory
*/
function getAllFiles(dir: string, fileList: string[] = []): string[] {
if (!fs.existsSync(dir)) {
console.error(`❌ Directory not found: ${dir}`);
process.exit(1);
}
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
getAllFiles(fullPath, fileList);
} else if (/\.(ts|js|tsx|jsx)$/.test(file)) {
fileList.push(fullPath);
}
}
return fileList;
}
/**
* Apply replacement rules to a single file
*/
function replaceInFile(
filePath: string,
patterns: [RegExp, string | ((match: string) => string)][]
) {
const content = fs.readFileSync(filePath, 'utf8');
let modified = content;
for (const [pattern, replacement] of patterns) {
modified = modified.replace(pattern, replacement as any);
}
if (modified !== content) {
fs.writeFileSync(filePath, modified, 'utf8');
console.log(`✅ Updated: ${filePath}`);
}
}
/**
* Main entry
*/
function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
console.error('❌ Usage: tsx scripts/orval-zod-v4-supports.ts <targetDir>');
process.exit(1);
}
const targetDir = path.resolve(args[0]);
console.log(`🔧 Processing: ${targetDir}`);
const files = getAllFiles(targetDir);
files.forEach((file) => replaceInFile(file, replacementPatterns));
console.log('🎉 All replacements complete.');
}
main();
Hey everyone. The release strategy for zod has changed midway through, so we're not able to keep up with it.
We'll definitely address this in the next release, so please wait a little while 👍
Instead of supporting zod/v4, it'd be better to completely switch the orval's API generation towards zod/v4-mini.
The DX portion of the docs doesn't apply to generated code.
[email protected] has been published to npm
@o-alexandrov @healtheloper PR's are welcome if you guys want to help upgrade to v4
@melloware This issue was reopened due to zod/v4 path for Zod v4 in version 3.25.x.
Now that [email protected] has been published to npm, the package root "zod" now exports Zod 4.
Therefore, I think this issue can be closed.