Skip to content

Fix json.encode of a dict keyed by a plain str Enum - #1118

Merged
Siyet merged 1 commit into
msgspec:mainfrom
binggao1230:fix-json-encode-str-enum-key
Jul 13, 2026
Merged

Siyet merged 1 commit into
msgspec:mainfrom
binggao1230:fix-json-encode-str-enum-key

Conversation

@binggao1230

Copy link
Copy Markdown
Contributor

Problem

Encoding a dict whose keys are members of a plain enum.Enum with str values
raises, even though the same key type round-trips through every other path:

import msgspec, enum

class Fruit(enum.Enum):
    APPLE = "apple"
    BANANA = "banana"

msgspec.json.decode(b'{"apple": 1}', type=dict[Fruit, int])  # ok
msgspec.msgpack.encode({Fruit.APPLE: 1})                     # ok
msgspec.to_builtins({Fruit.APPLE: 1})                        # ok -> {'apple': 1}
msgspec.json.encode({Fruit.APPLE: 1})
# TypeError: Only dicts with str-like or number-like keys are supported

dict[Fruit, int] is an accepted decode type and both msgpack.encode and
to_builtins handle the key, so json.encode rejecting it is a cross-codec
inconsistency.

Cause

json_encode_enum routes a key's underlying value through
json_encode_dict_key_noinline, which only handles non-str keys (str keys
are fast-pathed in json_encode_dict_key). A plain str-valued enum therefore
hits the "unsupported key" error.

Fix

When encoding an enum as a dict key, write a str value straight to the key
with json_encode_str; non-str values keep going through
json_encode_dict_key_noinline as before.

Tests

test_encode_dict_plain_enum_str_key covers a plain str Enum used as a dict
key (encode output and encode/decode round-trip), and FruitStr is added to the
existing mixed-key-type dict test.

pytest tests/unit/test_json.py → 1786 passed.

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 Siyet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@Siyet
Siyet added this pull request to the merge queue Jul 13, 2026
@codspeed

codspeed Bot commented Jul 13, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 139 untouched benchmarks
⏩ 135 skipped benchmarks1


Comparing gaoflow:fix-json-encode-str-enum-key (04a4045) with main (956a7a4)

Open in CodSpeed

Footnotes

  1. 135 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Merged via the queue into msgspec:main with commit a1ebbcd Jul 13, 2026
26 of 27 checks passed
Brohammad pushed a commit to Brohammad/msgspec that referenced this pull request Sep 4, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants