How to Use needle.Field with Annotated for Precise Tool Constraints in Python

needle.Field combined with typing.Annotated lets you attach JSON Schema constraints directly to function parameters, automatically generating OpenAI-compatible tool descriptions without manual schema writing.

The needle library streamlines the creation of LLM tool interfaces by inspecting Python type hints and metadata. By pairing needle.Field with typing.Annotated, you define validation rules, descriptions, and value constraints that compile into complete JSON Schema definitions for AI agents.

Understanding the Field Descriptor

The Field class in needle/agent/tools.py acts as a lightweight container for JSON Schema constraints. Implemented in lines 17-21, its __init__ method accepts arbitrary keyword arguments representing schema properties:


# Conceptual implementation from needle/agent/tools.py

class Field:
    def __init__(self, **kwargs):
        self.constraints = kwargs
    
    def apply(self, schema: dict) -> None:
        # Lines 35-48: Merges constraints into the parameter schema

        schema.update(self.constraints)

When decorated with @tool, your function undergoes signature inspection via the build_schema function. This process iterates over each parameter, extracts type hints including Annotated metadata, and constructs the final tool definition compatible with OpenAI's function-calling specification.

Detecting Annotated Metadata

The extraction logic relies on _field_of, defined in needle/agent/tools.py lines 84-91. This helper inspects each parameter annotation for typing.Annotated types and retrieves any embedded Field instances:

  1. Signature analysisbuild_schema retrieves the function's type hints with include_extras=True to preserve metadata
  2. Metadata scanning – For each parameter, _field_of checks if the annotation is an Annotated type and searches its metadata tuple for a Field instance
  3. Constraint application – If found, Field.apply (lines 35-48) merges the stored constraints into that parameter's JSON schema

The system automatically determines required fields by checking for default values. A parameter becomes required only when it lacks both a function default and a Field-provided default, and when its type is not Optional.

Practical Implementation Example

Here is a complete implementation demonstrating precision constraints for a translation tool:

from typing import Annotated
from needle.agent.tools import Field, tool

@tool
def translate(
    text: Annotated[
        str,
        Field(
            description="The text to translate",
            min_length=1,
            max_length=500,
        ),
    ],
    target_lang: Annotated[
        str,
        Field(
            description="ISO-639-1 language code",
            enum=["en", "es", "fr", "de"],
        ),
    ] = "en",
) -> str:
    """Translate text into target_lang."""
    return f"Translated '{text}' to {target_lang}"

When you access translate._needle_tool (the internal schema registry), needle generates the following OpenAI-compatible structure:

{
  "name": "translate",
  "description": "Translate text into target_lang.",
  "parameters": {
    "type": "object",
    "properties": {
      "text": {
        "type": "string",
        "description": "The text to translate",
        "minLength": 1,
        "maxLength": 500
      },
      "target_lang": {
        "type": "string",
        "description": "ISO-639-1 language code",
        "enum": ["en", "es", "fr", "de"]
      }
    },
    "required": ["text"]
  }
}

Notice that text appears in the required array because it has no default value, while target_lang remains optional due to its default of "en".

Supported Constraint Types

The Field class supports any JSON Schema-valid constraint passed as keyword arguments. Common validations include:

  • description – Human-readable explanation of the parameter
  • enum – Restricts values to a specific list
  • min_length/max_length – String length boundaries (rendered as minLength/maxLength in JSON Schema)
  • pattern – Regular expression validation for string formats
  • ge/le/gt/lt – Numeric range constraints (greater/less than or equal)
  • default – Static default value injected into the schema

These constraints map directly to JSON Schema specifications, ensuring compatibility with OpenAI's function-calling API and other LLM tool interfaces.

Summary

  • needle.Field stores validation metadata in needle/agent/tools.py and applies it via the apply method (lines 35-48)
  • _field_of (lines 84-91) extracts Field instances from typing.Annotated metadata during schema generation
  • Required detection automatically occurs based on the absence of default values and non-optional types
  • JSON Schema output includes all constraints (descriptions, enums, ranges) without manual dictionary construction
  • The @tool decorator in needle/agent/tools.py orchestrates the entire inspection and registration process

Frequently Asked Questions

What is the difference between using Field metadata and Python default values?

Field metadata defines schema constraints and descriptions for the LLM, while Python default values define runtime behavior. You can use both simultaneously: place constraints inside Field(...) and runtime defaults after the type annotation. If you specify a default within the Field object itself, it populates the JSON Schema but does not affect the Python function signature.

Can I use needle.Field without typing.Annotated?

No, needle.Field requires typing.Annotated to attach metadata to type hints without altering the runtime type. The _field_of helper specifically searches the __metadata__ tuple of Annotated types. Using Field directly as a default value (e.g., param: str = Field(...)) would conflict with Python's runtime expectations for string parameters.

How does needle handle Optional types with Field constraints?

Optional types (e.g., Optional[str] or str | None) are automatically detected and excluded from the required array in the generated schema. The build_schema function checks for None compatibility in the type hint. Even if you attach a Field with constraints to an Optional parameter, it remains optional in the JSON Schema unless you explicitly provide a non-None default value in the function signature.

Where is the generated tool schema stored after decoration?

The schema is stored in the _needle_tool attribute of the decorated function. After applying @tool, you can inspect the complete OpenAI-compatible definition via your_function._needle_tool. This dictionary contains the function name, description, and parameters object suitable for passing directly to LLM API calls.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →