mcpField
J
json-typed-attributes

json-typed-attributes

github.com/rolemodel开发737 次安装今天更新
代码仓库

关于 json-typed-attributes

这是一项 Rails 技能,通过 StoreJsonAttributes concern 在 JSON 列中定义类型化模型属性,涵盖类型转换、验证、表单、强参数和结构化数据查询。

主要功能

  1. 01JSON 属性类型转换
  2. 02支持七种属性类型
  3. 03类似 ActiveRecord 属性的验证
  4. 04集成 Rails 表单
  5. 05强参数与 JSON 查询

适用场景

  1. 01存储灵活的模型元数据
  2. 02处理类型化配置字段
  3. 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)
}

最佳实践

  1. 始终指定字段名 - 明确数据的存储位置

    store_typed_attributes %i[timeline], type: :string, field: :data
    
  2. 对多选数据使用数组 - 自动处理空值

    store_typed_attributes %i[categories], type: :array, field: :data
    
  3. 为有效值定义常量 - 让验证和表单更简单

    TIMELINES = ['30 Days', '90 Days', '180 Days'].freeze
    validates :timeline, inclusion: { in: TIMELINES, allow_blank: true }
    
  4. 添加验证 - 应像验证其他属性一样验证 JSON 属性

    validates :quantity, numericality: { greater_than: 0, allow_nil: true }
    
  5. 使用合适的类型 - 选择与数据相匹配的类型

    • 金额/百分比使用 :decimal(而不是 :integer)
    • 多选使用 :array(会自动移除空值)
    • 标记使用 :boolean(会创建谓词方法)
  6. 将属性包含在强参数中 - 别忘了数组类型的数组语法

    params.require(:model).permit(:timeline, categories: [])
    
  7. 考虑添加索引 - 对于经常查询的 JSON 属性,添加 GIN 索引

    add_index :table_name, :data, using: :gin
    

故障排查

属性未持久化
  • 确保数据库中存在 JSON 列
  • 检查字段名是否匹配:field: :data
  • 确认强参数中包含该属性
类型转换无效
  • 确认类型拼写正确::integer、:decimal、:string 等
  • 对于数组,确保设置的是数组值
  • 对于布尔值,使用谓词方法:record.active?
表单未显示正确的值
  • 对于数组,请检查是否使用了 multiple: true 并检查是否包含相应值
  • 对于选择框,请将当前值与 options_for_select 一起使用
  • 确保 getter 方法返回预期类型
验证失败
  • 检查该属性是否包含在强参数中
  • 确认常量与预期值完全一致
  • 对于数组,请记住空白值会被自动移除

评论

支持 Markdown