Normandy.Schema
(normandy v2.2.0)
View Source
Provides a macro-based DSL for defining structured data schemas.
This module allows you to define structs with typed fields, default values, validation rules, and metadata. It's the foundation for defining agents, messages, and other structured data in Normandy.
Features
- Type-safe field definitions
- Nested schema support with inline JSON Schema generation
- JSON Schema composition (anyOf, oneOf, allOf)
- Conditional schemas (if/then/else)
- Virtual and computed fields
- Default value support
- Field-level validation with JSON Schema constraints
- Automatic struct generation
- Field redaction: a
redact: truefield is left out ofinspect/1, of the JSON anio_schemasends to the model (to_json/1) and ofNormandy.InvalidChangesetErrormessages. Do not redact a value the model needs to see. - JSON Schema export for LLM tool calling
Example
defmodule User do
use Normandy.Schema
schema do
field(:name, :string, required: true)
field(:age, :integer, default: 0)
field(:email, :string, required: true)
end
end
user = %User{name: "Alice", email: "alice@example.com"}Nested Schemas
Normandy supports nested schemas with full JSON Schema generation:
defmodule Address do
use Normandy.Schema
io_schema "Address information" do
field(:street, :string, description: "Street address", required: true)
field(:city, :string, description: "City name", required: true)
field(:postal_code, :string, description: "Postal code", pattern: "^[0-9]{5}$")
end
end
defmodule User do
use Normandy.Schema
io_schema "User profile" do
field(:name, :string, description: "Full name", required: true)
# Single nested schema
field(:address, Address, description: "Primary address", required: true)
# Array of nested schemas
field(:previous_addresses, {:array, Address}, description: "Previous addresses")
end
end
# Export as JSON Schema
schema = User.get_json_schema()
# Nested schemas are inlined with all constraints preservedJSON Schema Composition
Normandy supports JSON Schema composition using anyOf, oneOf, and allOf:
defmodule StringOrNumber do
use Normandy.Schema
schema do
# Field can be either string or number
field(:value, :any,
description: "String or number value",
one_of: [
%{type: :string, minLength: 1},
%{type: :number, minimum: 0}
]
)
end
endYou can also reference schema modules in composition:
defmodule EmailContact do
use Normandy.Schema
schema do
field(:email, :string, required: true)
end
end
defmodule PhoneContact do
use Normandy.Schema
schema do
field(:phone, :string, required: true)
end
end
defmodule Contact do
use Normandy.Schema
schema do
# Contact must have either email or phone
field(:contact_info, :map,
one_of: [EmailContact, PhoneContact]
)
end
endComposition options:
:any_of- Value must match at least one of the schemas:one_of- Value must match exactly one of the schemas:all_of- Value must match all of the schemas
Conditional Schemas
Normandy supports JSON Schema conditional validation using if, then, and else:
defmodule ConditionalSchema do
use Normandy.Schema
schema do
# If type is "premium", then price must be >= 100
field(:subscription, :map,
description: "Subscription details",
if_schema: %{properties: %{type: %{const: "premium"}}},
then_schema: %{properties: %{price: %{minimum: 100}}}
)
end
endYou can use if/then/else together:
field(:value, :any,
if_schema: %{type: :string},
then_schema: %{minLength: 5},
else_schema: %{minimum: 0}
)Conditional options:
:if_schema- Condition to check (required for conditionals):then_schema- Schema to apply if condition is true:else_schema- Schema to apply if condition is false
Virtual Fields
Virtual fields exist in the struct but are not included in JSON Schema by default. They can be used for computed values or runtime-only data:
defmodule Product do
use Normandy.Schema
# Must be public: the capture is called from outside the module.
def compute_total(%{price: price, tax_rate: rate}) do
price * (1 + rate)
end
schema do
field(:price, :float, required: true)
field(:tax_rate, :float, default: 0.1)
# Virtual field computed from other fields
field(:total_price, :float, virtual: true, compute: &__MODULE__.compute_total/1)
# Virtual field that is included in JSON Schema
field(:metadata, :map, virtual: true, include_in_json_schema: true)
end
endVirtual field options:
:virtual- Mark field as virtual (excluded from JSON Schema by default):compute- Function to compute the field value from the struct, applied byNormandy.Schema.compute/1and byNormandy.Validate.apply_changes/1:include_in_json_schema- Include virtual field in JSON Schema
Note: Virtual fields cannot be marked as :required since they are computed or runtime-only.
Field Options
All field types support the following options:
:description- Field description for JSON Schema:required- Mark field as required:default- Default value for the field:examples- Example values for documentation
String Constraints
:min_length- Minimum string length:max_length- Maximum string length:pattern- Regular expression pattern (string):format- String format (e.g., "email", "uri", "uuid"):enum- List of allowed values
Number Constraints
:minimum- Minimum value (inclusive):maximum- Maximum value (inclusive):exclusive_minimum- Minimum value (exclusive):exclusive_maximum- Maximum value (exclusive)
Array Constraints
:min_items- Minimum number of items:max_items- Maximum number of items:unique_items- Whether items must be unique
Summary
Functions
Fills every compute: field of a schema struct, in declaration order. Each
compute function receives the struct, including fields computed before it.
Normandy.Validate.apply_changes/1 calls this, so cast results are computed.
Defines a field name of type in a schema or io_schema block.
Types
Functions
Fills every compute: field of a schema struct, in declaration order. Each
compute function receives the struct, including fields computed before it.
Normandy.Validate.apply_changes/1 calls this, so cast results are computed.
Defines a field name of type in a schema or io_schema block.
type is a base type, {:array, type}, {:map, type}, a module that implements
Normandy.Type or Normandy.ParameterizedType, or a schema module (a nested schema);
it defaults to :string. The options are in the moduledoc's "Field Options" and
"Virtual Fields", plus :redact (leave the value out of inspect/1 and of the JSON sent
to the model) and :skip_default_validation (do not check :default against type).
The composition options :any_of, :one_of and :all_of and the conditional options
:if_schema, :then_schema and :else_schema are described in the moduledoc's "JSON
Schema Composition" and "Conditional Schemas".
Raises ArgumentError at compile time for an unknown type or option, an option value of
the wrong kind, a :default that is invalid for type, a field name already used, a
virtual: true field that is required: true, and a compute: on a field that is not
virtual: true.