Rails に追加予定の has_json を触ってみた
リーナーの開発チームの lulu(@lulu_lul2) です!
最近の業務では LLMを活用したプロダクトの開発やデータ分析基盤チームのサポート等をやっており、プライベートでもLLMやRAGやデータモデリング関連の学習がメインになっています。
そのため最近あまり Ruby/Rails 周りの変更を追いかけられておらず、Rubyist と名乗って良いのか…?という状態です 😢
そんな折に先月の Rails の Weeklyのアップデートハイライトの記事 に興味のある変更を見つけたため早速手元で触ってみました!
Add schematized json for has_json
Provides a schema-enforced access object for a JSON attribute. This allows you to assign values directly from the UI as strings, and still have them set with the correct JSON type in the database.
対象PullRequest
注意点
執筆時点(2025/12)での内容かつ、 main branch にマージされた現段階での記事です。
正式リリース以降では振る舞いが異なっている可能性があります。
手元で試してみる
bundle install
まだリリース前の機能なので、 リポジトリの main branch を指定して bundle install します。
gem "rails", github: "rails/rails", branch: "main"
ActiveRecord のモデルでの has_json 定義
今回の変更で has_json と has_delegated_json の2つが追加されました。
class Product < ApplicationRecord
has_json :specifications,
weight: 0,
dimensions: "10x20x30",
material: "plastic",
waterproof: false
has_delegated_json :features,
color: :string,
warranty_months: :integer,
made_in: 'Japan',
official: :boolean
end
初期化周り
> product = Product.new(name: "ワイヤレスイヤホン")
> product
=> #<Product:0x000000012623f650
id: nil,
name: "ワイヤレスイヤホン",
specifications: nil,
features: nil, # このタイミングでは nil のまま
created_at: nil,
updated_at: nil>
# 初期値がセットされる
> product.specifications.weight
=> 0
> product.specifications.material
=> "plastic"
# 初期値が指定されない場合は nil がセットされる
> product.color
=> nil
> product.made_in
=> "Japan"
> product.official
=> nil
# has_delegated_json で delegate した場合も、JSON schema名経由で各keyにアクセスできる
> product.features.made_in
=> "Japan"
ただ現在の実装だとオプション指定等で特定の key のみ delegate することはできないようです。
初期化タイミング
Default values are set when a new model is instantiated and on before_save (if defined).
説明では インスタンス化された時とありますが、正確にはその JSON column が参照された時の様です。
> product = Product.new(name: "ワイヤレスイヤホン")
> product
=> #<Product:0x000000012623f650
id: nil,
name: "ワイヤレスイヤホン",
specifications: nil,
features: nil, # このタイミングでは nil のまま
created_at: nil,
updated_at: nil>
>
> product.features
=>
#<ActiveModel::SchematizedJson::DataAccessor:0x00000001278b60a0
@data={"color" => nil, "warranty_months" => nil, "made_in" => "Japan", "official" => nil},
@schema={color: :string, warranty_months: :integer, made_in: "Japan", official: :boolean}>
> product
=>
#<Product:0x0000000127a933a0
id: nil,
name: "ワイヤレスイヤホン",
specifications: nil,
features: {"color" => nil, "warranty_months" => nil, "made_in" => "Japan", "official" => nil},
created_at: nil,
updated_at: nil>
大抵の場合は問題にならないと思いますが、 #attributes を利用する時等は注意が必要そうです。
setter周り
> product.specifications.weight
=> 0
> product.specifications.weight = 10_000
=> 10000
> product.specifications.weight
=> 10000
> product.specifications.weight = '1万'
=> "1万"
> product.specifications.weight
=> 1 # 文字列を #to_i した結果1がセット
> product.specifications.weight = '一万'
=> "一万"
> product.specifications.weight
=> 0 # 文字列を #to_i した結果0がセット
定義と異なる型はセットさせない方針ではなく、セット時に強制的に宣言された型にキャストして定義通りのデータ型で保存されることを担保する形ですね。
Rails らしい割り切りで良いと思いますが、入ってくる値によっては意図しない数値が入ってしまい Validation をすり抜けることはありそうなので注意が必要そうです。
( "1万" の例等)
Validation
現状では、 has_json を使う場合はそれぞれカスタムバリデーションメソッドを定義する形になりそうです。
validate :validate_specifications_weight
def validate_specifications_weight
# バリデーションを実装
end
将来的に以下の様に delegate せずとも ActiveModel::Validations::HelperMethods の標準バリデータが使えるようになると使いやすくなる気がします。
validates :specifications, :weight, { presence: true, numericality: {...} }
has_delegated_json の場合は validates で標準バリデータが使えます。
# validation 定義
validates :warranty_months, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 100 }
> product.warranty_months = 0
=> 0
> product.valid?
=> false
> product.warranty_months = 10
=> 10
> product.valid?
=> true
ActiveModel 以外での利用
accessorと強制的な型キャストを利用したいだけならば、以下の様に ActiveModel::Attributes と一緒に include するだけで使えます。
class MyAgent
include ActiveModel::Attributes
include ActiveModel::SchematizedJson
フォームオブジェクトで params 経由で入力内容やオプション等受け取る場合等に使いやすい気がしますね。
触ってみた感想・まとめ
今までも store_accessor 等で JSON カラムを扱うことはできましたが、初期値のセットは自前でやる必要があったり、 型キャストが無いため意図せぬデータ型で保存されてしまうリスクがありました (integer / boolean に対して文字列で保存してしまう等)。
ネストした複雑なJSONを扱う場合は引き続き store_model gem 等を採用することにはなるとは思いますが、
簡素なJSONを扱う範囲ならば has_json が使いやすくて良い気がしますねー。
delegate すればバリデーションも簡素に書けるのも良いです。
まだリリース前の機能ですので、今後対応するデータ型が増えたり、ネストした JSON にも対応できるようになると増々使いやすくなりそうで期待大です!
宣伝
リーナーでは一緒に働いてくれるメンバーを募集中です!
カジュアル面談でもよいので気軽にお声がけくださいねー。
Discussion