# Needle JSON Schema Generation from Python Functions: A Complete Guide

> Easily generate JSON schemas from Python functions using Needle. Convert type hints and docstrings into OpenAI-compatible schemas. Get the complete guide today.

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

---

**Needle automatically converts Python functions and Pydantic models into OpenAI-compatible JSON schemas by inspecting type hints, docstrings, and `Field` metadata.**

Needle is an open-source agent framework that bridges Python code and large language model (LLM) function calling. This article explains exactly how Needle performs **JSON schema generation from Python functions**, walking through the source code in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) to show how raw callables become structured, type-safe tool definitions that LLMs can invoke.

## How Needle Converts Python Types to JSON Schema

The transformation happens in three coordinated stages inside [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py). Understanding each stage helps you debug schema output or extend the system for custom types.

### Type-to-JSON Mapping with `_json_type`

The `_json_type` function (lines 56-81) recursively translates Python type hints into JSON Schema fragments. It handles:

- **Basic types**: `str` → `"string"`, `int` → `"integer"`, `float` → `"number"`, `bool` → `"boolean"`
- **Optional and Union types**: `Optional[str]` or `str | None` unwraps to the inner type
- **Collections**: `list[T]`, `set[T]`, `tuple[T, ...]` become arrays with typed `items`
- **Literal types**: `Literal["a", "b"]` becomes `{"enum": ["a", "b"]}`
- **Annotated types**: `Annotated[str, Field(...)]` extracts metadata like `description`, `min_length`, `max_length`, `pattern`
- **Pydantic models**: Delegates to the model's own schema generation

```python

# needle/agent/tools.py (simplified conceptual flow)

def _json_type(t: Any) -> dict:
    # Handles typing.Annotated, Literal, list[T], Optional[T], etc.

    # Returns {"type": "...", "description": "...", ...}

    ...

```

This recursive approach ensures nested structures like `list[dict[str, int]]` or `Optional[list[Annotated[str, Field(min_length=1)]]]` unfold into correct, validatable schemas.

### Docstring Parsing with `_parse_doc`

The `_parse_doc` function (lines 94-107) extracts human-readable descriptions without requiring external documentation. It implements a lightweight parser that:

1. Pulls the first paragraph as the function's **main description**
2. Scans for `args:` or `Arguments:` sections
3. Extracts `argument_name: description` pairs for parameter documentation

```python
def _parse_doc(func: Callable) -> tuple[str, dict[str, str]]:
    """
    Returns: (description: str, arg_descriptions: dict)
    """
    ...

```

This design supports multiple docstring conventions. You can use Google-style, NumPy-style, or plain free-form text—Needle captures what it recognizes and gracefully ignores the rest.

### Schema Assembly with `build_schema`

The `build_schema` function (lines 110-140) orchestrates the final output. It:

- Calls `inspect.signature` to enumerate parameters
- Invokes `_json_type` on each parameter's annotation
- Merges `_parse_doc` descriptions into the appropriate parameter objects
- Marks **required fields**: parameters without defaults and not `Optional`
- Applies `Field` constraints: `enum`, `minLength`, `maxLength`, `pattern`, `minimum`, `maximum`, `default`

The result matches OpenAI's function-calling specification exactly:

```json
{
  "name": "function_name",
  "description": "...",
  "parameters": {
    "type": "object",
    "properties": {...},
    "required": [...]
  }
}

```

## The `@tool` Decorator: Automatic Schema Attachment

The `@tool` decorator (lines 62-65) provides the user-facing API. When applied to a function, it:

1. Calls `build_schema` at decoration time
2. Attaches the schema to the function as `._needle_tool`

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

@tool
def summarize(text: str, max_len: int = 100, language: str = "en"):
    """Summarize a piece of text.
    
    args:
        text: The original content to be summarized.
        max_len: Maximum length of the summary in characters.
        language: Language code, e.g. "en" or "fr".
    """
    pass

```

Accessing the generated schema:

```python
>>> summarize._needle_tool
{
  "name": "summarize",
  "parameters": {
    "type": "object",
    "properties": {
      "text": {"type": "string", "description": "The original content to be summarized."},
      "max_len": {"type": "integer", "description": "Maximum length of the summary in characters."},
      "language": {"type": "string", "description": "Language code, e.g. \"en\" or \"fr\"."}
    },
    "required": ["text"]
  },
  "description": "Summarize a piece of text."
}

