From 0dd477ebb0f16086891daf132bec3d3659c3051e Mon Sep 17 00:00:00 2001 From: Yashwant Bezawada Date: Wed, 5 Nov 2025 17:17:01 -0600 Subject: [PATCH] Fix: Handle empty/invalid JSON in client.chat.parse() Fixes #282 The client.chat.parse() method crashed with JSONDecodeError when the API returned empty, whitespace-only, or invalid JSON content. Changes: - Add content validation before JSON parsing in struct_chat.py - Strip whitespace and check if content is non-empty - Catch JSONDecodeError and set parsed to None instead of crashing - Add 4 comprehensive test cases for edge cases This change improves error handling without breaking existing functionality. Empty or invalid JSON content now results in parsed=None instead of raising an exception. --- src/mistralai/extra/struct_chat.py | 12 +- src/mistralai/extra/tests/test_struct_chat.py | 111 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/mistralai/extra/struct_chat.py b/src/mistralai/extra/struct_chat.py index 364b450f..66571858 100644 --- a/src/mistralai/extra/struct_chat.py +++ b/src/mistralai/extra/struct_chat.py @@ -24,7 +24,17 @@ def convert_to_parsed_chat_completion_response(response: ChatCompletionResponse, parsed=None ) if isinstance(parsed_message.content, str): - parsed_message.parsed = pydantic_model_from_json(json.loads(parsed_message.content), response_format) + # Validate content is non-empty and valid JSON before parsing + content = parsed_message.content.strip() + if content: + try: + parsed_message.parsed = pydantic_model_from_json(json.loads(content), response_format) + except json.JSONDecodeError: + # Handle invalid JSON gracefully - treat as unparseable content + parsed_message.parsed = None + else: + # Empty or whitespace-only content + parsed_message.parsed = None elif parsed_message.content is None: parsed_message.parsed = None else: diff --git a/src/mistralai/extra/tests/test_struct_chat.py b/src/mistralai/extra/tests/test_struct_chat.py index dd529ba5..685737e0 100644 --- a/src/mistralai/extra/tests/test_struct_chat.py +++ b/src/mistralai/extra/tests/test_struct_chat.py @@ -98,6 +98,117 @@ def test_convert_to_parsed_chat_completion_response(self): ) self.assertEqual(output, expected_response) + def test_empty_string_content(self): + """Test that empty string content is handled gracefully (issue #282)""" + response = ChatCompletionResponse( + id="test-empty", + object="chat.completion", + model="codestral-latest", + usage=UsageInfo(prompt_tokens=10, completion_tokens=0, total_tokens=10), + created=1234567890, + choices=[ + ChatCompletionChoice( + index=0, + message=AssistantMessage( + content="", # Empty string + tool_calls=None, + prefix=False, + role="assistant", + ), + finish_reason="stop", + ) + ], + ) + + # Should not raise JSONDecodeError + result = convert_to_parsed_chat_completion_response(response, MathDemonstration) + self.assertIsNotNone(result) + self.assertEqual(len(result.choices), 1) + self.assertIsNone(result.choices[0].message.parsed) + + def test_whitespace_only_content(self): + """Test that whitespace-only content is handled gracefully (issue #282)""" + response = ChatCompletionResponse( + id="test-whitespace", + object="chat.completion", + model="codestral-latest", + usage=UsageInfo(prompt_tokens=10, completion_tokens=0, total_tokens=10), + created=1234567890, + choices=[ + ChatCompletionChoice( + index=0, + message=AssistantMessage( + content=" ", # Whitespace only + tool_calls=None, + prefix=False, + role="assistant", + ), + finish_reason="stop", + ) + ], + ) + + # Should not raise JSONDecodeError + result = convert_to_parsed_chat_completion_response(response, MathDemonstration) + self.assertIsNotNone(result) + self.assertEqual(len(result.choices), 1) + self.assertIsNone(result.choices[0].message.parsed) + + def test_invalid_json_content(self): + """Test that invalid JSON content is handled gracefully (issue #282)""" + response = ChatCompletionResponse( + id="test-invalid-json", + object="chat.completion", + model="codestral-latest", + usage=UsageInfo(prompt_tokens=10, completion_tokens=20, total_tokens=30), + created=1234567890, + choices=[ + ChatCompletionChoice( + index=0, + message=AssistantMessage( + content="This is not JSON at all", # Invalid JSON + tool_calls=None, + prefix=False, + role="assistant", + ), + finish_reason="stop", + ) + ], + ) + + # Should not raise JSONDecodeError + result = convert_to_parsed_chat_completion_response(response, MathDemonstration) + self.assertIsNotNone(result) + self.assertEqual(len(result.choices), 1) + self.assertIsNone(result.choices[0].message.parsed) + + def test_none_content(self): + """Test that None content is handled correctly""" + response = ChatCompletionResponse( + id="test-none", + object="chat.completion", + model="codestral-latest", + usage=UsageInfo(prompt_tokens=10, completion_tokens=0, total_tokens=10), + created=1234567890, + choices=[ + ChatCompletionChoice( + index=0, + message=AssistantMessage( + content=None, # None + tool_calls=None, + prefix=False, + role="assistant", + ), + finish_reason="stop", + ) + ], + ) + + result = convert_to_parsed_chat_completion_response(response, MathDemonstration) + self.assertIsNotNone(result) + self.assertEqual(len(result.choices), 1) + self.assertIsNone(result.choices[0].message.parsed) + if __name__ == "__main__": unittest.main()