zod
zod copied to clipboard
v4: `.meta()` global registry conflicts
Having a global registry might cause some issues for people down the line?
My team have some monorepos where we publish multiple openapi specs in the same repo, this would mean that the ids would have to be globally unique even between schema which are used in different specs.
/Users/samc/work/zod-openapi-2/node_modules/.pnpm/@[email protected]/node_modules/@zod/core/dist/commonjs/registries.js:18
throw new Error(`ID ${meta.id} already exists in the registry`);
^
Error: ID jobId already exists in the registry
at $ZodJSONSchemaRegistry.add (/Users/samc/work/zod-openapi-2/node_modules/.pnpm/@[email protected]/node_modules/@zod/core/dist/commonjs/registries.js:18:23)
We also export them as consumer types so if a consumer was to import types from consumers where they've declared the same id we could be in trouble.
The .meta() method is just syntactic sugar for .register(z.globalRegistry, <metadata>). Anyone with advanced use cases will need to define their own registries (say, one per spec). This feels like a reasonable expectation if you want to duplicate id values.
const myReg = z.registry<z.GlobalMeta>();
z.string().register(myReg, {title: "My String"});
Though the only reason registries currently check for duplicate ids is because it's almost never desirable and this lets you catch it earlier. I could potentially remove this logic from the registry itself and enforce it inside toJSONSchema. Open to thoughts on that.
This behavior is causing some issues with hot module reloading. Just doing this in a file:
const someSchema = z.string().meta({id: 'foo'});
Will lead to an error being thrown whenever the file is being edited due to the shared global registry. In a HMR-context, you would actually like to silently replace the schema in the registry when it's redeclared. Not sure what the best course of action is though.
A similar problem i have is using a Zod Schema with an id in a nuxt-project. When i try to use a schema both in a server-handler and in a vue-component, i get the error, that the ID already exists. I suppose this is because both the server-handler and the vue-component in the SSR process are adding the schema to the same registry.
Minimum reproduction: https://stackblitz.com/edit/nuxt-starter-zngzuq7s?file=shared%2Fschema.ts
As a terrible workaround hack for local dev, I got it working like this:
export const schemaRegistry = z.registry<z.core.GlobalMeta>();
const originalAdd = schemaRegistry.add;
// terrible hack for vite's HMR:
// without this monkey-patch, zod will throw an error whenever editing a schema file that uses
// `.register` as it would try to re-register the schema with the same ID again
// with this patch, re-registering will just replace the schema in the registry
schemaRegistry.add = (
schema: Parameters<typeof originalAdd>[0],
meta: Parameters<typeof originalAdd>[1],
) => {
if (!meta.id) {
return originalAdd.call(schemaRegistry, schema, meta);
}
const existingSchema = schemaRegistry._idmap.get(meta.id);
if (existingSchema) {
schemaRegistry.remove(existingSchema);
schemaRegistry._idmap.delete(meta.id);
}
return originalAdd.call(schemaRegistry, schema, meta);
};
Didn't feel comfortable modifying the default registry, but this schemaRegistry will now simply overwrite if the same schema is being registered again.
This has been causing issues with Vite HMR and Tanstack Start SSR, guessing for similar reasons that screeny05 noted. To anyone looking for a quick (and super hacky) copy-paste version of romandecker's answer which modifies the global registry, you can take this and place is pretty much anywhere in your codebase:
const originalAdd = z.globalRegistry.add;
// terrible hack for vite's HMR:
// without this monkey-patch, zod will throw an error whenever editing a schema file that uses
// `.register` as it would try to re-register the schema with the same ID again
// with this patch, re-registering will just replace the schema in the registry
z.globalRegistry.add = (
schema: Parameters<typeof originalAdd>[0],
meta: Parameters<typeof originalAdd>[1],
) => {
if (!meta.id) {
return originalAdd.call(z.globalRegistry, schema, meta);
}
const existingSchema = z.globalRegistry._idmap.get(meta.id);
if (existingSchema) {
z.globalRegistry.remove(existingSchema);
z.globalRegistry._idmap.delete(meta.id);
}
return originalAdd.call(z.globalRegistry, schema, meta);
};
Same here in Next.js app, even though the ID is unique, an error is thrown on dev env
Yeah, hot reloading is broken now for specs, immediately ran into this after upgrading to Zod v4 in a Vite app.
Any schema change on a registered schema will throw the already exists in the registry error.
Having Zod be broken out-of-the-box for Vite and Next.js HMR, and immediately requiring a monkeypatch to work seems quite... undesirable.
Ran into this again today:
We had two versions of the same dependency which both perform .meta({ id: 'bla' }); in their code. While running the tests we would run into the already exists in the registry error :(
I hope this doesn’t come off harsh: I love Zod, and v4 sounds generally great. That said, I’m also running into problems with the global registry in Zod v4 when using it in monorepos.
I understand the appeal of a registry that’s accessible everywhere, but the current interface feels too error-prone. Implicit writes to z.globalRegistry make global state mutations unpredictable and too easy to trigger across packages/dependencies, which leads to ID collisions and breaks cross-package encapsulation.
Right now, Zod assumes the desired behaviour is to push IDs into a global registry whenever no registry is explicitly passed. That feels odd: as a general best practice, global contexts should usually be avoided, and when they are used, it should be extremely intentional. I agree there are valid cases for using global registry, but I’d expect those to opt in explicitly.
In short: I think Zod’s registry features would serve users better if z.globalRegistry were not the default target and could only be used when intentionally specified. This would also imply standalone schema declarations would not belong to any registry and associated metadata would need to be stored within the schema itself.
Though the only reason registries currently check for duplicate ids is because it's almost never desirable and this lets you catch it earlier. I could potentially remove this logic from the registry itself and enforce it inside toJSONSchema. Open to thoughts on that.
Maintaining IDs unique across the registry seems correct and sounds positive as long as schemas are not implicitly pushed into any any registry by default.