Tool Schema Validation

Validate custom tool schemas for OpenAI compatibility before they reach the LLM

Schema validation catches broken custom tools at import time, before they confuse the LLM at runtime.

python
from praisonaiagents import Agent, tool

@tool
def search(query: str) -> str:
    """Search the web."""
    return f"Results for: {query}"

agent = Agent(instructions="You search the web", tools=[search])
agent.start("Find AI news")

The user asks the agent to search; schema validation runs before the tool reaches the LLM.

Diagram
graph LR
    subgraph "Tool Schema Validation Flow"
        A[📝 Tool Definition] --> B[🔍 validate()]
        B --> C[🔄 validate_schema_roundtrip()]
        C --> D[✅ Ready for LLM]
        B -->|❌ Error| E[🚨 ToolValidationError]
        C -->|❌ Error| E
    end

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef validator fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef success fill:#10B981,stroke:#7C90A0,color:#fff
    classDef error fill:#F59E0B,stroke:#7C90A0,color:#fff

    class A input
    class B,C validator
    class D success
    class E error

Quick Start

  1. Automatic validation with @tool
    python
    from praisonaiagents import Agent, tool
    
    @tool
    def search(query: str) -> str:
        """Search the web."""
        return f"Results for: {query}"
    
    # @tool already validated the schema for you.
    agent = Agent(instructions="You search the web", tools=[search])
    agent.start("Find AI news")
  2. Explicit validation of a custom BaseTool
    python
    from praisonaiagents import BaseTool
    
    class WeatherTool(BaseTool):
        name = "weather"
        description = "Get current weather"
    
        def run(self, location: str) -> str:
            return f"Weather in {location}: 72°F"
    
    tool = WeatherTool()
    tool.validate()                    # raises ToolValidationError on bad schema
    tool.validate_schema_roundtrip()   # confirms JSON serializability
  3. Validate a list of tools at once
    python
    from praisonaiagents.tools.base import validate_tool_schema_consistency
    
    validate_tool_schema_consistency([search, WeatherTool(), my_other_tool])
    # Raises if any tool is invalid OR if any two tools share a name.

How It Works

Diagram
LLMAgentSchema Validator@tool decoratorUserLLMAgentSchema Validator@tool decoratorUserDefine tool functionvalidate()validate_schema_roundtrip()✅ or ⚠️ warning loggedTool readyCreate agent with toolsSend clean schema
PhaseWhat Happens
@tool decorationValidates schema structure and JSON compatibility
Agent creationChecks for duplicate names across tool list
LLM interactionClean, validated schemas prevent runtime errors

What Gets Checked

CheckWhat it catches
parameters.type presentMissing schema scaffold
parameters.properties presentMost common OpenAI-incompatible bug
get_schema() returns {type: "function", function: {...}}Wrong top-level shape
function.parameters is a dict with propertiesInner shape broken
JSON round-trip preserves structureNon-serializable values (functions, sets, etc.)
required is a list when presentCommon typo (string instead of list)
Unique names across a listDuplicate registration
Complex type annotationsOptional, Union, Literal, Enum, parameterized List/Dict translated to proper JSON Schema

Variadic Parameters

*args and **kwargs are excluded from the generated tool schema — they never appear as properties and are never marked required.

python
from praisonaiagents import tool

@tool
def search(query: str, **kwargs):
    """Search the web."""
    return f"Results for: {query}"

The schema for search requires only query. Previously a decorated def f(query: str, **kwargs) mis-emitted kwargs as a required property; the builder now skips VAR_POSITIONAL / VAR_KEYWORD params, matching the tool-definition builder.


Common Errors & Fixes

Missing 'properties' field

Error: 'parameters' must have a 'properties' field for OpenAI compatibility

Broken Code:

python
class BadTool(BaseTool):
    name = "bad"
    description = "..."
    parameters = {"type": "object"}  # missing 'properties'

    def run(self, q: str) -> str: 
        return q

Fix:

python
class GoodTool(BaseTool):
    name = "good"
    description = "..."
    parameters = {
        "type": "object",
        "properties": {
            "q": {"type": "string"}
        },
        "required": ["q"]
    }

    def run(self, q: str) -> str: 
        return q
Bad get_schema() return shape

Error: get_schema() must return schema with type='function'

Broken Code:

python
class BadTool(BaseTool):
    name = "bad"
    description = "..."

    def get_schema(self):
        return {"name": self.name}  # Wrong shape

    def run(self, q: str) -> str: 
        return q

Fix:

python
class GoodTool(BaseTool):
    name = "good"
    description = "..."

    def get_schema(self):
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": self.parameters
            }
        }

    def run(self, q: str) -> str: 
        return q
Non-JSON-serializable default values

Error: Schema round-trip validation failed

Broken Code:

python
from datetime import datetime

@tool
def process_data(data: str, timestamp=datetime.now()) -> str:
    """Process data with timestamp."""
    return f"{data} at {timestamp}"

Fix:

python
from datetime import datetime

@tool
def process_data(data: str, timestamp: str = None) -> str:
    """Process data with timestamp."""
    if timestamp is None:
        timestamp = datetime.now().isoformat()
    return f"{data} at {timestamp}"
Duplicate tool names

Error: Duplicate tool name 'search' found in tool list

Broken Code:

python
@tool(name="search")
def web_search(query: str) -> str:
    return "web results"

@tool(name="search")  # Same name!
def file_search(query: str) -> str:
    return "file results"

agent = Agent(tools=[web_search, file_search])  # Error

Fix:

python
@tool(name="web_search")
def web_search(query: str) -> str:
    return "web results"

@tool(name="file_search")  # Unique name
def file_search(query: str) -> str:
    return "file results"

agent = Agent(tools=[web_search, file_search])  # Works

Behavior of @tool Validation Failures


Best Practices

Type every tool parameter

Validation derives the schema from your function signature, so annotate each parameter (query: str, not bare query). Untyped parameters produce a weak or invalid schema that the LLM may misuse — explicit types give the model a precise contract.

Write a docstring the model can read

The tool docstring becomes the description the LLM sees. Keep it short and action-oriented (“Search the web.”) so the model knows when to call the tool. A missing docstring leaves the model guessing about intent.

Watch the logs for validation warnings

Broken schemas surface at import time as warnings like 'parameters' must have a 'properties' field for OpenAI compatibility. Treat these as errors during development — fix them before shipping rather than discovering the failure when the LLM first calls the tool.

Prefer supported parameter types

Stick to types the schema round-trip understands (str, int, bool, and typed Optional/Union/Literal/Enum). Exotic or unannotated types can validate loosely and then fail at call time; validated, supported types keep tool calls reliable.


Related