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.
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}