🌊

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
https://github.com/rails/rails/pull/56258

注意点

執筆時点(2025/12)での内容かつ、 main branch にマージされた現段階での記事です。
正式リリース以降では振る舞いが異なっている可能性があります。

手元で試してみる

bundle install

まだリリース前の機能なので、 リポジトリの main branch を指定して bundle install します。

Gemfile
gem "rails", github: "rails/rails", branch: "main"

ActiveRecord のモデルでの has_json 定義

今回の変更で has_json と has_delegated_json の2つが追加されました。

product.rb
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 にも対応できるようになると増々使いやすくなりそうで期待大です!

宣伝

リーナーでは一緒に働いてくれるメンバーを募集中です!
カジュアル面談でもよいので気軽にお声がけくださいねー。

https://careers.leaner.co.jp/engineering

リーナーテックブログ

Discussion