LangChain create_tool_calling_agent — tool schema validation errors
<code>create_tool_calling_agent</code> is the modern replacement for legacy agents. Tools failing schema validation is the top failure mode — most caused by Pydantic v1/v2 confusion, sparse docstrings, or provider-specific schema quirks.
Quick fix (TL;DR)
create_tool_calling_agent is either the @tool decorator with type hints + docstring, or a Pydantic v2 BaseModel with clear field descriptions. Fix schema errors by (a) using proper type hints on every parameter, (b) writing docstrings the LLM can actually understand (the arg descriptions are surfaced to the model), (c) avoiding Optional-heavy schemas that some providers reject, and (d) not mixing Pydantic v1 and v2.Real error messages you'll see
These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
openai.BadRequestError: Error code: 400 - {"error": {"message": "Invalid schema for function 'search': In context=('properties', 'query'), 'null' is not one of ['string', 'number', ...]"}}
# The tool used Optional[str] which serialized as ['string', 'null']anthropic.BadRequestError: Error code: 400 - {"type":"error","error":{"type":"invalid_request_error","message":"tools.0.description: Field required"}}
# Tool has no docstring; description field is emptyPydanticUserError: `BaseModel.__init_subclass__()` got unexpected keyword argument 'skip_on_failure' # Mixing langchain-core v0.3+ (Pydantic v2) with old code using Pydantic v1 BaseModels
Reference
Tool definition idioms and their trade-offs
| Idiom | Type hints | Best for |
|---|---|---|
@tool decorator on a function | Docstring + hints | Simple tools; readable code |
Pydantic v2 model as args_schema | Model fields | Complex tools; reuse schemas |
StructuredTool.from_function | Explicit | Sync + async pairs |
langchain_core.tools.tool class | Explicit | Custom subclasses |
Provider-specific schema quirks (2026)
| Provider | Notable requirement |
|---|---|
| OpenAI | Strict mode requires no Optional unless additionalProperties=false and all fields required |
| Anthropic | Every tool needs a non-empty description |
| Google Gemini | Nested objects; some type unions rejected |
| Bedrock (any model) | Follows underlying model provider's constraints |
| Vertex AI Claude | Same as Anthropic direct |
| Ollama / local | Provider-dependent; usually more permissive |
Root causes, ranked by frequency
Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.
- 25%Optional/union types in tool args rejected by strict mode. OpenAI strict mode +
Optional[str]= 400. - 18%Missing or empty docstring. Anthropic requires
description; empty docstring surfaces as validation error. - 14%Pydantic v1 vs v2 mismatch. Newer
langchain-coreis Pydantic v2-only; old code with v1 BaseModels crashes. - 10%Docstring format not parsed for arg descriptions. Need
Args:section (Google/Sphinx style) for per-arg docs. - 8%Complex nested schemas rejected. Some providers reject deeply-nested tool args; flatten.
- 7%Tool name has invalid characters. Some providers require snake_case or ban dots; auto-derived tool names inherit the function name and fail.
- 8%Async tool bound to a sync agent.
@toolon an async function without corresponding agent setup. - 10%Mixing agents and native provider tool APIs. Using
.bind_tools()directly then also passing tools to the agent constructor.
Fixes — copy-paste solutions
Use @tool with type hints and structured docstrings
Define tools with the @tool decorator. Every parameter needs a type hint. The docstring should have a description sentence plus an Args: section listing each parameter — LangChain surfaces the arg docs to the model.
from langchain_core.tools import tool from langchain_openai import ChatOpenAI from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain_core.prompts import ChatPromptTemplate @tool def search(query: str, max_results: int = 5) -> str: """Search the web for information. Args: query: The search query, e.g. "current NASDAQ index" max_results: How many results to return (default 5, max 20) Returns: Newline-delimited search result snippets. """ # Your implementation return f"Result 1 for {query}\nResult 2 for {query}" @tool def get_weather(city: str, units: str = "celsius") -> str: """Return current weather for a city. Args: city: City name, e.g. "Karachi" or "Cairo, Egypt" units: Temperature units, either "celsius" or "fahrenheit" Returns: A one-line weather report. """ return f"{city}: 32°C, humid" tools = [search, get_weather] # Build the agent model = ChatOpenAI(model="gpt-4o-mini") prompt = ChatPromptTemplate.from_messages([ ("system", "You are helpful. Use tools when they help."), ("human", "{input}"), ("placeholder", "{agent_scratchpad}"), ]) agent = create_tool_calling_agent(model, tools, prompt) executor = AgentExecutor(agent=agent, tools=tools, verbose=True) result = executor.invoke({"input": "What is the weather in Karachi?"}) print(result["output"])
Args: section is not decorative — LangChain parses it into per-argument descriptions that the model sees. Sparse docstrings mean the model does not understand what to pass.Use Pydantic v2 args_schema for complex tools
For tools with more than 3-4 args or nested types, define a Pydantic v2 model as args_schema and pass it explicitly. Cleaner than deep type hints on the function signature.
from typing import List, Literal from pydantic import BaseModel, Field from langchain_core.tools import StructuredTool class SearchInput(BaseModel): """Input schema for the search tool.""" query: str = Field(description="The search query in plain English") site_filter: List[str] = Field( default_factory=list, description="Optional list of domains to restrict search to, e.g. ['wikipedia.org']" ) result_type: Literal["general", "news", "images"] = Field( default="general", description="What kind of results to return" ) def _search_impl(query: str, site_filter: list, result_type: str) -> str: # Real implementation return f"[{result_type}] Results for {query} (sites: {site_filter or 'any'})" search_tool = StructuredTool.from_function( func=_search_impl, name="web_search", # explicit name — no invalid characters description="Search the web with optional site restrictions and result type filter.", args_schema=SearchInput, ) # Same pattern with an async twin async def _search_impl_async(query: str, site_filter: list, result_type: str) -> str: # Async real implementation return f"[{result_type} async] Results for {query}" search_tool_async = StructuredTool.from_function( func=_search_impl, coroutine=_search_impl_async, # async version name="web_search", description="...", args_schema=SearchInput, ) # Use with any tool-calling agent tools = [search_tool]
python -c "import pydantic; print(pydantic.VERSION)" — should be 2.x.Handle provider-specific schema constraints
Some providers have strict schema requirements (no Optional, no additionalProperties). For a portable tool, avoid Optional; use required fields with sentinel defaults, and validate provider constraints before shipping.
from typing import List, Literal from pydantic import BaseModel, Field from langchain_core.tools import tool # ❌ Optional-heavy: OpenAI strict mode rejects # @tool # def search(query: str, site_filter: Optional[List[str]] = None, # result_type: Optional[str] = None) -> str: # ... # ✓ Required fields with sensible defaults (portable) @tool def search( query: str, site_filter: List[str] = [], # empty list, not None result_type: Literal["general", "news", "images"] = "general", # enum with default ) -> str: """Search the web with optional filters. Args: query: The search query in plain English. site_filter: List of domains to restrict search to. Use [] for no restriction. result_type: One of general, news, images. """ return "results here" # Validate the tool schema against the provider you'll actually call def validate_for_openai_strict(tool): """Confirms tool schema is compatible with OpenAI strict mode.""" schema = tool.args_schema.model_json_schema() if tool.args_schema else tool.tool_call_schema.model_json_schema() # Strict mode requires: all fields in required, additionalProperties: false, no Optional props = schema.get("properties", {}) required = set(schema.get("required", [])) for name, prop in props.items(): if name not in required: raise ValueError(f"OpenAI strict: field '{name}' must be required") if "null" in prop.get("type", []) if isinstance(prop.get("type"), list) else False: raise ValueError(f"OpenAI strict: field '{name}' must not allow null") return True validate_for_openai_strict(search)
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Always use
@tooldecorator or Pydantic v2args_schema— no ad-hoc dict schemas. - Every tool needs a docstring with description sentence and
Args:section. - Type-hint every parameter; skip
Optionalwhen you can use required-with-default. - Standardize on Pydantic v2 across the codebase; do not mix v1 models with LangChain 0.3+.
- Validate tool schemas against each provider you support in CI.
- Use explicit
name=inStructuredTool.from_function— never rely on Python function names. - For async workflows, define both sync and async implementations via
func=+coroutine=.
Frequently asked questions
create_tool_calling_agent uses provider-native tool calling (JSON tool_use blocks). create_react_agent uses text-based ReAct prompting. Tool-calling agents are more reliable across providers that support it (OpenAI, Anthropic, Google). Prefer the tool-calling agent unless you specifically need ReAct behaviour.@tool decorator (or StructuredTool). Bare functions do not carry the metadata LangChain needs to construct the provider tool schema.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.