Skip to content

Python: send a Pydantic response_format to Gemini as response_schema - #7879

Open
Charles (anneheartrecord) wants to merge 1 commit into
microsoft:mainfrom
anneheartrecord:fix/gemini-response-format-pydantic
Open

Python: send a Pydantic response_format to Gemini as response_schema#7879
Charles (anneheartrecord) wants to merge 1 commit into
microsoft:mainfrom
anneheartrecord:fix/gemini-response-format-pydantic

Conversation

@anneheartrecord

Copy link
Copy Markdown
Contributor

What

response_format accepts "a Pydantic model type or JSON schema mapping" per its own documentation, but only the mapping half reaches Gemini as a response_schema. A model class falls through _extract_response_schema, so the request goes out with response_mime_type="application/json" and no schema attached.

This converts the model class with model_json_schema().

Why it slipped through

#5893 fixed exactly this failure for mapping-shaped values, and said so explicitly — it extracts schemas from "supported mapping-shaped response_format values". _extract_response_schema opens with if not isinstance(response_format, Mapping): return None, which is correct for that scope. The Pydantic class shape simply was not in it.

The existing tests sit either side of the gap without covering it:

  • test_response_format_sets_json_mime_type passes a Pydantic model and asserts response_mime_type, but not response_schema.
  • test_response_format_populates_value_on_chat_response asserts the inbound parse into that model, which works regardless.

So the outbound half is never checked for this shape, and the request has been going out unconstrained.

Effect

Gemini is asked for JSON without being told what shape, and _process_generate_response then parses whatever comes back into the model the caller asked for. Depending on what the model returns, that is either a ValidationError the caller did not expect or — worse — JSON that happens to validate while ignoring the intent of the schema. It is #5888's failure mode on a different shape.

Consistency with the other clients

_prepare_response_format in Mistral, _prepare_output_config in Bedrock, and the Anthropic equivalent all branch on isinstance(x, type) and issubclass(x, BaseModel) and call model_json_schema(). Gemini was the one client in that group without the branch. Ollama had the same gap and it was closed in #6782.

Tests

Two added:

  • test_response_format_pydantic_model_sets_response_schema — the regression. Verified red-before-green: with the source change reverted it fails with assert None == {'properties': {'text': ...}, 'title': 'Reply', 'type': 'object'}, None being GenerateContentConfig(response_mime_type='application/json').response_schema.
  • test_response_schema_option_wins_over_pydantic_response_format — a guard that an explicit response_schema still takes precedence, as it already does for mapping shapes.

ruff check and ruff format --check are clean on the package.

On the suite run: packages/gemini/tests/ hangs for me on the same test before and after this change — 146 tests pass then it stalls in an asyncio loop on main, 148 with these two added, no failures either way. It looks environmental on my machine rather than related to this diff, but flagging it rather than claiming a clean full-suite run I did not get.

One question

The docstring already promised Pydantic support, so I treated this as a defect rather than a feature and kept the change to the one branch. If you would rather _extract_response_schema stay mapping-only and have the conversion happen upstream where the option is validated, say so and I will move it — I did not want to widen the surface without asking.

microsoft#5893 taught the Gemini client to forward mapping-shaped response_format
values as a Gemini response_schema, and scoped itself to those shapes. A
Pydantic model class - the first shape the option's own documentation
offers - still falls through _extract_response_schema, so the request
carries response_mime_type="application/json" with no schema attached.

Gemini is then asked for JSON but is not constrained by the model, and the
free-form JSON it returns is handed to that same model for parsing on the
way back. That is the failure mode microsoft#5888 described, on the shape microsoft#5893 did
not cover.

Convert the model class with model_json_schema(), matching what the
Anthropic, Bedrock and Mistral clients already do for this option.

Copilot AI 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.

Pull request overview

Adds missing Gemini schema forwarding for Pydantic response models.

Changes:

  • Converts Pydantic model classes using model_json_schema().
  • Tests schema forwarding and explicit-schema precedence.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
_chat_client.py Extracts Gemini schemas from Pydantic models.
test_gemini_client.py Adds structured-output regression tests.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +941 to +942
Handles Pydantic model classes and the mapping shapes (raw JSON schema,
``json_schema`` envelopes, ``format`` envelopes).

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.

Reviewed the full change at 320e4791. The Pydantic model guard is correctly ordered before the mapping handling, and the existing explicit response_schema precedence remains intact. I also checked the downstream structured-response parsing and the existing raw-schema and envelope paths.

On Windows with Python 3.12, I ran the complete packages/gemini/tests/test_gemini_client.py file: 148 passed and 8 integration tests skipped. The focused response_format group passed 21 tests, Ruff reported no findings for the Gemini package, and Pyright completed with 0 errors and 0 warnings.

The existing inline note about documenting the direct {"schema": ...} envelope is valid, but I found no additional functional or test issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants