schematics
Version, currently 0.3.04 versions
- 0.5.0latestJan 11, 2026
- 0.4.0not indexedJan 11, 2026
- 0.3.0not indexedJan 11, 2026
- 0.2.0not indexedJan 11, 2026
github.com/qequ/schematics
a library to validate data using schemas
7 stars
0 dependents
License: MIT
Nothing has been indexed for 0.3.0 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.
Installation
# Add this to your shard.yml
dependencies:
schematics:
github: qequ/schematics
version: ~> 0.3.0Then run:
shards installshard.yml
No shard.yml has been indexed for 0.3.0. You can read it on the repository.
Dependencies
Unknown: the shard.yml for this version has not been read yet.
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
# Schematics
A modern data validation library for Crystal with rich error reporting and type safety, inspired by Python's Pydantic.
## Features
- **Pydantic-Style Models**: Declarative model definitions with automatic validation
- **Type Safe**: Leverages Crystal's compile-time type system
- **Rich Validators**: Built-in validators for common use cases (length, format, ranges, etc.)
- **Custom Validators**: Fluent API for adding constraints and rules
- **JSON Serialization**: Automatic `to_json` and `from_json` methods
- **High Performance**: ~2μs per validation with zero runtime overhead
- **Composable**: Build complex schemas from simple validators
## Installation
1. Add the dependency to your `shard.yml`:
```yaml
dependencies:
schematics:
github: qequ/schematics
```
2. Run `shards install`
## Quick Start
### Model DSL (Recommended)
Define Pydantic-style models with declarative field definitions:
```crystal
require "schematics"
class User < Schematics::Model
field email, String,
required: true,
validators: [
Schematics.min_length(5),
Schematics.format(/@/),
]
field username, String,
required: true,
validators: [Schematics.min_length(3)]
field age, Int32?,
validators: [
Schematics.gte(0),
Schematics.lte(120),
]
field role, String,
default: "user",
validators: [Schematics.one_of(["admin", "user", "guest"])]
end
# Create and validate
user = User.new(
email: "john@example.com",
username: "john_doe",
age: 25,
role: "user"
)
user.valid? # => true
user.errors # => {}
# JSON serialization
json = user.to_json
# => {"email":"john@example.com","username":"john_doe","age":25,"role":"user"}
# JSON deserialization
user = User.from_json(json)
```
For simpler use cases without models, see the [Schema-Based Validation](#schema-based-validation) section below.
## Model DSL
The Model DSL provides a Pydantic-style declarative approach to defining data models with automatic validation, JSON serialization, and type safety.
### Defining Models
```crystal
class Product < Schematics::Model
field name, String,
required: true,
validators: [
Schematics.min_length(1),
Schematics.max_length(100),
]
field price, Float64,
required: true,
validators: [Schematics.gt(0)]
field quantity, Int32,
default: 0,
validators: [Schematics.gte(0)]
field category, String,
validators: [Schematics.one_of(["electronics", "books", "clothing"])]
field tags, Array(String)?,
default: nil
end
```
### Built-in Validators
#### String Validators
```crystal
# Length constraints
Schematics.min_length(5) # Minimum 5 characters
Schematics.max_length(100) # Maximum 100 characters
# Pattern matching
Schematics.format(/@/) # Must contain '@'
Schematics.matches(/^[a-z]+$/) # Only lowercase letters
# Value constraints
Schematics.one_of(["admin", "user", "guest"]) # Must be one of these values
```
#### Numeric Validators
```crystal
# Comparison operators
Schematics.gte(0) # Greater than or equal to 0
Schematics.lte(120) # Less than or equal to 120
Schematics.gt(0) # Greater than 0
Schematics.lt(100) # Less than 100
# Range constraint
Schematics.range(1, 5) # Between 1 and 5 (inclusive)
```
### Field Options
```crystal
class User < Schematics::Model
# Required field (must be provided at initialization)
field email, String, required: true
# Optional/nilable field
field phone, String?
# Field with default value
field role, String, default: "user"
# Field with validators
field age, Int32,
validators: [Schematics.gte(0), Schematics.lte(120)]
# Field with type coercion (converts strings to target type from JSON)
field max_connections, Int32, coerce: true
# Combining options
field username, String,
required: true,
validators: [
Schematics.min_length(3),
Schematics.max_length(20),
Schematics.matches(/^[a-zA-Z0-9_]+$/),
]
end
```
### Custom Validation
Override the `validate_model` method for custom validation logic:
```crystal
class Account < Schematics::Model
field email, String, required: true
field age, Int32?
field account_type, String
def validate_model
# Custom cross-field validation
if age_val = age
if age_val < 18 && account_type == "premium"
add_error(:account_type, "Premium accounts require age 18+")
end
end
# Custom email domain validation
if email.ends_with?(".gov") && account_type != "government"
add_error(:email, "Government emails require government account type")
end
end
end
```
### Validation Methods
```crystal
user = User.new(email: "test@example.com", username: "john")
# Check if valid (returns Bool)
user.valid? # => true/false
# Get validation errors
user.errors # => Hash(Symbol, Array(String))
# Example: {:email => ["must contain @"], :age => ["must be >= 0"]}
# Validate and raise on error
user.validate! # Raises Schematics::ValidationError if invalid
```
### JSON Serialization
Models automatically get `to_json` and `from_json` methods:
```crystal
class Article < Schematics::Model
field title, String
field published, Bool
field views, Int32
end
# To JSON
article = Article.new(title: "Hello World", published: true, views: 100)
json = article.to_json
# => {"title":"Hello World","published":true,"views":100}
# From JSON
article = Article.from_json(json)
article.title # => "Hello World"
article.published # => true
article.views # => 100
```
### Type Safety
Models provide compile-time type checking:
```crystal
class Post < Schematics::Model
field title, String
field likes, Int32
field active, Bool
end
post = Post.new(title: "Hi", likes: 10, active: true)
# These are type-safe at compile time
title : String = post.title # ✓ OK
likes : Int32 = post.likes # ✓ OK
active : Bool = post.active # ✓ OK
# Property modification
post.title = "New Title"
post.likes = 20
```
### Working with Nilable Fields
```crystal
class Profile < Schematics::Model
field name, String, required: true
field bio, String? # Optional, defaults to nil
field age, Int32? # Optional, defaults to nil
field avatar, String? # Optional, defaults to nil
end
profile = Profile.new(name: "Alice", bio: nil, age: 25, avatar: nil)
# Safe access to nilable fields
if bio = profile.bio
puts "Bio: #{bio}"
else
puts "No bio"
end
# Or use try
profile.bio.try { |b| puts "Bio: #{b}" }
```
### Complete Example
```crystal
class BlogPost < Schematics::Model
field title, String,
required: true,
validators: [
Schematics.min_length(5),
Schematics.max_length(200),
]
field content, String,
required: true,
validators: [Schematics.min_length(10)]
field author, String,
required: true
field tags, Array(String)?,
default: nil
field status, String,
default: "draft",
validators: [Schematics.one_of(["draft", "published", "archived"])]
field views, Int32,
default: 0,
validators: [Schematics.gte(0)]
field published_at, String?
def validate_model
# Custom validation: published posts must have published_at
if status == "published" && published_at.nil?
add_error(:published_at, "Published posts must have a published date")
end
end
end
# Create a blog post
post = BlogPost.new(
title: "Getting Started with Crystal",
content: "Crystal is a statically typed language...",
author: "Alice",
tags: nil,
status: "draft",
views: 0,
published_at: nil
)
if post.valid?
puts "Post is valid!"
puts post.to_json
else
puts "Validation errors:"
post.errors.each do |field, messages|
puts " #{field}: #{messages.join(", ")}"
end
end
```
### Performance
The Model DSL uses compile-time macros for zero runtime overhead:
```crystal
# Validation performance
10_000.times do
user = User.new(email: "test@example.com", username: "test", age: 25, role: "user")
user.valid?
end
```
## Struct Support
For immutable value types, use `Schematics::Struct` instead of inheriting from `Model`:
```crystal
struct Point
include Schematics::Struct
field x, Float64, validators: [Schematics.gte(0.0)]
field y, Float64, validators: [Schematics.gte(0.0)]
end
struct ServerConfig
include Schematics::Struct
field host, String, required: true
field port, Int32, default: 8080, validators: [Schematics.range(1, 65535)]
field debug, Bool, default: false
end
# Usage is the same as Model
point = Point.new(x: 10.0, y: 20.0)
point.valid? # => true
point.x # => 10.0 (read-only)
config = ServerConfig.new(host: "localhost", port: 3000, debug: true)
config.to_json # => {"host":"localhost","port":3000,"debug":true}
```
### Custom Validation for Structs
Override `_collect_custom_errors` for custom validation logic:
```crystal
struct Rectangle
include Schematics::Struct
field width, Float64, validators: [Schematics.gt(0.0)]
field height, Float64, validators: [Schematics.gt(0.0)]
protected def _collect_custom_errors(errs : Hash(Symbol, Array(String)))
if width > height * 10
errs[:width] ||= [] of String
errs[:width] << "aspect ratio too extreme"
end
end
end
```
## Type Coercion
Schematics supports automatic type coercion when deserializing from JSON. Enable it per-field with `coerce: true`. By default, strict mode is used (no automatic coercion).
### Basic Usage
```crystal
class UserSettings < Schematics::Model
field username, String, required: true
field max_connections, Int32, coerce: true # "50" -> 50
field timeout, Float64, coerce: true # 60 -> 60.0
field debug_mode, Bool, coerce: true # "true" -> true
end
# JSON with string values gets coerced to proper types
json = %({"username": "john", "max_connections": "50", "timeout": 30, "debug_mode": "yes"})
settings = UserSettings.from_json(json)
settings.max_connections # => 50 (Int32)
settings.timeout # => 30.0 (Float64)
settings.debug_mode # => true (Bool)
```
### Supported Coercions
| Target Type | Accepted Source Types |
|-------------|----------------------|
| `Int32` | Int32, Int64 (in range), Float64 (whole numbers), String (numeric), Bool (0/1) |
| `Int64` | Int32, Int64, Float64 (whole numbers), String (numeric), Bool (0/1) |
| `Float64` | Float64, Int32, Int64, String (numeric) |
| `Bool` | Bool, Int (0/1), String (true/false/yes/no/on/off/1/0) |
| `String` | String, Int32, Int64, Float64, Bool |
### Boolean Coercion Values
The following string values are recognized for boolean coercion:
- **True**: `"true"`, `"yes"`, `"on"`, `"1"`, `"t"`, `"y"` (case-insensitive)
- **False**: `"false"`, `"no"`, `"off"`, `"0"`, `"f"`, `"n"` (case-insensitive)
### Strict Mode (Default)
Without `coerce: true`, fields require exact type matches:
```crystal
class StrictConfig < Schematics::Model
field name, String
field port, Int32 # No coercion
end
# This raises an error - port must be an actual integer
StrictConfig.from_json(%({"name": "app", "port": "8080"})) # Error!
# This works - port is an integer
StrictConfig.from_json(%({"name": "app", "port": 8080})) # OK
```
### Coercion with Structs
Type coercion also works with `Schematics::Struct`:
```crystal
struct Coordinate
include Schematics::Struct
field latitude, Float64, coerce: true
field longitude, Float64, coerce: true
end
coord = Coordinate.from_json(%({"latitude": "37.7749", "longitude": "-122.4194"}))
coord.latitude # => 37.7749 (Float64)
coord.longitude # => -122.4194 (Float64)
```
### Error Handling
When coercion fails, a descriptive error is raised:
```crystal
class Config < Schematics::Model
field port, Int32, coerce: true
end
Config.from_json(%({"port": "not a number"}))
# Raises: "Failed to coerce String to Int32 for field 'port'"
```
For nilable fields, failed coercion returns `nil` instead of raising:
```crystal
class OptionalConfig < Schematics::Model
field port, Int32?, coerce: true
end
config = OptionalConfig.from_json(%({"port": "not a number"}))
config.port # => nil
```
## Schema-Based Validation
For simpler use cases without models, use the schema API directly:
### Schema Types
```crystal
# String validation
schema = Schematics::Schema(String).new
schema.valid?("hello") # => true
schema.valid?(123) # => false
# Integer validation
schema = Schematics::Schema(Int32).new
schema.valid?(42) # => true
# Float validation
schema = Schematics::Schema(Float64).new
schema.valid?(3.14) # => true
```
### Arrays
```crystal
# Homogeneous arrays
schema = Schematics::Schema(Array(Int32)).new
schema.valid?([1, 2, 3]) # => true
schema.valid?([1, "two", 3]) # => false
# Nested arrays
schema = Schematics::Schema(Array(Array(String))).new
schema.valid?([["a", "b"], ["c", "d"]]) # => true
```
### Hashes
```crystal
# Simple hashes
schema = Schematics::Schema(Hash(String, Int32)).new
schema.valid?({"a" => 1, "b" => 2}) # => true
# Nested hashes
schema = Schematics::Schema(Hash(String, Hash(String, Int32))).new
schema.valid?({"a" => {"x" => 1}, "b" => {"y" => 2}}) # => true
# Hashes with arrays
schema = Schematics::Schema(Hash(String, Array(Int32))).new
schema.valid?({"numbers" => [1, 2, 3]}) # => true
```
### Custom Validators
Build schemas with constraints using the fluent `SchemaBuilder` API:
```crystal
# String with length constraints
email_schema = Schematics::SchemaBuilder(String).new
.min_length(5)
.max_length(100)
.add_validator("must contain @") { |s| s.includes?("@") }
.build
# Number with range constraints
age_schema = Schematics::SchemaBuilder(Int32).new
.min_value(18)
.max_value(120)
.build
# Array with size constraints
tags_schema = Schematics::SchemaBuilder(Array(String)).new
.min_size(1)
.max_size(10)
.build
```
### Rich Error Reporting
Get detailed information about validation failures:
```crystal
schema = Schematics::Schema(Array(Int32)).new
result = schema.validate([1, 2, "three", 4, "five"])
unless result.valid?
puts "Validation failed:"
result.errors.each do |error|
puts " Path: #{error.path}"
puts " Message: #{error.error_message}"
puts " Value: #{error.value}"
end
end
# Output:
# Path: root[2]
# Message: expected type Int32, got String
# Value: three
```
### Parse with Type Safety
The `parse` method returns typed values or raises on error:
```crystal
schema = Schematics::Schema(Int32).new
value = schema.parse(42) # Returns Int32
puts typeof(value) # => Int32
# Raises ValidationError on invalid data
begin
schema.parse("not a number")
rescue ex : Schematics::ValidationError
puts ex.message # => root: expected type Int32, got String
end
```
## Real-World Examples
### API Request Validation
```crystal
def validate_create_user(data)
name_schema = Schematics::SchemaBuilder(String).new
.min_length(2)
.max_length(50)
.build
email_schema = Schematics::SchemaBuilder(String).new
.min_length(5)
.add_validator("valid email") { |s| s.includes?("@") }
.build
age_schema = Schematics::SchemaBuilder(Int32).new
.min_value(18)
.build
errors = {} of String => String
unless name_schema.valid?(data["name"]?)
errors["name"] = "Invalid name"
end
unless email_schema.valid?(data["email"]?)
errors["email"] = "Invalid email"
end
unless age_schema.valid?(data["age"]?)
errors["age"] = "Must be 18 or older"
end
{valid: errors.empty?, errors: errors}
end
```
### Reusable Schemas
```crystal
module Schemas
EMAIL = Schematics::SchemaBuilder(String).new
.min_length(5)
.add_validator("must be valid email") { |s| s.includes?("@") }
.build
POSITIVE_INT = Schematics::SchemaBuilder(Int32).new
.min_value(1)
.build
USER_TAGS = Schematics::SchemaBuilder(Array(String)).new
.min_size(1)
.max_size(10)
.build
end
# Use throughout your application
Schemas::EMAIL.validate("user@example.com")
Schemas::POSITIVE_INT.validate(42)
```
## Roadmap
- [x] Basic type validation
- [x] Array and Hash validation
- [x] Custom validators
- [x] Rich error reporting
- [x] Min/max constraints
- [x] Model DSL (Pydantic-style classes)
- [x] JSON serialization/deserialization
- [x] Built-in validators (length, format, ranges, one_of)
- [x] Custom validation methods
- [x] Struct support (immutable value types)
- [x] Type coercion
- [ ] JSON Schema export
- [ ] Async validation
## Contributing
1. Fork it (<https://github.com/qequ/schematics/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
## Contributors
- [Alvaro Frias Garay](https://github.com/qequ) - creator and maintainer
## License
MIT License - see LICENSE file for details
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.3.0- Tagged
- Jan 11, 2026
- Commit
c13a2e7a541a- Indexed
- not yet
Dependents
No indexed shard depends on this one yet.
Repository
github.com/qequ/schematics
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 13, 2026
- Synced
- Aug 13, 2026
- Versions
- 4