Turns an Ecto embedded schema into a JSON Schema, and provider JSON back into
a struct.
There is no ExAgent-specific schema language. You describe the shape you want
with an ordinary Ecto.Schema and pass the module:
defmodule Invoice do
use Ecto.Schema
@primary_key false
embedded_schema do
field :total, :float
field :currency, Ecto.Enum, values: [:EUR, :USD, :GBP]
embeds_many :lines, Invoice.Line
end
end
{:ok, response} = ExAgent.chat(agent, "Extract the invoice", schema: Invoice)
response.structured #=> %Invoice{total: 128.4, currency: :EUR, lines: [...]}Ecto is an optional dependency of ExAgent, needed only for this.
The rules, and where they come from
Each was verified against a live API rather than inferred:
- Every field is required. OpenAI's strict mode rejects a schema whose
requiredlist omits any property, and Ecto has no field-levelrequiredanyway, so the two agree. Optional fields cannot be expressed. - A root list is wrapped. OpenAI refuses a schema whose root is
type: "array", so{:list, Invoice}becomes an object with a single"items"property, unwrapped again bycast/2. Callers never see it. additionalProperties: falseis required by OpenAI and rejected by Gemini, hence:dialect.- Primary keys are excluded.
embedded_schemacarries abinary_idprimary key by default, and a primary key is identity the application assigns, not a fact a model extracts. Asking for one invites a plausible hallucinated id.
Summary
Functions
Casts decoded provider JSON into the schema's struct.
Builds a JSON Schema from an Ecto embedded schema.
Types
Functions
@spec cast(schema(), term()) :: {:ok, struct() | [struct()]} | {:error, ExAgent.Error.t()}
Casts decoded provider JSON into the schema's struct.
Returns {:error, %ExAgent.Error{type: :invalid_response}} when the JSON does
not fit: a wrong type, a value outside an Ecto.Enum, or a missing field. A
missing field is an error rather than a nil because an endpoint that ignored
the schema would otherwise hand back a half-built struct with no signal.
@spec to_json_schema( schema(), keyword() ) :: {:ok, map()} | {:error, ExAgent.Error.t()}
Builds a JSON Schema from an Ecto embedded schema.
Options
:description- becomes the schema root's"description", which both OpenAI and Gemini pass to the model:dialect-:openai(default) emitsadditionalProperties: false;:geminiomits it, because Gemini rejects the key outright
Examples
{:ok, json} = ExAgent.Schema.to_json_schema(Invoice, description: "One invoice")
json["required"] #=> ["total", "currency", "issued_on", "lines"]
json["additionalProperties"] #=> false