Rbdantic

Ruby 数据验证与设置管理 - 一个受 Pydantic 启发的 Ruby 数据验证库。

Rbdantic 将 Pydantic 强大的数据验证能力引入 Ruby,提供运行时数据验证、序列化和 JSON Schema 生成,配合直观的 DSL 语法。

English Documentation

功能特性

  • 基础模型类 - 定义带有类型检查字段的数据模型
  • 字段约束 - 内置字符串、数字和数组约束
  • 自定义验证器 - 支持多种模式的字段级和模型级验证器
  • 类型强制转换 - 可配置严格程度的自动类型转换
  • 嵌套模型 - 支持嵌套模型验证
  • 模型继承 - 子类继承字段和验证器
  • 模型配置 - 灵活的配置选项(额外字段、冻结模型等)
  • 序列化 - 支持过滤选项的 Hash 或 JSON 转换
  • JSON Schema 生成 - 自动生成 API 文档所需的 JSON Schema
  • 详细错误报告 - 带位置路径的结构化验证错误

安装

添加到 Gemfile:

gem 'rbdantic'

或直接安装:

gem install rbdantic

快速入门

require 'rbdantic'

class User < Rbdantic::BaseModel
  field :name, String, min_length: 1, max_length: 100
  field :email, String, pattern: /\A[^@\s]+@[^@\s]+\z/
  field :age, Integer, gt: 0, le: 150
  field :tags, [String], default_factory: -> { [] }
end

# 创建有效用户
user = User.new(
  name: "Alice",
  email: "[email protected]",
  age: 30
)

puts user.name   # => "Alice"
puts user.age    # => 30
puts user.tags   # => []

# 序列化为 Hash
puts user.model_dump
# => { name: "Alice", email: "[email protected]", age: 30, tags: [] }

# to_h 是 model_dump 的别名
puts user.to_h
# => { name: "Alice", email: "[email protected]", age: 30, tags: [] }

# 序列化为 JSON
puts user.model_dump_json
# => {"name":"Alice","email":"[email protected]","age":30,"tags":[]}

# 验证错误
begin
  User.new(name: "", email: "invalid", age: -1)
rescue Rbdantic::ValidationError => e
  e.errors.each do |err|
    puts "#{err.loc.join('.')}: #{err.msg}"
  end
  # name: String must be at least 1 characters
  # email: String does not match pattern ...
  # age: Value must be greater than 0
end

字段定义

基本字段

class Product < Rbdantic::BaseModel
  field :id, Integer
  field :name, String
  field :price, Float
  field :active, Rbdantic::Boolean
end

默认值

class Config < Rbdantic::BaseModel
  # 静态默认值
  field :timeout, Integer, default: 30

  # 动态默认值(工厂)
  field :created_at, Time, default_factory: -> { Time.now }

  # 可选字段(可以为 nil)
  field :nickname, String, optional: true
end

字段约束

字符串约束

class User < Rbdantic::BaseModel
  field :username, String,
    min_length: 3,
    max_length: 20,
    pattern: /\A[a-zA-Z0-9_]+\z/
end

数字约束

class Product < Rbdantic::BaseModel
  field :price, Float,
    gt: 0,        # 大于
    le: 10000     # 小于或等于

  field :quantity, Integer,
    ge: 0,        # 大于或等于
    multiple_of: 1
end

数组约束

class Order < Rbdantic::BaseModel
  field :items, [String],
    min_items: 1,
    max_items: 100,
    unique_items: true

end

字段内自定义验证器

class User < Rbdantic::BaseModel
  # Proc 验证器,返回 false 表示失败
  field :email, String,
    validators: [->(v) { v.include?("@") || false }]

  # Proc 验证器,返回错误消息
  field :password, String,
    validators: [->(v) { v.length >= 8 ? nil : "密码长度至少8个字符" }]
end

模型配置

使用 model_config 配置模型行为:

class User < Rbdantic::BaseModel
  model_config(
    extra: :forbid,           # 拒绝额外字段
    frozen: true,             # 创建后不可变
    strict: true,             # 严格类型检查
    coerce_mode: :strict,     # 不进行类型转换
    validate_assignment: true # 字段赋值时验证
  )

  field :name, String
