feat(zod): respect an explicit type in schema metadata#1693
Draft
macalinao wants to merge 2 commits into
Draft
Conversation
Zod copies every `.meta()` field on top of its structural conversion but
never reconciles them, so a schema such as
`z.union([...]).meta({ type: 'string', format, pattern })` converts to
`{ anyOf: [...], type: 'string', format, pattern }` — the intended string
schema polluted with a redundant, contradictory `anyOf`.
When metadata pins a concrete JSON Schema `type`, treat it as authoritative
and drop the leftover structural composition keywords (`anyOf`/`oneOf`/
`allOf`). Scalar and object shapes are unaffected: zod already overwrites a
structural `type` on merge, and object schemas keep their `properties`.
This lets libraries (e.g. temporal-zod) express a validator's JSON Schema
directly via `.meta()` instead of shipping an external converter interceptor.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Only strip the leftover `anyOf`/`oneOf`/`allOf` when the pinned `type` is a primitive scalar (string/number/integer/boolean/null). For `object`/`array` the composition branches carry real structure the metadata does not restate, so dropping them would silently discard information rather than remove contradictory noise.
Member
|
I think we should request that Scalar or Zod support this use case instead of implementing it in oRPC. Relying on a custom meta key could conflict with future built-in Zod metadata Personally, I think this belongs in Scalar. |
Member
|
alternative #1695 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Make
ZodToJsonSchemaConverterhonor an explicit scalar JSON Schematypeset on a schema's.meta().Zod copies every
.meta()field on top of its structural conversion but never reconciles them. So a schema like a Temporal validator, which is az.union([...])under the hood with.meta({ type: 'string', format, pattern }), converts to:{ "anyOf": [{ "type": "string" }, ...], "type": "string", "format": "date-time", "pattern": "…" }— the intended string schema polluted with a redundant, contradictory
anyOf(the non-string branches can never satisfytype: 'string').This PR: when metadata pins a scalar
type, treat it as authoritative and drop the leftover structural composition keywords (anyOf/oneOf/allOf). The union above now converts to the clean:{ "type": "string", "format": "date-time", "pattern": "…" }Why this scope
I checked zod's own
toJSONSchemabehavior first (per the zod JSON Schema docs: "All metadata fields get copied into the resulting JSON Schema", andoverrideruns after the metadata merge). Empirically:z.number().meta({ type: 'string' }){ type: 'string' }typez.object({ a }).meta({ type: 'object', title }){ type: 'object', properties, required, title }z.union([string, number]).meta({ type: 'string' }){ anyOf: [...], type: 'string' }anyOfThe only case zod leaves inconsistent is a composition wrapper left next to a pinned type, so the fix strips only those keywords — it does not wholesale-replace the schema with the metadata (which would destroy
properties).Restricted to scalar types on purpose. For
object/array, the composition branches carry real structure the metadata does not restate — e.g.z.union([{a}, {b}]).meta({ type: 'object' })is a consistent{ anyOf: [...], type: 'object' }, not contradictory noise. Stripping there would silently discard the branch shapes, so those types are excluded and keep theiranyOf.Context
This replaces the need for an external converter interceptor. Downstream libraries (e.g.
temporal-zod, which currently ships a structural interceptor workaround) can now express a validator's JSON Schema directly through.meta()— no converter configuration required. All Temporal validators are ISO strings/durations, i.e. scalartype: 'string', so they are fully covered.Test plan
pnpm vitest run packages/zod/src/converter.test.ts— added cases: union pinning a scalar type, nested-in-object, object schema restatingtype(unchanged), and a union of objects pinned totype: 'object'that keeps itsanyOf.pnpm --filter @orpc/zod run type:checkpnpm exec eslint packages/zod/src/converter.ts packages/zod/src/converter.test.ts🤖 Generated with Claude Code