skills/json-typed-attributes/SKILL.md
Define typed attributes backed by JSON fields in Rails models. Use when models need flexible data storage with type casting, validations, and form integration. Supports integer, decimal, string, text, boolean, date, and array types.
npx skillsauth add rolemodel/rolemodel-skills json-typed-attributesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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: {}
class YourModel < ApplicationRecord
include StoreJsonAttributes
end
Use store_typed_attributes to define attributes with automatic type casting:
store_typed_attributes [:attribute_name], type: :type_name, field: :json_column_name
store_typed_attributes %i[timeline status], type: :string, field: :data
nil for blank valuesrecord.timeline = "30 Days"
record.timeline # => "30 Days"
store_typed_attributes %i[age count quantity], type: :integer, field: :data
nil for invalid valuesrecord.quantity = "1,500"
record.quantity # => 1500
store_typed_attributes %i[price revenue percentage], type: :decimal, field: :data
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
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
compact_blankrecord.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-backed attributes work seamlessly with Rails form helpers:
= 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" }
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 }
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: [])
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
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 }
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
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)
}
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
:decimal for money/percentages (not :integer):array for multi-select (automatically removes blanks):boolean for flags (creates predicate methods)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
field: :data:integer, :decimal, :string, etc.record.active?multiple: true and checking inclusionoptions_for_select with the current valuetesting
Verify what Ruby versions actually exist and install a specific Ruby via rbenv. Use BEFORE asserting that any Ruby version does or doesn't exist (e.g., "Ruby 4.0 isn't out yet", "the latest Ruby is 3.x", "Ruby X.Y.Z doesn't exist"). Also use when the user asks "what's the latest Ruby", "is Ruby X out", "does Ruby X.Y exist", "install Ruby", "switch to Ruby X", "what Ruby is installed", or mentions a specific Ruby version you're unsure about. Claude's training data may be out of date — run `check.sh` first.
development
Trace code through the stack — upward to entry points, downward to data, or laterally across boundaries. Use when the user asks "where does this get called from", "what calls this method", "trace this through the stack", "how does this request flow", "where does this data come from", "follow this through the code", or pastes/selects a piece of code and wants to understand where it fits in the larger system.
tools
Pick the single highest-priority unresolved Sentry issue and hand it off to a fixer skill. Use when triaging Sentry errors, running automated issue triage, or when asked to fix the top Sentry issue in a project.
tools
Find and fix issues from Sentry using MCP. Use when asked to fix Sentry errors, debug production issues, investigate exceptions, or resolve bugs reported in Sentry. Methodically analyzes stack traces, breadcrumbs, traces, and context to identify root causes.