dry-validationを使った入力値バリデーション方法の紹介
株式会社COUNTERWORKSでRuby on Railsを採用しているアプリの開発をしているまったんです。
この記事ではデータのスキーマとバリデーションルールを定義するDSLを提供するGemであるdry-validationを使った入力値バリデーションの方法を紹介します。
はじめに
みなさんはフォームなどの入力値に対するバリデーションはどのように行っているでしょうか?
私の観測範囲では以下の2つのパターンがほとんどでした。
- モデルのバリデーション機能に任せる
-
ActiveModel::Validations
をミックスインした、モデルに非依存なクラスに任せる
上記の2パターンだと以下のようなつらさがありました。
- ユースケースによって変えるようなアプリ(例えばユーザーの種類によってバリデーションルールを変えたり)だとモデルのバリデーション機能に任せるのはつらい
- モデルにはDBの入力値のバリデーションルールのみを定義し、フォーム等の入力値のバリデーションは
ActiveModel::Validations
をミックスインしたクラスに任せるようにしても、配列やオブジェクトのバリデーションルールの記述がつらい
上記のようなつらさを軽減するために、弊社では dry-validation
を使ってフォーム等の入力値をバリデーションしてから永続化処理をするようにしています。
dry-validationの使い方
dry-validation
を使って、以下のようなハッシュのバリデーション方法を紹介します。
{
name: "鈴木太郎",
age: 15,
start_date: "2025-01-01",
end_date: "2025-10-31",
is_published: false,
blood_type: "A",
categories: [
"food",
"animal"
],
guests: [
{
name: "田中拓哉",
height: 155.5
},
{
name: "渡辺大介"
}
]
}
スキーマ、バリデーションルールは以下の通りです。(キー名の前に*が付いていれば必須のキーです)
*name: 文字列/2文字以上
age: 整数/10以上
*start_date: 文字列/ISO8601に準拠した日付フォーマット/未来の日付
*end_date: 文字列/ISO8601に準拠した日付フォーマット/start_dateよりも未来の日付
is_published: 真偽値
*categories: 配列/要素数は2以上
categoriesの要素: 文字列/特定の値のみ許可(food/animal/fashion/travel)
*guests: 配列
guestsの要素: オブジェクト
*name: 文字列/1文字以上
height: 浮動小数点数/10超過280未満
dry-validation
を使って上記のスキーマ、バリデーションルールを表現すると次のようになります。
class Contract < Dry::Validation::Contract
params do
required(:name).filled(:str?, min_size?: 2)
optional(:age).maybe(:int?, gteq?: 10)
required(:start_date).filled(:date)
required(:end_date).filled(:date)
optional(:is_published).filled(:bool?)
required(:categories).filled(:array?, min_size?: 2).each(:string, included_in?: %w[food animal fashion travel])
required(:guests).value(:array?).each do
hash do
required(:name).value(:str?)
optional(:height).filled(:float?, gt?: 10, lt?: 280)
end
end
end
rule(:start_date, :end_date) do
unless values[:start_date] > Date.current
key(:start_date).failure('must be after today')
end
unless values[:end_date] > Date.current
key(:end_date).failure('must be after today')
end
unless values[:end_date] > values[:start_date]
key(:end_date).failure('must be after start date')
end
end
end
簡単に解説します。
-
params
バリデーションするデータの型強制などを決めます。paramsは"2023-01-01"
のような文字列をDateインスタンスにしてからバリデーションしてくれたります。
paramsの他にはjsonやschemaなどもあります。 -
required
/optional
キーが必須かどうかを定義します。 -
filled
/maybe
/value
値がnullableかどうかなどを決めます。
文字列型ならfilledはnullも空文字も許さない、配列型ならfilledはnullも空配列も許さないなど、値のデータ型によって若干振る舞いが違います。
(ActiveSupportのpresent?メソッドのような振る舞いをします。) -
str?
/int?
/float?
/date
/bool?
/array?
/hash
データ型を決めます。?がついてると厳しい型チェックをしてくれたりします。
例えば、整数型はint?かintegerで表現できますが、integerだと1
も"1"
も許容しますがint?では"1"
しか許容されません。 -
min_size?
/gteq?
/included_in?
/gt?
/lt?
値のルールを決めます。文字数や配列の要素数や数値の下限上限などを定義したり、文字列のパターンのセーフリストなどを定義したりできます。 -
rule
上記の方法では定義できないようなルールを自由に定義できます。
ルールに反していれば以下のようにエラーメッセージを受け取れます。
data = {
name: "鈴木太郎",
age: 1,
start_date: "2025-01-01",
end_date: "2025-10-31",
is_published: false,
categories: [
"food",
"animal"
],
guests: [
{
name: "田中拓哉",
height: 155.5,
},
{
name: "渡辺大介"
}
]
}
contract = Contract.new
result = contract.call(data)
puts result.errors
=> { age: ["must be greater than or equal to 10"] }
DSLに慣れてしまえば、直感的に少ない記述でスキーマ、バリデーションルールを定義できるのがいいところだと思っています。
使い方に関する詳しい情報は公式ドキュメントをご覧ください。
おわりに
最良の入力値のバリデーションの方法はサービスの特性や規模などによって変わると思っていますが、バリデーション方法で悩んでいる方たちに、この記事を通して新たな選択肢を提供できたらうれしいです。
自分たちがやりたいバリデーション方法が公式ドキュメントに書かれてなくて困っているなんてことがあればX(Twitter)でDM貰えれば助けられるかもしれません。
We are hiring!!
COUNTERWORKS では一緒に働く仲間を絶賛募集中です。
今後の更なる成長のためには圧倒的に仲間が不足しています。皆さまのご応募お待ちしております!
ポップアップストアや催事イベント向けの商業スペースを簡単に予約できる「SHOPCOUNTER」と商業施設向けリーシングDXシステム「SHOPCOUNTER Enterprise」を運営しています。エンジニア採用強化中ですので、興味ある方はお気軽にご連絡ください! counterworks.co.jp/
Discussion