Repository navigation
[Python] update_forward_refs ignores its namespace under pydantic v2, so recursive types through union aliases are never complete #18371
Description
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() # PydanticUserErrorCause
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
Activity