Rails Typed JSON Fields
I've been using [Rails JSON Serialized Fields](/2019/rails_json_serialized_fields/) for custom objects stored in Postgresql JSON fields. I found this approach and like it a lot more. It uses `ActiveModel::Type::Value` to create a custom type mapping for ActiveRecord.
I've been using Rails JSON Serialized Fields for
custom objects stored in Postgresql JSON fields. I found this approach and like it a lot more. It
uses ActiveModel::Type::Value
to create a custom type mapping for ActiveRecord.
The goal is to store JSON objects in Postgresql and have them strongly typed and validated in Rails.
For this example MyRecord
will be the ActiveRecord model. We want to store MyModel
as JSON in the database. The
my_model
attribute will store one MyModel
object and the my_models
attribute will store an array of MyModel
objects.
The Model
This is the ActiveModel object we want to store in a JSON field.
# app/models/my_model.rb
class MyModel
include ActiveModel::Model
include ActiveModel::Serialization
include ActiveModel::Attributes
include ActiveModel::AttributeMethods
extend ActiveModel::Naming
attribute :code, :string
attribute :severity, :integer
validates :code, presence: true
alias to_hash serializable_hash
def persisted?
false
end
def id
nil
end
def to_s
code
end
end
Custom Types
https://www.rubydoc.info/gems/activerecord/ActiveRecord/Type/Json
Create custom types for the MyModel
class. More details here ActiveModel::Type::Value.
I used the existing ActiveRecord::Type::Json
class with an overridden deserialize
method to return the MyModel
class.
Objects
# app/types/my_model_type.rb
class MyModelType < ActiveRecord::Type::Json
def deserialize raw_value
value = super # turns raw json string into array of hashes
if value.is_a? Hash
MyModel.new(value)
else
value
end
end
end
Arrays
Arrays where a bit tricky to figure out. This StackOverflow post helped a ton. That post uses JSONb. I changed to JSON for my needs.
# app/types/my_model_array_type.rb
class MyModelArrayType < ActiveRecord::Type::Json
def deserialize raw_value
value = super # turns raw json string into array of hashes
if value.is_a? Array
value.map { |h| MyModel.new(h) } # turns array of hashes into array of Variables
else
value
end
end
end
Type Registration
Starting with Rails 6, there is some autoloading changes that require these be registered in the reloader.to_prepare
callback.
More discusson
# config/initializers/types.rb
Rails.application.reloader.to_prepare do
ActiveRecord::Type.register(:my_model, MyModelType)
ActiveRecord::Type.register(:my_model_array, MyModelArrayType)
end
Using the Types
Now that we have custom types registered we can use them with the ActiveRecord::Attributes api.
# app/models/my_record.rb
class MyRecord < ApplicationRecord
attribute :my_model, :my_model
attribute :my_models, :my_model_array, default: []
validates :my_model, serialized: true
validates :my_model_array, serialized: true, presence: {message: "Please provide at least one MyModel."}
# ...
end
The serialized
validator is described in Rails JSON Serialized Fields - Validation.
Database Fields
class AddMyModelToMyRecord < ActiveRecord::Migration[6.0]
def change
add_column :my_record, :my_model, :json
add_column :my_record, :my_models, :json, default: []
end
end
Why I like this
- We don't need custom concerns to serialize the object, since it's actually stored as JSON. If you're not using Postgresql, then the serialized approach might be better.
- We can rely on more existing Rails code.
- Feels a bit cleaner.
Webmentions
These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: