Problem
Speakeasy's Python generator uses TYPE_CHECKING imports to break circular schema references, but only emits model_rebuild() for the immediate forward-referenced model — not for models that transitively depend on it. This causes PydanticUserError: not fully defined at instantiation time for any model in the dependency chain.
Affected Schema
RowFilteringOperationNot has a recursive conditions field:
RowFilteringOperationNot:
properties:
conditions:
type: array
items:
$ref: "#/components/schemas/RowFilteringOperation" # circular
RowFilteringOperation:
oneOf:
- $ref: "#/components/schemas/RowFilteringOperationEqual"
- $ref: "#/components/schemas/RowFilteringOperationNot" # back-reference
The upstream spec marks this: x-airbyte-circular-ref: true.
Symptoms
Speakeasy generates RowFilteringOperationNot1 (with 1 suffix from name collision) and imports RowFilteringOperation under TYPE_CHECKING. It emits RowFilteringOperationNot1.model_rebuild() in __init__.py but NOT for dependent models. Result:
PydanticUserError: \`ConnectionResponse\` is not fully defined;
you should define \`RowFilteringOperationNot1\`, then call \`ConnectionResponse.model_rebuild()\`.
3 models affected: ConnectionsResponse, RowFilteringMapperConfiguration, StreamConfigurations.
Workaround
We use an OpenAPI overlay to break the circular $ref by pointing RowFilteringOperationNot.conditions.items directly at RowFilteringOperationEqual instead of RowFilteringOperation:
# overlays/python_speakeasy.yaml
actions:
- target: "$.components.schemas.RowFilteringOperationNot.properties.conditions.items"
update:
$ref: "#/components/schemas/RowFilteringOperationEqual"
This removes the recursion (NOT(NOT(x)) = x, so nested NOT is redundant) and allows Speakeasy to generate clean direct imports without TYPE_CHECKING.
Investigation
Upstream Fix Needed
Speakeasy should emit model_rebuild() for ALL models that transitively depend on TYPE_CHECKING forward refs, not just the immediate one. This would fix the issue without needing the overlay workaround.
Related
Devin session
Reported by Aaron ("AJ") Steers (@aaronsteers), investigated and documented in Devin session.
Problem
Speakeasy's Python generator uses
TYPE_CHECKINGimports to break circular schema references, but only emitsmodel_rebuild()for the immediate forward-referenced model — not for models that transitively depend on it. This causesPydanticUserError: not fully definedat instantiation time for any model in the dependency chain.Affected Schema
RowFilteringOperationNothas a recursiveconditionsfield:The upstream spec marks this:
x-airbyte-circular-ref: true.Symptoms
Speakeasy generates
RowFilteringOperationNot1(with1suffix from name collision) and importsRowFilteringOperationunderTYPE_CHECKING. It emitsRowFilteringOperationNot1.model_rebuild()in__init__.pybut NOT for dependent models. Result:3 models affected:
ConnectionsResponse,RowFilteringMapperConfiguration,StreamConfigurations.Workaround
We use an OpenAPI overlay to break the circular
$refby pointingRowFilteringOperationNot.conditions.itemsdirectly atRowFilteringOperationEqualinstead ofRowFilteringOperation:This removes the recursion (
NOT(NOT(x)) = x, so nested NOT is redundant) and allows Speakeasy to generate clean direct imports withoutTYPE_CHECKING.Investigation
1suffix andTYPE_CHECKINGpattern are caused by the circular dependency, not the name collisionmodel_json_schema()Upstream Fix Needed
Speakeasy should emit
model_rebuild()for ALL models that transitively depend onTYPE_CHECKINGforward refs, not just the immediate one. This would fix the issue without needing the overlay workaround.Related
nameResolutionFeb2025+conflictResistantModelImportsFeb2026)Devin session
Reported by Aaron ("AJ") Steers (@aaronsteers), investigated and documented in Devin session.