关于 json-typed-attributes
这是一项 Rails 技能,通过 StoreJsonAttributes concern 在 JSON 列中定义类型化模型属性,涵盖类型转换、验证、表单、强参数和结构化数据查询。
主要功能
- 01JSON 属性类型转换
- 02支持七种属性类型
- 03类似 ActiveRecord 属性的验证
- 04集成 Rails 表单
- 05强参数与 JSON 查询
适用场景
- 01存储灵活的模型元数据
- 02处理类型化配置字段
- 03为 JSON 属性构建表单
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ rolemodel/ rolemodel-skills --skill json-typed-attributes安装后在 Claude Code 中输入/json-typed-attributes确认已加载。
详细介绍
JSON 类型化属性
此技能可帮助你在 Rails 模型中使用 StoreJsonAttributes concern 处理由 JSON 支持的属性。它提供类型转换、验证支持,并可与表单无缝集成。
适用场景
- 你需要灵活存储数据,而无需创建单独的数据库列
- 你希望在 JSON 列中存储结构化数据(例如配置、元数据或动态字段)
- 你需要为 JSON 属性进行正确的类型转换(数字、日期、布尔值、数组)
- 你希望像验证常规 ActiveRecord 属性一样验证由 JSON 支持的属性
- 你需要让 JSON 属性与 Rails 表单无缝协作
设置
1. 确认 JSON 列存在
模型必须有一个用于存储属性的 JSON 列。常见列名有 data、metadata 或 settings:
# In migration
add_column :table_name, :data, :jsonb, default: {}
2. 引入 Concern
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 }
强参数
始终将由 JSON 支持的属性包含在强参数中:
# 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 }
默认值
在初始化器或 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 } -
使用合适的类型 - 选择与数据相匹配的类型
- 金额/百分比使用
:decimal(而不是:integer) - 多选使用
:array(会自动移除空值) - 标记使用
:boolean(会创建谓词方法)
- 金额/百分比使用
-
将属性包含在强参数中 - 别忘了数组类型的数组语法
params.require(:model).permit(:timeline, categories: []) -
考虑添加索引 - 对于经常查询的 JSON 属性,添加 GIN 索引
add_index :table_name, :data, using: :gin
故障排查
属性未持久化
- 确保数据库中存在 JSON 列
- 检查字段名是否匹配:
field: :data - 确认强参数中包含该属性
类型转换无效
- 确认类型拼写正确:
:integer、:decimal、:string等 - 对于数组,确保设置的是数组值
- 对于布尔值,使用谓词方法:
record.active?
表单未显示正确的值
- 对于数组,请检查是否使用了
multiple: true并检查是否包含相应值 - 对于选择框,请将当前值与
options_for_select一起使用 - 确保 getter 方法返回预期类型
验证失败
- 检查该属性是否包含在强参数中
- 确认常量与预期值完全一致
- 对于数组,请记住空白值会被自动移除
评论
支持 Markdown