peargent.
Structured Output

Structured Output for Tools

Build tools that return reliable, schema-validated structured outputs.

Structured output means the Tool returns its result in a format you define, such as a dictionary, JSON-like object, or typed schema. Instead of returning raw data, the tool validates and returns typed Pydantic model instances with guaranteed structure and correctness.

Why Structured Output for Tools?

Structured output is useful because:

  • It ensures tools return consistent, validated data
  • Your agents can reliably use tool outputs without parsing errors
  • It catches malformed API responses, database records, or external data early
  • It provides type safety and IDE autocomplete for tool results
  • It makes tools reliable for production systems, APIs, and complex workflows

Validating Tool Output with Schema

We will be using pydantic package to validate tool outputs. Pydantic is a data validation and settings management using Python type annotations. More about pydantic: https://docs.pydantic.dev/latest/

Make sure to install pydantic package pip install pydantic.

from pydantic import BaseModel, Field
from peargent import create_tool

# 1. Define your output schema//
class WeatherData(BaseModel):
    temperature: float = Field(description="Temperature in Fahrenheit")
    condition: str = Field(description="Weather condition (e.g., Sunny, Cloudy)")
    humidity: int = Field(description="Humidity percentage", ge=0, le=100)

# 2. Create tool with output validation
def get_weather(city: str) -> dict:
    # Simulated API call
    return {
        "temperature": 72.5,
        "condition": "Sunny",
        "humidity": 45
    }

weather_tool = create_tool(
    name="get_weather",
    description="Get current weather for a city",
    input_parameters={"city": str},
    call_function=get_weather,
    output_schema=WeatherData,   # ← validate tool output
)

# 3. Run the tool
result = weather_tool.run({"city": "San Francisco"})
print(result)

Output (validated Pydantic model):

WeatherData(temperature=72.5, condition='Sunny', humidity=45)

# Access with type safety
print(result.temperature)  # 72.5
print(result.condition)    # "Sunny"
print(result.humidity)     # 45

Schema with Constraints and Validation

Tools can enforce strict validation rules using Pydantic field constraints. If the tool's raw output violates these constraints, validation will fail and the error will be handled based on the on_error parameter.

This is particularly useful for validating external API responses, database queries, or any tool that returns data from untrusted sources.

from pydantic import BaseModel, Field, field_validator
from peargent import create_tool

# Define schema with constraints
class UserProfile(BaseModel):
    user_id: int = Field(description="Unique user ID", gt=0)
    username: str = Field(description="Username", min_length=3, max_length=20)
    email: str = Field(description="Email address")
    age: int = Field(description="User age", ge=0, le=150)
    premium: bool = Field(description="Premium subscription status")

    # Custom validator: email must contain @ symbol
    @field_validator("email")
    @classmethod
    def validate_email(cls, v):
        """
        Ensure email contains @ symbol.
        This catches malformed email addresses from database or API.
        """
        if "@" not in v:
            raise ValueError("Invalid email format")
        return v

# Tool that fetches user data
def fetch_user(user_id: int) -> dict:
    # Simulated database query
    return {
        "user_id": user_id,
        "username": "john_doe",
        "email": "john@example.com",
        "age": 28,
        "premium": True
    }

user_tool = create_tool(
    name="fetch_user",
    description="Fetch user profile from database",
    input_parameters={"user_id": int},
    call_function=fetch_user,
    output_schema=UserProfile,
    on_error="return_error"  # Gracefully handle validation failures
)

# Use the tool
result = user_tool.run({"user_id": 123})
print(result)

Nested Output Schema

You can nest multiple Pydantic models inside each other for complex tool outputs. This is perfect for validating API responses, database records with relationships, or any hierarchical data structure.

from pydantic import BaseModel, Field
from typing import List
from peargent import create_tool

# ----- Nested Models -----
class Address(BaseModel):
    street: str = Field(description="Street address")
    city: str = Field(description="City name")
    state: str = Field(description="State code")
    zip_code: str = Field(description="ZIP code")

class PhoneNumber(BaseModel):
    type: str = Field(description="Phone type: mobile, home, or work")
    number: str = Field(description="Phone number")

class ContactInfo(BaseModel):
    name: str = Field(description="Full name")
    email: str = Field(description="Email address")
    # Nested schemas
    address: Address = Field(description="Mailing address")
    phone_numbers: List[PhoneNumber] = Field(description="Contact phone numbers")
    notes: str = Field(description="Additional notes", default="")

# ----- Create Tool with Nested Schema -----

def fetch_contact(contact_id: int) -> dict:
    # Simulated CRM API call
    return {
        "name": "Alice Johnson",
        "email": "alice@example.com",
        "address": {
            "street": "123 Main St",
            "city": "San Francisco",
            "state": "CA",
            "zip_code": "94102"
        },
        "phone_numbers": [
            {"type": "mobile", "number": "415-555-1234"},
            {"type": "work", "number": "415-555-5678"}
        ],
        "notes": "Preferred contact method: email"
    }

contact_tool = create_tool(
    name="fetch_contact",
    description="Fetch contact information from CRM",
    input_parameters={"contact_id": int},
    call_function=fetch_contact,
    output_schema=ContactInfo
)

contact = contact_tool.run({"contact_id": 456})
print(contact)

Output shape:

{
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "address": {
    "street": "123 Main St",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94102"
  },
  "phone_numbers": [
    {"type": "mobile", "number": "415-555-1234"},
    {"type": "work", "number": "415-555-5678"}
  ],
  "notes": "Preferred contact method: email"
}

How Structured Output Works for Tools

Tool Executes

Tool calls the call_function() which returns raw data (dict, object, etc.) from an API, database, or computation.

Output Schema Is Checked

If an output_schema is provided, Tool proceeds to validation. Otherwise, the raw output is returned as-is.

Pydantic Validates the Output

Tool attempts to convert the raw output into the Pydantic model, performing:

  • Type checking (str, int, float, bool, etc.)
  • Required field verification
  • Constraint validation (ge, le, min_length, max_length)
  • Custom validator execution (@field_validator)

If the output is already a Pydantic model instance of the correct type, it passes validation immediately.

Validation Success or Failure

If validation succeeds:

  • Tool returns the validated Pydantic model instance
  • Type-safe, guaranteed structure
  • Ready to use in agent workflows

If validation fails:

  • Error is handled based on on_error parameter
  • If max_retries > 0, tool automatically retries execution
  • Validation runs again on each retry
  • See Error Handling for details

Final Validated Output Returned

After successful validation, Tool returns a fully typed, fully validated Pydantic object ready for use by agents or downstream code.