Defining a Tool Schema

Ma Mahalakshmi V Updated 16 Sep 2026
11 min read ·Lesson 32 of 224

Schemas Are the Entire Interface

Lesson 1 introduced the shape of a tool definition at a high level. This lesson goes deeper into the parameters schema specifically, since it functions as the entire interface between the model and your function — the model never sees your function's actual Python signature, docstring, or implementation, only the JSON Schema you provide. Every piece of information the model needs to call a function correctly (what arguments exist, what type each one is, which are required, what values are valid) has to be encoded in that schema, because it is genuinely the only thing the model has to go on.

This is worth sitting with for a moment: a schema that under-specifies a parameter (a vague type, a missing description, no constraints on valid values) doesn't just produce worse documentation — it directly increases the chance the model sends arguments your function can't actually handle correctly, since the model is inferring your function's real requirements entirely from what the schema tells it.

Basic Types and When to Use Each

schema = {
    "type": "object",
    "properties": {
        "customer_id": {"type": "string", "description": "The unique customer identifier, e.g. 'CUST-4471'"},
        "order_count": {"type": "integer", "description": "Number of most recent orders to retrieve"},
        "include_cancelled": {"type": "boolean", "description": "Whether to include cancelled orders in the results"},
        "minimum_total": {"type": "number", "description": "Only include orders with a total at or above this amount"},
    },
    "required": ["customer_id"],
    "additionalProperties": False,
}

Each JSON Schema primitive type maps onto a specific kind of value, and choosing the right one matters for the same reason Unit 6 emphasized precise typing for structured outputs: string for text values (names, identifiers, free-form text), integer for whole-number counts or quantities where a fractional value would be meaningless (you can't request 3.5 orders), number for values that can legitimately have a fractional component (a monetary amount, a measurement), and boolean for a genuine yes/no flag. Using string for something that is really a number (representing a quantity as "3" instead of 3) works in the sense that JSON accepts it, but it pushes a parsing and validation burden onto your function that a correctly typed schema would have handled automatically, and it invites the model to occasionally send malformed numeric strings that a proper integer or number type would have prevented entirely.

Required Versus Optional Parameters

schema = {
    "type": "object",
    "properties": {
        "city": {"type": "string"},
        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit; defaults to celsius if not specified"},
    },
    "required": ["city"],
    "additionalProperties": False,
}

Only city appears in required here, meaning the model can supply unit when the user's request makes it relevant ("what's the temperature in Boston in Fahrenheit") but can omit it otherwise, in which case your function's own default behavior takes over. This mirrors Unit 6, Lesson 2's required-versus-nullable distinction directly: a parameter should be required only when the function genuinely cannot do its job without it, and left optional when a sensible default exists — marking every parameter required regardless of whether it's actually needed forces the model to either guess a value it has no real basis for, or avoid calling the function at all when it can't confidently fill in every field, neither of which is a good outcome.

Note: Some SDK versions and strict-mode configurations for function tools require every property to be listed in required, using a nullable type (["string", "null"]) to represent an optional value instead of omitting it from required — mirroring the strict-mode behavior Unit 6, Lesson 2 described for structured outputs. Confirm the current behavior for function-calling schemas specifically against your installed SDK version's documentation, since this detail is a frequent source of confusion between structured-output schemas and function-calling schemas.

Enums for Constrained Choices

schema = {
    "type": "object",
    "properties": {
        "priority": {
            "type": "string",
            "enum": ["low", "medium", "high", "urgent"],
            "description": "The priority level to assign to the new support ticket",
        },
        "category": {
            "type": "string",
            "enum": ["billing", "technical", "account", "other"],
        },
    },
    "required": ["priority", "category"],
    "additionalProperties": False,
}

Exactly as Unit 6, Lesson 1 argued for structured outputs, an enum is the right choice whenever a parameter's valid values form a fixed, known set, rather than leaving it as an unconstrained string and hoping the model happens to send one of the values your function actually recognizes. Without the enum constraint here, a create_support_ticket function might receive "High", "HIGH", "very high", or "urgent!!" for what should be one of exactly four values, forcing normalization logic into the function itself; with the enum constraint, the model is limited to the exact four strings listed, and that normalization problem simply does not arise.

Nested Objects and Arrays

Parameters aren't limited to flat, single-value fields — a function that needs a more complex argument shape can describe it with nested objects and arrays, the same way Unit 6 covered nested structures for output schemas.

schema = {
    "type": "object",
    "properties": {
        "recipient": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "email": {"type": "string"},
            },
            "required": ["name", "email"],
            "additionalProperties": False,
        },
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "product_name": {"type": "string"},
                    "quantity": {"type": "integer", "minimum": 1},
                },
                "required": ["product_name", "quantity"],
                "additionalProperties": False,
            },
        },
    },
    "required": ["recipient", "line_items"],
    "additionalProperties": False,
}

