Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/openai/types/responses/parsed_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
from typing import TYPE_CHECKING, List, Union, Generic, TypeVar, Optional
from typing_extensions import Annotated, TypeAlias

from ..._compat import PYDANTIC_V1

if not PYDANTIC_V1:
from pydantic import SerializeAsAny

from ..._utils import PropertyInfo
from .response import Response
from ..._models import GenericModel
Expand Down Expand Up @@ -53,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]
elif not PYDANTIC_V1:
content: List[SerializeAsAny[ParsedContent]]
else:
content: List[ParsedContent]
content: List[ParsedContent] # type: ignore[assignment]


class ParsedResponseFunctionToolCall(ResponseFunctionToolCall):
Expand Down Expand Up @@ -92,8 +99,10 @@ class ParsedResponseFunctionToolCall(ResponseFunctionToolCall):
class ParsedResponse(Response, GenericModel, Generic[ContentType]):
if TYPE_CHECKING:
output: List[ParsedResponseOutputItem[ContentType]] # type: ignore[assignment]
elif not PYDANTIC_V1:
output: List[SerializeAsAny[ParsedResponseOutputItem]]
else:
output: List[ParsedResponseOutputItem]
output: List[ParsedResponseOutputItem] # type: ignore[assignment]

@property
def output_parsed(self) -> Optional[ContentType]:
Expand Down
109 changes: 109 additions & 0 deletions tests/lib/responses/test_parsed_response_serialization.py
Original file line number Diff line number Diff line change
@@ -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"