The Ecto Schema Adapter allows you to reuse existing Ecto schemas with ExOutlines, eliminating the need for duplicate schema definitions.
Overview
When you already have Ecto schemas defined for your database models, you can automatically convert them to ExOutlines schemas using ExOutlines.Ecto.from_ecto_schema/2. This provides:
- Zero duplication - Reuse existing schema definitions
- Automatic validation extraction - Pull validation rules from changesets
- Type safety - Leverage Ecto's type system
- Seamless integration - Works with all ExOutlines features
Basic Usage
Simple Conversion
defmodule MyApp.User do
use Ecto.Schema
schema "users" do
field :email, :string
field :age, :integer
field :active, :boolean
end
end
# Convert to ExOutlines schema
alias ExOutlines.Ecto
schema = Ecto.from_ecto_schema(MyApp.User)
# Use with generate/2
ExOutlines.generate(schema,
backend: MyBackend,
backend_opts: [...]
)With Changeset Validations
The adapter can extract validation rules from your changeset functions:
defmodule MyApp.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :username, :string
field :age, :integer
field :bio, :string
end
def changeset(user, params) do
user
|> cast(params, [:email, :username, :age, :bio])
|> validate_required([:email, :username])
|> validate_format(:email, ~r/@/)
|> validate_length(:username, min: 3, max: 20)
|> validate_number(:age, greater_than: 0, less_than: 150)
|> validate_length(:bio, max: 500)
end
end
# Conversion automatically extracts validation rules
schema = Ecto.from_ecto_schema(MyApp.User)
# Resulting schema has:
# - email: required, pattern: ~r/@/
# - username: required, min_length: 3, max_length: 20
# - age: min: 1, max: 149
# - bio: max_length: 500Supported Ecto Types
The adapter maps Ecto types to ExOutlines types:
| Ecto Type | ExOutlines Type | Notes |
|---|---|---|
:string | :string | Direct mapping |
:integer | :integer | Direct mapping |
:boolean | :boolean | Direct mapping |
:float, :decimal | :number | Numeric types |
{:array, type} | {:array, spec} | Arrays of any type |
Ecto.Enum | {:enum, values} | Enum values extracted |
| Embedded schema | {:object, schema} | Nested objects |
| Custom types | :string | Fallback with description |
Advanced Features
Custom Changeset Functions
Specify a different changeset function:
defmodule User do
def registration_changeset(user, params) do
# Different validation rules
end
end
schema = Ecto.from_ecto_schema(User,
changeset: :registration_changeset
)Explicit Required Fields
Override required field detection:
schema = Ecto.from_ecto_schema(User,
required: [:email, :username, :age]
)Field Descriptions
Add descriptions for better LLM guidance:
schema = Ecto.from_ecto_schema(User,
descriptions: %{
email: "User's email address for authentication",
username: "Unique username for display (3-20 characters)",
age: "User's age in years (must be 0-150)",
bio: "Short biography (max 500 characters)"
}
)Embedded Schemas
The adapter handles embedded schemas automatically:
defmodule User do
use Ecto.Schema
defmodule Address do
use Ecto.Schema
embedded_schema do
field :street, :string
field :city, :string
field :zip, :string
end
end
schema "users" do
field :name, :string
embeds_one :address, Address
embeds_many :phone_numbers, PhoneNumber
end
end
# Conversion includes nested schemas
schema = Ecto.from_ecto_schema(User)
# address field becomes {:object, nested_schema}
# phone_numbers becomes {:array, {:object, nested_schema}}Ecto.Enum Support
Enum fields are converted to ExOutlines enum types:
defmodule User do
use Ecto.Schema
schema "users" do
field :role, Ecto.Enum, values: [:admin, :user, :guest]
field :status, Ecto.Enum, values: [:active, :inactive]
end
end
schema = Ecto.from_ecto_schema(User)
# role becomes {:enum, [:admin, :user, :guest]}
# status becomes {:enum, [:active, :inactive]}Integration Patterns
Pattern 1: Direct Generation
defmodule MyApp.AIService do
alias ExOutlines.{Ecto, Backend.Anthropic}
def generate_user(prompt) do
schema = Ecto.from_ecto_schema(MyApp.User)
ExOutlines.generate(schema,
backend: Anthropic,
backend_opts: [
api_key: System.get_env("ANTHROPIC_API_KEY")
],
prompt: prompt
)
end
endPattern 2: Generation + Database Insert
defmodule MyApp.UserService do
alias MyApp.{User, Repo}
alias ExOutlines.Ecto
def create_from_ai(prompt) do
schema = Ecto.from_ecto_schema(User)
with {:ok, user_data} <- ExOutlines.generate(schema, ...),
{:ok, user} <- create_user(user_data) do
{:ok, user}
end
end
defp create_user(attrs) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
endPattern 3: Cached Schema Conversion
defmodule MyApp.Schemas do
alias ExOutlines.Ecto
def user_schema do
# Cache the converted schema
:persistent_term.get({__MODULE__, :user}, fn ->
schema = Ecto.from_ecto_schema(MyApp.User)
:persistent_term.put({__MODULE__, :user}, schema)
schema
end)
end
endValidation Extraction
The adapter extracts these validations from changesets:
Required Fields
|> validate_required([:email, :name])
# → fields marked as required: trueLength Constraints
|> validate_length(:username, min: 3, max: 20)
# → min_length: 3, max_length: 20
|> validate_length(:bio, max: 500)
# → max_length: 500Number Constraints
|> validate_number(:age, greater_than: 0)
# → min: 1
|> validate_number(:age, greater_than_or_equal_to: 0)
# → min: 0
|> validate_number(:age, less_than: 150)
# → max: 149
|> validate_number(:age, less_than_or_equal_to: 150)
# → max: 150Format Constraints
|> validate_format(:email, ~r/@/)
# → pattern: ~r/@/Inclusion (Enum) Constraints
|> validate_inclusion(:status, ["active", "inactive"])
# → type: {:enum, ["active", "inactive"]}Limitations
Not Extracted
The following validations are NOT automatically extracted:
- Custom validations
- Database-level constraints (unique, foreign keys)
validate_acceptance/3validate_confirmation/3validate_exclusion/3validate_subset/3- Complex conditional validations
For these cases, either:
- Add explicit constraints to the converted schema
- Use changeset validation for database operations
- Define a separate ExOutlines schema with full control
Schema Definition Requirements
- Must be an Ecto schema (with
use Ecto.Schema) - Changeset function must accept 2 arguments:
(struct, params) - Validations must be in the main changeset function
Best Practices
1. Use Descriptive Field Names
# Good
field :email_address, :string
# Less clear
field :em, :string2. Add Descriptions for LLM Guidance
schema = Ecto.from_ecto_schema(User,
descriptions: %{
age: "Age in years (must be positive, typically 0-120)"
}
)3. Keep Changesets Simple
For best extraction results, keep validation logic straightforward:
# Good - clear validation rules
def changeset(user, params) do
user
|> cast(params, [:email, :age])
|> validate_required([:email])
|> validate_number(:age, greater_than: 0)
end
# Complex - harder to extract
def changeset(user, params) do
if user.admin? do
# conditional validation
end
end4. Cache Schema Conversion
Convert schemas once and reuse:
@user_schema Ecto.from_ecto_schema(User)
def generate_user do
ExOutlines.generate(@user_schema, ...)
end5. Combine with Manual Adjustments
base_schema = Ecto.from_ecto_schema(User)
# Add custom constraints not in changeset
enhanced_schema = %{base_schema |
fields: Map.update!(base_schema.fields, :bio, fn spec ->
Map.put(spec, :min_length, 10)
end)
}Troubleshooting
Schema Not Recognized
Error: ArgumentError: module is not an Ecto schema
Solution: Ensure the module uses Ecto.Schema:
defmodule User do
use Ecto.Schema # Required
schema "users" do
# ...
end
endValidations Not Extracted
Issue: Converted schema doesn't include expected validations
Solutions:
- Check changeset function is named correctly (default:
:changeset) - Specify custom function:
changeset: :custom_changeset - Ensure validations are in the main changeset function
- Add explicit required:
required: [:field1, :field2]
Type Conversion Issues
Issue: Ecto type not supported
Solution: The adapter falls back to :string with a description. You can:
- Cast to a supported type in Ecto
- Manually adjust the converted schema
- Define a custom ExOutlines schema for that field
Examples
See examples/ecto_schema_adapter.exs for a complete working example.