Skip to content

fix: resolve circular $ref in zod-to-json-schema for branded types#1766

Open
matantsach wants to merge 2 commits intoopenai:masterfrom
matantsach:fix/1739-zod-text-format-circular-ref
Open

fix: resolve circular $ref in zod-to-json-schema for branded types#1766
matantsach wants to merge 2 commits intoopenai:masterfrom
matantsach:fix/1739-zod-text-format-circular-ref

Conversation

@matantsach
Copy link

Summary

Fixes #1739

When a branded Zod type (e.g. z.string().brand<"Id">()) is reused across multiple fields, the generated JSON schema contains a self-referencing definition — a definition whose value is just a $ref back to itself. This makes the schema invalid and causes a cryptic SyntaxError: Unexpected end of JSON input when used with zodTextFormat or zodResponseFormat.

Root Cause

parseBrandedDef delegates to parseDef for the inner type but doesn't pass through the forceResolution parameter. When a definition is being force-resolved, the branded parser's inner call hits refs.seen and creates a $ref back to the same definition path — producing the circular reference.

The same structural issue exists in all transparent wrapper parsers (catch, default, promise, readonly) that delegate to parseDef without forwarding forceResolution. Only parseEffectsDef was already doing this correctly.

Changes

  • Pass forceResolution through parseBrandedDef, parseCatchDef, parseDefaultDef, parsePromiseDef, and parseReadonlyDef — matching the existing pattern in parseEffectsDef
  • Add test reproducing the exact circular $ref from the issue (branded type reused in multiple fields)

Testing

  • New test verifies no self-referencing $ref in definitions and that the branded type resolves to {type: "string"}
  • All 116 helper/lib tests pass
  • Test is Zod v3 only (v4 uses z4.toJSONSchema() directly, not the vendored converter)

matantsach and others added 2 commits March 13, 2026 13:17
When a branded Zod type is reused across multiple fields, the generated
JSON schema contains a self-referencing definition (e.g. a definition
whose value is just a $ref back to itself). This happens because
parseBrandedDef doesn't pass forceResolution through to parseDef when
delegating to the inner type, so definition resolution hits refs.seen
and creates a circular reference.

Pass forceResolution through parseBrandedDef, matching the existing
pattern in parseEffectsDef.

Fixes openai#1739

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a branded Zod type is reused across multiple fields, the generated
JSON schema contains a self-referencing definition (e.g. a definition
whose value is just a $ref back to itself). This happens because
parseBrandedDef doesn't pass forceResolution through to parseDef when
delegating to the inner type, so definition resolution hits refs.seen
and creates a circular reference.

Pass forceResolution through all transparent wrapper parsers (branded,
catch, default, promise, readonly), matching the existing pattern in
parseEffectsDef.

Fixes openai#1739

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@matantsach matantsach requested a review from a team as a code owner March 13, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

zodTextFormat generates invalid circular $ref in JSON schema definitions

1 participant