dry-schema
Rubyにおける高度なデータ検証と型強制を提供する宣言的スキーマライブラリ
概要
dry-schemaは、Rubyにおけるデータ検証と型強制を提供する宣言的スキーマライブラリです。dry-rbエコシステムの一部として、関数型プログラミングの原則に基づいて設計され、型安全性と表現力豊かなバリデーションルールを提供します。
主な特徴
- 宣言的スキーマ定義: DSLを使用した直感的なスキーマ定義
- 自動型強制: 文字列から数値、日付などへの自動変換
- ネストした構造サポート: ハッシュや配列のネスト構造の検証
- カスタム述語: 独自の検証ルールの追加
- 国際化対応: エラーメッセージの多言語サポート
- 拡張性: プラグインシステムによる機能拡張
インストール
Gemfileに追加
gem 'dry-schema'
bundlerを使用してインストール
bundle install
単体でインストール
gem install dry-schema
基本的な使用方法
基本的なスキーマ定義
require 'dry-schema'
schema = Dry::Schema.Params do
required(:email).filled(:string)
required(:age).filled(:integer, gt?: 18)
end
# 検証の実行
result = schema.call(email: '[email protected]', age: 21)
puts result.success? # => true
puts result.to_h # => {:email=>"jane@doe.org", :age=>21}
エラーハンドリング
schema = Dry::Schema.Params do
required(:email).filled(:string)
required(:age).filled(:integer, gt?: 18)
end
result = schema.call(email: '', age: 17)
puts result.success? # => false
puts result.errors.to_h
# => {:email=>["must be filled"], :age=>["must be greater than 18"]}
# フルエラーメッセージ
puts result.errors(full: true).to_h
# => {:email=>["email must be filled"], :age=>["age must be greater than 18"]}
基本的なマクロ
value マクロ
Dry::Schema.Params do
# 年齢は整数で18より大きい必要がある
required(:age).value(:integer, gt?: 18)
# IDは文字列または整数
required(:id).value([:string, :integer])
end
filled マクロ
Dry::Schema.Params do
# 文字列で空でない必要がある
required(:name).filled(:string)
# 配列で空でない必要がある
required(:tags).filled(:array)
end
maybe マクロ
Dry::Schema.Params do
# nilまたは整数
required(:age).maybe(:integer)
# nilまたは文字列の配列
required(:tags).maybe(array[:string])
end
ネストした構造の検証
ハッシュの検証
schema = Dry::Schema.Params do
required(:address).hash do
required(:street).filled(:string)
required(:city).filled(:string)
required(:country).hash do
required(:name).filled(:string)
required(:code).filled(:string)
end
end
end
result = schema.call(
address: {
street: '123 Main St',
city: 'NYC',
country: { name: 'USA', code: 'US' }
}
)
puts result.success? # => true
配列の検証
# プリミティブ値の配列
schema = Dry::Schema.Params do
required(:tags).array(:string)
end
# ハッシュの配列
schema = Dry::Schema.Params do
required(:users).array(:hash) do
required(:name).filled(:string)
required(:age).filled(:integer, gteq?: 18)
end
end
# サイズ制約付きの配列
schema = Dry::Schema.Params do
required(:numbers).value(array[:integer], size?: 3)
end
オプションのネスト構造
schema = Dry::Schema.Params do
required(:address).maybe(:hash) do
required(:street).filled(:string)
required(:city).filled(:string)
end
end
# nilも有効
puts schema.call(address: nil).success? # => true
型強制
Paramsスキーマ(文字列から変換)
schema = Dry::Schema.Params do
required(:age).filled(:integer)
required(:active).filled(:bool)
required(:tags).maybe(:array)
end
result = schema.call(
'age' => '25',
'active' => 'true',
'tags' => ''
)
puts result.to_h
# => {:age=>25, :active=>true, :tags=>nil}
JSONスキーマ
schema = Dry::Schema.JSON do
required(:email).filled(:string)
required(:age).value(:integer, gt?: 18)
end
result = schema.call(
'email' => '[email protected]',
'age' => 21
)
カスタム述語
カスタム述語モジュールの定義
module MyPredicates
include Dry::Logic::Predicates
def self.future_date?(date)
date > Date.today
end
def self.email_format?(value)
value.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
end
カスタム述語の使用
schema = Dry::Schema.Params do
config.predicates = MyPredicates
required(:email).value(:string, :email_format?)
required(:release_date).value(:date, :future_date?)
end
高度な機能
スキーマの再利用
AddressSchema = Dry::Schema.Params do
required(:street).filled(:string)
required(:city).filled(:string)
required(:zipcode).filled(:string)
end
UserSchema = Dry::Schema.Params do
required(:name).filled(:string)
required(:email).filled(:string)
required(:address).hash(AddressSchema)
end
スキーマの合成
# AND演算子 - 両方のスキーマが成功する必要がある
Role = Dry::Schema.JSON do
required(:id).filled(:string)
end
Expirable = Dry::Schema.JSON do
required(:expires_on).value(:date)
end
UserSchema = Dry::Schema.JSON do
required(:name).filled(:string)
required(:role).hash(Role & Expirable)
end
# OR演算子 - いずれかのスキーマが成功すればよい
RoleID = Dry::Schema.JSON do
required(:id).filled(:string)
end
RoleTitle = Dry::Schema.JSON do
required(:title).filled(:string)
end
UserSchema = Dry::Schema.JSON do
required(:name).filled(:string)
required(:role).hash(RoleID | RoleTitle)
end
フィルタリング
schema = Dry::Schema.Params do
required(:email).filled
required(:birthday).filter(format?: /\d{4}-\d{2}-\d{2}/).value(:date)
end
# 無効なフォーマット
result = schema.call(
'email' => '[email protected]',
'birthday' => '1981-1-1'
)
puts result.errors.to_h
# => {:birthday=>["is in invalid format"]}
# 有効なフォーマット
result = schema.call(
'email' => '[email protected]',
'birthday' => '1981-01-01'
)
puts result.to_h
# => {:email=>"jane@doe.org", :birthday=>#<Date: 1981-01-01>}
エラーメッセージのカスタマイズ
カスタムエラーメッセージの読み込み
schema = Dry::Schema.Params do
config.messages.load_paths << '/path/to/my/errors.yml'
required(:email).filled(:string)
required(:age).filled(:integer, gt?: 18)
end
YAML設定例
ja:
dry_schema:
errors:
filled?: "入力が必要です"
rules:
email:
filled?: "メールアドレスを入力してください"
age:
gt?: "%{num}より大きな値を入力してください"
名前空間の設定
schema = Dry::Schema.Params do
config.messages.namespace = :user
config.messages.top_namespace = :validation_schema
end
拡張機能
Monad拡張
require 'dry/schema'
require 'dry/monads'
Dry::Schema.load_extensions(:monads)
schema = Dry::Schema.Params do
required(:name).filled(:string, size?: 2..4)
end
# Success/Failureモナドとして結果を取得
monad_result = schema.call(name: 'Jane').to_monad
# => Dry::Monads::Success(#<Dry::Schema::Result{:name=>"Jane"} errors={}>)
# パターンマッチングとの組み合わせ
case schema.call(input).to_monad
in Success(name:)
"Hello #{name}"
in Failure(name:)
"#{name} is not a valid name"
end
Hints拡張
Dry::Schema.load_extensions(:hints)
schema = Dry::Schema.Params do
required(:email).filled(:string)
required(:age).filled(:integer, gt?: 18)
end
result = schema.call(email: '[email protected]', age: '')
# エラー
puts result.errors.to_h
# => {:age=>["must be filled"]}
# ヒント(評価されなかった述語)
puts result.hints.to_h
# => {:age=>["must be greater than 18"]}
# エラーとヒントの組み合わせ
puts result.messages.to_h
# => {:age=>["must be filled", "must be greater than 18"]}
JSON Schema変換
Dry::Schema.load_extensions(:json_schema)
schema = Dry::Schema.JSON do
required(:email).filled(:str?, min_size?: 8)
optional(:favorite_color).filled(:str?, included_in?: %w[red green blue])
optional(:age).filled(:int?)
end
json_schema = schema.json_schema
puts json_schema
# => {
# "type": "object",
# "properties": {
# "email": {
# "type": "string",
# "minLength": 8
# },
# "favorite_color": {
# "type": "string",
# "enum": ["red", "green", "blue"]
# },
# "age": {
# "type": "integer"
# }
# },
# "required": ["email"]
# }
dry-validationとの統合
require 'dry/validation'
# dry-schemaで基本的な型検証とデータ変換
UserContract = Dry::Validation.Contract do
# スキーマの定義
params do
required(:email).filled(:string)
required(:age).filled(:integer)
end
# カスタムルール
rule(:email) do
unless value.include?('@')
key.failure('must be a valid email')
end
end
rule(:age) do
key.failure('must be at least 18') if value < 18
end
end
result = UserContract.new.call(
email: '[email protected]',
age: 21
)
puts result.success? # => true
puts result.to_h # => {:email=>"jane@doe.org", :age=>21}
実践的な例
APIパラメータの検証
class UserRegistrationSchema
Schema = Dry::Schema.Params do
required(:user).hash do
required(:email).filled(:string)
required(:password).filled(:string, min_size?: 8)
required(:password_confirmation).filled(:string)
required(:profile).hash do
required(:first_name).filled(:string)
required(:last_name).filled(:string)
optional(:bio).filled(:string, max_size?: 500)
optional(:website).filled(:string)
end
optional(:preferences).hash do
optional(:newsletter).filled(:bool)
optional(:theme).filled(:string, included_in?: %w[light dark])
end
end
end
def self.call(params)
Schema.call(params)
end
end
# 使用例
params = {
user: {
email: '[email protected]',
password: 'securepassword',
password_confirmation: 'securepassword',
profile: {
first_name: 'Jane',
last_name: 'Doe',
bio: 'Software developer'
},
preferences: {
newsletter: true,
theme: 'dark'
}
}
}
result = UserRegistrationSchema.call(params)
puts result.success? # => true
設定ファイルの検証
ConfigSchema = Dry::Schema.JSON do
required(:database).hash do
required(:host).filled(:string)
required(:port).filled(:integer, gteq?: 1, lteq?: 65535)
required(:name).filled(:string)
optional(:pool_size).filled(:integer, gteq?: 1)
end
required(:redis).hash do
required(:url).filled(:string)
optional(:timeout).filled(:integer, gteq?: 1)
end
optional(:logging).hash do
optional(:level).filled(:string, included_in?: %w[debug info warn error])
optional(:output).filled(:string)
end
end
# JSON設定ファイルの検証
config = JSON.parse(File.read('config.json'))
result = ConfigSchema.call(config)
if result.success?
puts "設定は有効です"
app_config = result.to_h
else
puts "設定エラー:"
result.errors.to_h.each do |key, errors|
puts " #{key}: #{errors.join(', ')}"
end
end
ベストプラクティス
1. 適切なスキーマタイプの選択
# HTTPパラメータ(文字列)の場合
Dry::Schema.Params do
# 自動的に文字列から変換される
end
# JSONデータの場合
Dry::Schema.JSON do
# JSONの型がそのまま使用される
end
# 純粋なRubyオブジェクトの場合
Dry::Schema.define do
# 型強制なし
end
2. スキーマの分割と再利用
# 共通スキーマを分離
module CommonSchemas
Address = Dry::Schema.Params do
required(:street).filled(:string)
required(:city).filled(:string)
required(:postal_code).filled(:string)
end
Contact = Dry::Schema.Params do
required(:email).filled(:string)
optional(:phone).filled(:string)
end
end
# 再利用
UserSchema = Dry::Schema.Params do
required(:name).filled(:string)
required(:address).hash(CommonSchemas::Address)
required(:contact).hash(CommonSchemas::Contact)
end
3. エラーハンドリングの改善
def validate_user_input(params)
result = UserSchema.call(params)
if result.success?
# 成功時の処理
process_user_data(result.to_h)
else
# エラー時の処理
handle_validation_errors(result.errors)
end
end
def handle_validation_errors(errors)
formatted_errors = errors.to_h.map do |field, messages|
"#{field}: #{messages.join(', ')}"
end
raise ValidationError, formatted_errors.join('; ')
end
まとめ
dry-schemaは、Rubyアプリケーションにおける堅牢なデータ検証システムを構築するための強力なツールです。宣言的なDSL、型強制、拡張性により、複雑なデータ構造の検証を簡潔に記述できます。dry-validationとの組み合わせにより、さらに高度な検証ロジックも実装可能です。
関連リンク
- 公式ドキュメント
- GitHub リポジトリ
- dry-validation - より高度な検証ルールのために
- dry-types - 型システムの基盤