fix(dspy): preserve typed dict/map value schemas in enforce_required - #94
Open
detail-app[bot] wants to merge 1 commit into
Open
Conversation
Greptile SummaryThis PR corrects strict structured-output schema normalization so nested typed dictionaries retain their declared value schemas instead of becoming closed empty objects.
Confidence Score: 5/5The PR appears safe to merge, with the typed-map schema fix covered across normalization, routing, conversion, and parsing paths. No actionable failures remain; the changed branch preserves typed map schemas while retaining the prior behavior for open-ended and fixed-property objects. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Pydantic output schema] --> B{Object has properties?}
B -->|Yes| C[Require every property]
C --> D[Set additionalProperties false]
B -->|No| E{additionalProperties value}
E -->|Typed schema| F[Preserve and recursively normalize value schema]
E -->|true or absent| G[Set additionalProperties false]
E -->|false| H[Keep closed map]
F --> I[Add required empty list]
G --> I
H --> I
I --> J[Strict structured-output response format]
Reviews (1): Last reviewed commit: "fix(dspy): preserve typed dict/map value..." | Re-trigger Greptile |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
GitHub issue creation failed
Detail attempted to publish this bug to GitHub, but the issue could not be created. This fix PR was created without that issue, and missing tracker references are shown as
Unknown issue.You can review and merge this PR normally. Please review your tracker integration settings before the next publish run.
Detail bug report: View on Detail
📝 Changes Description
This MR/PR contains the following changes:
JSONAdapter._get_structured_outputs_response_formatpost-processes the pydantic JSON schema with an innerenforce_requirednormalizer (dspy/adapters/json_adapter.py). Its no-propertiesbranch — written as a defensive fallback — unconditionally rewrote anytype: objectschema lacking apropertieskey to{"properties": {}, "required": [], "additionalProperties": false}. Pydantic emits typed dict/map fields (dict[str, int],dict[str, dict[str, int]], etc.) as exactly such an object —{"type": "object", "additionalProperties": <value-schema>}with noproperties— so the typedadditionalPropertieswas destroyed for any dict field that reached the strict path: nested in a container (list[dict[str, int]]) or as a sub-field of a pydanticBaseModeloutput (reached via$defsrecursion). The top-level guard_has_open_ended_mappingonly intercepts top-leveldict[K, V], so nested typed dicts silently degraded on-wire to a closed empty object, andJSONAdapter.parsethen accepted[]/[{}, …]with the declared contents erased — silent data loss with no exception or warning. Introduced inb8d9092(Fix JSON Adapter's first attempt, all Adapters for ReAct trajectories stanfordnlp/dspy#8051).enforce_required's no-propertiesbranch mirror OpenAI's own strict-mode converter (openai/lib/_pydantic.py:49-51, which only setsadditionalProperties: falsewhen the key is absent):additionalProperties(the dict value schema),additionalProperties: true(dict[str, Any]) tofalse,additionalProperties: falsewhen absent; add a vacuousrequired: [].properties: {}key is dropped on the wire — semantically identical). Fixed-property objects and the top-leveldict[K, V]routing guard are untouched.✅ Contributor Checklist
uv run ruff checkclean on both changed filesfix(dspy): preserve typed dict/map value schemas in enforce_requiredfix(dspy): preserve typed dict/map value schemas in enforce_requiredTesting summary. Verified via unit tests, mocked end-to-end routing, the OpenAI strict-converter transform, and the parse path:
tests/adapters/test_json_adapter.py) assert, at the schema level, thatlist[dict[str, int]]preservesitems.additionalProperties: {"type": "integer"}; thatdict[str, dict[str, int]]recursion preserves the inner typed map; that adict[str, int]sub-field of a pydanticBaseModeloutput is preserved via$defs; and that open-endedlist[dict[str, Any]]still collapses to the closed-empty-object (additionalProperties: false, noproperties). Inverse regressions verify fixed-property objects (required = [all keys],additionalProperties: false), scalar arrays, and top-leveldict[str, int]still routing tojson_object.list[dict[str, int]]stays on the strict pydantic-model path (not thejson_objectfallback), and a populated emission{"metadata": [{"score": 3, "rank": 1}, {"score": 9}]}now parses back to the declared typed dicts (no silent data loss).openai.lib._pydantic.to_strict_json_schema(the local transform litellm applies before sending to the OpenAI strict API) and asserts the typedadditionalPropertiessurvives — i.e. the API receives the typed map, not the closed empty object.ruff checkpasses on both files;pytest tests/adapters/ tests/predict/→ 540 passed, 63 skipped; the broader sweep includingtests/evaluate/test_evaluate.py(which uses anentities: list[dict[str, str]]signature) → 552 passed, 88 skipped; CI-parity invocation with-n auto --dist worksteal -m 'not extra and not deno'→ 540 passed, 2 skipped. ExistingLMError-propagation tests still pass, confirming provider errors still re-raise without a JSON-mode retry.[{...}, {...}]entries against the corrected on-wire schema. Attempted the smoke with no key (LMServerError: Missing credentials) and a dummy key (LMAuthError: Incorrect API key); both blocked on credentials, not on the fix. Everything the live call would prove beyond the committed tests is the model's literal emission cardinality, which is the only predicted-not-observed gap.AI disclosure. Authored by Detail (automatic bug-fix tool). The fix and tests were generated and verified locally; no live API key was available. A maintainer with an OpenAI key can run the smoke snippet from the issue to confirm the populated-emission consequence end-to-end.
Automatic Fixes PRs can be configured here.