Create Tool Schemas

Su Suriya Ravichandran Updated 10 Nov 2026
9 min read ·Lesson 83 of 200

What a Tool Schema Is

Every tool definition has an input_schema: a JSON Schema that describes the input object Claude must produce when it calls the tool (Lesson 82). JSON Schema is a standard format for describing the structure of JSON data: which fields exist, their types, which are required, and which values are allowed.

"input_schema": {
    "type": "object",
    "properties": {
        "city": {"type": "string", "description": "City name, e.g. Chennai"},
        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
    },
    "required": ["city"],
}

The schema serves two purposes:

  1. It guides Claude: Claude reads it to understand what to send.
  2. It defines a contract: your code can rely on, and verify, the shape of the input.

Schema Building Blocks

The Top Level

The top level of an input_schema is always an object:

KeywordPurpose
"type": "object"The input is a JSON object (a Python dict)
"properties"The fields the object may contain, each with its own schema
"required"The fields that must always be present
"additionalProperties": FalseNo fields other than those listed are allowed

Types

JSON Schema typePython valueExample
"string"str"Chennai"
"integer"int3
"number"int or float2.5
"boolean"boolTrue
"array"list["django", "sql"]
"object"dict{"min": 1, "max": 5}
"null"NoneNone

Useful Keywords

KeywordExampleEffect
description"description": "Date in YYYY-MM-DD format"Explains meaning and format to Claude
enum"enum": ["beginner", "intermediate", "advanced"]Allows only the listed values
items"items": {"type": "string"}The schema of each element in an array
format"format": "date"A known string format, such as a date
default"default": 60Documents the default; your code must apply it

Note: default only describes a default value. Claude may omit the field, and the API does not fill it in for you. Your Python function must use the same default, for example duration_minutes: int = 60.

Arrays and Nested Objects

"properties": {
    "topics": {
        "type": "array",
        "items": {"type": "string"},
        "description": "Topics to cover, e.g. ['models', 'views']",
    },
    "contact": {
        "type": "object",
        "properties": {
            "email": {"type": "string", "description": "Contact email address"},
            "phone": {"type": "string", "description": "Optional phone number with country code"},
        },
        "required": ["email"],
    },
}

Nested objects have their own properties and required lists. Keep nesting shallow: flat inputs are easier for Claude to fill correctly, and easier for your code to validate.


Required vs. Optional Fields

FieldWhen to make it required
RequiredThe tool cannot work without it, for example city for a weather tool
OptionalIt has a sensible default, or it only refines the request, for example unit or level

For optional fields, say in the description what happens when the field is omitted: "Omit it to search all levels." This helps Claude decide whether to include it.


Strict Tool Use: Guaranteed Schema Conformance

Without extra settings, Claude follows your schema very reliably, but it is not guaranteed: an input could occasionally contain "2" instead of 2, or miss a required field. For tools where a malformed input would break your code, set "strict": True on the tool definition:

{
    "name": "schedule_session",
    "description": "...",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {...},
        "required": [...],
        "additionalProperties": False,
    },
}

According to the strict tool use documentation, strict: true constrains the model's output so that the tool's input always follows the schema, and the tool name is always valid. It uses the same technique as structured outputs (Lesson 36), and it is available on every model.

