Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 328 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ A modern data validation library for Crystal with rich error reporting and type

## 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
Expand All @@ -22,26 +26,335 @@ A modern data validation library for Crystal with rich error reporting and type

## Quick Start

### Model DSL (Recommended)

Define Pydantic-style models with declarative field definitions:

```crystal
require "schematics"

# Basic type validation
schema = Schematics::Schema(String).new
result = schema.validate("hello")
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)]

# 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

if result.valid?
puts "Valid!"
# 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
result.errors.each { |error| puts error }
puts "No bio"
end

# Or use the boolean shortcut
schema.valid?("hello") # => true
# Or use try
profile.bio.try { |b| puts "Bio: #{b}" }
```

## Usage Examples
### 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
```

## Schema-Based Validation

For simpler use cases without models, use the schema API directly:

### Basic Types
### Schema Types

```crystal
# String validation
Expand Down Expand Up @@ -221,7 +534,11 @@ Schemas::POSITIVE_INT.validate(42)
- [x] Custom validators
- [x] Rich error reporting
- [x] Min/max constraints
- [ ] Model DSL (Pydantic-style classes)
- [x] Model DSL (Pydantic-style classes)
- [x] JSON serialization/deserialization
- [x] Built-in validators (length, format, ranges, one_of)
- [x] Custom validation methods
- [ ] Struct support in Model DSL
- [ ] Type coercion
- [ ] JSON Schema export
- [ ] Async validation
Expand Down
2 changes: 1 addition & 1 deletion shard.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: schematics
description: a library to validate data using schemas
version: 0.2.0
version: 0.3.0

authors:
- Alvaro Frias Garay <alvarofriasgaray@gmail.com>
Expand Down
Loading