Structuring LLM Tool Calls with Pydantic: A Practical Guide
Build reliable LLM tools with Pydantic, validation, JSON Schema, LangChain, and type-safe Python code
Large Language Models are very good at understanding natural language.
But production software needs something more than natural language.
An application might need the model to produce:
{
"city": "Hyderabad",
"temperature_unit": "celsius"
}
instead of:
"Sure! I'll check the weather in Hyderabad using Celsius."
That difference is extremely important when building AI applications.
If the LLM output is going directly into a Python function, REST API, database, workflow, or another AI agent, we need the output to be structured, validated, and predictable.
This is where Pydantic becomes extremely useful.
In this article, we will learn how Pydantic can be used with LLM tool calls, how validation works, how to serialize models to JSON, how Literal restricts tool operations, how JSON Schema is generated, and how these concepts fit into a real-world AI application.
What Is Pydantic?
Pydantic is a Python data validation library based heavily on Python type hints.
Instead of manually checking every field, you define what valid data should look like.
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
Now we have a clear contract:
namemust be a stringagemust be an integer
We can create an object like this:
user = User(
name="Sushil",
age=34
)
print(user)
Pydantic validates the input when the model is created.
This becomes particularly useful in AI applications because LLMs generate data dynamically.
Why Is Pydantic Useful for LLM Applications?
Imagine an LLM receives:
"Book me a flight from Hyderabad to Delhi tomorrow."
A production application might need:
{
"source": "Hyderabad",
"destination": "Delhi",
"date": "2026-08-21"
}
The LLM understands the request, but your application needs a reliable data structure.
Without validation, you might receive:
{
"source": "Hyderabad",
"destination": 123
}
or:
{
"source": "Hyderabad"
}
or even completely unexpected output.
Pydantic gives us a validation layer between the LLM and our application.
This means we can treat LLM-generated data as untrusted input that must be validated before execution.
That is an important production engineering principle.
Pydantic's Core Concept: BaseModel
The most common Pydantic pattern is creating a class that inherits from BaseModel.
from pydantic import BaseModel
class WeatherRequest(BaseModel):
city: str
temperature_unit: str
Now:
request = WeatherRequest(
city="Hyderabad",
temperature_unit="celsius"
)
print(request)
We have a Python object whose fields have defined types.
Pydantic Validation
One of the biggest advantages of Pydantic is automatic validation.
Consider:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
user = User(
name="John",
age="25"
)
print(user.age)
Depending on the validation rules and conversion possible, Pydantic can convert compatible input types.
This is known as lax validation/coercion.
But sometimes we don't want conversion.
For example, if an API must receive an actual integer and not a string representing an integer, we can use strict validation.
Strict vs Lax Validation
Pydantic supports both lax and strict validation.
Lax validation
Pydantic attempts to convert compatible input into the expected type.
from pydantic import BaseModel
class Product(BaseModel):
price: int
product = Product(price="100")
print(product.price)
The input may be converted to:
100
Strict validation
Strict validation prevents this kind of conversion.
from pydantic import BaseModel, ConfigDict
class Product(BaseModel):
model_config = ConfigDict(strict=True)
price: int
Now:
Product(price="100")
will fail validation.
This distinction is useful when building production APIs and LLM tools where implicit conversion could create unexpected behavior.
Field: Give the LLM More Information
Field() allows us to add metadata and validation rules.
For example:
from pydantic import BaseModel, Field
class WeatherRequest(BaseModel):
city: str = Field(
description="Name of the city"
)
temperature_unit: str = Field(
description="Temperature unit such as celsius or fahrenheit"
)
The descriptions are especially useful when schemas are exposed to LLMs.
They help the model understand what each argument means.
Using Literal for Safer Tool Calls
Suppose our calculator supports:
addsubtractmultiplydivide
We don't want the LLM to generate:
{
"operation": "square_root"
}
if that operation isn't supported.
We can use Python's Literal.
from typing import Literal
from pydantic import BaseModel, Field
class CalculatorRequest(BaseModel):
operation: Literal[
"add",
"subtract",
"multiply",
"divide"
] = Field(
description="Mathematical operation"
)
a: float = Field(
description="First number"
)
b: float = Field(
description="Second number"
)
Now only these operations are valid:
addsubtractmultiplydivide
An invalid value will fail validation.
This is extremely useful for LLM tool calling.
ValidationError: Handling Invalid LLM Data
LLM-generated data should never automatically be trusted.
Pydantic provides ValidationError when validation fails.
from pydantic import BaseModel, ValidationError
class User(BaseModel):
name: str
age: int
try:
user = User(
name="John",
age="invalid"
)
except ValidationError as error:
print(error)
We can also inspect structured errors:
try:
User(
name="John",
age="invalid"
)
except ValidationError as error:
print(error.errors())
This is useful when building:
- API validation
- LLM tool validation
- agent workflows
- database input validation
- retry mechanisms
Serialization: Converting Pydantic Models to Data
After validation, we often need to send the data somewhere else.
For example:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
user = User(
name="John",
age=30
)
We can convert it to a Python dictionary:
data = user.model_dump()
print(data)
Output:
{
"name": "John",
"age": 30
}
This is very useful when passing validated data to:
- REST APIs
- databases
- Python functions
- message queues
- other services
Convert Pydantic Models to JSON
We can also serialize the model directly to JSON.
json_data = user.model_dump_json()
print(json_data)
Output:
{"name":"John","age":30}
This makes Pydantic especially useful in distributed AI systems.
Pydantic and JSON Schema
One of Pydantic's important capabilities is generating JSON Schema.
For example:
from pydantic import BaseModel
class WeatherRequest(BaseModel):
city: str
temperature_unit: str
We can generate the schema:
schema = WeatherRequest.model_json_schema()
print(schema)
This produces a JSON Schema representation of our model.
Conceptually, it describes:
{
"properties": {
"city": {
"type": "string"
},
"temperature_unit": {
"type": "string"
}
}
}
JSON Schema is important because many AI frameworks and APIs use schemas to describe structured model inputs and outputs.
The Connection Between Pydantic and LLM Tool Calling
Now we can connect everything.
Suppose we have:
class Add(BaseModel):
a: int
b: int
This class describes what our tool expects.
The LLM receives the tool schema and can produce arguments such as:
{
"a": 10,
"b": 20
}
Our application can then validate those arguments with Pydantic before executing the actual function.
This gives us a clean separation:
LLM
Understands the user's natural language.
Pydantic
Validates the structured arguments.
Python function
Performs the actual business logic.
External API/database
Performs the real-world operation.
This separation is one of the most important ideas when building reliable agentic AI systems.
Real Example: LLM Tool Calling with LangChain
Let's build a simple calculator tool.
First install the packages:
pip install pydantic langchain langchain-openai
Then define our schema:
from pydantic import BaseModel, Field
class Add(BaseModel):
"""Add two numbers together."""
a: int = Field(
description="First number"
)
b: int = Field(
description="Second number"
)
Now create the LLM:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4.1-nano"
)
Bind the Pydantic schema as a tool:
calculator_llm = llm.bind_tools(
tools=[Add]
)
Now send a natural-language request:
response = calculator_llm.invoke(
"Add 10 and 25"
)
The model can produce a tool call containing structured arguments.
We can inspect them:
print(response.tool_calls)
Conceptually, we may receive:
[
{
"name": "Add",
"args": {
"a": 10,
"b": 25
}
}
]
Now we can extract the arguments:
tool_call = response.tool_calls[0]
a = tool_call["args"]["a"]
b = tool_call["args"]["b"]
result = a + b
print(result)
Output:
35
Notice something important.
The LLM did not perform the addition itself.
It interpreted:
"Add 10 and 25"
and generated structured arguments.
Our application performed:
a + b
This is the basic idea behind tool calling.
A More Production-Friendly Version
Instead of manually trusting the dictionary, we can validate it.
from pydantic import BaseModel, Field
class Add(BaseModel):
a: int = Field(description="First number")
b: int = Field(description="Second number")
def add_numbers(data: Add) -> int:
return data.a + data.b
Now validate the LLM arguments:
tool_call = response.tool_calls[0]
validated_input = Add(
**tool_call["args"]
)
result = add_numbers(
validated_input
)
print(result)
Now the flow is:
- User request
- LLM
- Tool-call arguments
- Pydantic validation
- Python function
- Result
The important part is that business logic receives validated data.
Reusable Pydantic Models
In real projects, multiple tools often share common fields.
For example:
from pydantic import BaseModel
class TwoOperands(BaseModel):
a: float
b: float
Now we can reuse this model:
class AddInput(TwoOperands):
pass
class SubtractInput(TwoOperands):
pass
Our functions become:
def add_tool(data: AddInput) -> float:
return data.a + data.b
def subtract_tool(data: SubtractInput) -> float:
return data.a - data.b
This is much easier to maintain than duplicating validation logic everywhere.
A Complete Calculator Example
Let's make the example slightly more realistic.
from typing import Literal
from pydantic import BaseModel, Field
class CalculatorRequest(BaseModel):
operation: Literal[
"add",
"subtract",
"multiply",
"divide"
]
a: float = Field(
description="First number"
)
b: float = Field(
description="Second number"
)
class CalculatorResponse(BaseModel):
result: float
Now implement the business logic:
def calculate(
request: CalculatorRequest
) -> CalculatorResponse:
if request.operation == "add":
result = request.a + request.b
elif request.operation == "subtract":
result = request.a - request.b
elif request.operation == "multiply":
result = request.a * request.b
elif request.operation == "divide":
if request.b == 0:
raise ValueError(
"Cannot divide by zero"
)
result = request.a / request.b
else:
raise ValueError(
"Unsupported operation"
)
return CalculatorResponse(
result=result
)
Test it:
request = CalculatorRequest(
operation="multiply",
a=7,
b=8
)
response = calculate(request)
print(response.model_dump())
Output:
{
"result": 56.0
}
And JSON:
print(response.model_dump_json())
Output:
{"result":56.0}
Nested Models for Complex AI Tools
Real-world tools are rarely just two numbers.
For example, a flight booking tool might look like:
from datetime import date
from pydantic import BaseModel
class Passenger(BaseModel):
name: str
age: int
class FlightBookingRequest(BaseModel):
source: str
destination: str
travel_date: date
passenger: Passenger
Now the LLM can produce structured information for a complex operation.
Example:
{
"source": "Hyderabad",
"destination": "Delhi",
"travel_date": "2026-08-25",
"passenger": {
"name": "Rahul",
"age": 30
}
}
Pydantic validates the complete nested structure.
Custom Validation
Sometimes basic types are not enough.
For example, suppose an application requires a positive price.
Pydantic provides validation mechanisms for custom business rules.
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str
price: float = Field(gt=0)
Now:
Product(
name="Laptop",
price=1000
)
is valid.
But:
Product(
name="Laptop",
price=-100
)
will fail validation.
This is useful for enforcing business constraints before data reaches your application logic.
Pydantic Dataclasses and TypedDict
Pydantic isn't limited to BaseModel.
It also supports other Python data structures such as:
- dataclasses
- TypedDict
- standard Python types
For example, Pydantic dataclasses can be useful when you want dataclass-style structures while still benefiting from validation.
However, for LLM tool schemas, BaseModel is often the easiest and most explicit choice because it provides a rich validation and schema API.
Pydantic Custom Serializers
Sometimes the internal Python representation of data isn't the format your API expects.
Pydantic provides serializers that allow you to customize how values are represented during serialization.
This becomes useful for:
- dates
- timestamps
- custom classes
- API-specific formats
- database representations
In production AI systems, this can become important when the validated output must match an external API contract exactly.
Pydantic in a Real AI Architecture
A typical AI application may contain several layers:
- User
- Natural-language request
- LLM
- Structured tool arguments
- Pydantic validation
- Tool or business logic
- External API or database
- Validated result
- Final response
For example:
"Find weather for Hyderabad in Celsius."
The LLM could produce:
{
"city": "Hyderabad",
"temperature_unit": "celsius"
}
Pydantic validates it:
class WeatherRequest(BaseModel):
city: str
temperature_unit: Literal[
"celsius",
"fahrenheit"
]
Only after validation should the application call the weather API.
This is much safer than directly passing arbitrary LLM-generated text into an external service.
Pydantic Logfire for AI Observability
Validation solves one problem:
Is the data valid?
But production AI applications have another problem:
What actually happened during execution?
This is where Pydantic Logfire becomes interesting.
Logfire is an observability platform from the Pydantic ecosystem designed to help developers inspect application behavior.
For AI systems, observability can help you understand:
- LLM calls
- tool calls
- application requests
- database operations
- latency
- traces
- errors
For agentic AI systems with multiple model calls and tools, observability becomes increasingly valuable.
Why Pydantic Is Important for Agentic AI
As AI applications become more complex, agents may interact with:
- search tools
- databases
- APIs
- payment systems
- calendars
- CRMs
- email systems
- internal company services
Every tool needs structured input.
For example:
class SendEmailRequest(BaseModel):
recipient: str
subject: str
body: str
Another tool might require:
class SearchRequest(BaseModel):
query: str
limit: int
Another:
class DatabaseQuery(BaseModel):
table: str
filters: dict
Pydantic allows us to define these contracts clearly.
Common Mistake: Trusting the LLM Output
One of the biggest mistakes beginners make is assuming:
"The LLM generated JSON, so it must be valid."
Not necessarily.
LLMs generate probabilities, not database constraints.
Even if a model usually produces the correct structure, production systems should still validate important inputs.
Instead of:
result = external_api(
response.tool_calls[0]["args"]
)
prefer:
request = ToolRequest(
**response.tool_calls[0]["args"]
)
result = external_api(
request.model_dump()
)
The second approach creates a clear validation boundary.
Pydantic Tool-Calling Best Practices
1. Keep schemas small
Avoid giving a tool dozens of unnecessary parameters.
2. Use clear field descriptions
city: str = Field(
description="City where the weather should be checked"
)
3. Use Literal for fixed choices
operation: Literal[
"add",
"subtract"
]
4. Validate before executing
Never blindly trust LLM-generated arguments.
5. Keep business logic separate
Your Pydantic model should define the data contract.
Your function should perform the actual operation.
6. Return structured results
Use a Pydantic response model where appropriate.
7. Generate JSON Schema when integrating systems
schema = MyModel.model_json_schema()
8. Handle validation errors
from pydantic import ValidationError
9. Use strict validation when required
Don't allow unwanted type coercion when exact types matter.
10. Add observability
For complex agentic applications, logging and tracing become essential.
A Practical Mental Model
When working with LLM tools, think about Pydantic as a contract between the AI world and the software world.
The LLM deals with:
Natural language
Your application deals with:
- Types
- Functions
- APIs
- Databases
- Business rules
Pydantic helps connect those two worlds.
For example:
"Add 10 and 20"
LLM
{
"a": 10,
"b": 20
}
Pydantic
Validated Add model
Python
10 + 20
Result
30
The important idea isn't simply "use Pydantic."
The important idea is:
Don't allow unpredictable model output to directly control deterministic application logic without validation.
Final Takeaway
Pydantic is much more than a way to define Python classes.
For modern AI/ML applications, it can become a validation and serialization layer between:
- LLMs
- tool calls
- APIs
- databases
- backend services
- agent workflows
The most important concepts to remember are:
| Pydantic Feature | Why It Matters in AI |
|---|---|
BaseModel | Define structured data |
| Type hints | Describe expected data types |
Field | Add descriptions and constraints |
Literal | Restrict tool choices |
ValidationError | Handle invalid model output |
model_dump() | Convert models to dictionaries |
model_dump_json() | Serialize models to JSON |
model_json_schema() | Generate JSON Schema |
| Strict validation | Prevent unwanted conversions |
| Custom validators | Enforce business rules |
| Custom serializers | Control output formats |
| Nested models | Represent complex tool inputs |
| Dataclasses / TypedDict | Alternative supported structures |
| Logfire | Observe production application behavior |
The result is a much more reliable architecture:
LLM
Structured Tool Call
Pydantic Validation
Business Logic
API / Database
Structured Result
If you're learning Generative AI, LangChain, Agentic AI, FastAPI, or AI backend development, understanding this pattern is extremely valuable.
Because the next step after making an LLM generate text is not simply making it generate JSON.
The real engineering challenge is making that generated data safe, validated, testable, observable, and usable by production software.
And that is where Pydantic becomes a powerful part of the AI engineering stack.
Official Pydantic Resources
For the latest Pydantic concepts and APIs, always refer to the official documentation:
model_dump(), model_dump_json(), and
model_json_schema() rather than older v1 patterns such as
.json() and .parse_raw().
This makes the examples appropriate for current Pydantic projects.
0 Comments