Without strictWith strict: true
Inputs follow the schemaAlmost alwaysGuaranteed
TypesUsually correctAlways correct (2, never "2")
Required fieldsUsually presentAlways present
Schema featuresFull JSON Schema as hintsA supported subset (see the structured outputs documentation's "JSON Schema limitations")

Use "additionalProperties": False in strict schemas, as the documentation's examples do. If the API rejects a strict schema, check it against the documented limitations.

Note: strict guarantees the shape of the input, not that the values make sense. A date can be a valid string in the right format and still be in the past. Business rules still need validation in your code (Lessons 86 and 94).


Generating Schemas from Pydantic Models

Writing JSON Schemas by hand is repetitive and error-prone. Pydantic (installed with the SDK) can generate a schema from a Python class, and also validate inputs against that same class. One model then serves as the single source of truth:

from pydantic import BaseModel, ConfigDict, Field


class WeatherInput(BaseModel):
    model_config = ConfigDict(extra="forbid")

    city: str = Field(description="City name, e.g. Chennai")
    unit: str = Field(default="celsius", description="Temperature unit: celsius or fahrenheit")


schema = WeatherInput.model_json_schema()
  • Field(description=...) becomes the property's description.
  • Fields without defaults become required.
  • ConfigDict(extra="forbid") produces "additionalProperties": false, and also makes validation reject unknown fields.
  • model_json_schema() returns the schema as a dictionary.

Pydantic also adds title entries to the schema, for the model and for each field. They are harmless, but they add tokens without helping Claude, so the helper in the practical example removes them, carefully, without touching a field that happens to be named title.


Practical Example: Tools from Pydantic Models

This module builds tool definitions from Pydantic models and uses the same models to validate tool inputs. The schema Claude sees and the validation your code performs can never drift apart.

"""pydantic_tools.py: generate tool schemas from Pydantic models, and validate inputs with them."""

import json

from pydantic import BaseModel, ConfigDict, Field, ValidationError


def strip_titles(schema):
    """Remove Pydantic's 'title' annotations, but keep fields that are named 'title'."""
    if isinstance(schema, list):
        return [strip_titles(item) for item in schema]
    if not isinstance(schema, dict):
        return schema
    cleaned = {}
    for key, value in schema.items():
        if key == "title" and isinstance(value, str):
            continue                                     # A title annotation: drop it
        if key in ("properties", "$defs"):
            cleaned[key] = {name: strip_titles(sub) for name, sub in value.items()}
        else:
            cleaned[key] = strip_titles(value)
    return cleaned


def tool_from_model(name: str, description: str, model: type[BaseModel], strict: bool = True) -> dict:
    definition = {
        "name": name,
        "description": description,
        "input_schema": strip_titles(model.model_json_schema()),
    }
    if strict:
        definition["strict"] = True
    return definition


class SessionRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    course_id: str = Field(description="Course slug, e.g. 'django-101'")
    date: str = Field(description="Session date in YYYY-MM-DD format")
    time: str = Field(description="Start time in 24-hour HH:MM format, e.g. '19:00'")
    duration_minutes: int = Field(default=60, description="Length in minutes; omit for 60")
    topics: list[str] = Field(default_factory=list, description="Topics to cover; may be empty")


SCHEDULE_SESSION = tool_from_model(
    name="schedule_session",
    description=(
        "Schedules a live tutoring session for a course. Use it when a student asks to book "
        "or arrange a live session. Do not use it to check existing bookings. "
        "Returns a confirmation with the session ID, or an error if the slot is unavailable."
    ),
    model=SessionRequest,
)

if __name__ == "__main__":
    schema = SCHEDULE_SESSION["input_schema"]
    print("properties:", list(schema["properties"]))
    print("required:  ", schema["required"])
    print("strict:    ", SCHEDULE_SESSION.get("strict"))
    print("additionalProperties:", schema["additionalProperties"])
    print()

    samples = [
        {"course_id": "django-101", "date": "2026-11-12", "time": "19:00"},
        {"course_id": "django-101", "time": "19:00"},
        {"course_id": "django-101", "date": "2026-11-12", "time": "19:00", "room": "A1"},
    ]
    for sample in samples:
        try:
            request = SessionRequest.model_validate(sample)
            print("valid:  ", request.model_dump())
        except ValidationError as error:
            first = error.errors()[0]
            print("invalid:", first["loc"], first["msg"])

    print()
    print(json.dumps(schema["properties"]["duration_minutes"], indent=2))

Output:

properties: ['course_id', 'date', 'time', 'duration_minutes', 'topics']
required:   ['course_id', 'date', 'time']
strict:     True
additionalProperties: False

valid:   {'course_id': 'django-101', 'date': '2026-11-12', 'time': '19:00', 'duration_minutes': 60, 'topics': []}
invalid: ('date',) Field required
invalid: ('room',) Extra inputs are not permitted

{
  "default": 60,
  "description": "Length in minutes; omit for 60",
  "type": "integer"
}

How It Works

  1. SessionRequest describes the tool's input as a Python class. Field types become schema types, Field(description=...) becomes descriptions, and fields without defaults become required.
  2. ConfigDict(extra="forbid") adds "additionalProperties": false to the schema and makes validation reject unknown fields, as the third sample shows.
  3. strip_titles() walks the schema recursively and removes Pydantic's title annotations, the "title": "..." string entries on the model and each field. Inside properties (and $defs, used for nested models), the keys are field names, so they are always kept. A model with a field called title, such as a course or blog post, therefore keeps that field. Only string-valued title keys elsewhere are dropped.
  4. tool_from_model() assembles the tool definition with strict: true by default, so Claude's inputs are guaranteed to match the schema.
  5. model_validate() checks an input against the same model. A valid input becomes a typed SessionRequest object with defaults applied: duration_minutes=60 and topics=[] were filled in by Pydantic, not by Claude or the API.
  6. error.errors() returns structured error details: loc is the path to the problem field, and msg is a human-readable message. Lesson 90 sends such messages back to Claude so it can correct its input.

Even with strict: true, validating with the model is useful: it converts the input into a typed object, applies defaults, and protects functions that may also be called from other code paths.


Common Mistakes

Missing Descriptions on Properties

What happens: Claude guesses formats, for example 12/11/2026 instead of 2026-11-12.

Fix: describe every property, including its format and an example.

Relying on default to Fill Values

What happens: your function receives no value and fails, because the API does not apply schema defaults.

Fix: apply defaults in your code, or with Pydantic.

Overly Deep Nesting

What happens: more complex inputs, more mistakes, harder validation.

Fix: keep schemas flat where possible, and split very complex tools.

Using Unsupported Features in Strict Schemas

What happens: the API rejects the tool definition.

Fix: check the documented JSON Schema limitations for strict mode, and keep strict schemas simple.

Treating strict as Business Validation

What happens: well-formed but invalid values, such as past dates or unknown course IDs, reach your systems.

Fix: validate values in your code before acting on them.


Best Practices

  • Describe every property, with formats and examples.
  • Use enum for fixed sets of values.
  • Mark only truly necessary fields as required, and explain optional ones.
  • Use strict: true when malformed inputs would break your code.
  • Add "additionalProperties": False to object schemas.
  • Generate schemas from Pydantic models and validate with the same models.
  • Apply defaults in code, and validate business rules separately.

Exercise

  1. Write a Pydantic model TicketInput with subject (string), category (one of billing, technical, account), and priority (integer, default 3). Generate a tool definition from it. Hint: use typing.Literal["billing", "technical", "account"] for the category.
  2. Print the generated schema for category. Which JSON Schema keyword did Pydantic use?
  3. Add a field named title to SessionRequest, generate the tool again, and confirm that title still appears in properties and required.
  4. Explain why topics: list[str] = Field(default_factory=list) is better than topics: list[str] = [] in a Pydantic model.
Show answers for steps 1, 2, and 4

Step 1:

from typing import Literal


class TicketInput(BaseModel):
    model_config = ConfigDict(extra="forbid")

    subject: str = Field(description="One-line summary of the issue")
    category: Literal["billing", "technical", "account"] = Field(description="Ticket category")
    priority: int = Field(default=3, description="Priority from 1 (urgent) to 5 (low); omit for 3")


CREATE_TICKET = tool_from_model(
    "create_ticket",
    "Creates a support ticket. Use it when a student reports a problem that needs follow-up "
    "by the support team. Do not use it for general questions. Returns the new ticket ID.",
    TicketInput,
)

Step 2: Pydantic represents Literal[...] with an enum list: ["billing", "technical", "account"], together with the type string.

Step 4: default_factory=list creates a new empty list for each object. Pydantic does copy plain mutable defaults, but default_factory makes the intent explicit, and it is the standard Python pattern elsewhere, for example in dataclasses, where a shared mutable default would be a real bug.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Create Tool Schemas and get answers drawn from it.

Signed-in readers only.