Skip to content

[Python] update_forward_refs ignores its namespace under pydantic v2, so recursive types through union aliases are never complete #18371

Description

@coreyatpulse

Component

SDKs

Priority

P1 - High (Strongly needed)

SDK Language(s)

Python

Bug Description

What happens

A response type that recurses through a union alias can't be parsed. The first call raises:

pydantic.errors.PydanticUserError: `TypeAdapter[<class 'repro.types.result.Result'>]` is not fully defined; you should define `<class 'repro.types.result.Result'>` and all referenced types, then call `.rebuild()` on the instance.

Repro

OpenAPI:

openapi: 3.1.0
info: {title: Repro, version: "1"}
paths:
  /result:
    get:
      operationId: getResult
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Result"}
components:
  schemas:
    Result:
      type: object
      properties:
        data: {$ref: "#/components/schemas/Branch"}
      required: [data]
    Node:
      anyOf:
        - $ref: "#/components/schemas/Branch"
        - $ref: "#/components/schemas/Leaf"
        - $ref: "#/components/schemas/Items"
    Branch:
      type: object
      additionalProperties: {$ref: "#/components/schemas/Node"}
    Items:
      type: array
      items: {$ref: "#/components/schemas/Node"}
    Leaf:
      type: object
      properties:
        value: {type: string}
      required: [value]

generators.yml (wrapped_aliases makes Branch and Items classes, so the cycle runs through classes rather than through bare aliases):

groups:
  python:
    generators:
      - name: fernapi/fern-python-sdk
        version: 5.41.3
        output: {location: local-file-system, path: ../out}
        config:
          client: {class_name: Repro}
          pydantic_config: {version: v2, wrapped_aliases: true}

Call it:

import httpx
from repro import Repro

body = {"data": {"a": {"value": "x"}, "b": [{"c": {"value": "y"}}]}}
http = httpx.Client(transport=httpx.MockTransport(lambda r: httpx.Response(200, json=body)))
Repro(base_url="https://api.test", httpx_client=http).get_result()  # PydanticUserError

Cause

Node is generated with its members imported only under TYPE_CHECKING:

if typing.TYPE_CHECKING:
    from .branch import Branch
    from .items import Items
Node = typing.Union["Branch", Leaf, "Items"]

The modules then call update_forward_refs(Result, Branch=Branch, Items=Items, Node=Node), which would resolve those names. But under pydantic v2, core/pydantic_utilities.py drops the namespace it is given:

def update_forward_refs(model: Type["Model"], **localns: Any) -> None:
    if IS_PYDANTIC_V2:
        model.model_rebuild(raise_errors=False)  # localns is ignored
    else:
        model.update_forward_refs(**localns)

raise_errors=False hides the failure until the first parse.

Suggested fix

Pass the namespace through:

        model.model_rebuild(raise_errors=False, _types_namespace=localns or None)

With only that line changed, the repro parses fully typed:

data=Branch(root={'a': Leaf(value='x'), 'b': Items(root=[Branch(root={'c': Leaf(value='y')})])})

Related: #7063 and #2900 (closed).

The recursion fixes in 4.36.1, 4.36.2, 4.45.1 and 5.0.6 cover cycles between models, and 5.41.3 includes them. This cycle passes through a union alias (Node), which those fixes don't reach.

Environment: macOS 26 (arm64), Node.js 26.4, generators run locally in Docker (fern generate --local). Python 3.12, pydantic 2.13.4.

Versions
Fern CLI version: 5.163.1
SDK Generator versions:

  • fernapi/fern-python-sdk 5.41.3

Versions

Fern CLI version: 5.163.1
SDK Generator versions:

  • fernapi/fern-python-sdk 5.41.3

Workaround

None in the spec. Editing the generated core/pydantic_utilities.py as above works, but it has to be kept in .fernignore and redone on every generator upgrade. skip_validation: true doesn't help: the nested values stay plain dicts.

Are you interested in contributing a fix?

Yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions