fix(openapi): handle recursive PEP 695 type aliases in schema generation#4848
fix(openapi): handle recursive PEP 695 type aliases in schema generation#4848declaresub wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4848 +/- ##
==========================================
+ Coverage 67.27% 67.39% +0.11%
==========================================
Files 293 293
Lines 15235 15290 +55
Branches 1727 1741 +14
==========================================
+ Hits 10250 10304 +54
- Misses 4838 4839 +1
Partials 147 147 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
472865e to
2690c8c
Compare
`SchemaCreator.for_type_alias_type` unwrapped a `type` alias's `__value__` and recursed with no cycle guard, so a self-referential alias such as `type JSON = None | bool | str | list[JSON] | dict[str, JSON]` blew the stack with a RecursionError when building the OpenAPI document. Litestar already guards self-referential *models* via a registered `$ref`, but that guard was never extended to the type-alias path added in litestar-org#3715. A self-referential alias is now registered as a named component and its recursive occurrences resolve to a `$ref`, mirroring how recursive models are handled; non-recursive aliases are still inlined. Mutually recursive aliases terminate as well: the cycle entry point becomes the component, and aliases reached while expanding it are inlined. When an alias's value is itself a single component (e.g. `type Node = Container` where `Container` refers back to `Node`), the component defers to it via `allOf`. `_get_normalized_schema_key` now keys `TypeAliasType`, which exposes `__name__` but no `__qualname__`. Because schemas for recursive aliases now generate, example generation becomes reachable for them and would itself recurse without bound (the example factory guards recursive models but not aliases). Example generation is now skipped for any annotation that is, or contains, a self-referential alias; non-recursive aliases reused within a single annotation are unaffected. Fixes litestar-org#4843 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2690c8c to
e352224
Compare
Relationship to #4846Heads up for reviewers: #4846 (by @Sujit-1509) fixes the same issue (#4843) and was opened first. This PR is an alternative approach, not an attempt to duplicate it — the two differ in schema shape and in one functional respect worth weighing before choosing. Schema shape — free-form For
Both are valid. #4846 is simpler; the argument that a recursive Functional difference — example generation Independent of the schema shape, the example factory ( type JSON = None | bool | str | float | int | list[JSON] | dict[str, JSON]
@get("/")
async def h() -> dict[str, JSON]: ...
# OpenAPIConfig(..., create_examples=True)
app.openapi_schema.to_schema() # #4846: hangs; this PR: returnsThis PR adds a guard so example generation is skipped for any annotation that is, or contains, a self-referential alias (non-recursive aliases reused in one annotation are unaffected). If #4846 is the preferred direction, that guard is independently useful and I'm happy to contribute it there instead. Also covered here: mutually recursive aliases, and Happy to defer to #4846 and close this if the simpler shape is preferred — flagging the example-generation case so it isn't lost either way. |
Description
Fixes #4843
A recursive PEP 695
typealias used as a handler annotation crashed OpenAPIschema generation with
RecursionError.SchemaCreator.for_type_alias_typeunwrapped the alias's
__value__and recursed with no cycle guard, so aself-referential alias such as
type JSON = None | bool | str | list[JSON] | dict[str, JSON]expanded forever.Litestar already guards self-referential models via a registered
$ref(#2429 / #2869), but that guard was never extended to the
type-alias pathadded in #3715.
Changes
recursive occurrences resolve to a
$ref, mirroring how recursive models arehandled. Non-recursive aliases are still inlined, unchanged.
the component, and aliases reached while expanding it are inlined.
type Node = Containerwhere
Containerrefers back toNode), the component defers to it viaallOf._get_normalized_schema_keynow keysTypeAliasType, which exposes__name__but no
__qualname__.for them and would itself recurse without bound (the example factory guards
recursive models but not aliases). Example generation is now skipped for any
annotation that is, or contains, a self-referential alias; non-recursive
aliases reused within a single annotation are unaffected.
Known limitation
Mutually recursive aliases used at several independent entry points produce
valid but non-minimal components (each inlines a copy of the other's value).
This is documented in
for_type_alias_type.Tests
Added coverage in
tests/unit/test_openapi/test_schema.pyfor direct recursion,mutual recursion, alias-to-recursive-model (
allOf), example generation onrecursive aliases, and the recursion-detection helper (including the
non-recursive-reuse case).
📚 Documentation preview 📚: https://litestar-org.github.io/litestar-docs-preview/4848