We all use JSON columns in Rails. They are great for storing extra data like user preferences or vendor specific configuration that does not need its own column.
The downside is that JSON columns have no enforced schema and no types. You are responsible for casting values from forms, validating keys, and keeping the structure consistent. Until now, a JSON column was just a hash. If a form submits the string "true" for a boolean preference, Rails will happily store it without complaint.
A recent PR (#56258) introduced a
new feature that helps to fix this using has_json and has_delegated_json.
Before
If you had a preferences JSON column, you usually treated it like a simple Ruby hash.
# Old way
user.preferences = { "theme" => "dark", "notifications" => "true", "language" => "en" }
This caused a few problems:
- Form values need to be cast manually, otherwise you end up with strings where you expected booleans or integers
- You have to remember what keys exist and what their defaults should be.
After
The new has_json allows you to define a schema for your JSON columns directly
in your model, and Rails will handle defaults and type casting for you.
class User < ApplicationRecord
has_json :preferences,
theme: "light", # Infers string, sets default to "light"
notifications: true, # Infers boolean, sets default to true
language: :string # Explicit type, no default
end
Now, interacting with this column is much simpler:
user = User.new
# Default values
user.preferences.theme # => "light"
user.preferences.notifications? # => true
user.preferences.language # => nil
# Automatic casting
user.preferences.notifications = "false"
user.preferences.notifications? # => false
# Bulk assignment
user.preferences = { "theme" => "dark", "notifications" => "true", "language" => "fr" }
user.preferences.theme # => "dark"
user.preferences.notifications? # => true
user.preferences.language # => "fr"
If you don't want to type user.preferences.theme every time, you can use
has_delegated_json instead.
Supported types and limitations
Currently, the feature supports three basic types:
- boolean
- integer
- string
As of the initial merge, nested structures are not supported. This feature is designed for flat key-value pairs within a JSON.
Resources:
- Add schematized json for has_json (#56258)
- store_model gem
- jsonb_accessor gem