This schema describes a function like create_invoice(recipient, line_items), where recipient is itself a small object with its own required fields, and line_items is an array where every element must independently satisfy its own nested schema. Nested structures like this let a single function call carry as much structured information as the underlying operation genuinely needs — an invoice with multiple line items is a single real-world action, and modeling it as one function call with an array argument is more faithful to that reality than forcing the model to make one function call per line item, which would also lose the natural grouping of "these items belong to the same invoice."

Constraining String and Numeric Values Further

Beyond basic types and enums, JSON Schema supports additional constraints worth using whenever a parameter has a known valid range or format, narrowing the space of arguments a model can validly produce.

schema = {
    "type": "object",
    "properties": {
        "zip_code": {"type": "string", "pattern": "^[0-9]{5}$", "description": "Five-digit US ZIP code"},
        "age": {"type": "integer", "minimum": 0, "maximum": 120},
        "discount_percentage": {"type": "number", "minimum": 0, "maximum": 100},
        "confirmation_code": {"type": "string", "minLength": 6, "maxLength": 6},
    },
    "required": ["zip_code"],
    "additionalProperties": False,
}

pattern applies a regular expression to a string field (useful for a known fixed format like a ZIP code, a product SKU, or a phone number), while minimum/maximum on numeric fields and minLength/maxLength on string fields bound values to a known valid range. As with the duration_minutes example in Lesson 1, these constraints are enforced before your function ever receives the arguments, meaning a schema with well-chosen constraints does part of your function's input validation for you — not a replacement for validating within the function itself (a point Lesson 5 returns to when discussing untrusted arguments), but a first line of defense that catches a meaningful share of malformed values for free.

Writing Descriptions That Actually Help

A property's description field is not optional documentation — it is one of the few places you can give the model context it has no other way to infer.

schema = {
    "type": "object",
    "properties": {
        "date_range_start": {
            "type": "string",
            "description": "Start date in YYYY-MM-DD format. If the user says a relative date like 'last week', compute the actual calendar date based on today's date before calling this function.",
        },
        "sort_by": {
            "type": "string",
            "enum": ["date", "amount", "customer_name"],
            "description": "Field to sort results by. Use 'date' unless the user explicitly asks for a different order.",
        },
    },
    "required": ["date_range_start"],
    "additionalProperties": False,
}

The date_range_start description here does more than name the field's format — it tells the model what to do with an ambiguous or relative input ("last week") before calling the function at all, which is a genuinely useful thing to communicate through the schema, since the model has no other channel for this kind of calling-convention guidance beyond the function's own top-level description and each parameter's description. Treating these description fields as a place to put real, functional guidance (not just a restatement of the field name) tends to produce noticeably more reliable function calls than leaving them as an afterthought.

A Complete, Realistic Tool Definition

Pulling the pieces above together, a well-specified real-world tool definition tends to combine several of these techniques at once.

