LangChain create_tool_calling_agent — tool schema validation errors across providers (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LangChain Tool schema errors
LangChain Agents · Tool Calling Severity: High HTTP n/a

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.

By Sana K. · Cloud AI Reliability Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: The recommended way to define tools for 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 rejects tool schema
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 rejects tool schema
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 empty
Pydantic mixing v1 and v2
PydanticUserError: `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

IdiomType hintsBest for
@tool decorator on a functionDocstring + hintsSimple tools; readable code
Pydantic v2 model as args_schemaModel fieldsComplex tools; reuse schemas
StructuredTool.from_functionExplicitSync + async pairs
langchain_core.tools.tool classExplicitCustom subclasses

Provider-specific schema quirks (2026)

ProviderNotable requirement
OpenAIStrict mode requires no Optional unless additionalProperties=false and all fields required
AnthropicEvery tool needs a non-empty description
Google GeminiNested objects; some type unions rejected
Bedrock (any model)Follows underlying model provider's constraints
Vertex AI ClaudeSame as Anthropic direct
Ollama / localProvider-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-core is 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. @tool on 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

Fix #1

Use @tool with type hints and structured docstrings

The 90% pattern that works across every provider.

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.

tool_decorator.py
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"])
The 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.
Fix #2

Use Pydantic v2 args_schema for complex tools

When you need nested types, defaults, or validators.

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.

pydantic_tool.py
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]
Ensure you are on Pydantic v2 across your project. LangChain 0.3+ requires it. Check with python -c "import pydantic; print(pydantic.VERSION)" — should be 2.x.
Fix #3

Handle provider-specific schema constraints

When you need one tool to work across OpenAI strict, Anthropic, and Gemini.

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.

portable_tool.py
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)
For truly cross-provider portability, keep tool schemas simple and required. Complex Optional/Union schemas are a premature-optimization trap that breaks the moment you swap models.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Always use @tool decorator or Pydantic v2 args_schema — no ad-hoc dict schemas.
  • Every tool needs a docstring with description sentence and Args: section.
  • Type-hint every parameter; skip Optional when 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= in StructuredTool.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.
You need the @tool decorator (or StructuredTool). Bare functions do not carry the metadata LangChain needs to construct the provider tool schema.
Practical limit is ~20-30 for reliable model tool selection. Beyond that, models start hallucinating tools or making poor selections. For large tool inventories, use a router pattern that first picks a category of tools.
Yes — tools can return any JSON-serializable value. The tool result is stringified for the model but the raw value is available via the agent scratchpad. For complex return types, consider returning a JSON string explicitly.
Yes if you want provider portability. LangChain tools work across every supported model provider from one definition. If you are tied to OpenAI's API, native functions may be simpler.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.