Generate OpenAPI schemas for unions on Python 3.14 - #157
Merged
Merged
Conversation
Python 3.14 unified `types.UnionType` with `typing.Union`, so `str | None` now reports an origin (`typing.Union`) where on 3.13 it had none. The `hasattr(t, "__origin__")` branch ran first and raised "Unknown type" for anything it didn't recognize, so every optional field in an API schema raised and serving `openapi.json` returned a 500. Unions are now matched explicitly, before the list/dict branches, via `get_origin(t) in (Union, UnionType)` — so `X | None`, `Optional[X]`, and `Union[X, None]` all work. The root cause was probing `__origin__` / `__args__` directly instead of asking `typing`, so the whole function now goes through `get_origin`/`get_args`. A union of several named types used to raise; it now emits `anyOf`, with `nullable: true` when `None` is a member. `X | None` keeps its existing shape, so no already-generated document changes.
Contributor
|
Next steps:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
plain api generate-openapiraisesValueError: Unknown type: str | Noneon Python 3.14, so any view with an optional field in its response schema produces no document and the served/api/openapi.json500s. Shipped in 0.36.0; found tonight by plainframework.com's own upgrade, which held its deploy over it.Cause. Python 3.14 unified
types.UnionTypewithtyping.Union, so(str | None).__origin__now exists and istyping.Unionwhere on 3.13 it raised AttributeError.schema_from_type()probedhasattr(t, "__origin__"), handledlistanddict, and raised on anything else — and the branch that handlesX | Nonesat after it. On 3.13 unions fell through to the right branch; on 3.14 they're caught and rejected.Fix. Match unions explicitly and first, and use
typing.get_origin/get_argsrather than probing dunder attributes, which is the actual root cause: attribute probing on typing internals is what broke across a Python release.X | Nonekeeps byte-identical output (member schema plusnullable: true), so no document that generated on 3.13 changes. A union of several named members now emitsanyOfplusnullablewhenNoneis among them, instead of raising —str | intis a legal annotation, and taking down the whole document over one property is the same failure mode this PR exists to fix. A subscripted generic the generator doesn't model (tuple[int, str],set[int],Annotated[...]) still raises, as before.Audit.
__origin__/__args__appear in exactly one function repo-wide, the one fixed here. Every other union check in the repo (plain-mcp's_type_to_schema, plain-postgres'written.pyand preflight, plain's settings parser, plain-api's owntyped_dict_from_annotation) already testsget_origin(t) in (Union, UnionType)with the union branch ahead of the generic branches, so nothing else is exposed. plain-mcp's version is the same function written correctly.Eight tests, all confirmed failing against the pre-fix code, including one that generates a whole document with
str | None,list[str | None]andstr | int | Nonefields and validates it against the OpenAPI 3.0.3 schema — the regression at the level where the 500 appears.Verified:
./scripts/fixclean; plain-api 53 passed;./scripts/type-check plain-apiand./scripts/type-validateclean.