From b4cd54235ba58b2de0d7005479547ae5319f080c Mon Sep 17 00:00:00 2001 From: Blake Ledden Date: Sun, 22 Feb 2026 14:25:13 -0800 Subject: [PATCH 1/2] fix: resolve PydanticSerializationUnexpectedValue warnings in responses.parse() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When calling model_dump() or model_dump_json() on a ParsedResponse returned by responses.parse(), Pydantic emits ~19 PydanticSerializationUnexpectedValue warnings. This happens because the runtime types (ParsedResponseOutputMessage[TypeVar], ParsedResponseOutputText[TypeVar]) don't match the declared union member types in the schema, so the serializer tries every variant and warns on each mismatch. The fix wraps the `output` and `content` fields with SerializeAsAny, which tells Pydantic to serialize using the runtime type's schema instead of the declared union member's schema. This only affects serialization — validation and deserialization are unchanged. Fixes #2872 --- src/openai/types/responses/parsed_response.py | 6 +- .../test_parsed_response_serialization.py | 109 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 tests/lib/responses/test_parsed_response_serialization.py diff --git a/src/openai/types/responses/parsed_response.py b/src/openai/types/responses/parsed_response.py index a859710590..ce81e3b488 100644 --- a/src/openai/types/responses/parsed_response.py +++ b/src/openai/types/responses/parsed_response.py @@ -3,6 +3,8 @@ from typing import TYPE_CHECKING, List, Union, Generic, TypeVar, Optional from typing_extensions import Annotated, TypeAlias +from pydantic import SerializeAsAny + from ..._utils import PropertyInfo from .response import Response from ..._models import GenericModel @@ -54,7 +56,7 @@ class ParsedResponseOutputMessage(ResponseOutputMessage, GenericModel, Generic[C if TYPE_CHECKING: content: List[ParsedContent[ContentType]] # type: ignore[assignment] else: - content: List[ParsedContent] + content: List[SerializeAsAny[ParsedContent]] class ParsedResponseFunctionToolCall(ResponseFunctionToolCall): @@ -93,7 +95,7 @@ class ParsedResponse(Response, GenericModel, Generic[ContentType]): if TYPE_CHECKING: output: List[ParsedResponseOutputItem[ContentType]] # type: ignore[assignment] else: - output: List[ParsedResponseOutputItem] + output: List[SerializeAsAny[ParsedResponseOutputItem]] @property def output_parsed(self) -> Optional[ContentType]: diff --git a/tests/lib/responses/test_parsed_response_serialization.py b/tests/lib/responses/test_parsed_response_serialization.py new file mode 100644 index 0000000000..095e81cc1b --- /dev/null +++ b/tests/lib/responses/test_parsed_response_serialization.py @@ -0,0 +1,109 @@ +"""Regression tests for PydanticSerializationUnexpectedValue warnings. + +See https://github.com/openai/openai-python/issues/2872 +""" +from __future__ import annotations + +import warnings + +from pydantic import BaseModel + +from openai._models import construct_type_unchecked +from openai.types.responses import Response +from openai.lib._parsing._responses import parse_response + + +class GuardrailDecision(BaseModel): + triggered: bool + reason: str + + +def _make_raw_response() -> Response: + """Build a minimal Response object from a dict — no API call needed.""" + return construct_type_unchecked( + type_=Response, + value={ + "id": "resp_test123", + "object": "response", + "created_at": 1234567890.0, + "model": "gpt-4o-mini", + "output": [ + { + "id": "msg_test123", + "type": "message", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": '{"triggered": true, "reason": "test content flagged"}', + "annotations": [], + } + ], + } + ], + "parallel_tool_calls": True, + "tool_choice": "auto", + "tools": [], + "temperature": 1.0, + "top_p": 1.0, + "text": {"format": {"type": "text"}}, + "truncation": "disabled", + }, + ) + + +def test_parsed_response_model_dump_no_warnings() -> None: + """model_dump() should not emit PydanticSerializationUnexpectedValue warnings.""" + raw = _make_raw_response() + parsed = parse_response( + text_format=GuardrailDecision, input_tools=None, response=raw + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + data = parsed.model_dump() + pydantic_warnings = [ + x for x in w if "PydanticSerializationUnexpectedValue" in str(x.message) + ] + + assert len(pydantic_warnings) == 0, ( + f"Expected 0 PydanticSerializationUnexpectedValue warnings, " + f"got {len(pydantic_warnings)}: {[str(x.message) for x in pydantic_warnings]}" + ) + + # Verify the parsed data is preserved correctly + assert data["output"][0]["content"][0]["parsed"] == { + "triggered": True, + "reason": "test content flagged", + } + + +def test_parsed_response_model_dump_json_no_warnings() -> None: + """model_dump_json() should not emit PydanticSerializationUnexpectedValue warnings.""" + raw = _make_raw_response() + parsed = parse_response( + text_format=GuardrailDecision, input_tools=None, response=raw + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parsed.model_dump_json() + pydantic_warnings = [ + x for x in w if "PydanticSerializationUnexpectedValue" in str(x.message) + ] + + assert len(pydantic_warnings) == 0 + + +def test_parsed_response_output_parsed() -> None: + """output_parsed property should return the parsed object.""" + raw = _make_raw_response() + parsed = parse_response( + text_format=GuardrailDecision, input_tools=None, response=raw + ) + + result = parsed.output_parsed + assert isinstance(result, GuardrailDecision) + assert result.triggered is True + assert result.reason == "test content flagged" From e61b3f09690280d4b503d4b0301483cf308c78a4 Mon Sep 17 00:00:00 2001 From: Blake Ledden Date: Sun, 22 Feb 2026 15:37:12 -0800 Subject: [PATCH 2/2] fix: guard SerializeAsAny import for Pydantic v1 compatibility The unconditional `from pydantic import SerializeAsAny` import raises ImportError on Pydantic v1, which the SDK still supports (pydantic>=1.9.0). Guard the import behind PYDANTIC_V1 and add v1 fallback branches in the class bodies that use the original undecorated type annotations. Co-Authored-By: Claude Opus 4.6 --- src/openai/types/responses/parsed_response.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/openai/types/responses/parsed_response.py b/src/openai/types/responses/parsed_response.py index ce81e3b488..5a27488e3d 100644 --- a/src/openai/types/responses/parsed_response.py +++ b/src/openai/types/responses/parsed_response.py @@ -3,7 +3,10 @@ from typing import TYPE_CHECKING, List, Union, Generic, TypeVar, Optional from typing_extensions import Annotated, TypeAlias -from pydantic import SerializeAsAny +from ..._compat import PYDANTIC_V1 + +if not PYDANTIC_V1: + from pydantic import SerializeAsAny from ..._utils import PropertyInfo from .response import Response @@ -55,8 +58,10 @@ class ParsedResponseOutputText(ResponseOutputText, GenericModel, Generic[Content class ParsedResponseOutputMessage(ResponseOutputMessage, GenericModel, Generic[ContentType]): if TYPE_CHECKING: content: List[ParsedContent[ContentType]] # type: ignore[assignment] - else: + elif not PYDANTIC_V1: content: List[SerializeAsAny[ParsedContent]] + else: + content: List[ParsedContent] # type: ignore[assignment] class ParsedResponseFunctionToolCall(ResponseFunctionToolCall): @@ -94,8 +99,10 @@ class ParsedResponseFunctionToolCall(ResponseFunctionToolCall): class ParsedResponse(Response, GenericModel, Generic[ContentType]): if TYPE_CHECKING: output: List[ParsedResponseOutputItem[ContentType]] # type: ignore[assignment] - else: + elif not PYDANTIC_V1: output: List[SerializeAsAny[ParsedResponseOutputItem]] + else: + output: List[ParsedResponseOutputItem] # type: ignore[assignment] @property def output_parsed(self) -> Optional[ContentType]: