A code generator for Dart that automatically produces provider-compatible tool schemas for Large Language Models (LLMs) from your annotated Dart functions.
If you are building AI agents with Gemini, OpenAI, Claude, or other LLMs, you often need to provide a schema describing the tools (functions) the model can call. Instead of writing and maintaining provider-specific JSON maps by hand, tool_schema_generator lets you write standard Dart functions and automatically generates the precise schemas your LLM needs.
- Zero Boilerplate: Automatically infers types, names, and nullability directly from Dart syntax.
- Full Analyzer Support: Supports
String,int,double,bool,List<T>,Map<String, Object?>,enums, and custom nested classes. - Provider-Shaped Schemas: Generates OpenAI, Anthropic, and Gemini tool schema shapes from the same Dart functions.
- Seamless Integration: Uses the canonical
source_gencombining builder. It outputs to a standard.g.dartfile and plays nicely alongside other generators likejson_serializable. - Customizable: Override tool names and descriptions, or let it automatically extract descriptions from your Dart doc comments.
- Runtime Injection: Hide app-controlled parameters from the LLM schema with
@Inject()while still passing them during dispatch.
Add the package to your pubspec.yaml:
dependencies:
tool_schema_generator: ^0.4.0
dev_dependencies:
build_runner: ^2.4.0Create a .dart file and use the @Tool() annotation on your top-level functions. You can use the @Describe() annotation to add rich descriptions to individual parameters.
// lib/tools.dart
import 'package:tool_schema_generator/tool_schema_generator.dart';
// IMPORTANT: Declare the part file
part 'tools.g.dart';
/// Sends an email to a specific user.
@Tool()
void sendEmail(
@Describe('The email address of the recipient') String to,
@Describe('The subject line of the email') String subject, {
@Describe('The main body content') required String body,
bool isHtml = false,
}) {
// Your logic here
}Run the build runner command in your terminal:
dart run build_runner buildThe generator creates a tools.g.dart file containing a toolRegistry instance. This registry contains all your schemas and automatically routes LLM tool calls back to your Dart functions safely.
You can pass provider-shaped schemas directly to your LLM framework using toolRegistry.schemasFor(...), or select individual OpenAI-compatible schemas via strongly-typed getters like toolRegistry.sendEmail.
toolRegistry.schemasFor(SchemaFlavor.openAi)gives you OpenAI function tool schemas.toolRegistry.schemasFor(SchemaFlavor.anthropic)gives you Anthropic tool schemas usinginput_schema.toolRegistry.schemasFor(SchemaFlavor.gemini)gives you Gemini function declarations.toolRegistry.allSchemasremains an OpenAI-compatible alias.toolRegistry.sendEmailgives you a single OpenAI-compatibleJsonObjectjust for that tool.
Then dispatch when the LLM replies:
final value = await toolRegistry.call(
toolCall.name,
toolCall.arguments
);The registry takes the raw string name and raw JsonObject (Map<String, Object?>) arguments from the LLM, finds the right Dart closure, validates all arguments, calls your function, and awaits the raw result.
Argument and execution failures are surfaced as typed exceptions:
ToolNotFoundExceptionMissingToolArgumentExceptionInvalidToolArgumentExceptionToolExecutionException
This gives you a completely type-safe, boilerplate-free bridge between Dart code and LLM agent loops.
import 'tools.dart';
void main() async {
// 1. Pass the schemas to your LLM
final response = await llm.generate(
prompt: "Send an email to hello@example.com saying Hi!",
tools: toolRegistry.schemasFor(SchemaFlavor.openAi),
);
// 2. When the LLM decides to call a tool, dispatch it.
for (final toolCall in response.toolCalls) {
try {
final value = await toolRegistry.call(
toolCall.name,
toolCall.arguments,
);
print("Tool returned: $value");
} on ToolCallException catch (error) {
print("Tool failed: ${error.message}");
}
}
}Enums are automatically converted to JSON Schema string enums:
enum Priority { low, normal, high }
@Tool()
void setTaskPriority(Priority priority) {}
// Generates: {"type": "string", "enum": ["low", "normal", "high"]}Custom classes are introspected. The generator looks at the class's constructor parameters to build a nested JSON Schema object:
class Location {
final double lat;
final double lng;
Location({required this.lat, required this.lng});
}
@Tool()
void updateLocation(Location location) {}
// Generates nested object with properties `lat` and `lng` (both required).If you don't want to use the Dart function name or doc comment, you can override them directly in the annotation:
@Tool(
name: 'custom_search_tool',
description: 'A highly specific search tool description.'
)
void search(String query) {}By default, every tool is generated for OpenAI, Anthropic, and Gemini. You can limit a tool to specific provider shapes:
@Tool(flavors: [SchemaFlavor.anthropic])
Future<String> searchClaudeOnly(String query) async => '...';The generated registry groups schemas by flavor:
final anthropicTools = toolRegistry.schemasFor(SchemaFlavor.anthropic);
final openAiSendEmail = toolRegistry.sendEmail;Use @Inject() for app-controlled named parameters that should not appear in
the schema sent to the LLM. Injected parameters are still read from the same
arguments map passed to toolRegistry.call, so you can merge values like user
IDs, tenant IDs, or request locale at invocation time.
Injected parameters must be optional: nullable, have a Dart default value, or both.
@Tool()
Future<void> createTask(
@Describe('Task title') String title, {
@Inject() String? userId,
@Inject() String locale = 'en',
}) async {
// userId and locale are available here, but only title is in the schema.
}
final value = await toolRegistry.call(toolCall.name, {
...toolCall.arguments,
'userId': currentUser.id,
'locale': request.locale,
});Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.