ExAgent.OutputSchema (ExAgent v2.2.2)

View Source

Structured output via Ecto schemas and changesets.

Given any module defining an embedded_schema (plus a changeset/2), this module:

  • json_schema/1 derives the JSON Schema the model is told to produce, by walking the schema's fields and types (scalars, enums, arrays, embedded schemas, datetimes).
  • validate/2 builds a changeset from the model's data, applies the schema's validations, and returns either {:ok, struct} or {:error, errors} — feeding the errors back to the model for a retry.

The convention is the standard Ecto one: a changeset(struct, attrs) callback. Its declared required fields are authoritative, including an empty list. Optional fields accept omission or null through an anyOf branch, keeping reflected non-null constraints intact. Optional embeds_many is the Ecto exception: it accepts omission or an array, not null. Required fields are not nullable. These rules apply recursively to embedded schemas. Schemas without a changeset keep the default that casts and requires all fields.

Reflection inspects a changeset built with empty attributes; it cannot fully describe arbitrary custom or conditional validations. validate/2 always runs the actual changeset and remains the final authority. Provider support for this JSON Schema (including anyOf and strict-mode restrictions) must be checked against the actual backend.

Reflected inclusion/exclusion values retain their JSON types. Ecto.Enum schemas advertise the declared storage values with their integer or string type, and inclusion/exclusion constraints map enum members to that same vocabulary. Plain atom enums still use their string names, including boolean-named members. Array lengths use minItems/maxItems, while string lengths use minLength/maxLength, including exact (:is) bounds. Length bounds intersect across options and validation calls; contradictory bounds remain unsatisfiable, as in the changeset. JSON string lengths count Unicode codepoints. Ecto's default grapheme counting and optional byte counting can differ; use count: :codepoints when exact alignment is required. The real changeset remains authoritative for those count modes.

Example

defmodule WeatherReport do
  use Ecto.Schema

  embedded_schema do
    field :city, :string
    field :temp_c, :float
    field :condition, Ecto.Enum, values: [:sunny, :rainy, :cloudy]
  end

  def changeset(schema, attrs) do
    schema
    |> Ecto.Changeset.cast(attrs, [:city, :temp_c, :condition])
    |> Ecto.Changeset.validate_required([:city, :temp_c])
    |> Ecto.Changeset.validate_number(:temp_c, greater_than: -100, less_than: 100)
  end
end

ExAgent.OutputSchema.json_schema(WeatherReport)
# => %{type: "object", properties: %{city: %{type: "string"}, ...}, required: [...]}

ExAgent.OutputSchema.validate(WeatherReport, %{"city" => "Madrid", "temp_c" => 22.0})
# => {:ok, %WeatherReport{city: "Madrid", temp_c: 22.0, ...}}

Summary

Functions

Derive a JSON Schema from an Ecto schema module.

Validate data (a map, typically decoded from the model's JSON) against an Ecto schema, returning {:ok, struct} or {:error, [error_map]}.

Functions

json_schema(mod)

@spec json_schema(module()) :: map()

Derive a JSON Schema from an Ecto schema module.

validate(mod, data)

@spec validate(module(), map()) :: {:ok, struct()} | {:error, [map()]}

Validate data (a map, typically decoded from the model's JSON) against an Ecto schema, returning {:ok, struct} or {:error, [error_map]}.