```

Notice that `text` appears in `required` because it has no default, while `max_len` and `language` are omitted due to their default values.

## Pydantic Model Shortcut: `pydantic_schema`

For Pydantic `BaseModel` classes, Needle provides `pydantic_schema` (lines 49-58) as a direct path. Instead of re-analyzing the model field-by-field, it calls `model_json_schema()` (Pydantic v2) or `.schema()` (Pydantic v1) and repackages the output into the OpenAI function format.

```python
from pydantic import BaseModel
from needle.agent.tools import pydantic_schema

class ChatMessage(BaseModel):
    """A single chat entry."""
    role: str
    content: str

>>> pydantic_schema(ChatMessage)
{
  "name": "ChatMessage",
  "parameters": {
    "type": "object",
    "properties": {
      "role": {"type": "string"},
      "content": {"type": "string"}
    },
    "required": ["role", "content"]
  },
  "description": "A single chat entry."
}

```

This shortcut preserves Pydantic's full validation power—including `Field` constraints, validators, and custom types—while ensuring LLM compatibility.

## Adding Constraints with `Field`

Needle's `Field` function (re-exported or defined in [`tools.py`](https://github.com/cactus-compute/needle/blob/main/tools.py)) lets you inject JSON Schema constraints without leaving Python:

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

@tool
def search(
    query: Annotated[str, Field(min_length=3, max_length=100)],
    category: Annotated[str, Field(enum=["news", "blog", "paper"])],
    max_results: int = 10
):
    """Search the knowledge base."""
    pass

```

Generated constraints appear directly in the schema:

```python
>>> search._needle_tool["parameters"]["properties"]
{
  "query": {"type": "string", "minLength": 3, "maxLength": 100},
  "category": {"type": "string", "enum": ["news", "blog", "paper"]},
  "max_results": {"type": "integer"}
}

```

These constraints help LLMs produce valid calls and enable client-side validation before execution.

## File Structure and Source Locations

| File | Purpose |
|------|---------|
| [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) | Core implementation: `_json_type`, `_parse_doc`, `build_schema`, `pydantic_schema`, `@tool` decorator |
| [`needle/agent/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/__init__.py) | Public API exports (`tool`, `pydantic_schema`, `Field`) |
| [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) | Validation suite for schema generation across type hint variations |

All implementations referenced above are from the `main` branch of `cactus-compute/needle`.

## Summary

- **Needle JSON schema generation from Python functions** occurs in three stages: type mapping (`_json_type`), docstring parsing (`_parse_doc`), and final assembly (`build_schema`).
- The `@tool` decorator caches the result as `._needle_tool` for runtime retrieval.
- Pydantic models bypass field-by-field analysis via `pydantic_schema`, using native Pydantic schema generation.
- `Annotated[..., Field(...)]` syntax injects JSON Schema constraints without breaking Python type safety.
- Required parameters are determined automatically from signature defaults and `Optional` wrappers.

## Frequently Asked Questions

### Does Needle support Python 3.10+ union syntax like `str \| None`?

Yes. The `_json_type` implementation in [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) handles both `typing.Optional[T]` and the newer `T | None` syntax introduced in PEP 604. Both forms correctly unwrap to the inner type with `null` allowed in the schema when appropriate.

### Can I use Needle with Pydantic v1 and v2?

Yes. The `pydantic_schema` function detects the Pydantic version and calls `model_json_schema()` for v2 or `.schema()` for v1. The output shape is normalized to a consistent OpenAI-compatible format regardless of version.

### What happens if my function has no docstring?

`build_schema` proceeds without descriptions. The function `name`, `parameters`, and `required` fields are still populated from the signature and type hints. You can add descriptions later by manually editing the `._needle_tool` dict or by adding a minimal docstring with an `args:` section.

### How does Needle handle complex nested types like `list[dict[str, Any]]`?

`_json_type` recurses through container types. A `list[dict[str, int]]` becomes `{"type": "array", "items": {"type": "object", "additionalProperties": {"type": "integer"}}}`. For `Any`, Needle typically emits `{}` (unconstrained object) since no type information is available—this matches JSON Schema's default behavior.