python: decode boolean-discriminated unions - #2123
Open
examon wants to merge 1 commit into
Open
Conversation
The Python generator captured a union discriminator's JSON Schema `const`
with `String()`, so a boolean const became the string "true"/"false" and the
emitted dispatcher matched `case "true":`. A JSON boolean decodes to Python
`True`, which never equals `"true"`, so every boolean-discriminated union
fell through to `raise ValueError`.
Two unions are affected. `sessions.list()` raised
`ValueError: Unknown SessionListEntry isRemote: False` for any non-empty
session list, and `QueuedCommandHandled.to_dict()` put the string `"true"` on
the wire where the schema declares `{"type": "boolean", "const": true}`.
Keep the const's JSON type through codegen and render it as a Python literal
(`True`/`False`), annotating the discriminator `ClassVar` as `bool`. This
mirrors how `go.ts` already models discriminator values. Regenerating changes
six lines of `python/copilot/generated/rpc.py`; no other language changes.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes Python decoding and serialization for boolean-discriminated RPC unions.
Changes:
- Preserves boolean discriminator values during Python code generation.
- Regenerates affected RPC models and dispatchers.
- Adds decode, encode, round-trip, and string-discriminator regression tests.
Show a summary per file
| File | Description |
|---|---|
scripts/codegen/python.ts |
Emits correctly typed Python discriminator literals. |
python/copilot/generated/rpc.py |
Uses boolean constants and match arms. |
python/test_rpc_generated.py |
Tests boolean union serialization and dispatch. |
Review details
- Files reviewed: 2/3 changed files
- Comments generated: 0
- Review effort level: Medium
Contributor
Cross-SDK Consistency Review ✅This PR fixes a Python-specific codegen bug where boolean JSON Schema Consistency check result: No issues found.
The changes maintain full cross-SDK consistency.
|
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.
Fixes #2122
The problem
scripts/codegen/python.tscaptured a union discriminator's JSON SchemaconstwithString(...), so a boolean const became the JavaScript string"true"/"false"before any emitter ran. The dispatch table was typedArray<{ value: string; typeName: string }>, so the type could not survive even if it had been captured.The generated Python therefore matched strings:
A JSON boolean decodes to Python
True, which never equals"true", so both boolean-discriminated unions in the schema fell through toraise ValueError:sessions.list()raisedValueError: Unknown SessionListEntry isRemote: Falsefor any non-empty session list.QueuedCommandResultfailed to decode, andQueuedCommandHandled.to_dict()emitted{"handled": "true"}where the schema declares{"type": "boolean", "const": true}withadditionalProperties: false.The fix
Keep the const's JSON type through codegen and render it as a Python literal.
PyDiscriminatorValue = string | boolean, with the capture keeping booleans as booleans and stringifying everything else exactly as before.True/Falsefor booleans.JSON.stringifycannot be reused here: it yields lowercasetrue, which Python parses as a capture pattern rather than a literal, and in a multi-armmatchthat is a hardSyntaxError.ClassVaris annotatedboolwhen the const is boolean, so the encode direction puts a real JSON boolean on the wire.This mirrors
scripts/codegen/go.ts, which already models discriminator values asstring | booleanand is unaffected by the bug.Regenerating changes six lines of
python/copilot/generated/rpc.py:No other generated file changes, in any language.
Scope
Deliberately left alone:
findPyDiscriminator'smapping.set(String(...)). Those keys feed only the variant-count validity check and the flat-union path, which both boolean unions bypass because they are$ref-based. No union in the schema reaches it today.ClassVarcollapse pass's field lookup, which builds its regex from the raw schema property name (isRemote) and so never matches the snake_casedis_remote. That is a separate defect, and it is currently load-bearing: it is whyLocalSessionMetadataValuekeeps a realis_remote: boolfield that already encodes correctly. Changing it would drop a required constructor parameter.Verification
Against the published 1.0.8 wheel, the reproducer raises
ValueError: Unknown SessionListEntry isRemote: Falseand exits 1. Against this branch it exits 0:Also verified in a scratch environment against the built branch:
ValueErrorLocalSessionMetadataValueValueErrorRemoteSessionMetadataValueValueError"isRemote": false/true_load_QueuedCommandResultontrue/falseValueErrorQueuedCommandHandled/QueuedCommandNotHandledjson.dumps(QueuedCommandHandled().to_dict()){"handled": "true"}{"handled": true}handled: "true"(a string)QueuedCommandHandledValueErrorisRemotemissing /None/"yes"/1/0ValueError(orAssertionError)ValueErrorcase True:compiles to an identity comparison, so1and0do not match it despiteTrue == 1. The error path stays intact.Tests
python/test_rpc_generated.pygains coverage for both unions, routed through the real dispatchers rather than the variant classes (a variant'sfrom_dictignores the discriminator, so calling it directly would pass even with the bug present):SessionList.from_dictdecodes a local and a remote entry in one payload to the right variants.CommandsRespondToQueuedCommandRequest.from_dictdecodes bothhandledvalues.is True/is Falseidentity and thatjson.dumpsproduces{"handled": true}/{"handled": false}.The new tests fail against the pre-fix generated file and pass after it.
Checks run
npm run generatefor all five languages plus the pinned nightlycargo fmtstep, leaving the tree byte-clean apart from the six intended Python lines.uv run ruff format --check .,uv run ruff check,uv run ty check copilot(two pre-existing warnings in unrelated hand-written files, exit 0).uv run pytest test_rpc_generated.py -v: 7 passed, repeated across runs and on Python 3.11.