json-typed-attributes
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
More from rolemodel/rolemodel-skills
bem-structure
Expert guidance for writing, refactoring, and structuring CSS using BEM (Block Element Modifier) methodology. Provides proper CSS class naming conventions, component structure, and Optics design system integration for maintainable, scalable stylesheets.
83optics-context
Use the Optics design framework for styling applications. Apply Optics classes for layout, spacing, typography, colors, and components. Use when working on CSS, styling views, or implementing design system guidelines.
37routing-patterns
Review, generate, and update Rails routes following professional patterns and best practices. Covers RESTful resource routing, route concerns for code reusability, shallow nesting strategies, and advanced route configurations.
28turbo-fetch
Implement dynamic form updates using Turbo Streams and Stimulus. Use when forms need to update fields based on user selections without full page reloads, such as cascading dropdowns, conditional fields, or dynamic option lists.
27stimulus-controllers
Create and register Stimulus controllers for interactive JavaScript features. Use when adding client-side interactivity, dynamic UI updates, or when the user mentions Stimulus controllers or JavaScript behavior.
26controller-patterns
Review and update existing Rails controllers and generate new controllers following professional patterns and best practices. Covers RESTful conventions, authorization patterns, proper error handling, and maintainable code organization.
26