Skip to content

fix: prevent circular $ref in zodTextFormat for branded types#1777

Open
fuleinist wants to merge 1 commit intoopenai:masterfrom
fuleinist:fix/issue-1739
Open

fix: prevent circular $ref in zodTextFormat for branded types#1777
fuleinist wants to merge 1 commit intoopenai:masterfrom
fuleinist:fix/issue-1739

Conversation

@fuleinist
Copy link

Summary

Fixes circular $ref generation in zodTextFormat when using Zod branded types (e.g., z.string().brand()).

Problem

When using a branded type like z.string().brand("SlideId">()) and reusing it multiple times in a schema, zodTextFormat was generating a circular $ref that references itself:

{
  "definitions": {
    "test_properties_a": {
      "$ref": "#/definitions/test_properties_a"
    }
  }
}

This circular reference is invalid and cannot be resolved.

Root Cause

The issue was in parseDef function in the vendored zod-to-json-schema package. When parsing a branded type:

  1. The branded type def was added to refs.seen with the current path
  2. parseBrandedDef then parsed the underlying type, which also got added to refs.seen with the SAME path
  3. When the branded type was encountered again, a $ref was created pointing to that path
  4. When resolving the definition, the underlying type was found in refs.seen at that same path, causing a circular reference

Solution

Modified parseDef to treat branded types as transparent wrappers. When a branded type is encountered, it now directly parses the underlying type without adding the branded def to refs.seen. This prevents the circular reference issue.

Changes

  • Modified src/_vendor/zod-to-json-schema/parseDef.ts to handle ZodBranded types specially by parsing the underlying type directly

Testing

Verified with the reproduction case from the issue - no more circular references in the generated schema.

Fixes #1739

Fixes openai#1739

When using z.string().brand(), zodTextFormat was creating a self-referencing
$ref definition. Now extracts the underlying type correctly by treating
branded types as transparent wrappers.
@fuleinist fuleinist requested a review from a team as a code owner March 17, 2026 20:36
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 457f908b77

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +101 to +102
if ((def as any).typeName === ZodFirstPartyTypeKind.ZodBranded) {
return parseDef((def as any).type._def, refs, forceResolution);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve branded schema metadata when unwrapping

Returning immediately for ZodBranded bypasses the addMeta(def, refs, jsonSchema) block below, so metadata defined on the branded wrapper (not the inner type) is silently dropped. This changes output for schemas like z.string().brand(...).describe("..."), where description/markdownDescription used to be emitted from the branded def but now disappear, which can materially change generated JSON schema guidance in zodTextFormat/zodResponseFormat.

Useful? React with 👍 / 👎.

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

2 participants