Schema Validation
Familia supports optional JSON Schema validation for model data. Schemas are defined in external JSON files and can be shared with frontend validation, API documentation generators, and other tools.
Overview
- External schemas: Define schemas as JSON files, not Ruby code
- Shared validation: Same schema validates Ruby models, REST APIs, frontend forms
- Opt-in per model: Enable with
feature :schema_validation - Migration support: Validate data before/after transforms
Setup
1. Install json_schemer
Add to your Gemfile:
gem 'json_schemer', '~> 2.0'
2. Create Schema Files
Create JSON Schema files (draft 2020-12 recommended):
your_app/
schemas/
customer.json
session.json
user.json
3. Configure Familia
# config/initializers/familia.rb (Rails)
# or at application boot
Familia.configure do |config|
# Option 1: Convention-based (recommended)
# Files named {class_name.underscore}.json
config.schema_path = 'schemas'
# Option 2: Explicit mapping
config.schemas = {
'Customer' => 'schemas/customer.json',
'UserSession' => 'schemas/user_session.json'
}
# Option 3: Disable validation
config.schema_validator = :none
end
4. Enable on Models
class Customer < Familia::Horreum
feature :schema_validation
identifier_field :custid
field :custid
field :email
field :name
field :status
end
Usage
Model Validation
customer = Customer.new(custid: 'c1', email: 'test@example.com', name: 'Test')
# Check validity
customer.valid_against_schema? # => true
# Get validation errors
customer.schema_validation_errors # => []
# Validate or raise exception
customer.validate_against_schema! # => true (or raises SchemaValidationError)
Invalid Data Example
customer = Customer.new(custid: 'c2', email: 'not-an-email')
customer.valid_against_schema? # => false
customer.schema_validation_errors
# => [{"type"=>"format", "data_pointer"=>"/email", ...}]
customer.validate_against_schema!
# raises Familia::SchemaValidationError
Migration Validation
Validate data during migrations to catch corruption:
class NormalizeEmails < Familia::Migration::Model
self.migration_id = '20260131_normalize_emails'
# Enable validation hooks
def validate_before_transform?
true
end
def validate_after_transform?
true
end
def process_record(customer, key)
customer.email = customer.email.downcase
for_realsies_this_time? { customer.save }
end
end
Or validate manually:
class DataCleanup < Familia::Migration::Base
def migrate
# Validate before changes
validate_schema!(obj, context: 'before cleanup')
# Make changes
obj.status = 'active'
# Validate after changes
validate_schema!(obj, context: 'after cleanup')
obj.save
end
end
Schema File Naming
Convention-based (when using schema_path):
| Class Name | Schema File |
|---|---|
Customer |
customer.json |
UserSession |
user_session.json |
APIToken |
api_token.json |
Explicit mapping overrides convention:
config.schemas = {
'Customer' => 'schemas/v2/customer.json',
'LegacyUser' => 'schemas/legacy/user.json'
}
Graceful Degradation
- No schema file: Validation returns
true(no-op) - No json_schemer gem: Warning logged, validation disabled
- Invalid JSON: Warning logged, schema skipped
API Reference
Class Methods (on models with feature)
| Method | Returns | Description |
|---|---|---|
schema |
Hash or nil |
The parsed JSON schema |
schema_defined? |
Boolean |
Whether a schema exists |
Instance Methods (on models with feature)
| Method | Returns | Description |
|---|---|---|
schema |
Hash or nil |
The schema for this class |
valid_against_schema? |
Boolean |
True if valid or no schema |
schema_validation_errors |
Array |
List of error hashes |
validate_against_schema! |
true |
Returns true or raises |
Migration Methods
| Method | Description |
|---|---|
validate_schema(obj, context:) |
Returns {valid:, errors:} |
validate_schema!(obj, context:) |
Raises on failure |
schema_validation_enabled? |
Check if enabled |
skip_schema_validation! |
Disable for this migration |
See Also
- JSON Schema Specification
- json_schemer gem
- Example schemas in
examples/schemas/