Normandy.Schema
(normandy v0.6.1)
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
- Metadata tracking
- Field redaction support
- 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
defp 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: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
Types
@type schema() :: %{ optional(atom()) => any(), __struct__: atom(), __meta__: Normandy.Metadata.t() }
@type t() :: schema()