Fix json.encode of a dict keyed by a plain str Enum - #1118
Conversation
Encoding a dict whose keys are members of a plain enum.Enum with str values raised "Only dicts with str-like or number-like keys are supported", even though dict[MyEnum, int] is an accepted decode type and both msgpack.encode and to_builtins handle such keys. The asymmetry came from json_encode_enum routing a key's underlying value through json_encode_dict_key_noinline, which only handles non-str keys (str keys are fast-pathed elsewhere) and therefore rejected the str value. Write a str enum value straight to the key with json_encode_str; non-str values keep going through json_encode_dict_key_noinline as before.
Siyet
left a comment
There was a problem hiding this comment.
Verified against main: json.encode of a dict keyed by a plain str Enum (and the same nested in a Struct, or round-tripped) raises on main and works here, while every other key path stays byte-identical: plain int Enum keys, IntEnum keys, str-mixin Enum keys, plain str/int keys, and enum-as-value are all unchanged. json.encode now agrees with msgpack.encode, to_builtins, and the dict[FruitStr, int] decode type, closing the cross-codec inconsistency.
The C fix mirrors json_encode_dict_key's own PyUnicode_Check(value) ? json_encode_str : json_encode_dict_key_noinline dispatch, so an enum key with a str value takes exactly the same path as a plain str key. Refcounting on value is unchanged (single DECREF on all paths). tests/unit/test_json.py passes (1786). Thanks!
Merging this PR will not alter performance
Comparing Footnotes
|
The 0.21.1 to 0.22.0 changelog batch (msgspec#1112) landed on 2026-07-03. Four user-visible changes merged between then and the 0.22.0 release on 2026-08-11 never got an entry: - msgspec#700, overloads on the `Meta` stub, so type checkers reject mixing `gt` with `ge` and `lt` with `le` - msgspec#1028, `null` placed last in the `anyOf` generated for optional unions - msgspec#1114, `__struct_encode_fields__` added to `src/msgspec/__init__.pyi` (its companion msgspec#813 only touched `tests/typing`, so it needs no entry of its own) - msgspec#1118, `json.encode` raising `TypeError` for a `dict` keyed by a plain `str`-valued `enum.Enum` I went over every PR merged in that window. The rest need no entry: msgspec#813 and msgspec#1117 are tests only, msgspec#1121 fixes an unreleased regression from msgspec#1028, msgspec#1145 is an unused variable under free-threaded builds, and msgspec#1146 and msgspec#1148 are CI and tooling. msgspec#1127, msgspec#1135 and msgspec#1080 already have entries. Docs build clean with `--fail-on-warning`. Note for whoever retries the release: the section is still headed `Version 0.22.0 (2026-08-11)`, and that date will need updating, since the tag was deleted after the upload failed. Refs msgspec#1134. Co-authored-by: Siyet <Siyet@users.noreply.github.com>
Problem
Encoding a dict whose keys are members of a plain
enum.Enumwithstrvaluesraises, even though the same key type round-trips through every other path:
dict[Fruit, int]is an accepted decode type and bothmsgpack.encodeandto_builtinshandle the key, sojson.encoderejecting it is a cross-codecinconsistency.
Cause
json_encode_enumroutes a key's underlying value throughjson_encode_dict_key_noinline, which only handles non-strkeys (strkeysare fast-pathed in
json_encode_dict_key). A plainstr-valued enum thereforehits the "unsupported key" error.
Fix
When encoding an enum as a dict key, write a
strvalue straight to the keywith
json_encode_str; non-strvalues keep going throughjson_encode_dict_key_noinlineas before.Tests
test_encode_dict_plain_enum_str_keycovers a plain strEnumused as a dictkey (encode output and encode/decode round-trip), and
FruitStris added to theexisting mixed-key-type dict test.
pytest tests/unit/test_json.py→ 1786 passed.