search_orders_tool = {
    "type": "function",
    "name": "search_customer_orders",
    "description": (
        "Search a customer's order history. Use this whenever the user asks about "
        "past orders, order status, or purchase history for a specific customer. "
        "Requires the customer's ID; if you don't have it, ask the user for it "
        "before calling this function."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "customer_id": {
                "type": "string",
                "description": "The unique customer identifier, e.g. 'CUST-4471'.",
            },
            "status": {
                "type": "string",
                "enum": ["pending", "shipped", "delivered", "cancelled", "any"],
                "description": "Filter by order status. Use 'any' if the user doesn't specify a status.",
            },
            "max_results": {
                "type": "integer",
                "minimum": 1,
                "maximum": 50,
                "description": "Maximum number of orders to return. Default to 10 if the user doesn't specify a number.",
            },
        },
        "required": ["customer_id", "status", "max_results"],
        "additionalProperties": False,
    },
}

Notice how the top-level description includes a piece of behavioral guidance ("if you don't have it, ask the user for it before calling this function") that goes beyond describing what the function does — it also shapes when the model should call it at all, addressing a realistic scenario where a user's request implies this function but doesn't supply every needed piece of information. This kind of guidance, placed directly in the schema the model actually sees, is more reliable than hoping the model infers the right behavior on its own.

Mapping Schema Types to Your Function's Actual Parameters

Since the schema is the model's only view into your function, it's worth keeping the schema and the actual Python function signature deliberately in sync — a mismatch between the two is a bug that won't surface until a real call is attempted, since nothing checks the schema against the function's signature automatically.

JSON SchemaTypical Python equivalentNotes
"type": "string"strAlso used for enums, patterns, dates encoded as strings
"type": "integer"intUse for counts, quantities, anything without a fractional component
"type": "number"float (or int)Use when fractional values are meaningful
"type": "boolean"boolGenuine binary flags only
"type": "array"listPair with "items" describing the element schema
"type": "object"dict or a nested dataclass/modelPair with "properties" and its own "required"

Keeping this mapping explicit in mind while writing a schema — and re-checking it whenever the underlying function's signature changes — helps catch drift early: a function that gains a new required parameter needs its schema updated at the same time, or the model will keep generating calls missing that argument, and a parameter removed from the function needs the same field pulled from the schema, or the model may keep sending an argument the function silently ignores or errors on. Treating the schema as a piece of code that must stay synchronized with the function it describes, rather than a one-time description written once and forgotten, avoids a class of quiet integration bugs that are easy to miss during manual testing but surface as real failures once the tool is used against varied, unscripted user input.

Common Mistakes

Leaving every parameter as an unconstrained string, even for values that are really numbers, booleans, or one of a known fixed set, pushing type coercion and validation work onto the function that the schema could have handled automatically.

Writing descriptions that just restate the parameter name ("city: the city") rather than providing real guidance about format, valid values, or what to do in ambiguous cases.

Marking every parameter as required regardless of whether the function actually needs it, forcing the model to guess values for fields that have a perfectly good default, or avoid calling the function when it can't confidently fill in every field.

Omitting enum for a parameter with a small, fixed set of valid values, inviting inconsistent variations of what should be a small number of exact string values.

Best Practices

Choose the most specific applicable JSON Schema type for each parameter — integer over string for counts, enum over free-text string for fixed categorical choices, boolean for genuine yes/no flags — so the schema itself enforces as much correctness as possible before your function ever runs.

Use required deliberately, listing only the parameters the function genuinely cannot operate without, and documenting sensible defaults for the rest in their descriptions.

Write parameter and function descriptions as functional guidance, not just labels, including how to handle ambiguous input, what defaults to assume, and when the function should or shouldn't be called.

Apply value constraints (enum, pattern, minimum/maximum, minLength/maxLength) wherever a parameter has a known valid range or format, catching a meaningful share of malformed arguments at the schema level rather than relying entirely on validation inside the function.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Defining a Tool Schema and get answers drawn from it.

Signed-in readers only.