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

Needle 2 automatically converts Python function signatures into OpenAI-compatible JSON schemas using introspection, type mapping, and docstring parsing in 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 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.

Stage 1: Introspect the Callable

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

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.

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:

{
    "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:

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:

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:

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

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

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 with comprehensive test coverage in 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.

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 →