Submission checklist
Package (Required)
Related Issues / PRs
Related but distinct: #36349 (model returns plain text instead of calling the structured-output tool — a different failure mode). No existing issue covers the validation skip below.
Supersedes #38714, which was auto-closed by the triage automation because the API submission carried no issue type; re-filed through the form.
Reproduction Steps / Example Code (Python)
"""Repro: ToolStrategy with a raw JSON schema dict skips ALL output validation.
Tested on langchain 1.3.11 / langchain-core 1.4.8 / pydantic 2.13.4 (clean venv, `pip install langchain`).
"""
from collections.abc import Iterator, Sequence
from typing import Any
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
from langchain_core.runnables import Runnable
from pydantic import BaseModel
class ScriptedToolCallingModel(GenericFakeChatModel):
"""Fake chat model that accepts tool binding and replays scripted messages."""
def bind_tools(self, tools: Sequence[Any], **kwargs: Any) -> Runnable:
del tools, kwargs
return self
REPORT_JSON_SCHEMA = {
"title": "Report",
"type": "object",
"properties": {
"summary": {"type": "string"},
"key_findings": {
"type": "array",
"items": {
"type": "object",
"properties": {"finding": {"type": "string"}},
},
},
},
"required": ["summary", "key_findings"],
}
# The model calls the structured-output tool with key_findings as the STRING "[]"
# even though the schema requires an ARRAY of objects.
BAD_ARGS = {"summary": "All good.", "key_findings": "[]"}
# A corrected call, only ever reached if the agent validates and retries.
GOOD_ARGS = {"summary": "All good.", "key_findings": [{"finding": "ok"}]}
def scripted_model() -> ScriptedToolCallingModel:
messages: Iterator[AIMessage] = iter(
[
AIMessage(
content="",
tool_calls=[{"name": "Report", "args": BAD_ARGS, "id": "call_1", "type": "tool_call"}],
),
AIMessage(
content="",
tool_calls=[{"name": "Report", "args": GOOD_ARGS, "id": "call_2", "type": "tool_call"}],
),
]
)
return ScriptedToolCallingModel(messages=messages)
print("=== 1. response_format = ToolStrategy(<raw JSON schema dict>) ===")
agent = create_agent(
model=scripted_model(),
tools=[],
response_format=ToolStrategy(REPORT_JSON_SCHEMA),
)
result = agent.invoke({"messages": [("user", "Write a report")]})
sr = result["structured_response"]
print("structured_response:", sr)
print("type of key_findings:", type(sr["key_findings"]).__name__)
print("model turns taken:", sum(isinstance(m, AIMessage) for m in result["messages"]))
print()
print("=== 2. Same scripted bad args, response_format = ToolStrategy(<Pydantic model>) ===")
class Report(BaseModel):
summary: str
key_findings: list[dict]
agent = create_agent(
model=scripted_model(),
tools=[],
response_format=ToolStrategy(Report),
)
result = agent.invoke({"messages": [("user", "Write a report")]})
sr = result["structured_response"]
print("structured_response:", sr)
print("type of key_findings:", type(sr.key_findings).__name__)
print("model turns taken:", sum(isinstance(m, AIMessage) for m in result["messages"]))
Error Message and Stack Trace (if applicable)
# No exception is raised — that is the bug (the invalid output is returned silently). Actual program output:
=== 1. response_format = ToolStrategy(<raw JSON schema dict>) ===
structured_response: {'summary': 'All good.', 'key_findings': '[]'}
type of key_findings: str
model turns taken: 1
=== 2. Same scripted bad args, response_format = ToolStrategy(<Pydantic model>) ===
structured_response: summary='All good.' key_findings=[{'finding': 'ok'}]
type of key_findings: list
model turns taken: 2
Description
When response_format carries a raw JSON schema dict, the agent never validates the model's structured output against it: _parse_with_schema (langchain/agents/structured_output.py, lines 94-96) early-returns the data for schema_kind == "json_schema" ("Raw JSON schema has no corresponding Python type to instantiate"). Because OutputToolBinding.parse therefore can never raise for dict schemas, the StructuredOutputValidationError handling in factory.py's _handle_model_output — the machinery behind ToolStrategy.handle_errors (error ToolMessage + retry) — is structurally unreachable for exactly this schema kind. The same skip also applies on the ProviderStrategy parse path (ProviderStrategyBinding.parse funnels through the same function), where only JSON-decode failures are caught client-side.
In the repro, case 1 shows a scripted model returning key_findings as the string "[]" where the schema requires an array of objects: the agent returns it as structured_response in one turn — no error, no retry. Case 2 shows the identical bad tool call against a Pydantic-class schema: validation raises, handle_errors=True sends the error back to the model, and the second (corrected) scripted call is used. The behavioral contract should not depend on how the same schema is expressed.
Expected: dict-schema outputs are validated against the JSON schema (e.g. via jsonschema.Draft202012Validator), so schema-violating output raises inside parse() and the existing handle_errors retry machinery fires exactly as it does for Pydantic/dataclass/TypedDict schemas. If a validation dependency is unwanted, an explicit documented statement that dict schemas are never validated (and that handle_errors is inert for them) would at least make the limitation discoverable — ToolStrategy.handle_errors documents catching validation errors with no dict-schema carve-out, and OutputToolBinding.parse documents Raises: ValueError: If parsing fails.
This matters in practice because dict schemas are the only option when the schema arrives dynamically (built from configuration or another runtime), and weaker models do emit degenerate structured output (placeholder strings where arrays belong) that currently sails through unvalidated.
The skip is unchanged on current master (libs/langchain_v1/langchain/agents/structured_output.py, same lines).
System Info
System Information
------------------
> OS: Windows
> OS Version: 10.0.26200
> Python Version: 3.12.0 (main, Oct 2 2023, 23:53:49) [MSC v.1929 64 bit (AMD64)]
Package Information
-------------------
> langchain_core: 1.4.8
> langchain: 1.3.11
> langsmith: 0.9.1
> langchain_openai: 1.3.3
> langgraph_sdk: 0.4.2
Other Dependencies
------------------
> langgraph: 1.2.6
> pydantic: 2.13.4
Submission checklist
Package (Required)
Related Issues / PRs
Related but distinct: #36349 (model returns plain text instead of calling the structured-output tool — a different failure mode). No existing issue covers the validation skip below.
Supersedes #38714, which was auto-closed by the triage automation because the API submission carried no issue type; re-filed through the form.
Reproduction Steps / Example Code (Python)
Error Message and Stack Trace (if applicable)
Description
When
response_formatcarries a raw JSON schema dict, the agent never validates the model's structured output against it:_parse_with_schema(langchain/agents/structured_output.py, lines 94-96) early-returns the data forschema_kind == "json_schema"("Raw JSON schema has no corresponding Python type to instantiate"). BecauseOutputToolBinding.parsetherefore can never raise for dict schemas, theStructuredOutputValidationErrorhandling infactory.py's_handle_model_output— the machinery behindToolStrategy.handle_errors(errorToolMessage+ retry) — is structurally unreachable for exactly this schema kind. The same skip also applies on theProviderStrategyparse path (ProviderStrategyBinding.parsefunnels through the same function), where only JSON-decode failures are caught client-side.In the repro, case 1 shows a scripted model returning
key_findingsas the string"[]"where the schema requires an array of objects: the agent returns it asstructured_responsein one turn — no error, no retry. Case 2 shows the identical bad tool call against a Pydantic-class schema: validation raises,handle_errors=Truesends the error back to the model, and the second (corrected) scripted call is used. The behavioral contract should not depend on how the same schema is expressed.Expected: dict-schema outputs are validated against the JSON schema (e.g. via
jsonschema.Draft202012Validator), so schema-violating output raises insideparse()and the existinghandle_errorsretry machinery fires exactly as it does for Pydantic/dataclass/TypedDict schemas. If a validation dependency is unwanted, an explicit documented statement that dict schemas are never validated (and thathandle_errorsis inert for them) would at least make the limitation discoverable —ToolStrategy.handle_errorsdocuments catching validation errors with no dict-schema carve-out, andOutputToolBinding.parsedocumentsRaises: ValueError: If parsing fails.This matters in practice because dict schemas are the only option when the schema arrives dynamically (built from configuration or another runtime), and weaker models do emit degenerate structured output (placeholder strings where arrays belong) that currently sails through unvalidated.
The skip is unchanged on current
master(libs/langchain_v1/langchain/agents/structured_output.py, same lines).System Info