How Needle Handles Optional Fields in Tool Calls: A Complete Guide to JSON-Schema Generation
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, 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 toUnion[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.
# 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), 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:
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
Noneor aFieldwith a default) - Its type annotation passes the
_is_optionalcheck (e.g.,Optional[str]orstr | 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
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:
@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:
@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:
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 contains explicit tests validating this behavior:
test_optional_annotation_not_required(lines 54-60): Confirms thattyping.Optional[int]parameters are excluded from the required arraytest_pep604_none_union_not_required(lines 62-70): Verifies identical behavior forT | Nonesyntax
These tests ensure that schema generation remains consistent across Python versions and type annotation styles.
Summary
- Detection mechanism: The
_is_optionalhelper inneedle/agent/tools.pyrecognizesOptional[T],Union[T, None], andT | Noneby checking forNonein 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
NoneandFielddefaults) 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.pyprevent 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 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, specifically the build_schema function (lines 34-42 and 133-139) and the _is_optional helper (lines 52-55). The needle/__init__.py file exports tool and Field for public use, while tests/test_tools.py contains verification tests including test_optional_annotation_not_required and test_pep604_none_union_not_required.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →