This guide covers common schema patterns, validation techniques, and best practices for using ExOutlines schemas effectively.
Table of Contents
- Basic Schema Patterns
- String Validation Patterns
- Numeric Constraints
- Array Patterns
- Nested Object Patterns
- Union Types for Flexibility
- Enum Patterns
- Common Validation Scenarios
- Schema Composition
- Best Practices
Basic Schema Patterns
Required vs Optional Fields
alias ExOutlines.Spec.Schema
# All fields required
strict_schema = Schema.new(%{
name: %{type: :string, required: true},
email: %{type: :string, required: true},
age: %{type: :integer, required: true}
})
# Mix of required and optional
flexible_schema = Schema.new(%{
name: %{type: :string, required: true},
email: %{type: :string, required: true},
nickname: %{type: :string, required: false}, # Optional
bio: %{type: :string, required: false} # Optional
})Rule: Only mark fields as required if they are absolutely necessary. Optional fields provide flexibility for incomplete or varied data.
Field Descriptions
Field descriptions guide the LLM in generating appropriate content.
schema = Schema.new(%{
title: %{
type: :string,
required: true,
min_length: 5,
max_length: 100,
description: "A clear, concise title that summarizes the content"
},
summary: %{
type: :string,
required: true,
min_length: 50,
max_length: 300,
description: "A detailed summary providing context and key points"
}
})Best Practice: Write descriptions that explain purpose and constraints, not just field names.
String Validation Patterns
Length Constraints
# Username constraints
username_schema = Schema.new(%{
username: %{
type: :string,
required: true,
min_length: 3,
max_length: 20,
description: "Alphanumeric username"
}
})
# Tweet-like content
tweet_schema = Schema.new(%{
content: %{
type: :string,
required: true,
max_length: 280,
description: "Tweet content"
}
})
# Article with minimum content
article_schema = Schema.new(%{
body: %{
type: :string,
required: true,
min_length: 500,
max_length: 5000,
description: "Article body text"
}
})Pattern Matching with Regex
# Email validation
email_schema = Schema.new(%{
email: %{
type: :string,
required: true,
pattern: ~r/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
description: "Valid email address"
}
})
# Phone number (US format)
phone_schema = Schema.new(%{
phone: %{
type: :string,
required: true,
pattern: ~r/^\d{3}-\d{3}-\d{4}$/,
description: "Phone number in format XXX-XXX-XXXX"
}
})
# Product SKU
sku_schema = Schema.new(%{
sku: %{
type: :string,
required: true,
pattern: ~r/^[A-Z]{3}\d{6}$/,
description: "Product SKU (3 letters + 6 digits)"
}
})
# ISO Date
date_schema = Schema.new(%{
date: %{
type: :string,
required: true,
pattern: ~r/^\d{4}-\d{2}-\d{2}$/,
description: "Date in YYYY-MM-DD format"
}
})Built-in Format Validation
# Using format shortcuts
contact_schema = Schema.new(%{
email: %{type: :string, format: :email, required: true},
website: %{type: :string, format: :url, required: false},
id: %{type: :string, format: :uuid, required: true}
})Available formats: :email, :url, :uuid, :phone, :date
Numeric Constraints
Integer Ranges
# Age validation
age_schema = Schema.new(%{
age: %{
type: :integer,
required: true,
min: 0,
max: 120,
description: "Person's age in years"
}
})
# Quantity (positive only)
quantity_schema = Schema.new(%{
quantity: %{
type: :integer,
required: true,
min: 1,
max: 9999,
description: "Order quantity"
}
})
# Rating system
rating_schema = Schema.new(%{
rating: %{
type: :integer,
required: true,
min: 1,
max: 5,
description: "Star rating from 1 to 5"
}
})Float/Decimal Validation
# Price with reasonable bounds
price_schema = Schema.new(%{
price: %{
type: :number,
required: true,
min: 0.01,
max: 999999.99,
description: "Price in dollars"
}
})
# Temperature (Celsius)
temperature_schema = Schema.new(%{
temperature: %{
type: :number,
required: true,
min: -273.15, # Absolute zero
max: 5778, # Sun's surface temperature
description: "Temperature in Celsius"
}
})
# Percentage
percentage_schema = Schema.new(%{
confidence: %{
type: :number,
required: true,
min: 0,
max: 100,
description: "Confidence percentage"
}
})Array Patterns
Fixed-Length Arrays
# RGB color (exactly 3 values)
rgb_schema = Schema.new(%{
color: %{
type: {:array, %{type: :integer, min: 0, max: 255}},
required: true,
min_items: 3,
max_items: 3,
description: "RGB color values [red, green, blue]"
}
})Variable-Length Arrays
# Tags (1-10 items)
tags_schema = Schema.new(%{
tags: %{
type: {:array, %{type: :string, min_length: 2, max_length: 20}},
required: true,
min_items: 1,
max_items: 10,
description: "Content tags"
}
})
# Unlimited items (with item constraints)
comments_schema = Schema.new(%{
comments: %{
type: {:array, %{type: :string, max_length: 500}},
required: false,
description: "User comments"
}
})Unique Items
# Category selection (no duplicates)
categories_schema = Schema.new(%{
categories: %{
type: {:array, %{type: :string}},
required: true,
unique_items: true,
min_items: 1,
max_items: 5,
description: "Selected categories (no duplicates)"
}
})Arrays of Enums
# Multiple choice selection
skills_schema = Schema.new(%{
skills: %{
type: {:array, %{type: {:enum, ["elixir", "python", "rust", "go", "javascript"]}}},
required: true,
unique_items: true,
min_items: 1,
max_items: 5,
description: "Programming skills"
}
})Nested Object Patterns
Simple Nesting
# Address as nested object
address_schema = Schema.new(%{
street: %{type: :string, required: true},
city: %{type: :string, required: true},
state: %{type: :string, required: true, min_length: 2, max_length: 2},
zip_code: %{type: :string, required: true, pattern: ~r/^\d{5}$/}
})
user_schema = Schema.new(%{
name: %{type: :string, required: true},
email: %{type: :string, required: true, format: :email},
address: %{type: {:object, address_schema}, required: true}
})Deep Nesting
# Location -> Address -> User
location_schema = Schema.new(%{
latitude: %{type: :number, required: true, min: -90, max: 90},
longitude: %{type: :number, required: true, min: -180, max: 180}
})
full_address_schema = Schema.new(%{
street: %{type: :string, required: true},
city: %{type: :string, required: true},
state: %{type: :string, required: true},
zip_code: %{type: :string, required: true},
location: %{type: {:object, location_schema}, required: false}
})
complete_user_schema = Schema.new(%{
name: %{type: :string, required: true},
email: %{type: :string, required: true},
address: %{type: {:object, full_address_schema}, required: true}
})Access Pattern: Nested validation errors include full path (e.g., address.location.latitude).
Optional Nested Objects
profile_schema = Schema.new(%{
bio: %{type: :string, required: false, max_length: 500}
})
user_schema = Schema.new(%{
username: %{type: :string, required: true},
# Profile is optional - can be nil
profile: %{type: {:object, profile_schema}, required: false}
})Arrays of Objects
# Product reviews
review_schema = Schema.new(%{
rating: %{type: :integer, required: true, min: 1, max: 5},
comment: %{type: :string, required: true, min_length: 10, max_length: 500},
reviewer: %{type: :string, required: true}
})
product_schema = Schema.new(%{
name: %{type: :string, required: true},
reviews: %{
type: {:array, %{type: {:object, review_schema}}},
required: false,
max_items: 50
}
})Union Types for Flexibility
Basic Union Types
# ID can be string or integer
flexible_id_schema = Schema.new(%{
id: %{
type: {:union, [
%{type: :string, pattern: ~r/^[A-Z]{3}\d{6}$/},
%{type: :integer, positive: true}
]},
required: true,
description: "Product ID (string SKU or numeric ID)"
}
})Nullable Fields
# Middle name is optional - can be string or null
name_schema = Schema.new(%{
first_name: %{type: :string, required: true},
middle_name: %{
type: {:union, [
%{type: :string, max_length: 50},
%{type: :null}
]},
required: false,
description: "Middle name (optional)"
},
last_name: %{type: :string, required: true}
})Multiple Type Options
# Contact can be email or phone
contact_schema = Schema.new(%{
contact_method: %{
type: {:union, [
%{type: :string, format: :email},
%{type: :string, format: :phone}
]},
required: true,
description: "Email address or phone number"
}
})Complex Union Types
# Response can be success with data or error with message
response_schema = Schema.new(%{
status: %{type: {:enum, ["success", "error"]}, required: true},
result: %{
type: {:union, [
%{type: :string}, # Error message
%{type: :integer}, # Success result
%{type: :null} # No result
]},
required: true
}
})Enum Patterns
Simple Enums
# Status field
status_schema = Schema.new(%{
status: %{
type: {:enum, ["pending", "approved", "rejected"]},
required: true,
description: "Application status"
}
})
# Priority levels
priority_schema = Schema.new(%{
priority: %{
type: {:enum, ["low", "medium", "high", "critical"]},
required: true,
description: "Task priority"
}
})Enums with Descriptions
# Category taxonomy
category_schema = Schema.new(%{
category: %{
type: {:enum, [
"electronics",
"clothing",
"home",
"sports",
"toys",
"books"
]},
required: true,
description: """
Primary product category:
- electronics: Computers, phones, gadgets
- clothing: Apparel, footwear, accessories
- home: Furniture, appliances, decor
- sports: Equipment, fitness, outdoor
- toys: Games, educational, collectibles
- books: Physical and digital books
"""
}
})Multiple Enums
# Task management
task_schema = Schema.new(%{
status: %{
type: {:enum, ["todo", "in_progress", "done", "blocked"]},
required: true
},
priority: %{
type: {:enum, ["low", "medium", "high"]},
required: true
},
category: %{
type: {:enum, ["bug", "feature", "docs", "test"]},
required: true
}
})Common Validation Scenarios
User Registration
registration_schema = Schema.new(%{
username: %{
type: :string,
required: true,
min_length: 3,
max_length: 20,
pattern: ~r/^[a-zA-Z0-9_]+$/,
description: "Alphanumeric username with underscores"
},
email: %{
type: :string,
required: true,
format: :email,
description: "Valid email address"
},
password: %{
type: :string,
required: true,
min_length: 8,
description: "Password (minimum 8 characters)"
},
age: %{
type: :integer,
required: true,
min: 13,
max: 120,
description: "Age (must be 13 or older)"
}
})Product Catalog Entry
product_schema = Schema.new(%{
name: %{
type: :string,
required: true,
min_length: 3,
max_length: 100,
description: "Product name"
},
sku: %{
type: :string,
required: true,
pattern: ~r/^[A-Z]{3}\d{6}$/,
description: "Stock keeping unit (SKU)"
},
price: %{
type: :number,
required: true,
min: 0.01,
description: "Price in USD"
},
category: %{
type: {:enum, ["electronics", "clothing", "home", "sports"]},
required: true
},
tags: %{
type: {:array, %{type: :string, min_length: 2, max_length: 20}},
required: false,
unique_items: true,
max_items: 10
},
in_stock: %{
type: :boolean,
required: true,
description: "Whether product is currently in stock"
}
})Blog Post Metadata
blog_post_schema = Schema.new(%{
title: %{
type: :string,
required: true,
min_length: 10,
max_length: 100,
description: "Post title"
},
slug: %{
type: :string,
required: true,
pattern: ~r/^[a-z0-9-]+$/,
description: "URL-safe slug"
},
excerpt: %{
type: :string,
required: true,
min_length: 50,
max_length: 300,
description: "Brief summary for previews"
},
content: %{
type: :string,
required: true,
min_length: 500,
description: "Full post content"
},
published_date: %{
type: :string,
required: true,
pattern: ~r/^\d{4}-\d{2}-\d{2}$/,
description: "Publication date (YYYY-MM-DD)"
},
tags: %{
type: {:array, %{type: :string}},
required: true,
min_items: 1,
max_items: 5,
unique_items: true
},
author: %{
type: :string,
required: true,
description: "Author name"
}
})API Error Response
error_schema = Schema.new(%{
error_code: %{
type: :string,
required: true,
pattern: ~r/^[A-Z_]+$/,
description: "Error code (uppercase with underscores)"
},
message: %{
type: :string,
required: true,
min_length: 10,
max_length: 200,
description: "Human-readable error message"
},
details: %{
type: {:union, [
%{type: :string},
%{type: :null}
]},
required: false,
description: "Additional error details"
},
timestamp: %{
type: :string,
required: true,
description: "ISO 8601 timestamp"
}
})Schema Composition
Building Complex Schemas from Simple Ones
# Define reusable schemas
name_schema = Schema.new(%{
first_name: %{type: :string, required: true},
last_name: %{type: :string, required: true}
})
contact_schema = Schema.new(%{
email: %{type: :string, required: true, format: :email},
phone: %{type: :string, required: false, format: :phone}
})
address_schema = Schema.new(%{
street: %{type: :string, required: true},
city: %{type: :string, required: true},
state: %{type: :string, required: true},
zip: %{type: :string, required: true}
})
# Compose into complete schema
customer_schema = Schema.new(%{
name: %{type: {:object, name_schema}, required: true},
contact: %{type: {:object, contact_schema}, required: true},
billing_address: %{type: {:object, address_schema}, required: true},
shipping_address: %{type: {:object, address_schema}, required: false}
})Reusable Patterns Module
defmodule MyApp.Schemas do
alias ExOutlines.Spec.Schema
def email_field do
%{type: :string, required: true, format: :email}
end
def phone_field do
%{type: :string, required: false, format: :phone}
end
def username_field do
%{
type: :string,
required: true,
min_length: 3,
max_length: 20,
pattern: ~r/^[a-zA-Z0-9_]+$/
}
end
def date_field do
%{
type: :string,
required: true,
pattern: ~r/^\d{4}-\d{2}-\d{2}$/
}
end
def tags_field(max_items \\ 10) do
%{
type: {:array, %{type: :string, min_length: 2, max_length: 20}},
required: false,
unique_items: true,
max_items: max_items
}
end
# Use in schemas
def user_schema do
Schema.new(%{
username: username_field(),
email: email_field(),
phone: phone_field()
})
end
endBest Practices
1. Start Simple, Add Constraints Gradually
# Start with basic schema
basic = Schema.new(%{
title: %{type: :string, required: true}
})
# Add constraints as needed
constrained = Schema.new(%{
title: %{
type: :string,
required: true,
min_length: 5,
max_length: 100,
description: "Article title"
}
})2. Use Descriptive Field Names
# Good - clear and specific
good_schema = Schema.new(%{
user_email: %{type: :string, format: :email, required: true},
registration_date: %{type: :string, pattern: ~r/^\d{4}-\d{2}-\d{2}$/}
})
# Avoid - too generic
bad_schema = Schema.new(%{
data: %{type: :string},
value: %{type: :string}
})3. Provide Meaningful Descriptions
# Good - explains purpose and format
schema = Schema.new(%{
api_key: %{
type: :string,
required: true,
pattern: ~r/^sk-[a-zA-Z0-9]{48}$/,
description: "OpenAI API key starting with 'sk-' followed by 48 alphanumeric characters"
}
})4. Balance Constraints with Flexibility
# Too strict - may cause unnecessary retries
too_strict = Schema.new(%{
comment: %{
type: :string,
required: true,
min_length: 50,
max_length: 50 # Exactly 50 characters - too rigid
}
})
# Better - allows reasonable range
better = Schema.new(%{
comment: %{
type: :string,
required: true,
min_length: 20,
max_length: 500 # Flexible range
}
})5. Use Enums for Known Sets
# Good - predefined categories
good = Schema.new(%{
status: %{type: {:enum, ["draft", "published", "archived"]}, required: true}
})
# Avoid - free text for categorical data
avoid = Schema.new(%{
status: %{type: :string, required: true} # Could be any string
})6. Validate Format Early
# Use regex/format to prevent invalid data
validated = Schema.new(%{
email: %{type: :string, format: :email, required: true},
phone: %{type: :string, format: :phone, required: false},
url: %{type: :string, format: :url, required: false}
})7. Consider Optional Fields for Graceful Degradation
# Required core fields, optional metadata
schema = Schema.new(%{
# Core data (required)
name: %{type: :string, required: true},
email: %{type: :string, required: true},
# Metadata (optional - system can function without these)
bio: %{type: :string, required: false},
avatar_url: %{type: :string, required: false},
preferences: %{type: {:object, preferences_schema}, required: false}
})8. Document Complex Schemas
defmodule MyApp.OrderSchema do
@moduledoc """
Schema for e-commerce order processing.
Required fields:
- order_id: Unique identifier
- customer: Nested customer object
- items: Array of order items (minimum 1)
- total: Order total in USD
Optional fields:
- discount_code: Promotional code
- notes: Customer notes
"""
alias ExOutlines.Spec.Schema
def schema do
Schema.new(%{
order_id: %{type: :string, required: true, format: :uuid},
customer: %{type: {:object, customer_schema()}, required: true},
items: %{
type: {:array, %{type: {:object, item_schema()}}},
required: true,
min_items: 1
},
total: %{type: :number, required: true, min: 0.01},
discount_code: %{type: :string, required: false},
notes: %{type: :string, required: false, max_length: 500}
})
end
defp customer_schema do
# Customer schema definition
end
defp item_schema do
# Item schema definition
end
endNext Steps
- Read the Core Concepts guide for understanding validation mechanics
- Explore Error Handling guide for dealing with validation failures
- See Phoenix Integration guide for using schemas in web applications
- Review production examples in the
examples/directory
Further Reading
- JSON Schema Specification
- Regular Expressions in Elixir
- Ecto Schema Adapter for database integration