json-typed-attributes
npx skills add https://github.com/rolemodel/rolemodel-skills --skill json-typed-attributes
Agent 安装分布
Skill 文档
JSON Typed Attributes
This skill helps you work with JSON-backed attributes in Rails models using the StoreJsonAttributes concern. It provides type casting, validation support, and seamless form integration.
When to Use
- You need flexible data storage without creating separate database columns
- You want to store structured data (like configuration, metadata, or dynamic fields) in a JSON column
- You need proper type casting for JSON attributes (numbers, dates, booleans, arrays)
- You want to validate JSON-backed attributes like regular ActiveRecord attributes
- You need JSON attributes to work seamlessly with Rails forms
Setup
1. Ensure JSON Column Exists
Your model must have a JSON column to store the attributes. Common names are data, metadata, or settings:
# In migration
add_column :table_name, :data, :jsonb, default: {}
2. Include the Concern
class YourModel < ApplicationRecord
include StoreJsonAttributes
end
3. Define Typed Attributes
Use store_typed_attributes to define attributes with automatic type casting:
store_typed_attributes [:attribute_name], type: :type_name, field: :json_column_name
Supported Types
String
store_typed_attributes %i[timeline status], type: :string, field: :data
- Casts values to strings
- Returns
nilfor blank values - Example usage:
record.timeline = "30 Days" record.timeline # => "30 Days"
Integer
store_typed_attributes %i[age count quantity], type: :integer, field: :data
- Casts values to integers
- Automatically strips commas and spaces from input (“1,000” â 1000)
- Returns
nilfor invalid values - Example usage:
record.quantity = "1,500" record.quantity # => 1500
Decimal
store_typed_attributes %i[price revenue percentage], type: :decimal, field: :data
- Casts values to BigDecimal
- Automatically strips commas and spaces from input
- Preserves precision
- Example usage:
record.price = "1,234.56" record.price # => BigDecimal("1234.56")
Boolean
store_typed_attributes %i[active enabled verified], type: :boolean, field: :data
- Creates predicate methods (ending in
?) - Casts truthy/falsy values correctly
- Example usage:
record.active = "1" record.active? # => true record.active = "0" record.active? # => false
Date
store_typed_attributes %i[started_at completed_at], type: :date, field: :data
- Casts strings to Date objects
- Handles various date formats
- Example usage:
record.started_at = "2026-02-04" record.started_at # => Date object record.started_at.strftime("%B %d, %Y") # => "February 04, 2026"
Array
store_typed_attributes %i[categories tags], type: :array, field: :data
- Always returns an array (empty array if nil)
- Automatically removes blank values with
compact_blank - Example usage:
record.categories = ["Revenue Generation", "Operations Management"] record.categories # => ["Revenue Generation", "Operations Management"] record.categories = ["", "Valid", nil, "Another"] record.categories # => ["Valid", "Another"]
Text
store_typed_attributes %i[notes description], type: :text, field: :data
- Similar to string but intended for longer content
- Creates predicate method (ending in
?) - Example usage:
record.notes = "Long text content..." record.notes? # => true (if present)
Complete Example
# 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
Working with Forms
JSON-backed attributes work seamlessly with Rails form helpers:
Simple Fields
= form.text_field :timeline
= form.number_field :quantity
= form.check_box :active
Array Fields (Checkboxes)
- CATEGORIES.each do |category|
= form.check_box :categories,
{ multiple: true, checked: form.object.categories.include?(category) },
category,
nil
= category
Select Fields
= form.select :timeline,
options_for_select(TIMELINES, form.object.timeline),
{ include_blank: "Select timeline" }
Adding Validations
Validate JSON-backed attributes like regular attributes:
# 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
Always include JSON-backed attributes in your 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: [])
Multiple JSON Fields
You can use different JSON fields for different concerns:
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
Common Patterns
Constants for Validation
Define constants for valid values:
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 }
Default Values
Set defaults in initializer or 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
Scopes and Queries
Query JSON attributes using PostgreSQL JSON operators:
# 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)
}
Best Practices
-
Always specify the field name – Makes it clear where data is stored
store_typed_attributes %i[timeline], type: :string, field: :data -
Use arrays for multi-select data – Automatically handles blank values
store_typed_attributes %i[categories], type: :array, field: :data -
Define constants for valid values – Makes validations and forms easier
TIMELINES = ['30 Days', '90 Days', '180 Days'].freeze validates :timeline, inclusion: { in: TIMELINES, allow_blank: true } -
Add validations – JSON attributes should be validated like any other attribute
validates :quantity, numericality: { greater_than: 0, allow_nil: true } -
Use appropriate types – Choose the type that matches your data
- Use
:decimalfor money/percentages (not:integer) - Use
:arrayfor multi-select (automatically removes blanks) - Use
:booleanfor flags (creates predicate methods)
- Use
-
Include in strong parameters – Don’t forget array syntax for array types
params.require(:model).permit(:timeline, categories: []) -
Consider indexing – For frequently queried JSON attributes, add GIN indexes
add_index :table_name, :data, using: :gin
Troubleshooting
Attribute not persisting
- Ensure the JSON column exists in the database
- Check that the field name matches:
field: :data - Verify strong parameters include the attribute
Type casting not working
- Verify the type is spelled correctly:
:integer,:decimal,:string, etc. - For arrays, ensure you’re setting an array value
- For booleans, use the predicate method:
record.active?
Form not displaying correct values
- For arrays, check that you’re using
multiple: trueand checking inclusion - For selects, use
options_for_selectwith the current value - Ensure the getter method returns the expected type
Validation failing
- Check that the attribute is included in strong parameters
- Verify constants match the expected values exactly
- For arrays, remember blank values are automatically removed