Needle JSON Schema Generation from Python Functions: A Complete Guide
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 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. 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]orstr | Noneunwraps to the inner type - Collections:
list[T],set[T],tuple[T, ...]become arrays with typeditems - Literal types:
Literal["a", "b"]becomes{"enum": ["a", "b"]} - Annotated types:
Annotated[str, Field(...)]extracts metadata likedescription,min_length,max_length,pattern - Pydantic models: Delegates to the model's own schema generation
# 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:
- Pulls the first paragraph as the function's main description
- Scans for
args:orArguments:sections - Extracts
argument_name: descriptionpairs for parameter documentation
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.signatureto enumerate parameters - Invokes
_json_typeon each parameter's annotation - Merges
_parse_docdescriptions into the appropriate parameter objects - Marks required fields: parameters without defaults and not
Optional - Applies
Fieldconstraints:enum,minLength,maxLength,pattern,minimum,maximum,default
The result matches OpenAI's function-calling specification exactly:
{
"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:
- Calls
build_schemaat decoration time - Attaches the schema to the function as
._needle_tool
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:
>>> 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.
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) lets you inject JSON Schema constraints without leaving 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:
>>> 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 |
Core implementation: _json_type, _parse_doc, build_schema, pydantic_schema, @tool decorator |
needle/agent/__init__.py |
Public API exports (tool, pydantic_schema, Field) |
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
@tooldecorator caches the result as._needle_toolfor 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
Optionalwrappers.
Frequently Asked Questions
Does Needle support Python 3.10+ union syntax like str \| None?
Yes. The _json_type implementation in 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.
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 →