end

配置选项

选项 可选值 说明
extra :ignore, :forbid, :allow 如何处理未定义的额外字段
frozen true, false 初始化后冻结模型使其不可变
strict true, false 严格类型检查(不转换类型)
coerce_mode :strict, :coerce 启用/禁用类型强制转换
validate_assignment true, false 字段赋值时进行验证

额外字段行为

# 忽略额外字段(默认)
class ModelA < Rbdantic::BaseModel
  model_config extra: :ignore
  field :name, String
end
ModelA.new(name: "test", extra: "data")  # extra 字段被丢弃

# 禁止额外字段
class ModelB < Rbdantic::BaseModel
  model_config extra: :forbid
  field :name, String
end
ModelB.new(name: "test", extra: "data")  # 抛出 ValidationError

# 允许额外字段
class ModelC < Rbdantic::BaseModel
  model_config extra: :allow
  field :name, String
end
m = ModelC.new(name: "test", extra: "data")
m[:extra]  # => "data"

验证器

字段验证器

字段验证器在不同阶段运行:

class User < Rbdantic::BaseModel
  field :email, String

  # 验证前 - 可转换值
  field_validator :email, mode: :before do |value, ctx|
    value&.downcase
  end

  # 验证后 - 验证转换后的值
  field_validator :email, mode: :after do |value, ctx|
    raise "邮箱格式无效" unless value.include?("@")
    value
  end
end

验证器模式

模式 说明
:before 类型验证前运行,可转换值
:after 类型验证后运行,验证最终值
:plain 替代类型验证运行(跳过类型检查)
:wrap 所有其他验证器之后运行

模型验证器

模型验证器验证整个模型:

class Account < Rbdantic::BaseModel
  field :password, String
  field :confirm_password, String

  # 前置验证器 - 预处理输入数据
  model_validator mode: :before do |data|
    data[:password] = data[:password]&.strip
    data
  end

  # 后置验证器 - 验证模型状态
  model_validator mode: :after do |model|
    if model.password != model.confirm_password
      raise "密码不匹配"
    end
  end
end

嵌套模型

Rbdantic 像 Pydantic 一样支持嵌套模型,让你可以构建带有层级验证的复杂数据结构。

单层嵌套模型

class Address < Rbdantic::BaseModel
  field :street, String, min_length: 1
  field :city, String, min_length: 1
  field :zip_code, String, pattern: /\A\d{5}\z/
end

class User < Rbdantic::BaseModel
  field :name, String
  field :address, Address  # 嵌套模型类型
end

# 从哈希创建 - 嵌套模型自动验证
user = User.new(
  name: "Alice",
  address: {
    street: "123 Main St",
    city: "Boston",
    zip_code: "02134"
  }
)

puts user.address.class  # => Address
puts user.address.city   # => "Boston"

# 或传入已构建的嵌套模型实例
address = Address.new(street: "456 Oak Ave", city: "Cambridge", zip_code: "02139")
user = User.new(name: "Jane", address: address)

# 序列化 - 嵌套模型递归输出
user.model_dump
# => { name: "Jane", address: { street: "456 Oak Ave", city: "Cambridge", zip_code: "02139" } }

多层嵌套模型

可以任意深度嵌套模型:

class Country < Rbdantic::BaseModel
  field :code, String, pattern: /\A[A-Z]{2}\z/
  field :name, String
end

class City < Rbdantic::BaseModel
  field :name, String
  field :country, Country  # 嵌套中的嵌套
end

class Person < Rbdantic::BaseModel
  field :name, String
  field :birthplace, City  # 两层嵌套
end

# 创建多层嵌套结构
person = Person.new(
  name: "Alice",
  birthplace: {
    name: "Paris",
    country: {
      code: "FR",
      name: "France"
    }
  }
)

puts person.birthplace.country.code  # => "FR"

嵌套模型数组

使用 [Type] 简写验证嵌套模型数组:

class Item < Rbdantic::BaseModel
  field :name, String, min_length: 1
  field :quantity, Integer, gt: 0
  field :price, Float, ge: 0
end

class Order < Rbdantic::BaseModel
  field :order_id, String
  field :items, [Item], min_items: 1
end

# 创建包含多个商品的订单
order = Order.new(
  order_id: "ORD-001",
  items: [
    { name: "Widget", quantity: 5, price: 9.99 },
    { name: "Gadget", quantity: 2, price: 19.99 }
  ]
)

puts order.items[0].class  # => Item
puts order.items.length    # => 2

# 序列化 - 数组元素递归输出
order.model_dump
# => { order_id: "ORD-001", items: [{ name: "Widget", quantity: 5, price: 9.99 }, ...] }

可选嵌套模型

class Profile < Rbdantic::BaseModel
  field :bio, String
  field :avatar_url, String
end

class User < Rbdantic::BaseModel
  field :name, String
  field :profile, Profile, optional: true  # 可以是 nil
end

# 不带 profile
user = User.new(name: "Bob")
puts user.profile  # => nil

# 带 profile
user = User.new(name: "Bob", profile: { bio: "Developer", avatar_url: "..." })
puts user.profile.bio  # => "Developer"

嵌套模型验证错误

嵌套模型中的错误包含完整路径:

begin
  User.new(
    name: "Alice",
    address: {
      street: "",           # 无效: 太短
      city: "Boston",
      zip_code: "invalid"   # 无效: 模式不匹配
    }
  )
rescue Rbdantic::ValidationError => e
  e.errors.each do |err|
    puts "#{err.loc.join('.')} - #{err.msg}"
  end
  # address.street - String must be at least 1 characters
  # address.zip_code - String does not match pattern ...
end

# 多层嵌套错误路径
begin
  Person.new(
    name: "Bob",
    birthplace: {
      name: "London",
      country: { code: "invalid", name: "UK" }
    }
  )
rescue Rbdantic::ValidationError => e
  puts e.errors.first.loc  # => [:birthplace, :country, :code]
end

# 数组元素错误路径
begin
  Order.new(
    order_id: "ORD-001",
    items: [
      { name: "Widget", quantity: 5, price: 9.99 },
      { name: "", quantity: 0, price: -1 }  # 索引1处的无效元素
    ]
  )
rescue Rbdantic::ValidationError => e
  e.errors.each do |err|
    puts "#{err.loc.join('.')} - #{err.msg}"
  end
  # items.1.name - String must be at least 1 characters
  # items.1.quantity - Value must be greater than 0
  # items.1.price - Value must be greater than or equal to 0
end

自引用模型

模型可以引用自身实现递归结构:

class TreeNode < Rbdantic::BaseModel
  field :value, String
  field :children, [TreeNode], default_factory: -> { [] }
end

tree = TreeNode.new(
  value: "root",
  children: [
    { value: "child1", children: [{ value: "grandchild1" }] },
    { value: "child2" }
  ]
)

puts tree.children[0].children[0].value  # => "grandchild1"

继承

字段、验证器和配置均可继承:

class Animal < Rbdantic::BaseModel
  field :name, String
  field :age, Integer, gt: 0

  model_config extra: :ignore
end

class Dog < Animal
  field :breed, String  # 继承 name 和 age
end

class Cat < Animal
  model_config extra: :allow
end

注意: 子类会继承父类的 model_config,只需要覆盖想修改的配置项。

序列化

model_dump

将模型转换为 Hash,支持多种选项:

class User < Rbdantic::BaseModel
  field :name, String
  field :role, String, default: "user"
  field :active, Rbdantic::Boolean, default: true
end

user = User.new(name: "Alice")

# 完整输出
user.model_dump
# => { name: "Alice", role: "user", active: true }

# 排除默认值字段
user.model_dump(exclude_defaults: true)
# => { name: "Alice" }

# 只包含指定字段
user.model_dump(include: [:name])
# => { name: "Alice" }

# 排除指定字段
user.model_dump(exclude: [:active])
# => { name: "Alice", role: "user" }

# 排除未设置字段(初始化时未提供的)
user.model_dump(exclude_unset: true)
# => { name: "Alice" }

model_dump_json

转换为 JSON 字符串:

user.model_dump_json
# => {"name":"Alice","role":"user","active":true}

# 带缩进
user.model_dump_json(indent: 2)
# => {
#      "name": "Alice",
#      "role": "user",
#      "active": true
#    }

JSON Schema 生成

为 API 文档自动生成 JSON Schema:

class User < Rbdantic::BaseModel
  field :id, Integer, gt: 0
  field :name, String, min_length: 1, max_length: 100
  field :email, String, pattern: /\A[^@\s]+@[^@\s]+\z/
  field :age, Integer, optional: true, ge: 0, le: 150
end

schema = User.model_json_schema
# => {
#   "$schema": "https://json-schema.org/draft/2020-12/schema",
#   "type": "object",
#   "title": "User",
#   "properties": {
#     "id": { "type": "integer", "exclusiveMinimum": 0 },
#     "name": { "type": "string", "minLength": 1, "maxLength": 100 },
#     "email": { "type": "string", "pattern": "^[^@\\s]+@[^@\\s]+$" },
#     "age": { "type": ["integer", "null"], "minimum": 0, "maximum": 150 }
#   },
#   "required": ["id", "name", "email"]
# }

类型强制转换

当设置 coerce_mode: :coerce 时自动进行类型转换:

class Config < Rbdantic::BaseModel
  model_config coerce_mode: :coerce

  field :count, Integer
  field :price, Float
  field :enabled, Rbdantic::Boolean
end

config = Config.new(
  count: "42",       # 转换为 42
  price: "19.99",    # 转换为 19.99
  enabled: "yes"     # 转换为 true
)

config.count   # => 42 (Integer)
config.price   # => 19.99 (Float)
config.enabled # => true

支持的类型转换

目标类型 源示例
String 任何有 to_s 方法的值
Integer "42", 42.0
Float "3.14", 42
Rbdantic::Boolean "true", "yes", "on", "1", 1, "false", "no", "off", "0", 0
Array 可用 split 分割的字符串,任何有 to_a 方法的值
Hash 键值对数组,任何有 to_h 方法的值
Time ISO8601 字符串, Date, DateTime, Unix 时间戳 (Integer/Float)
Date ISO8601 字符串, Time, DateTime, 纪元天数 (Integer/Float)
DateTime ISO8601 字符串, Time, Date, Unix 时间戳 (Integer/Float)

验证错误

ValidationError 提供详细的错误信息:

begin
  User.new(name: "", age: -1)
rescue Rbdantic::ValidationError => e
  e.error_count  # => 2
  e.errors       # => ErrorDetail 数组
  e.as_json      # => { errors: [...], error_count: 2 }
  e.to_h         # => 同 as_json

  e.errors.each do |err|
    err.type   # => :string_too_short, :value_not_greater_than
    err.loc    # => [:name], [:age]  (位置路径)
    err.msg    # => "String must be at least..."
    err.input  # => ""  (原始输入值)
  end
end

支持的类型

类型 说明
String 内置字符串类型
Integer 内置整数类型
Float 内置浮点数类型
Rbdantic::Boolean 布尔字段,接受 true/false
Symbol Ruby 符号,最大长度 256 字符(防止 DoS 攻击)
[Type] 带元素校验的数组
Hash 键值哈希类型
Time Ruby Time 类型
Date Ruby Date 类型
DateTime Ruby DateTime 类型
Rbdantic::BaseModel 子类 嵌套模型验证

格式验证

内置常用格式的验证器:

class User < Rbdantic::BaseModel
  field :email, String, format: :email    # 基础邮箱验证
  field :website, String, format: :uri    # URI 验证 (http/https)
end
格式 模式
:email 基础邮箱检查 (user@domain)
:uri HTTP/HTTPS URI

复杂验证请使用自定义 pattern 正则或 field_validator

限制与安全

安全限制

限制 目的
Symbol 最大长度 256 字符 防止 Symbol DoS 攻击
嵌套模型深度 ~20 层 防止栈溢出

这些限制防止恶意输入耗尽内存或导致栈溢出。

线程安全

模型初始化后的读取操作是线程安全的。但需注意:

  • 初始化过程中的验证不是线程安全的(使用内部状态)
  • validate_assignment 模式使用实例级锁
  • 变更期间避免跨线程共享模型实例

与 Pydantic 的差异

