json-typed-attributes について
StoreJsonAttributes concern を使い、JSON カラムに型付きモデル属性を定義する Rails スキルです。型変換、バリデーション、フォーム、Strong Parameters、構造化データの検索を扱います。
主な機能
- 01JSON 属性の型変換
- 027 種類の属性型に対応
- 03ActiveRecord 属性形式のバリデーション
- 04Rails フォームとの連携
- 05Strong Parameters と JSON 検索
活用シーン
- 01柔軟なモデルメタデータの保存
- 02型付き設定フィールドの処理
- 03JSON 属性用フォームの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rolemodel/ rolemodel-skills --skill json-typed-attributesインストール後、Claude Code で/json-typed-attributesを入力して読み込まれたか確認します。
詳細
JSON 型付き属性
このスキルは、StoreJsonAttributes concern を使用して、Rails モデルの JSON ベースの属性を扱うのに役立ちます。型変換、バリデーションのサポート、フォームとのシームレスな連携を提供します。
使用する場面
- データベースに個別のカラムを作成せず、柔軟にデータを保存したい場合
- JSON カラムに構造化データ(設定、メタデータ、動的フィールドなど)を保存したい場合
- JSON 属性を適切に型変換したい場合(数値、日付、ブール値、配列)
- JSON ベースの属性を通常の ActiveRecord 属性のようにバリデーションしたい場合
- JSON 属性を Rails フォームとシームレスに連携させたい場合
セットアップ
1. JSON カラムがあることを確認する
属性を保存するため、モデルに JSON カラムが必要です。一般的な名前は data、metadata、settings です。
# In migration
add_column :table_name, :data, :jsonb, default: {}
2. Concern を include する
class YourModel < ApplicationRecord
include StoreJsonAttributes
end
3. 型付き属性を定義する
store_typed_attributes を使用して、自動型変換を行う属性を定義します。
store_typed_attributes [:attribute_name], type: :type_name, field: :json_column_name
サポートされる型
文字列
store_typed_attributes %i[timeline status], type: :string, field: :data
- 値を文字列に型変換します
- 空白値の場合は
nilを返します - 使用例:
record.timeline = "30 Days" record.timeline # => "30 Days"
整数
store_typed_attributes %i[age count quantity], type: :integer, field: :data
- 値を整数に型変換します
- 入力値からカンマとスペースを自動的に削除します("1,000" → 1000)
- 無効な値の場合は
nilを返します - 使用例:
record.quantity = "1,500" record.quantity # => 1500
小数
store_typed_attributes %i[price revenue percentage], type: :decimal, field: :data
- 値を BigDecimal に型変換します
- 入力値からカンマとスペースを自動的に削除します
- 精度を保持します
- 使用例:
record.price = "1,234.56" record.price # => BigDecimal("1234.56")
ブール値
store_typed_attributes %i[active enabled verified], type: :boolean, field: :data
- 述語メソッド(末尾が
?)を作成します - 真偽値を正しく型変換します
- 使用例:
record.active = "1" record.active? # => true record.active = "0" record.active? # => false
日付
store_typed_attributes %i[started_at completed_at], type: :date, field: :data
- 文字列を Date オブジェクトに型変換します
- さまざまな日付形式に対応します
- 使用例:
record.started_at = "2026-02-04" record.started_at # => Date object record.started_at.strftime("%B %d, %Y") # => "February 04, 2026"
配列
store_typed_attributes %i[categories tags], type: :array, field: :data
- 常に配列を返します(nil の場合は空配列)
compact_blankで空白値を自動的に削除します- 使用例:
record.categories = ["Revenue Generation", "Operations Management"] record.categories # => ["Revenue Generation", "Operations Management"] record.categories = ["", "Valid", nil, "Another"] record.categories # => ["Valid", "Another"]
テキスト
store_typed_attributes %i[notes description], type: :text, field: :data
- 文字列と似ていますが、より長い内容を想定しています
- 述語メソッド(末尾が
?)を作成します - 使用例:
record.notes = "Long text content..." record.notes? # => true (if present)
完全な例
# frozen_string_literal: true
class CBPComponents::KeyQuestion < CoreBusinessPresentationComponent
CATEGORIES = [
'Revenue Generation',
'Operations Management',
'Organizational Development',
'Financial Management',
'Ministry',
'Personal Issue',
].freeze
TIMELINES = ['30 Days', '90 Days', '180 Days', '1 Year', 'More than 1 Year'].freeze
# Define JSON-backed typed attributes
store_typed_attributes %i[timeline], type: :string, field: :data
store_typed_attributes %i[categories], type: :array, field: :data
# Add validations like any other attribute
validates :timeline, inclusion: { in: TIMELINES, allow_blank: true }
validates :categories, inclusion: { in: CATEGORIES }, allow_blank: true
# Use in strong parameters
private
def base_params
super.concat([:timeline, :summary, categories: []])
end
end
フォームでの使用
JSON ベースの属性は Rails のフォームヘルパーとシームレスに連携します。
単純なフィールド
= form.text_field :timeline
= form.number_field :quantity
= form.check_box :active
配列フィールド(チェックボックス)
- CATEGORIES.each do |category|
= form.check_box :categories,
{ multiple: true, checked: form.object.categories.include?(category) },
category,
nil
= category
選択フィールド
= form.select :timeline,
options_for_select(TIMELINES, form.object.timeline),
{ include_blank: "Select timeline" }
バリデーションの追加
通常の属性と同様に、JSON ベースの属性をバリデーションします。
# Presence
validates :timeline, presence: true
# Inclusion
validates :timeline, inclusion: { in: TIMELINES }
# Length
validates :categories, length: { minimum: 1, message: "must select at least one" }
# Custom validation
validate :categories_must_be_valid
private
def categories_must_be_valid
invalid_categories = categories - CATEGORIES
if invalid_categories.any?
errors.add(:categories, "contains invalid categories: #{invalid_categories.join(', ')}")
end
end
# Numericality
validates :quantity, numericality: { greater_than: 0, allow_nil: true }
# Format
validates :status, format: { with: /\A[A-Z][a-z]+\z/, allow_blank: true }
Strong Parameters
JSON ベースの属性は、必ず strong parameters に含めてください。
# For simple types (string, integer, decimal, boolean, date)
params.require(:model).permit(:timeline, :quantity, :active, :started_at)
# For arrays, use array syntax
params.require(:model).permit(:timeline, categories: [])
複数の JSON フィールド
用途ごとに異なる JSON フィールドを使用できます。
class Product < ApplicationRecord
# Pricing data
store_typed_attributes %i[base_price discount_percentage], type: :decimal, field: :pricing_data
# Inventory data
store_typed_attributes %i[quantity threshold], type: :integer, field: :inventory_data
# Feature flags
store_typed_attributes %i[featured new_arrival on_sale], type: :boolean, field: :flags
end
よくあるパターン
バリデーション用の定数
有効な値を定数として定義します。
STATUSES = %w[pending approved rejected].freeze
PRIORITIES = %w[low medium high urgent].freeze
store_typed_attributes %i[status], type: :string, field: :data
store_typed_attributes %i[priority], type: :string, field: :data
validates :status, inclusion: { in: STATUSES, allow_blank: true }
validates :priority, inclusion: { in: PRIORITIES, allow_blank: true }
デフォルト値
initializer または after_initialize でデフォルト値を設定します。
after_initialize :set_defaults, if: :new_record?
private
def set_defaults
self.categories ||= []
self.timeline ||= '90 Days'
self.active = true if active.nil?
end
スコープとクエリ
PostgreSQL の JSON 演算子を使用して JSON 属性を検索します。
# Find records with specific value
scope :with_timeline, ->(timeline) {
where("data->>'timeline' = ?", timeline)
}
# Find records where array contains value
scope :with_category, ->(category) {
where("data->'categories' ? :category", category: category)
}
# Find records with any of multiple values
scope :with_any_category, ->(categories) {
where("data->'categories' ?| array[:categories]", categories: categories)
}
ベストプラクティス
-
フィールド名を必ず指定する - データの保存先が明確になります
store_typed_attributes %i[timeline], type: :string, field: :data -
複数選択データには配列を使用する - 空白値を自動的に処理します
store_typed_attributes %i[categories], type: :array, field: :data -
有効な値を定数として定義する - バリデーションやフォームの作成が容易になります
TIMELINES = ['30 Days', '90 Days', '180 Days'].freeze validates :timeline, inclusion: { in: TIMELINES, allow_blank: true } -
バリデーションを追加する - JSON 属性も他の属性と同様にバリデーションする必要があります
validates :quantity, numericality: { greater_than: 0, allow_nil: true } -
適切な型を使用する - データに合った型を選択します
- 金額やパーセンテージには
:integerではなく:decimalを使用します - 複数選択には
:arrayを使用します(空白値を自動的に削除します) - フラグには
:booleanを使用します(述語メソッドを作成します)
- 金額やパーセンテージには
-
strong parameters に含める - 配列型には配列構文が必要なことを忘れないでください
params.require(:model).permit(:timeline, categories: []) -
インデックスを検討する - 頻繁に検索する JSON 属性には GIN インデックスを追加します
add_index :table_name, :data, using: :gin
トラブルシューティング
属性が永続化されない
- データベースに JSON カラムがあることを確認します
- フィールド名が一致することを確認します:
field: :data - strong parameters に属性が含まれていることを確認します
型変換が機能しない
- 型名のスペルが正しいことを確認します:
:integer、:decimal、:stringなど - 配列の場合は、配列値を設定していることを確認します
- ブール値の場合は、述語メソッドを使用します:
record.active?
フォームに正しい値が表示されない場合
- 配列の場合は、
multiple: trueを使用し、値が含まれているか確認してください - セレクトの場合は、現在の値を指定して
options_for_selectを使用してください - ゲッターメソッドが期待される型を返すことを確認してください
バリデーションに失敗する場合
- 属性がストロングパラメーターに含まれていることを確認してください
- 定数が期待される値と完全に一致していることを確認してください
- 配列の場合、空の値は自動的に削除されることを覚えておいてください