JSON Schema Generation¶
The schema builder converts Python type annotations into JSON Schema objects. This schema is sent to LLM providers in the tool definitions, enabling the model to understand what arguments each tool expects and in what format.
Purpose¶
Schema generation serves two critical functions in the axio framework:
Tool input validation - The JSON schema defines the expected structure, types, and constraints for tool arguments. LLM providers use this schema to validate and format tool calls before sending them back to the agent.
Transport communication - Each
CompletionTransportsends the tool’sinput_schemaproperty to the LLM backend. The schema must be valid JSON Schema compatible with the provider’s expectations.
from axio import Tool, Field
from typing import Annotated
async def search(
query: Annotated[str, Field(description="Search query")],
limit: Annotated[int, Field(default=10, ge=1, le=100)] = 10,
) -> str:
"""Search the knowledge base."""
return f"results for {query!r}"
tool = Tool(name="search", handler=search)
# tool.input_schema contains the JSON schema sent to the LLM
Type Mappings¶
The schema builder maps Python types to JSON Schema types as follows:
Python Type |
JSON Schema |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
`T |
None |
from typing import Literal, Optional, get_type_hints
from axio.schema import build_tool_schema
def example(
name: str,
count: int,
ratio: float,
enabled: bool,
tags: list[str],
mode: Literal["read", "write"],
description: Optional[str] = None,
) -> str:
return "example"
schema = build_tool_schema(example)
assert schema["type"] == "object"
assert schema["properties"]["name"]["type"] == "string"
assert schema["properties"]["count"]["type"] == "integer"
assert schema["properties"]["ratio"]["type"] == "number"
assert schema["properties"]["enabled"]["type"] == "boolean"
assert schema["properties"]["tags"]["type"] == "array"
assert schema["properties"]["tags"]["items"]["type"] == "string"
assert schema["properties"]["mode"]["enum"] == ["read", "write"]
Field Metadata¶
Use Field() to add descriptions, defaults, and constraints to parameters.
The schema builder extracts FieldInfo metadata and translates it into JSON
Schema keywords:
Field Parameter |
JSON Schema Key |
Meaning |
|---|---|---|
|
|
Human-readable description |
|
|
Minimum value (inclusive) |
|
|
Maximum value (inclusive) |
|
(not in schema) |
Used for required field detection |
from typing import Annotated
from axio import Field
from axio.schema import build_tool_schema
def constrained(
username: Annotated[str, Field(description="User identifier")],
age: Annotated[int, Field(ge=0, le=150)],
score: Annotated[float, Field(ge=0.0, le=100.0)] = 0.0,
) -> str:
return f"{username}: {age}"
schema = build_tool_schema(constrained)
props = schema["properties"]
assert props["username"]["description"] == "User identifier"
assert props["age"]["minimum"] == 0
assert props["age"]["maximum"] == 150
assert props["score"]["minimum"] == 0.0
assert props["score"]["maximum"] == 100.0
Required vs Optional Fields¶
A field is required when it has no default value. The schema builder checks three sources for defaults:
Field(default=...)- Explicit field defaultFunction signature default -
param=default_valueClass attribute default - For class-based handlers
Fields without any of these are added to the "required" list.
from typing import Annotated
from axio import Field
from axio.field import MISSING
from axio.schema import build_tool_schema
def required_vs_optional(
required_str: str, # required
annotated_required: Annotated[str, Field(description="no default")], # required
optional_default: str = "default", # optional
field_default: Annotated[str, Field(default="field")] = "x", # optional (Field default)
) -> str:
return "test"
schema = build_tool_schema(required_vs_optional)
assert "required_str" in schema.get("required", [])
assert "annotated_required" in schema.get("required", [])
assert "optional_default" not in schema.get("required", [])
assert "field_default" not in schema.get("required", [])
Examples¶
Basic Tool Schema¶
from axio import Field, Tool
from typing import Annotated
async def greet(name: Annotated[str, Field(description="Name to greet")],
repeat: Annotated[int, Field(default=1, ge=1, le=5)] = 1) -> str:
"""Greet someone by name."""
return " ".join([f"Hello, {name}!" for _ in range(repeat)])
tool = Tool(name="greet", handler=greet)
# The generated schema:
# {
# "type": "object",
# "properties": {
# "name": {
# "type": "string",
# "description": "Name to greet"
# },
# "repeat": {
# "type": "integer",
# "minimum": 1,
# "maximum": 5
# }
# },
# "required": ["name"]
# }
Literal Enum¶
from typing import Annotated, Literal
from axio import Field
def set_mode(mode: Annotated[Literal["on", "off", "toggle"], Field(description="Power mode")]) -> str:
return f"Mode set to {mode}"
from axio.schema import build_tool_schema
schema = build_tool_schema(set_mode)
assert schema["properties"]["mode"]["enum"] == ["on", "off", "toggle"]
assert schema["properties"]["mode"]["description"] == "Power mode"
Array of Strings¶
from typing import Annotated
from axio import Field
def create_tags(tags: Annotated[list[str], Field(description="List of tags")]) -> str:
return f"Created tags: {tags}"
from axio.schema import build_tool_schema
schema = build_tool_schema(create_tags)
assert schema["properties"]["tags"]["type"] == "array"
assert schema["properties"]["tags"]["items"]["type"] == "string"
assert schema["properties"]["tags"]["description"] == "List of tags"
Integration with Transports¶
Transports call build_tool_schema() internally to generate the input_schema
property sent to LLM providers. The schema is included in tool definitions
alongside the tool’s name and description.
See Tool System for how tools use the schema and Field Metadata System for detailed Field metadata options.
See Also¶
Field Metadata System -
FieldInfoandField()metadata systemTool System - Tool handlers and schema consumption