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 STANDARD mode they are omitted from the "required" array.
  • In OPENAI / OPENAI_TOOLS modes all fields remain required (per OpenAI spec), but optional fields use ["type", "null"] to allow a null value.
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
  }
}
Note

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}