Skip to main content
Lesson 7 of 14 · Function Calling with Grok

Pydantic and Tool Definition with the xAI SDK

Lesson 7 of 14 Updated November 3, 2026 Developer
On this page
  1. Simplify Tool Definition
  2. Pydantic Approach
  3. The xAI SDK tool() Function
  4. A Custom Decorator for Speed
  5. Comparing the Three Approaches
  6. Best Practices for Descriptions
  7. Key Takeaways

Updated November 3, 2026 based on xAI Function calling documentation and xai-sdk 1.21.0: the SDK offers neither a @client.tool() decorator nor automatic function execution; you build tools with tool() (using a Pydantic schema, for example) and return results with tool_result. Source: Function calling.

Simplify Tool Definition

Writing JSON schemas by hand is verbose and error-prone. Two approaches let you generate tool definitions from your Python code: Pydantic models, which the xAI SDK transforms into tools with its tool() function, and a simple custom decorator that reads your function signatures.

Pydantic Approach

Pydantic is a Python library that lets you define data models with automatic validation. The Grok API can use these models to generate JSON schemas for tools.

from pydantic import BaseModel, Field
from typing import Optional

class WeatherParams(BaseModel):
    city: str = Field(description="City name")
    unit: Optional[str] = Field(
        default="celsius",
        description="Temperature unit",
        enum=["celsius", "fahrenheit"]
    )

This Pydantic model automatically generates the JSON schema equivalent to what you would have written manually. Benefits include:

  • Type checking: Python checks types at runtime
  • Validation: Pydantic validates data automatically
  • Documentation: Field(description=...) entries serve as documentation
  • Reusability: the same model works for validation and API documentation

Complex Models

Pydantic handles nested structures, lists, and advanced types:

from typing import List, Optional
from pydantic import BaseModel, Field

class Address(BaseModel):
    street: str = Field(description="Street number and name")
    city: str = Field(description="City")
    zip_code: str = Field(description="ZIP code")
    country: str = Field(default="FR", description="ISO country code")

class CreateOrderParams(BaseModel):
    product_id: str = Field(description="Product identifier")
    quantity: int = Field(description="Ordered quantity", ge=1, le=100)
    shipping_address: Address = Field(description="Shipping address")
    notes: Optional[str] = Field(
        default=None,
        description="Special delivery instructions"
    )
    tags: List[str] = Field(
        default=[],
        description="Tags for order tracking"
    )

The ge (greater or equal) and le (less or equal) constraints translate into JSON Schema constraints that the model enforces.

The xAI SDK tool() Function

The xAI SDK does not take a Pydantic class as-is: it expects a tool definition, which its tool() function builds from a name, a description, and a JSON schema. Pydantic provides exactly this schema via model_json_schema():

from xai_sdk.chat import tool

tools = [
    tool(
        name="get_weather",
        description="Get the current weather for a city",
        parameters=WeatherParams.model_json_schema(),
    ),
]

Using with the SDK

You declare the tool when creating the conversation. The SDK does not execute your functions: when Grok requests a call, your code executes it, adds the result with tool_result, then resumes generation.

import json

from xai_sdk import Client
from xai_sdk.chat import tool_result, user

client = Client(api_key="your-api-key")

chat = client.chat.create(model="grok-4.5", tools=tools)
chat.append(user("Weather in Bordeaux?"))
response = chat.sample()

if response.tool_calls:
    chat.append(response)
    for call in response.tool_calls:
        args = WeatherParams.model_validate_json(call.function.arguments)
        result = {"temp": 22, "unit": args.unit, "city": args.city}  # your implementation
        chat.append(tool_result(json.dumps(result)))
    response = chat.sample()

print(response.content)

model_validate_json also checks that the arguments produced by the model match the schema: a missing argument or wrong type raises an error before your function runs.

A Custom Decorator for Speed

The SDK does not provide a @tool decorator. Nothing stops you from writing one in a few lines that reads a function’s signature and docstring:

import inspect

from pydantic import create_model
from xai_sdk.chat import tool


def custom_tool(func):
    """Builds the xAI tool definition from a function's signature and docstring."""
    fields = {
        name: (p.annotation, ... if p.default is inspect.Parameter.empty else p.default)
        for name, p in inspect.signature(func).parameters.items()
    }
    schema = create_model(func.__name__, **fields).model_json_schema()
    description = (inspect.getdoc(func) or func.__name__).split("\n\n")[0]
    return tool(name=func.__name__, description=description, parameters=schema)


def get_weather(city: str, unit: str = "celsius") -> dict:
    """Get the current weather for a city (e.g., Paris, Lyon)."""
    return {"temp": 22, "unit": unit, "city": city}


tools = [custom_tool(get_weather)]

The function name becomes the tool name, the first paragraph of the docstring becomes the description, and parameters without default values become required. Parameter descriptions, however, are not extracted: for ambiguous parameters, revert to a Pydantic model with Field(description=...).

Comparing the Three Approaches

ApproachBenefitsLimitations
Manual JSON schemaTotal control, no dependenciesVerbose, typos go undetected
Pydantic model + tool()Argument validation, per-field descriptionsOne class per tool
Custom decoratorMost conciseNo per-parameter descriptions, code to maintain

Best Practices for Descriptions

Regardless of the approach, the description is what the model reads to decide whether to call the tool. State when to use it, what it returns, and the parameter format:

class SearchProductsParams(BaseModel):
    query: str = Field(description="Search terms: product name or keywords")
    category: str = Field(default="all", description="Category: electronics, clothing, food, or all")
    max_results: int = Field(default=10, ge=1, le=50, description="Maximum number of results")


search = tool(
    name="search_products",
    description=(
        "Search for products in the e-commerce catalog. Use when "
        "the user looks for a product by name, description, or category. "
        "Returns a list of products with price, availability, and link."
    ),
    parameters=SearchProductsParams.model_json_schema(),
)

Key Takeaways

  • Pydantic generates JSON schemas from Python models, with argument validation
  • The xAI SDK builds tools with tool(name=..., description=..., parameters=...)
  • The SDK does not execute your functions: your code does, then returns the result with tool_result
  • The SDK has no @tool decorator; a custom decorator is possible, at the cost of per-parameter descriptions
  • Polish your descriptions: the model reads them to choose a tool