fix(python): resolve enum discriminator tags in dict union input
Merged
2Y
1d ago+122+122+012345678901234567890123456789-19-19-01234567890123456789
Why
With enumFormat: enum, a closed discriminated union fails validation on Python 3.11+ when it receives a dict whose discriminator holds an enum member instead of a raw string. On 3.10 the same input works:
sdk.unions.nullable_one_of_schema_post(
request={"type": shared.TypedObject1Type.OBJ1, "value": "one"}
)
# 3.11+: union_tag_invalid
# Input tag 'TypedObject1Type.OBJ1' found using <lambda>() does not match any of the expected tags: 'obj1', 'obj2'
Generated usage snippets build request dicts this way when enumFormat: enum, so copying them reproduces the error.
get_discriminator (utils/annotations.py) builds the discriminator from a dict with f"{field[key]}". Python 3.11 changed Enum.__format__ so that a mixed-in (str, Enum) or (int, Enum) member formats as ClassName.MEMBER. Before 3.11 it formatted as its value. The attribute branches of the same function already unwrapped .value; only the dict branch was affected.
What changed
get_discriminatorconverts discriminator values through a nestedto_discriminatorhelper, which uses.valuefor enum members. The dict branch and both attribute branches use it, and the two duplicated attribute-branch blocks are gone. Behavior is unchanged on Python 3.10, where mixed-in enums already formatted as their value.- Forward-compatible (open) unions are not affected.
parse_open_unionlooks the value up in a dict keyed by string, and a(str, Enum)member hashes and compares equal to its value. - New
secondarytest fragmentoneof-enum-tag-nested.yamladdsoneOfEnumTagNestedPost. Its request body uses a oneOf, told apart by a single-value enumtypeproperty, once as an object property and once as array items. The oneOf is only used in a request, so it stays closed and goes throughget_discriminator. - New pythonv2 secondary tests (
enumFormat: enum) send enum-member discriminators in dicts at the top level (existingnullableOneOfSchemaPost) and nested (new operation). - Review SDK regenerated: only
utils/annotations.pyand itsgen.lockchecksum change.
Testing
- New tests fail on Python 3.12 with the previous
annotations.pyand pass on 3.10. With the fix they pass on 3.10, 3.11, 3.12 and 3.13. TARGET=secondary make test-pythonv2: 384 passed, 1 skipped (local Python 3.10). CI covers 3.10–3.14.TARGET=review make test-pythonv2: passed.TARGET=primary make test-pythonv2: passed (run before the helper was renamed; no behavior change since).TARGET=secondary make build-<target>for go, typescriptv2, csharp, javav2, php, ruby, cli and mcp-typescript: passed, since the new fragment applies to every target's secondary variant. Their test suites, and terraform, postman and unity, were not run locally.make lint: passed.
Public-safety check
- This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
- Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (
.claude/skills/public-repo-communication/SKILL.md). - Generated fixtures and review SDK changes are public-safe.
- I reviewed
git diff --check.