Advertisement

New In-article Ad

Structuring LLM Tool Calls with Pydantic: A Practical Guide

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:

  • name must be a string
  • age must 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:

  • add
  • subtract
  • multiply
  • divide

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:

  • add
  • subtract
  • multiply
  • divide

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
BaseModelDefine structured data
Type hintsDescribe expected data types
FieldAdd descriptions and constraints
LiteralRestrict tool choices
ValidationErrorHandle invalid model output
model_dump()Convert models to dictionaries
model_dump_json()Serialize models to JSON
model_json_schema()Generate JSON Schema
Strict validationPrevent unwanted conversions
Custom validatorsEnforce business rules
Custom serializersControl output formats
Nested modelsRepresent complex tool inputs
Dataclasses / TypedDictAlternative supported structures
LogfireObserve 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:

Important: This tutorial uses modern Pydantic v2 APIs such as 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.

Post a Comment

0 Comments

Multiplex ad