Guide
Annotating Types
Every type that you want to generate a schema for needs an annotate method that returns an Annotation. The annotation carries a name, description, and per-field metadata.
using DescribedTypes
using JSON
struct Weather
location::String
temperature::Float64
unit::String
end
DescribedTypes.annotate(::Type{Weather}) = Annotation(
name="Weather",
description="Current weather observation.",
parameters=Dict(
:location => Annotation(name="location", description="City name"),
:temperature => Annotation(name="temperature", description="Temperature value"),
:unit => Annotation(name="unit", description="Unit of measurement", enum=["celsius", "fahrenheit"]),
),
)If you don't define annotate for a type, a default annotation is generated using the type name, with generic field descriptions.
Generating Schemas
Use schema to produce a JSON Schema dictionary.
Plain JSON Schema (STANDARD):
d = schema(Weather)
print(JSON.json(d, 2)){
"type": "object",
"properties": {
"location": {
"type": "string"
},
"temperature": {
"type": "number"
},
"unit": {
"type": "string"
}
},
"required": [
"location",
"temperature",
"unit"
]
}OpenAI response-format wrapper (OPENAI):
d = schema(Weather, llm_adapter=OPENAI)
print(JSON.json(d, 2)){
"name": "Weather",
"description": "Current weather observation.",
"strict": true,
"schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
},
"temperature": {
"type": "number",
"description": "Temperature value"
},
"unit": {
"type": "string",
"description": "Unit of measurement",
"enum": [
"celsius",
"fahrenheit"
]
}
},
"required": [
"location",
"temperature",
"unit"
],
"additionalProperties": false
}
}OpenAI function/tool-calling wrapper (OPENAI_TOOLS):
d = schema(Weather, llm_adapter=OPENAI_TOOLS)
print(JSON.json(d, 2)){
"type": "function",
"name": "Weather",
"description": "Current weather observation.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
},
"temperature": {
"type": "number",
"description": "Temperature value"
},
"unit": {
"type": "string",
"description": "Unit of measurement",
"enum": [
"celsius",
"fahrenheit"
]
}
},
"required": [
"location",
"temperature",
"unit"
],
"additionalProperties": false
}
}Optional Fields
Fields typed as Union{Nothing, T} are treated as optional:
- In
STANDARDmode they are omitted from the"required"array. - In
OPENAI/OPENAI_TOOLSmodes all fields remain required (per OpenAI spec), but optional fields use["type", "null"]to allow anullvalue.
struct Query
text::String
max_tokens::Union{Nothing, Int}
end
DescribedTypes.annotate(::Type{Query}) = Annotation(
name="Query",
description="A search query.",
parameters=Dict(
:text => Annotation(name="text", description="The query string"),
:max_tokens => Annotation(name="max_tokens", description="Optional token limit"),
),
)Standard schema (optional fields omitted from required):
print(JSON.json(schema(Query), 2)){
"type": "object",
"properties": {
"text": {
"type": "string"
},
"max_tokens": {
"type": "integer"
}
},
"required": [
"text"
]
}OpenAI schema (optional fields use ["type", "null"]):
print(JSON.json(schema(Query, llm_adapter=OPENAI), 2)){
"name": "Query",
"description": "A search query.",
"strict": true,
"schema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The query string"
},
"max_tokens": {
"type": [
"integer",
"null"
],
"description": "Optional token limit"
}
},
"required": [
"text",
"max_tokens"
],
"additionalProperties": false
}
}Enum Fields
There are two ways to represent enums:
1. Julia @enum types
Automatically serialised to their string representations:
@enum Color red green blue
struct Palette
primary::Color
end
print(JSON.json(schema(Palette), 2)){
"type": "object",
"properties": {
"primary": {
"type": "string",
"enum": [
"red",
"green",
"blue"
]
}
},
"required": [
"primary"
]
}You can also annotate the enum field with a description — the enum values are still inferred from the Julia type, so you don't need to repeat them:
DescribedTypes.annotate(::Type{Palette}) = Annotation(
name="Palette",
description="A color palette.",
parameters=Dict(
:primary => Annotation(name="primary", description="The primary color"),
),
)
print(JSON.json(schema(Palette, llm_adapter=OPENAI), 2)){
"name": "Palette",
"description": "A color palette.",
"strict": true,
"schema": {
"type": "object",
"properties": {
"primary": {
"type": "string",
"enum": [
"red",
"green",
"blue"
],
"description": "The primary color"
}
},
"required": [
"primary"
],
"additionalProperties": false
}
}2. String fields with enum annotations
Constrain allowed values via the enum keyword in Annotation:
struct Shirt
color::String
end
DescribedTypes.annotate(::Type{Shirt}) = Annotation(
name="Shirt",
description="A shirt.",
parameters=Dict(
:color => Annotation(name="color", description="Shirt color", enum=["red", "green", "blue"]),
),
)
print(JSON.json(schema(Shirt, llm_adapter=OPENAI), 2)){
"name": "Shirt",
"description": "A shirt.",
"strict": true,
"schema": {
"type": "object",
"properties": {
"color": {
"type": "string",
"description": "Shirt color",
"enum": [
"red",
"green",
"blue"
]
}
},
"required": [
"color"
],
"additionalProperties": false
}
}The enum keyword in annotations only takes effect in OpenAI modes (OPENAI / OPENAI_TOOLS). In STANDARD mode it is ignored.
You can also use symbols in the annotation enum values:
DescribedTypes.annotate(::Type{Shirt}) = Annotation(
name="Shirt",
description="A shirt.",
parameters=Dict(
:color => Annotation(name="color", description="Shirt color", enum=[:red, :green, :blue]),
),
)
print(JSON.json(schema(Shirt, llm_adapter=OPENAI), 2)){
"name": "Shirt",
"description": "A shirt.",
"strict": true,
"schema": {
"type": "object",
"properties": {
"color": {
"type": "string",
"description": "Shirt color",
"enum": [
"red",
"green",
"blue"
]
}
},
"required": [
"color"
],
"additionalProperties": false
}
}Duplicate handling is configurable when generating schema:
# default: :dedupe
schema(Shirt, llm_adapter=OPENAI)
# strict: error on duplicates after normalization
# schema(Shirt, llm_adapter=OPENAI, enum_duplicate_policy=:error)JSON.Object{String, Any} with 4 entries:
"name" => "Shirt"
"description" => "A shirt."
"strict" => true
"schema" => Object{String, Any}("type"=>"object", "properties"=>Object{S…Nested Types
Nested structs are expanded inline by default:
struct Address
street::String
city::String
end
struct Person
name::String
address::Address
end
print(JSON.json(schema(Person), 2)){
"type": "object",
"properties": {
"name": {
"type": "string"
},
"address": {
"type": "object",
"properties": {
"street": {
"type": "string"
},
"city": {
"type": "string"
}
},
"required": [
"street",
"city"
]
}
},
"required": [
"name",
"address"
]
}Schema References
For deeply nested or repeated types, pass use_references=true to factor shared types into $defs and reference them via $ref:
print(JSON.json(schema(Person, use_references=true), 2)){
"type": "object",
"properties": {
"name": {
"type": "string"
},
"address": {
"$ref": "#/$defs/Main.Address",
"description": "Semantic of address in the context of the schema"
}
},
"required": [
"name",
"address"
],
"$defs": {
"Main.Address": {
"type": "object",
"properties": {
"street": {
"type": "string"
},
"city": {
"type": "string"
}
},
"required": [
"street",
"city"
]
}
}
}Custom Dict Type
By default schemas use JSON.Object (preserves insertion order). You can switch to Dict if order doesn't matter:
d = schema(Person, dict_type=Dict)
typeof(d)Dict{String, Any}Function Method Schemas
DescribedTypes can also extract function-method signatures and generate tool-ready JSON schemas.
"""
Weather lookup helper.
"""
function weather(city::String, days::Int=3; unit::String="celsius", include_humidity::Bool=false)
return (; city, days, unit, include_humidity)
end
sig = extractsignature(weather)
sig.name, length(sig.args)(:weather, 4)You can customize method/argument metadata with MethodAnnotation and ArgAnnotation:
DescribedTypes.annotate(::typeof(weather), ms::MethodSignature) = MethodAnnotation(
name=:weather_tool,
description="Weather lookup tool.",
argsannot=Dict(
:city => ArgAnnotation(name=:city, description="City name", required=true),
:days => ArgAnnotation(name=:days, description="Forecast horizon", required=false),
:unit => ArgAnnotation(name=:unit, description="Temperature unit", enum=["celsius", "fahrenheit"], required=false),
:include_humidity => ArgAnnotation(name=:include_humidity, description="Include humidity signal", required=false),
),
)Generate an OpenAI tool schema:
print(JSON.json(schema(weather, llm_adapter=OPENAI_TOOLS), 2)){
"type": "function",
"name": "weather_tool",
"description": "Weather lookup tool.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
},
"days": {
"type": [
"integer",
"null"
],
"description": "Forecast horizon"
},
"unit": {
"type": [
"string",
"null"
],
"description": "Temperature unit",
"enum": [
"celsius",
"fahrenheit"
]
},
"include_humidity": {
"type": [
"boolean",
"null"
],
"description": "Include humidity signal"
}
},
"required": [
"city",
"days",
"unit",
"include_humidity"
],
"additionalProperties": false
}
}Method Selection (selector)
For functions with multiple methods, pick the exact method using selector. You can pass an index, a Method, or a selector function.
function weather_multi(city::String)
return (; city, mode="quick")
end
function weather_multi(city::String, days::Int)
return (; city, days, mode="full")
end
selected = first(methods(weather_multi, (String, Int)))
schema_selected = schema(weather_multi, selector=selected, llm_adapter=OPENAI_TOOLS)
print(JSON.json(schema_selected, 2)){
"type": "function",
"name": "weather_multi",
"description": "Semantic of weather_multi in the context of function calling",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Semantic of city in the context of weather_multi"
},
"days": {
"type": [
"integer",
"null"
],
"description": "Semantic of days in the context of weather_multi"
}
},
"required": [
"city",
"days"
],
"additionalProperties": false
}
}Explicit Method Annotation
You can pass method_annotation directly instead of defining annotate(::typeof(fn), ::MethodSignature).
manual_annot = MethodAnnotation(
name=:weather_manual,
description="Weather tool with explicit annotation argument.",
argsannot=Dict(
:city => ArgAnnotation(name=:city, description="City", required=true),
:days => ArgAnnotation(name=:days, description="Days", required=false),
:unit => ArgAnnotation(name=:unit, description="Unit", enum=["celsius", "fahrenheit"], required=false),
:include_humidity => ArgAnnotation(name=:include_humidity, description="Humidity toggle", required=false),
),
)
print(JSON.json(schema(weather, method_annotation=manual_annot, llm_adapter=OPENAI_TOOLS), 2)){
"type": "function",
"name": "weather_manual",
"description": "Weather tool with explicit annotation argument.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City"
},
"days": {
"type": [
"integer",
"null"
],
"description": "Days"
},
"unit": {
"type": [
"string",
"null"
],
"description": "Unit",
"enum": [
"celsius",
"fahrenheit"
]
},
"include_humidity": {
"type": [
"boolean",
"null"
],
"description": "Humidity toggle"
}
},
"required": [
"city",
"days",
"unit",
"include_humidity"
],
"additionalProperties": false
}
}JSON → Function Calling
Use callfunction to validate/coerce JSON-style arguments and invoke the Julia function:
res1 = callfunction(weather, Dict("city" => "Paris"))
res2 = callfunction(weather, "{\"city\":\"Berlin\",\"days\":1,\"unit\":\"fahrenheit\"}")
(res1, res2)((city = "Paris", days = 3, unit = "celsius", include_humidity = false), (city = "Berlin", days = 1, unit = "fahrenheit", include_humidity = false))OpenAI-style wrapped payloads are also accepted:
callfunction(weather, Dict("arguments" => "{\"city\":\"Rome\",\"unit\":\"celsius\"}"))(city = "Rome", days = 3, unit = "celsius", include_humidity = false)You can combine this with method selection for overloaded functions:
callfunction(weather_multi, Dict("city" => "Paris", "days" => 2), selector=selected)(city = "Paris", days = 2, mode = "full")Current Function-Extraction Limits
- Varargs (
args...) and keyword splats (kwargs...) are currently rejected. - Runtime fallback extraction (used when source is not available through
CodeTracking) may not fully recover required-keyword semantics. - In OpenAI modes, optional/defaulted function args are emitted as required nullable fields (
["type", "null"]) to match strict-tool conventions.