# How Needle 2 Generates JSON Schemas from Function Signatures: A Deep Dive

> Discover how Needle 2 generates JSON schemas from Python function signatures using introspection and type mapping for AI tool integration.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: deep-dive
- Published: 2026-08-14

---

**Needle 2 automatically converts Python function signatures into OpenAI-compatible JSON schemas using introspection, type mapping, and docstring parsing in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py).**

Needle 2 is an open-source Python framework that simplifies building AI agents with function calling support. One of its core capabilities is generating JSON schemas directly from Python function signatures—no manual schema writing required. This article explains exactly how Needle 2 implements this transformation, with references to the actual source code in the [cactus-compute/needle](https://github.com/cactus-compute/needle) repository.

## Three-Stage Schema Generation Process

The schema generation pipeline in Needle 2 operates in three distinct stages, all implemented in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py).

### Stage 1: Introspect the Callable

Needle 2 begins by extracting the function's signature and type hints using Python's standard library:

```python
import inspect
import typing

signature = inspect.signature(fn)
hints = typing.get_type_hints(fn, include_extras=True)

```

The `include_extras=True` parameter is critical—it ensures that `typing.Annotated` metadata is preserved, enabling rich parameter descriptions and constraints. This logic appears in lines 11–15 of [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py).

Parameters named `self` or `cls`, along with variadic arguments (`*args`, `**kwargs`), are filtered out during processing since they don't translate to JSON schema properties.

### Stage 2: Map Python Types to JSON Schema Types

The `_json_type` function (lines 56–81) handles the conversion from Python types to JSON Schema fragments. It recognizes:

| Python Type | JSON Schema Output |
|-------------|------------------|
| `str`, `int`, `float`, `bool` | `"string"`, `"integer"`, `"number"`, `"boolean"` |
| `list[T]` | `{"type": "array", "items": <T-schema>}` |
| `dict[str, T]` | `{"type": "object", "additionalProperties": <T-schema>}` |
| `enum.Enum`, `Literal[...]` | `{"enum": [...]}` |
| Pydantic `BaseModel` | Nested schema via `model_json_schema()` |
| `Optional[T]`, `Union[T, None]` | Unwrapped to `T` with nullability handled |
| Unrecognized types | Fallback to `"string"` |

Optional and Union types are automatically unwrapped. When no specific mapping exists, Needle 2 gracefully falls back to `"string"` to maintain compatibility.

### Stage 3: Assemble the Complete Schema

The `build_schema` function (lines 110–140) orchestrates final assembly:

```python
{
    "name": fn.__name__,
    "description": <parsed docstring description>,
    "parameters": {
        "type": "object",
        "properties": { ... },      # Per-parameter schemas

        "required": [ ... ]         # Omitted if empty

    }
}

```

Two supporting components enrich this output:

- **`_parse_doc`** — Parses the function docstring into a top-level description and per-argument documentation, injected as `"description"` fields
- **`Field.apply`** — Merges constraint metadata (descriptions, enums, numeric bounds like `ge`/`le`) into parameter schemas

Required fields are determined by checking for the absence of default values and non-optional type hints.

## Field: Adding Constraints and Metadata

Needle 2 provides a lightweight `Field` class for attaching OpenAI-compatible constraints directly to parameter annotations:

```python
from needle import Field, tool

@tool
def search(
    query: str = Field(description="Search terms"),
    limit: int = Field(default=10, ge=1, le=100, description="Max results")
):
    """Perform a search."""
    ...

```

The `Field` descriptor carries:
- `description` — Human-readable explanation
- `default` — Default value when parameter is omitted
- `enum` — Allowed values list
- `ge`, `le`, `gt`, `lt` — Numeric range constraints

During schema generation, `Field.apply` merges these constraints into the corresponding JSON Schema fragment.

## Pydantic Model Support

For Pydantic `BaseModel` classes, Needle 2 provides `pydantic_schema` (lines 49–58) as a shortcut:

