Normandy.Schema (normandy v0.6.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
  • 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 preserved

JSON 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
end

You 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
end

Composition 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
end

You 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
end

Virtual 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

schema()

@type schema() :: %{
  optional(atom()) => any(),
  __struct__: atom(),
  __meta__: Normandy.Metadata.t()
}

t()

@type t() :: schema()

Functions

field(name, type \\ :string, opts \\ [])

(macro)