fix(python): resolve enum discriminator tags in dict union input

Merged
2Y
1d ago
+122-19

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_discriminator converts discriminator values through a nested to_discriminator helper, which uses .value for 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_union looks the value up in a dict keyed by string, and a (str, Enum) member hashes and compares equal to its value.
  • New secondary test fragment oneof-enum-tag-nested.yaml adds oneOfEnumTagNestedPost. Its request body uses a oneOf, told apart by a single-value enum type property, once as an object property and once as array items. The oneOf is only used in a request, so it stays closed and goes through get_discriminator.
  • New pythonv2 secondary tests (enumFormat: enum) send enum-member discriminators in dicts at the top level (existing nullableOneOfSchemaPost) and nested (new operation).
  • Review SDK regenerated: only utils/annotations.py and its gen.lock checksum change.

Testing

  • New tests fail on Python 3.12 with the previous annotations.py and 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.