Schematics - a declarative data validation library

Hi everyone,

I’ve just released Schematics, a modern data validation library for Crystal, and I wanted to share it here in case it’s useful to others.

Repository: github.com/qequ/schematics

Latest release: v0.3.0

## What is it?

Schematics is a data validation library inspired by Python’s Pydantic. It provides a declarative way to define data models with automatic validation, type safety, and JSON serialization — all with zero runtime overhead thanks to Crystal’s macros.

Whether you’re building APIs, validating user input, or working with JSON data, Schematics makes it easy to define your data structures once and get validation, serialization, and type safety for free.

While building Schematics, I discovered just how powerful Crystal’s macro system is for type validation, unlike runtime validation in most languages.

## Key Features

  • Pydantic-style Models – Declarative field definitions with automatic validation

  • Type Safe – Full compile-time type checking leveraging Crystal’s type system

  • Rich Validators – Built-in validators for strings (length, format, patterns) and numbers (ranges, comparisons)

  • Custom Validation – Override validate_model for complex cross-field validation logic

  • JSON Serialization – Automatic to_json and from_json methods

  • High Performance – ~2μs per validation using compile-time macros

  • Schema API – Alternative schema-based validation for simpler use cases

## Quick Example

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),
      Schematics.max_length(20),
      Schematics.matches(/^[a-zA-Z0-9_]+$/),
    ]

  field age, Int32?,
    validators: [
      Schematics.gte(0),
      Schematics.lte(120),
    ]

  field role, String,
    default: "user",
    validators: [Schematics.one_of(["admin", "user", "guest"])]

  # Custom validation for complex rules
  def validate_model
    if age_val = age
      if age_val < 18 && email.ends_with?(".gov")
        add_error(:email, "Government emails require age 18+")
      end
    end
  end
end

# Create and validate
user = User.new(
  email: "john@example.com",
  username: "john_doe",
  age: 25,
  role: "user"
)

user.valid?  # => true
user.errors  # => {}

# Automatic JSON serialization
json = user.to_json
# => {"email":"john@example.com","username":"john_doe","age":25,"role":"user"}

# Automatic JSON deserialization
user = User.from_json(json)

# Type-safe property access
email : String = user.email
age : Int32? = user.age

## Status

This is v0.3.0 with a stable API. The library is fully tested and ready for production use. The Model DSL is complete, and I’m looking to iterate based on community feedback.

Some ideas for future development:

  • Struct support in Model DSL
  • Type coercion
  • JSON Schema export
  • Async validation

Any thoughts, suggestions, or feedback are very welcome!

Thanks for reading!

1 Like

Have you considered relying more on the stdlib/normal Crystal constructs for some/most of the implementation? This could make it look more Crystal like and decoupled from your particular library. I was never a fan of APIs that are baked into the instance, like ActiveRecord from Ruby.

E.g. something like how some other validator libs handle this, which provides same functionality, but is basically no different than a normal Crystal class

class User
  include AVD::Validatable
  include JSON::Serializable

  @[Assert::NotBlank]
  @[Assert::Email]
  property email : String

  @[Assert::Length(3..20)]
  @[Assert::Regex(/^[a-zA-Z0-9_]+$/)]
  property username : String

  @[Assert::GreaterThanOrEqual(0)]
  @[Assert::LessThanOrEqual(120)]
  property age : Int32?

  @[Assert::Choice(["admin", "user", "guest"])]
  property role : String = "user"

  def initialize(@email : String, @username : String, @age : Int32? = nil, @role : String = "user"); end
end

user = User.new(
  email: "john@example.com",
  username: "john!doe",
  age: 25,
  role: "user"
)

errors = AVD.validator.validate user

# ...
5 Likes

Is there a crystal library that validates model instances against json schema files?

I think it’s quite popular to use json schema for validations and it works for both front-end and back-end.

You could use the following as a base GitHub - aarongodin/jsonschema: JSON Schema validation in Crystal