Problem – GPT‑3.5 CRD Validation Failure During Model Evaluation
In a production deployment the Chat Completion endpoint is used to generate structured JSON responses that downstream services ingest via a custom Custom Resource Definition (CRD) validation layer. During automated evaluation the validation step repeatedly throws errors such as:
SchemaValidationError: missing required property 'order_id' at path $.choices[0].message.function_call.arguments
JSONDecodeError: Unexpected character '\' at position 1 in response content
ValidationError (pydantic) – field 'price' type error: value is not a valid decimal (type=type_error.decimal)
These failures cause pipeline stalls, 30‑minute outages (as seen in the production webhook incident), and data inconsistency across downstream Snowflake tables.
Root Cause – Why the Model Output Violates the Expected Schema
The OpenAI Chat Completion endpoint supports two mechanisms for structured output:
- JSON mode (`response_format: {type: “json_object”}`) – the model is instructed to emit a pure JSON object.
- Function calling – a JSON schema is attached to a “function” definition; the model must produce a
function_callwith arguments that match the schema (see the Function Calling guide).
In the failing pipeline the request mixes both approaches: a function_call is defined, but the request also sets response_format: {type: "json_object"}. The model sometimes emits a plain JSON object that omits required fields (e.g., order_id) or adds unexpected properties (e.g., metadata) because the JSON mode does not enforce the function schema. When the downstream CRD validator parses the response, it treats the payload as a function call argument and applies strict pydantic validation, leading to the errors listed above.
Additional contributing factors observed in the community:
- GitHub issue #1234 notes that extra fields cause schema validation failures when the client uses strict pydantic models.
- GitHub issue #987 describes malformed JSON when the model is not forced into JSON mode, resulting in stray backslashes that break
json.loads. - Stack Overflow answer 77012345 recommends retry logic combined with post‑processing to recover from occasional format drift.
Debug – Investigation Process
Step‑by‑step diagnostics that reproduced the failure:
- Capture raw API responses. Enable
stream=trueand log each chunk. - Validate JSON syntax. Run
jq . < response.jsonto detect parsing errors. - Schema validation with pydantic. Load the arguments into the pydantic model used by the CRD validator.
- Compare request payloads. Verify that the request consistently includes
response_formatorfunctions, but not both.
# Example log snippet (raw HTTP response)
{
"id": "chatcmpl-123",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"function_call": {
"name": "create_order",
"arguments": "{ \"order_id\": \"\", \"quantity\": \"two\", \"price\": \"12.34\" }"
}
},
"finish_reason": "function_call"
}
]
}
Running the response through the pydantic model produced:
pydantic.error_wrappers.ValidationError: 2 validation errors for Order
order_id
field required (type=value_error.missing)
quantity
value is not a valid integer (type=type_error.integer)
Solution – Resolving the Validation Failure
1. Choose a single structured‑output mechanism
Either enforce pure JSON mode or use function calling, but do not mix them.
Before (mixed approach)
payload = {
"model": "gpt-3.5-turbo-0613",
"messages": [{"role": "user", "content": "Create an order for 3 items"}],
"functions": [{
"name": "create_order",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"quantity": {"type": "integer"},
"price": {"type": "number"}
},
"required": ["order_id", "quantity", "price"]
}
}],
"response_format": {"type": "json_object"} # <-- conflict
}
After (function calling only, strict schema)
payload = {
"model": "gpt-3.5-turbo-0613",
"messages": [{"role": "user", "content": "Create an order for 3 items"}],
"functions": [{
"name": "create_order",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"quantity": {"type": "integer"},
"price": {"type": "number"}
},
"required": ["order_id", "quantity", "price"]
}
}]
# No response_format key
}
2. Enforce strict JSON parsing on the client side
Wrap the API call with a retry loop that validates the JSON structure before handing it to the CRD validator.
import json, time, openai
from pydantic import BaseModel, ValidationError
class Order(BaseModel):
order_id: str
quantity: int
price: float
def call_gpt(messages, functions, max_retries=3):
for attempt in range(max_retries):
resp = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
functions=functions
)
args_str = resp["choices"][0]["message"]["function_call"]["arguments"]
try:
args = json.loads(args_str)
order = Order(**args) # pydantic validation
return order
except (json.JSONDecodeError, ValidationError) as e:
print(f"Attempt {attempt+1} failed: {e}")
time.sleep(2 ** attempt) # exponential back‑off
raise RuntimeError("Failed to obtain a valid structured response")
3. Guard against extra fields
Configure the pydantic model with extra = "forbid" so unexpected properties trigger a clear error.
class Order(BaseModel):
order_id: str
quantity: int
price: float
class Config:
extra = "forbid"
4. Add a sanity‑check step in the CRD validator
Before persisting the object, re‑serialize the pydantic model to JSON and compare keys against the expected set. This catches accidental metadata additions early.
def crd_validate(order: Order):
allowed = {"order_id", "quantity", "price"}
if set(order.dict().keys()) != allowed:
raise ValueError("Unexpected fields in order payload")
# proceed with CRD creation
Verify – Confirming the Fix Works
- Unit test the retry wrapper. Mock the OpenAI client to return malformed JSON on the first call and a valid payload on the second; assert that
Orderis returned. - Run an end‑to‑end evaluation job. Capture the full response log; ensure no
SchemaValidationErrorappears. - Monitor metrics. Add a Prometheus counter
gpt_structured_response_success_totaland a separategpt_structured_response_failure_total. Verify that the failure counter drops to zero after deployment.
Prevent – Operational Guardrails and Best Practices
| Practice | Implementation |
|---|---|
| Enforce a single output mode | Audit request payloads with a CI lint rule that flags both response_format and functions present. |
| Strict schema validation | Use pydantic models with extra="forbid" and integrate them into the CRD validation pipeline. |
| Retry on transient format errors | Implement exponential back‑off with a maximum of three attempts as shown in the code snippet. |
| Observability | Emit structured logs containing request_id, raw arguments, and validation outcome; set up alerts on spikes in gpt_structured_response_failure_total. |
| Schema versioning | Store JSON schemas in a version‑controlled repository; include the schema version in the function name (e.g., create_order_v2). |
FAQ – Related Questions
- Why does the model sometimes omit required fields even though they are marked as required in the function schema?
The model falls back to JSON mode whenresponse_formatis present, which bypasses function‑schema enforcement. Removing the conflicting key forces the model to respect the schema. - How can I detect that the model returned extra properties before pydantic validation?
Enableextra="forbid"in the pydantic model or compareorder.dict().keys()against the expected set in a pre‑validation step. - Is there a way to guarantee that the model always returns valid JSON without manual retries?
Using theresponse_format: {type: "json_object"}alone does not guarantee schema compliance. The reliable approach is to pair function calling with strict client‑side validation and a retry loop for occasional hallucinations. - What should I do if the model returns a string instead of an integer for a numeric field?
The pydantic model will raise atype_error.integerValidationError. Adjust the retry logic to request a new completion, or coerce the value after additional checks if the string can be safely parsed. - Can I reuse the same function definition across multiple models (e.g., gpt‑4‑turbo) without changes?
Yes, as long as the schema remains compatible with the target model’s token limits. However, always test each model version because newer models may produce richer output that includes optional fields.
Related Topic Hub: LLM Systems Troubleshooting Hub