1.4.1

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