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.
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.
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 errorQuick Start
- 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") - 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 - 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
| Phase | What Happens |
|---|---|
| @tool decoration | Validates schema structure and JSON compatibility |
| Agent creation | Checks for duplicate names across tool list |
| LLM interaction | Clean, validated schemas prevent runtime errors |
What Gets Checked
| Check | What it catches |
|---|---|
parameters.type present | Missing schema scaffold |
parameters.properties present | Most common OpenAI-incompatible bug |
get_schema() returns {type: "function", function: {...}} | Wrong top-level shape |
function.parameters is a dict with properties | Inner shape broken |
| JSON round-trip preserves structure | Non-serializable values (functions, sets, etc.) |
required is a list when present | Common typo (string instead of list) |
| Unique names across a list | Duplicate registration |
| Complex type annotations | Optional, 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.
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:
class BadTool(BaseTool):
name = "bad"
description = "..."
parameters = {"type": "object"} # missing 'properties'
def run(self, q: str) -> str:
return qFix:
class GoodTool(BaseTool):
name = "good"
description = "..."
parameters = {
"type": "object",
"properties": {
"q": {"type": "string"}
},
"required": ["q"]
}
def run(self, q: str) -> str:
return qBad get_schema() return shape
Error: get_schema() must return schema with type='function'
Broken Code:
class BadTool(BaseTool):
name = "bad"
description = "..."
def get_schema(self):
return {"name": self.name} # Wrong shape
def run(self, q: str) -> str:
return qFix:
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 qNon-JSON-serializable default values
Error: Schema round-trip validation failed
Broken Code:
from datetime import datetime
@tool
def process_data(data: str, timestamp=datetime.now()) -> str:
"""Process data with timestamp."""
return f"{data} at {timestamp}"Fix:
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:
@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]) # ErrorFix:
@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]) # WorksBehavior 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.