From e59d0068032bf1ffba31f3e6a0180bde7c8cfc5a Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 18:06:43 +0100 Subject: [PATCH] fix: handle ValidationError in structured output parsing gracefully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the API returns malformed content (e.g., whitespace-only or truncated JSON) in a structured output response, model_parse_json raises a pydantic.ValidationError that propagates unhandled to the caller. Fix: Catch pydantic.ValidationError in maybe_parse_content() and return None instead, setting parsed=None on the message. This matches the existing behavior for refusals — the raw content remains available via message.content while parsed is None. A warning is logged to help users diagnose the issue. Fixes #1763 --- src/openai/lib/_parsing/_completions.py | 10 ++++++- tests/lib/chat/test_completions.py | 38 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index 7a1bded1de..4f1e3da40e 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -193,7 +193,15 @@ def maybe_parse_content( message: ChatCompletionMessage | ParsedChatCompletionMessage[object], ) -> ResponseFormatT | None: if has_rich_response_format(response_format) and message.content and not message.refusal: - return _parse_content(response_format, message.content) + try: + return _parse_content(response_format, message.content) + except pydantic.ValidationError: + log.warning( + "Failed to parse structured output content into %s; " + "`.parsed` will be `None`. Raw content is available via `.content`.", + response_format.__name__ if hasattr(response_format, "__name__") else response_format, + ) + return None return None diff --git a/tests/lib/chat/test_completions.py b/tests/lib/chat/test_completions.py index 85bab4f095..fbd7843d93 100644 --- a/tests/lib/chat/test_completions.py +++ b/tests/lib/chat/test_completions.py @@ -993,3 +993,41 @@ def test_parse_method_in_sync(sync: bool, client: OpenAI, async_client: AsyncOpe checking_client.chat.completions.parse, exclude_params={"response_format", "stream"}, ) + + +@pytest.mark.respx(base_url=base_url) +def test_parse_invalid_json_returns_none( + client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch +) -> None: + """When the API returns non-JSON content (e.g. whitespace), `parsed` should be None + rather than raising an unhandled ValidationError. + + Regression test for https://github.com/openai/openai-python/issues/1763 + """ + + class Location(BaseModel): + city: str + temperature: float + units: Literal["c", "f"] + + completion = make_snapshot_request( + lambda c: c.chat.completions.parse( + model="gpt-4o-2024-08-06", + messages=[ + { + "role": "user", + "content": "What's the weather like in SF?", + }, + ], + response_format=Location, + ), + content_snapshot=snapshot( + '{"id": "chatcmpl-invalid-json", "object": "chat.completion", "created": 1727346143, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "\\n \\n\\n \\n \\n", "refusal": null}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 79, "completion_tokens": 14, "total_tokens": 93, "completion_tokens_details": {"reasoning_tokens": 0}}, "system_fingerprint": "fp_5050236cbd"}' + ), + path="/chat/completions", + mock_client=client, + respx_mock=respx_mock, + ) + + assert completion.choices[0].message.parsed is None + assert completion.choices[0].message.content == "\n \n\n \n \n"