fix(python): try integer before number in union deserialization#3059
Merged
Conversation
For a union of `integer` and `number` (e.g. JSON Schema `type: ["integer", "number"]`), the Python target emitted `from_union([from_float, from_int], ...)`. `from_float` accepts plain ints and coerces them to `float`, so `from_int` was unreachable: every integer value silently turned into a float and the round-trip was not bijective (`5` became `5.0`). Reorder Python union member (de)serializer dispatch so the `integer` branch is always tried before `number`/`double`. Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
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.
Bug
For a schema whose type is a union of
integerandnumber(e.g."type": ["integer", "number"]), the Python target generated:from_floatis tried first and its assertion (isinstance(x, (float, int)) and not isinstance(x, bool)) accepts plain Pythonints too, coercing themto
float. Because of that,from_intwas unreachable for this union: everyinteger value silently became a float, so
to_dict(from_dict(x))did notround-trip (
5became5.0).Root cause
JSONPythonRendererbuilt the list of union-member (de)serializer lambdas inthe union's natural member order, without regard for the fact that the
double/floatdeserializer in this renderer also accepts integers. Whendoublehappened to precedeintegerin that order, the more specificintegercase became dead code.Fix
Added a small
unionMembershelper inJSONPythonRenderer.tsthat, when aunion contains both an
integerand adoublemember, movesintegerahead of
doublein the emitted dispatch list (for both deserialization andserialization), while leaving other members' relative order untouched.
Test coverage
test/inputs/schema/integer-before-number.schema(
"type": ["integer", "number"], matching the issue's repro) with a plaininteger sample, a real float sample, and a
.fail.union.jsonnegativesample. Wired to run only for the
pythonfixture — I added an explicitskipSchemaentry (with comment) to every other language config, sincethis fixture's purpose is specifically to pin down Python's union member
ordering and I didn't want to change behavior/coverage for ~30 other
unrelated language targets.
fixture round-trip check (
compareJsonFileToJson→JSON.parse+numeric equality) can't actually distinguish
5from5.0, sinceJavaScript has a single
numbertype and5 === 5.0. So atest/unit/python-union-order.test.tsunit test was added as anecessary complement, asserting the generated source literally contains
from_union([from_int, from_float](this is exactly the "fixtures can'texpress this" case called out in the repo's testing conventions).
Verification
npm run buildpasses.npm run test:unitpasses (177 tests, including the new one).QUICKTEST=true FIXTURE=schema-python script/testpasses (72/72 fixtures,including the new
integer-before-number.schema).geo.schemaexample and confirmedthe generated code now emits
from_union([from_int, from_float], ...),and that in a real Python run
geo_from_dict(5)returnsint5(not5.0) and round-trips correctly, while5.5still round-trips as a float.this change.
Fixes #1318
🤖 Generated with Claude Code