#6613·zod

toJSONSchema: format/length/integer-bound constraints dropped for some schemas when converting a large multi-schema document (4.5.4 → 4.6.5)

Author: BigGillyStyleCreated Sep 17, 2026Updated Sep 17, 2026

Versions

  • Regression introduced somewhere in 4.5.44.6.5 (bisected the endpoints; haven't bisected the individual 4.6.x patches)
  • Consumed via @orpc/[email protected]'s ZodToJsonSchemaConverter (from @orpc/openapi's OpenAPIGenerator), not called directly

Summary

When @orpc/zod's ZodToJsonSchemaConverter converts a large router (~100+ procedures, several hundred Zod schemas) into one OpenAPI document, many unrelated schemas lose format (uuid/email/uri), minLength, and integer bounds in the emitted JSON Schema going from zod 4.5.4 to 4.6.5 — despite runtime parsing behaving identically. I could not reproduce this with the same schema shapes converted individually or in a small z.registry() batch (2 schemas), only at the scale of our real application's document. I'm filing this without a minimal repro because I've spent a while trying to shrink it and haven't succeeded — happy to help narrow it down further if that's useful, or if this looks like it belongs against @orpc/zod instead, let me know and I'll move it.

What we saw

Three examples pulled directly from our diff ([email protected] output vs [email protected] output, same schemas, same @orpc/[email protected]):

An integer column (drizzle-zod's z.int().gte(min).lte(max) pattern):

 "id": {
-  "maximum": 2147483647,
-  "minimum": -2147483648,
-  "type": "integer"
+  "type": "number"
 }

A UUID/email string field:

 "id": {
-  "format": "uuid",
   "type": "string"
 }
 ...
 "submittedBy": {
-  "format": "email",
   "type": "string"
 }

A minLength constraint:

 "aoName": {
-  "minLength": 2,
   "type": "string"
 }

In every case the runtime schema is unchanged (same .int()/.gte()/.lte(), same .uuid()/.email(), same .min()) — only the JSON Schema output for that specific field, in that specific full-document conversion, loses the constraint. Not every occurrence of a given pattern is affected; e.g. some uuid-formatted fields in the same document keep their format while others don't, which is what pointed me at something scale/reuse-related rather than a straightforward per-schema regression.

What I tried that did not reproduce it

Isolated calls to z.toJSONSchema() on the same schema constructs, on both 4.5.4 and 4.6.5, produced identical (correct) output in every case I tried:

import { z } from "zod";

// int32 column pattern (drizzle-zod)
z.toJSONSchema(z.int().gte(-2147483648).lte(2147483647));
// identical on both versions: { type: "integer", minimum: -2147483648, maximum: 2147483647 }

// object wrapping it, including a nullable variant
z.toJSONSchema(z.object({
  id: z.int().gte(-2147483648).lte(2147483647),
  homeRegionId: z.int().gte(-2147483648).lte(2147483647).nullable(),
}));
// identical on both versions

// a registry with two schemas sharing structurally-identical fields
const registry = z.registry();
registry.add(schemaA, { id: "SchemaA" });
registry.add(schemaB, { id: "SchemaB" });
z.toJSONSchema(registry);
// identical on both versions

// z.url().superRefine() (the shape @better-auth/core's SafeUrlSchema uses)
z.toJSONSchema(z.url().superRefine(() => {}));
// identical on both versions: { type: "string", format: "uri" }

I did not try scaling this up to dozens/hundreds of schemas in one registry/document — that's the gap between my repro attempts and the real failure, and I'd guess the actual trigger lives somewhere in that range rather than being a 2-schema-reproducible bug.

Context (may or may not be relevant)

This surfaced while investigating a Renovate dependency-bump PR that also introduced a second, unrelated cause of similar symptoms in our repo: a pnpm workspace where a different package (@scalar/types) bundles its own independent zod dependency, and pnpm's peer-resolution occasionally picked that instance instead of our pinned one for a schema built by yet another package (@better-auth/core) — a classic dual-module-instance issue, not a zod bug. I mention it only so it isn't mistaken for part of this report: the multi-schema constraint-loss described above reproduces with a single, consistent zod instance throughout (we pinned our whole workspace to exactly one physical [email protected] copy and the bulk regression was still present).

Possibly related

#6550 / PR #6554 (landed in 4.6.0) fixed a chain-order bug where a later check's onattach clobbered an earlier check's tighter bounds in the JSON Schema bag. That's a plausible area for this to have regressed in the same rewrite, but the symptom here doesn't match: #6550's bug fell back to a wide safe-integer range ({minimum: -9007199254740991, maximum: 9007199254740991}); what we're seeing is the constraint (and sometimes the type: "integer"/format) disappearing outright, and only for some occurrences of a pattern within one large converted document, not all of them.

Happy to share the actual generated document (~30k lines) or try more targeted repro attempts if that would help narrow this down.