# How to Apply Field Constraints to Tool Parameters Using `typing.Annotated` in Needle

> Learn to apply field constraints to tool parameters in Needle using typing.Annotated and needle.agent.tools.Field. Automatically generate JSON Schema for LLM function calling.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: how-to-guide
- Published: 2026-09-04

---

**Use `typing.Annotated` combined with `needle.agent.tools.Field` to attach validation constraints directly to function parameters, which Needle automatically converts into JSON Schema for LLM function calling.**

Needle’s tool-definition system transforms Python callables into JSON-schema specifications compatible with language-model function-calling APIs. By leveraging `typing.Annotated`, developers declaratively specify validation rules—such as minimum/maximum values, string lengths, and enumerations—directly on function signatures, keeping code clean while providing precise constraints to the model.

## Understanding the Core Components

The constraint system is implemented in **[`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)**, which provides the machinery to extract metadata from type hints and inject it into generated schemas.

### The `Field` Class

The **`Field`** class acts as a container for constraint metadata. According to the source code in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) (lines 18-33), it accepts parameters like `ge` (greater than or equal), `le` (less than or equal), `min_length`, `max_length`, and `enum`. 

The class exposes an **`apply()`** method that takes a JSON-schema dictionary and enriches it with the stored constraints. For example, when a `Field` contains `ge=0` and `le=5`, the `apply()` method adds `"minimum": 0` and `"maximum": 5` to the schema properties.

### The `_field_of()` Function

Located at lines 89-100 in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py), **`_field_of()`** traverses type annotations to extract metadata. When it encounters a `typing.Annotated` wrapper, it inspects the metadata arguments for instances of `Field`. If found, it returns the `Field` object; otherwise, it returns `None`.

This function enables Needle to distinguish between plain type hints and those carrying additional validation constraints.

## How Schema Generation Works

The **`build_schema()`** function (lines 20-51 in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)) orchestrates the conversion of Python function signatures into OpenAI-compatible function descriptions. For each parameter, it executes the following steps:

1. **Resolves the type hint**, including unpacking any `typing.Annotated` wrappers.
2. **Calls `_json_type()`** to determine the base JSON Schema type (string, integer, object, etc.).
3. **Calls `_field_of()`** to retrieve any `Field` instances attached via `Annotated`.
4. **Applies constraints** by calling `field.apply(schema)` if a `Field` is present, injecting validation rules into the schema dictionary.
5. **Determines requirement status** based on whether the parameter has a default value.

The final output is a fully-formed JSON Schema object within the `parameters` field, enabling the LLM to enforce constraints before invoking the tool.

## Practical Implementation Examples

### Basic String and Integer Constraints

Use `Annotated` with `Field` to constrain string length and numeric ranges:

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

@tool
def greet(
    name: Annotated[str, Field(min_length=3, max_length=30)], 
    excitement: Annotated[int, Field(ge=0, le=5)] = 1
) -> str:
    """Generate a greeting."""
    return f"{'!' * excitement} Hello, {name}"

```

This generates the following schema (simplified):

```json
{
  "name": "greet",
  "description": "Generate a greeting.",
  "parameters": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "minLength": 3,
        "maxLength": 30
      },
      "excitement": {
        "type": "integer",
        "minimum": 0,
        "maximum": 5,
        "default": 1
      }
    },
    "required": ["name"]
  }
}

```

### Enum Constraints with Literal Types

Restrict parameters to specific allowed values using `Literal` combined with `Field`:

```python
from typing import Literal, Annotated
from needle.agent.tools import Field, tool

@tool
def set_mode(
    mode: Annotated[Literal["auto", "manual"], Field(enum=["auto", "manual"])]
) -> None:
    """Switch the device mode."""
    pass

```

The generated schema includes `"enum": ["auto", "manual"]` for the `mode` parameter, restricting the LLM to only these valid options.

### Optional Parameters with Default Values

Combine `Annotated` with default values to create optional constrained parameters:

```python
from needle.agent.tools import tool
from typing import Annotated

@tool
def multiply(
    a: int,
    b: Annotated[int, Field(ge=0, le=10)] = 1,
) -> int:
    """Return the product of a and b."""
    return a * b

```

Here, `b` is optional (defaults to 1) but must be between 0 and 10 when provided. Needle marks `a` as required in the JSON Schema while omitting `b` from the required array, yet preserves the numeric constraints.

## Summary

- **[`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)** contains the core implementation of the constraint system, specifically the `Field` class, `_field_of()` function, and `build_schema()` logic.
- **`typing.Annotated`** serves as the bridge between Python type hints and Needle's constraint metadata, allowing arbitrary `Field` attachments without changing the runtime type.
- **`Field.apply()`** injects constraints into JSON Schema dictionaries, converting Python validation rules into LLM-compatible schema properties.
- **Parameters with defaults** become optional in the schema, while unconstrained parameters use the base type mapping without additional metadata.

## Frequently Asked Questions

### What is `typing.Annotated` in Python?

`typing.Annotated` is a standard Python type hint wrapper introduced in PEP 593 that allows attaching arbitrary metadata to type hints without affecting the actual type. In Needle, it serves as the mechanism to attach `Field` constraint objects to function parameters while preserving static type checking compatibility.

### How does Needle convert `Field` constraints to JSON Schema?

When `build_schema()` processes a parameter, it first extracts the base JSON type via `_json_type()`, then retrieves any `Field` instance via `_field_of()`. If present, `field.apply(schema)` is called, which mutates the schema dictionary to include standard JSON Schema keys like `minimum`, `maximum`, `minLength`, `maxLength`, or `enum` based on the `Field` configuration.

### Can I use multiple constraints on a single parameter?

Yes. The `Field` class accepts multiple constraint arguments simultaneously. For example, `Field(ge=0, le=100, description="Percentage")` applies both numeric bounds and a description. All constraints stored in the `Field` instance are applied to the generated schema when `build_schema()` processes the `Annotated` type hint.

### Where does Needle handle the extraction of `Annotated` metadata?

The extraction logic resides in the **`_field_of()`** function within [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) (lines 89-100). This function specifically checks for `typing.Annotated` types, iterates through the metadata arguments, and returns the first `Field` instance it encounters, enabling the schema builder to access constraint data.