# How Needle Handles Optional Fields in Tool Calls: A Complete Guide to JSON-Schema Generation

> Learn how Needle handles optional fields in tool calls. Discover its automatic JSON schema generation for Optional[T] or T | None, even with default values.

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

---

**Needle automatically excludes parameters annotated with `Optional[T]` or `T | None` from the `"required"` array in generated JSON schemas, regardless of whether a default value is provided.**

Needle is a lightweight framework for building LLM-native tools that converts Python function signatures into JSON-Schema definitions usable by function-calling models. Understanding how Needle handles optional fields in tool calls is critical for building correct, model-compatible tool definitions. This article examines the implementation details in the Needle source code, including the detection logic, schema generation rules, and verification through test cases.

## How Needle Detects Optional Type Annotations

The foundation of optional field handling begins with type inspection. In [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py), the private helper `_is_optional` determines whether a parameter's annotation should be treated as optional.

This function recognizes both traditional and modern Python union syntaxes:

- `typing.Optional[T]` (equivalent to `Union[T, None]`)
- `T | None` (PEP 604 union syntax, Python 3.10+)

The implementation checks two conditions: whether the type's origin is a union type, and whether `None` appears among the union's arguments.

```python

# From needle/agent/tools.py lines 52-55

def _is_optional(annotation):
    origin = get_origin(annotation)
    if origin is Union:
        return None in get_args(annotation)
    return False

```

This detection runs before schema construction, ensuring that the optionality information is available when building the final JSON-Schema document.

## The Schema Building Logic for Required vs. Optional Parameters

Inside `build_schema` (also in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)), Needle evaluates each parameter to determine membership in the `"required"` array. The logic follows a precise decision tree at lines 34-42 and 133-139:

```python
has_default = param.default is not param.empty and not isinstance(param.default, Field)
if field and field.has_default():
    has_default = True

if not has_default and not _is_optional(annotation):
    required.append(name)

```

**A parameter becomes optional in the schema if either:**

- It has a default value (including `None` or a `Field` with a default)
- Its type annotation passes the `_is_optional` check (e.g., `Optional[str]` or `str | None`)

**A parameter becomes required only when:**

- It lacks a default value **AND**
- Its annotation is not detected as optional

This dual-check system means you can declare `def func(x: Optional[int])` without providing a default value, and Needle will still treat `x` as optional in the generated schema.

## Code Examples: Optional Field Handling in Practice

### Optional Annotation Without Default Value

```python
from needle import tool
from typing import Optional

@tool
def get_weather(city: str, units: Optional[str] = None):
    """Fetch weather data for a location."""
    return f"Weather for {city} in {units or 'metric'} units"

# Schema inspection

print(get_weather._needle_tool["parameters"]["required"])

# Output: ['city']

print(get_weather._needle_tool["parameters"]["properties"].keys())

# Output: dict_keys(['city', 'units'])

```

Even though `units` defaults to `None` in this example, the same behavior occurs without any default:

```python
@tool
def search_docs(query: str, max_results: Optional[int]):
    """Search documentation with optional result limit."""
    ...
    

# 'max_results' is still omitted from required due to Optional annotation

```

### PEP 604 Union Syntax Support

Needle equally supports Python 3.10+ pipe union syntax:

```python
@tool
def add_numbers(a: int, b: int | None = None):
    """Add two numbers, with optional second operand."""
    if b is None:
        b = 0
    return a + b

print(add_numbers._needle_tool["parameters"]["required"])

# Output: ['a']

```

The `b` parameter is excluded from `"required"` because `int | None` passes the `_is_optional` check.

### Interaction with Field Defaults

When combining `Field` metadata with optional annotations, Needle respects explicit defaults:

```python
from needle import tool, Field
from typing import Optional

@tool
def configure_timeout(
    host: str,
    timeout: Optional[int] = Field(default=30, description="Seconds to wait")
):
    """Configure connection with custom timeout."""
    ...

# Both parameters are optional; 'timeout' has explicit Field default

print(configure_timeout._needle_tool["parameters"].get("required"))

# Output: None or [] depending on JSON serialization

```

## Test Coverage and Verification

The Needle test suite in [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) contains explicit tests validating this behavior:

- `test_optional_annotation_not_required` (lines 54-60): Confirms that `typing.Optional[int]` parameters are excluded from the required array
- `test_pep604_none_union_not_required` (lines 62-70): Verifies identical behavior for `T | None` syntax

These tests ensure that schema generation remains consistent across Python versions and type annotation styles.

## Summary

- **Detection mechanism**: The `_is_optional` helper in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) recognizes `Optional[T]`, `Union[T, None]`, and `T | None` by checking for `None` in union arguments
- **Required array construction**: Parameters are added to `"required"` only when they lack both a default value and an optional type annotation
- **Default value handling**: Explicit defaults (including `None` and `Field` defaults) always mark parameters as optional
- **Modern syntax support**: PEP 604 union syntax works identically to `typing.Optional`
- **Verified behavior**: Unit tests in [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) prevent regressions in optional field detection

## Frequently Asked Questions

### Does Needle require default values for optional parameters?

No. Needle treats any parameter annotated with `Optional[T]` or `T | None` as optional in the generated schema, even without an explicit default value. The parameter will be omitted from the `"required"` array based solely on its type annotation.

### What types of optional annotations does Needle support?

Needle supports both `typing.Optional[T]` (and its equivalent `Union[T, None]`) and the newer `T | None` syntax introduced in PEP 604. The `_is_optional` function in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) handles both cases uniformly by inspecting the type's origin and arguments.

### How does Needle handle Field defaults with optional types?

When a parameter uses `Field(default=...)`, Needle checks `field.has_default()` in addition to inspecting the raw parameter default. This means `Optional[int] = Field(default=5)` correctly marks the parameter as optional, with the schema reflecting both the type constraints and the default value.

### Where can I find the implementation details?

The core logic resides in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py), specifically the `build_schema` function (lines 34-42 and 133-139) and the `_is_optional` helper (lines 52-55). The [`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) file exports `tool` and `Field` for public use, while [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) contains verification tests including `test_optional_annotation_not_required` and `test_pep604_none_union_not_required`.