```python
from pydantic import BaseModel, Field as PField
from needle import pydantic_schema

class WeatherQuery(BaseModel):
    """Get weather information."""
    city: str = PField(description="City name")
    units: str = PField(default="metric", description="Units (metric/imperial)")

schema = pydantic_schema(WeatherQuery)

```

This extracts the model's native JSON schema via `model_json_schema()` (Pydantic v2) or `.schema()` (Pydantic v1), then wraps it in the OpenAI function-calling format with `name` and `description` fields.

## The @tool Decorator: Automatic Schema Attachment

The `@tool` decorator automates schema generation and attachment:

```python
from needle import tool

@tool
def calculate(a: int, b: int, operation: Literal["add", "multiply"] = "add") -> int:
    """Perform basic arithmetic."""
    if operation == "add":
        return a + b
    return a * b

# Schema is automatically available

print(calculate._needle_tool["parameters"]["properties"]["operation"])

# → {"enum": ["add", "multiply"], "type": "string", "default": "add"}

```

The decorator internally calls `build_schema(fn)` and stores the result as `fn._needle_tool`, making it immediately usable by Needle's agent runtime.

## Complete Working Examples

### Basic Function with Full Introspection

```python
from needle import build_schema

def create_user(
    name: str,
    age: int,
    email: str | None = None
) -> dict:
    """Create a new user account.
    
    Args:
        name: Full name of the user
        age: User's age in years
        email: Optional contact email
    """
    return {"name": name, "age": age, "email": email}

schema = build_schema(create_user)

assert schema["name"] == "create_user"
assert schema["parameters"]["required"] == ["name", "age"]  # email has default

assert schema["parameters"]["properties"]["age"]["type"] == "integer"
assert "description" in schema["parameters"]["properties"]["name"]

```

### Nested Types and Containers

```python
from typing import List
from needle import tool

@tool
def process_items(
    tags: List[str],
    metadata: dict[str, int] = Field(default_factory=dict, description="Item metadata")
):
    """Process a collection of items."""
    ...

# tags becomes: {"type": "array", "items": {"type": "string"}}

# metadata becomes: {"type": "object", "additionalProperties": {"type": "integer"}}

```

## Summary

Needle 2 generates JSON schemas from function signatures through a rigorous three-stage pipeline:

- **Introspection** with `inspect.signature` and `typing.get_type_hints` captures parameters and type annotations, including `Annotated` metadata
- **Type mapping** via `_json_type` converts Python types to JSON Schema fragments, handling primitives, containers, enums, literals, and Pydantic models
- **Assembly** in `build_schema` combines docstring parsing, `Field` constraint application, and required field detection into OpenAI-compatible output

The `@tool` decorator automates this process, while `pydantic_schema` provides optimized handling for Pydantic models. All implementation resides in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) with comprehensive test coverage in [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py).

## Frequently Asked Questions

### How does Needle 2 handle forward references in type annotations?

Needle 2 relies on `typing.get_type_hints(fn, include_extras=True)` to resolve forward references automatically. This standard library function evaluates string annotations in the proper namespace, ensuring that types defined after the function signature are correctly resolved before schema generation begins.

### What happens when a parameter has no type annotation?

When `typing.get_type_hints` fails to resolve a type or when a parameter lacks an annotation entirely, Needle 2 falls back to `"string"` as the JSON Schema type. This conservative default ensures the schema remains valid and usable by OpenAI's function-calling API even with incomplete type information.

### Can I use Pydantic v1 and v2 models interchangeably with Needle 2?

Yes. The `pydantic_schema` function detects the Pydantic version and calls `model_json_schema()` for Pydantic v2 models or `.schema()` for Pydantic v1 models. Both paths produce compatible JSON Schema output that Needle 2 wraps in the standard function-calling format.

### Why does Needle 2 filter out `self`, `cls`, `*args`, and `**kwargs`?

These parameters don't map to valid JSON Schema properties. `self` and `cls` are instance/class references internal to Python's object model, while `*args` and `**kwargs` represent variadic arguments that JSON Schema's fixed-property structure cannot directly represent. Filtering ensures clean, valid schemas for LLM consumption.