功能 Pydantic Rbdantic
字段别名 Field(alias="name") alias_name: 配合 by_alias: true
计算字段 @computed_field 不支持
泛型模型 BaseModel[T] 不支持
序列化别名 serialization_alias 使用 alias_name: 与 dump/schema 的 by_alias:
模型复制/更新 model.copy(update={}) 提供 copy(deep:)update(**data) 辅助方法
判断联合类型 Annotated[Union, Field(discriminator)] 不支持
自定义类型适配器 TypeAdapter 使用验证器替代
布尔类型 bool Rbdantic::Boolean
配置类 BaseModelConfig model_config 哈希

API 命名差异

Pydantic Rbdantic
Field() field :name, Type, **options
@field_validator field_validator :name, mode: ...
@model_validator model_validator mode: ...
model_config = ConfigDict(...) model_config(...)
model_dump() model_dump()
model_dump_json() model_dump_json()
model_validate() Model.model_validate(data)

系统要求

  • Ruby >= 2.7(支持关键字参数和模式匹配)
  • 无外部依赖(纯 Ruby 实现)

错误处理最佳实践

捕获特定字段错误

begin
  User.new(name: "", email: "invalid")
rescue Rbdantic::ValidationError => e
  # 查找特定字段的错误
  name_errors = e.errors.select { |err| err.loc.first == :name }
  puts "名称错误: #{name_errors.map(&:msg).join(', ')}"

  # 按字段分组错误
  errors_by_field = e.errors.group_by { |err| err.loc.first }
  errors_by_field.each do |field, errs|
    puts "#{field}: #{errs.map(&:msg).join(', ')}"
  end
end

自定义错误消息

使用 field_validator 自定义消息:

class User < Rbdantic::BaseModel
  field :password, String

  field_validator :password, mode: :after do |value, ctx|
    if value.length < 8
      raise Rbdantic::ValidationError::ErrorDetail.new(
        type: :password_too_short,
        loc: [:password],
        msg: "密码至少需要8个字符(当前#{value.length}个)",
        input: value
      )
    end
    value
  end
end

API 错误 JSON 响应

rescue Rbdantic::ValidationError => e
  # 返回 JSON 用于 API 响应
  status 400
  json e.as_json
  # => { "errors": [...], "error_count": 2 }

API 参考

Rbdantic::BaseModel 类方法

方法 说明
field(name, type, **options) 定义字段及其类型和约束
model_config(**options) 配置模型行为
field_validator(name, mode:, &block) 定义字段级验证器
model_validator(mode:, &block) 定义模型级验证器
model_json_schema(**options) 生成 JSON Schema
model_fields 返回字段定义哈希
model_config 返回模型配置
inherited(subclass) 继承钩子(内部使用)

实例方法

方法 说明
initialize(data = {}) 创建并验证模型
model_dump(**options) 转换为 Hash
to_h model_dump 的别名
model_dump_json(indent: nil) 转换为 JSON 字符串
[name] 括号访问字段值
[name] = value 括号赋值字段值

字段选项

选项 类型 说明
default Any 静态默认值
default_factory Proc 动态默认值生成器
optional Boolean 允许 nil 值
required Boolean 设为 false 允许 nil(等同于 optional: true
validators Array 自定义验证器 Proc
alias_name Symbol 输入/输出的替代名称(配合 by_alias: true 使用)
format Symbol 内置格式验证器(:email:uri:uuid
min_length Integer 字符串最小长度
max_length Integer 字符串最大长度
pattern Regexp 字符串正则匹配
gt Numeric 大于
ge Numeric 大于或等于
lt Numeric 小于
le Numeric 小于或等于
multiple_of Numeric 必须是该数的倍数
min_items Integer 数组最小元素数
max_items Integer 数组最大元素数
unique_items Boolean 数组元素必须唯一

开发

检出仓库后:

bin/setup        # 安装依赖
rake spec        # 运行测试
bin/console      # 交互式提示
bundle exec rake install  # 本地安装 gem

贡献

欢迎提交 Bug 报告和 Pull Request。

许可证

本 gem 基于 MIT 许可证 开源。

致谢

本库受 Pydantic 启发 - 优秀的 Python 数据验证库。

开发说明

本库主要由 AI (Claude) 协助开发,展示了 AI 工具如何加速软件开发,同时保持代码质量和全面测试。