Schema Basics
This guide covers the fundamentals of defining schemas in Amber 2.0.
Schema Definition
A schema is a class that inherits from Amber::Schema::Definition:
class CreateUserSchema < Amber::Schema::Definition
content_type "application/json"
field :email, String, required: true, format: :email
field :name, String, required: true
field :age, Int32, min: 18
validates_to UserRequest, UserValidationError
end
Field Types
Basic Types
field :name, String # String field
field :age, Int32 # Integer field
field :price, Float64 # Float field
field :active, Bool # Boolean field
field :id, UUID # UUID field
field :created_at, Time # Time field
Collections
field :tags, Array(String) # Array of strings
field :scores, Array(Int32) # Array of integers
field :metadata, Hash(String, String) # Hash/dictionary
Nested Objects
field :address, AddressSchema # Single nested object
field :addresses, Array(AddressSchema) # Array of nested objects
Field Options
Required Fields
field :email, String, required: true # Must be present
field :nickname, String? # Optional (can be nil)
field :bio, String # Optional by default
Default Values
field :role, String, default: "user"
field :active, Bool, default: true
field :page, Int32, default: 1
Field Aliases
Map different input names to your field:
field :email, String, as: "user_email" # JSON: {"user_email": "..."}
field :full_name, String, as: "fullName" # CamelCase input
Normalization
Transform values before validation:
field :email, String,
normalize: ->(s : String) { s.downcase.strip }
field :phone, String,
normalize: ->(s : String) { s.gsub(/\D/, "") }
field :tags, Array(String),
normalize: ->(tags : Array(String)) { tags.map(&.downcase).uniq }
Parameter Sources
Specify where parameters come from:
class SearchSchema < Amber::Schema::Definition
# From URL query string: ?q=search&page=1
from_query do
field :q, String, as: :query
field :page, Int32, default: 1
field :per_page, Int32, default: 20
end
# From URL path: /categories/:category_id/products
from_path do
field :category_id, Int32
end
# From HTTP headers
from_header do
field :api_key, String, key: "X-API-Key"
field :version, String, key: "X-API-Version", default: "v1"
end
# From request body
from_body do
field :filters, SearchFilters
end
validates_to SearchRequest, SearchValidationError
end
Nested Schemas
Create reusable schemas for nested objects:
class AddressSchema < Amber::Schema::Definition
field :street, String, required: true
field :city, String, required: true
field :state, String, required: true, length: 2
field :zip, String, required: true, format: /^\d{5}(-\d{4})?$/
validates_to Address, AddressValidationError
end
class UserSchema < Amber::Schema::Definition
field :name, String, required: true
field :email, String, required: true, format: :email
# Single nested object
field :primary_address, AddressSchema
# Array of nested objects
field :addresses, Array(AddressSchema), max_items: 5
validates_to User, UserValidationError
end
Schema Inheritance
Share common fields across schemas:
# Base schema with common fields
abstract class BaseUserSchema < Amber::Schema::Definition
field :email, String, required: true, format: :email
field :name, String, required: true
end
# Registration adds password
class RegistrationSchema < BaseUserSchema
field :password, String, required: true, min_length: 8
field :password_confirmation, String, required: true
field :terms_accepted, Bool, required: true
validate :password_matches
validates_to NewUser, RegistrationError
end
# Update doesn't require password
class UpdateUserSchema < BaseUserSchema
field :bio, String, max_length: 500
field :avatar_url, String, format: :url
validates_to UserUpdate, UpdateError
end
Type Coercion
The schema system automatically converts string inputs:
# Input: {"age": "25", "active": "true", "price": "19.99"}
class ProductSchema < Amber::Schema::Definition
field :age, Int32 # "25" -> 25
field :active, Bool # "true" -> true
field :price, Float64 # "19.99" -> 19.99
end
Custom Coercion
class DateRangeSchema < Amber::Schema::Definition
field :start_date, Time,
coerce: ->(s : String) { Time.parse(s, "%Y-%m-%d", Time::Location::UTC) }
field :status, Status,
coerce: ->(s : String) { Status.parse(s) }
end
State-Based Types
Schemas validate to specific success and failure types:
# Success type - immutable, validated data
class UserRequest < Amber::Schema::ValidatedRequest
getter email : String
getter name : String
getter age : Int32
# Computed properties
def adult? : Bool
age >= 18
end
end
# Failure type - contains validation errors
class UserValidationError < Amber::Schema::ValidationError
def to_response
{
message: "User validation failed",
errors: errors,
fields: errors.keys
}
end
end
Transformations
Apply transformations after validation:
class RegistrationSchema < Amber::Schema::Definition
field :first_name, String, required: true
field :last_name, String, required: true
field :email, String, required: true
# Add computed fields after validation
transform do |data|
data.full_name = "#{data.first_name} #{data.last_name}"
data.username = data.email.split("@").first
end
validates_to Registration, RegistrationError
end
Documentation Metadata
Add documentation for API generation:
class APISchema < Amber::Schema::Definition
description "Creates a new user account"
field :email, String,
required: true,
format: :email,
description: "User's email address",
example: "[email protected]"
field :role, String,
enum: ["admin", "user", "guest"],
default: "user",
description: "User's role in the system"
end