Wrap JSON-backed database columns with ActiveModel-like classes using store_model. Use when creating configuration objects, managing nested JSON attributes, or adding validations to JSON data. Triggers on JSON columns, store_model, typed JSON, configuration objects, or nested attributes.
Wrap JSON-backed database columns with ActiveModel-like classes for type safety, validations, and clean separation of concerns. Use when creating configuration objects, managing nested JSON attributes, or adding validations to JSON data. Triggers on JSON columns, store_model, typed JSON, configuration objects, or nested attributes.
/plugin marketplace add majesticlabs-dev/majestic-marketplace/plugin install majestic-rails@majestic-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/advanced-patterns.mdWrap JSON-backed database columns with ActiveModel-like classes for type safety, validations, and clean separation of concerns.
| Scenario | Better Alternative |
|---|---|
| Simple key-value settings | ActiveRecord::Store or store_accessor |
| Need database-level queries on JSON | Raw jsonb with PostgreSQL operators |
| Data needs relationships/joins | Normalize into separate tables |
| Truly simple JSON without validation | Plain JSON column access |
# Gemfile
gem "store_model", "~> 3.0"
# app/models/configuration.rb
class Configuration
include StoreModel::Model
attribute :model, :string
attribute :color, :string
attribute :max_speed, :integer, default: 100
validates :model, presence: true
validates :max_speed, numericality: { greater_than: 0 }
end
# app/models/product.rb
class Product < ApplicationRecord
attribute :configuration, Configuration.to_type
end
product = Product.new
product.configuration = { model: "rocket", color: "red" }
product.configuration.model # => "rocket"
product.configuration.color = "blue"
product.save!
# Also accepts StoreModel instances
product.configuration = Configuration.new(model: "shuttle")
class Configuration
include StoreModel::Model
attribute :model, :string
enum :status, %i[draft active archived], default: :draft
# With custom values
enum :priority, { low: 0, medium: 1, high: 2 }, default: :medium
end
config = Configuration.new
config.status # => "draft"
config.active? # => false
config.active! # Sets status to :active
config.status # => "active"
class Address
include StoreModel::Model
attribute :street, :string
attribute :city, :string
attribute :zip, :string
attribute :country, :string, default: "US"
validates :street, :city, :zip, presence: true
validates :zip, format: { with: /\A\d{5}(-\d{4})?\z/ }, if: -> { country == "US" }
end
class User < ApplicationRecord
attribute :address, Address.to_type
validates :address, store_model: { merge_errors: true }
end
user = User.new(address: { street: "", city: "" })
user.valid?
user.errors.full_messages
# => ["Address street can't be blank", "Address city can't be blank", "Address zip can't be blank"]
class Coordinate
include StoreModel::Model
attribute :latitude, :float
attribute :longitude, :float
validates :latitude, :longitude, presence: true
end
class Location
include StoreModel::Model
attribute :name, :string
attribute :coordinate, Coordinate.to_type
validates :name, presence: true
validates :coordinate, store_model: { merge_errors: true }
end
class LineItem
include StoreModel::Model
attribute :name, :string
attribute :quantity, :integer, default: 1
attribute :price_cents, :integer
validates :name, :price_cents, presence: true
end
class Order < ApplicationRecord
attribute :line_items, LineItem.to_array_type
validates :line_items, store_model: { merge_array_errors: true }
end
order = Order.new
order.line_items = [
{ name: "Widget", quantity: 2, price_cents: 1000 },
{ name: "Gadget", quantity: 1, price_cents: 2500 }
]
order.line_items.first.name # => "Widget"
order.line_items.sum(&:price_cents) # => 3500
StoreModel doesn't automatically detect nested changes. Use one of these approaches:
# Option 1: Reassign the entire object
product.configuration = product.configuration.dup.tap { |c| c.color = "green" }
# Option 2: Mark as changed explicitly
product.configuration.color = "green"
product.configuration_will_change!
product.save!
# Option 3: Use attribute assignment
product.configuration = { **product.configuration.attributes, color: "green" }
class ProductsController < ApplicationController
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
private
def product_params
params.require(:product).permit(
:name,
configuration: [:model, :color, :max_speed, :status]
)
end
end
RSpec.describe Configuration do
describe "validations" do
it "requires model" do
config = described_class.new(model: nil)
expect(config).not_to be_valid
expect(config.errors[:model]).to include("can't be blank")
end
end
describe "enums" do
it "defaults to draft status" do
config = described_class.new
expect(config).to be_draft
end
it "transitions status" do
config = described_class.new
config.active!
expect(config).to be_active
end
end
end
RSpec.describe Product do
describe "configuration" do
it "accepts hash" do
product = described_class.new(configuration: { model: "rocket" })
expect(product.configuration.model).to eq("rocket")
end
it "merges validation errors" do
product = described_class.new(configuration: { model: nil })
expect(product).not_to be_valid
expect(product.errors[:configuration]).to be_present
end
end
end
For advanced patterns:
references/advanced-patterns.md - Nested attributes, custom types, one-of types, parent trackingThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.