Methods
Instance Public methods
serialize(attr_name, class_name_or_coder = nil, coder: nil, type: Object, yaml: {}, **options)
If you have an attribute that needs to be saved to the database as a serialized object, and retrieved by deserializing into the same object, then specify the name of that attribute using this method and serialization will be handled automatically.
The serialization format may be YAML, JSON, or any custom format using a custom coder class.
Keep in mind that database adapters handle certain serialization tasks for you. For instance: json
and jsonb
types in PostgreSQL will be converted between JSON object/array syntax and Ruby Hash
or Array
objects transparently. There is no need to use serialize
in this case.
For more complex cases, such as conversion to or from your application domain objects, consider using the ActiveRecord::Attributes
API.
Parameters
-
attr_name
- The name of the attribute to serialize. -
coder
The serializer implementation to use, e.g.JSON
.-
The attribute value will be serialized using the coder’s
dump(value)
method, and will be deserialized using the coder’sload(string)
method. Thedump
method may returnnil
to serialize the value asNULL
.
-
-
type
- Optional. What the type of the serialized object should be.-
Attempting to serialize another type will raise an
ActiveRecord::SerializationTypeMismatch
error. -
If the column is
NULL
or starting from a new record, the default value will set totype.new
-
-
yaml
- Optional. Yaml specific options. The allowed config is:-
:permitted_classes
-Array
with the permitted classes. -
:unsafe_load
- Unsafely load YAML blobs, allow YAML to load any class.
-
Options
-
:default
- The default value to use when no value is provided. If this option is not passed, the previous default value (if any) will be used. Otherwise, the default will benil
.
Choosing a serializer
While any serialization format can be used, it is recommended to carefully evaluate the properties of a serializer before using it, as migrating to another format later on can be difficult.
Avoid accepting arbitrary types
When serializing data in a column, it is heavily recommended to make sure only expected types will be serialized. For instance some serializer like Marshal
or YAML
are capable of serializing almost any Ruby object.
This can lead to unexpected types being serialized, and it is important that type serialization remains backward and forward compatible as long as some database records still contain these serialized types.
class Address
def initialize(line, city, country)
@line, @city, @country = line, city, country
end
end
In the above example, if any of the Address
attributes is renamed, instances that were persisted before the change will be loaded with the old attributes. This problem is even worse when the serialized type comes from a dependency which doesn’t expect to be serialized this way and may change its internal representation without notice.
As such, it is heavily recommended to instead convert these objects into primitives of the serialization format, for example:
class Address
attr_reader :line, :city, :country
def self.load(payload)
data = YAML.safe_load(payload)
new(data["line"], data["city"], data["country"])
end
def self.dump(address)
YAML.safe_dump(
"line" => address.line,
"city" => address.city,
"country" => address.country,
)
end
def initialize(line, city, country)
@line, @city, @country = line, city, country
end
end
class User < ActiveRecord::Base
serialize :address, coder: Address
end
This pattern allows to be more deliberate about what is serialized, and to evolve the format in a backward compatible way.
Ensure serialization stability
Some serialization methods may accept some types they don’t support by silently casting them to other types. This can cause bugs when the data is deserialized.
For instance the JSON
serializer provided in the standard library will silently cast unsupported types to String
:
>> JSON.parse(JSON.dump(Struct.new(:foo)))
=> "#<Class:0x000000013090b4c0>"
Examples
Serialize the preferences
attribute using YAML
class User < ActiveRecord::Base
serialize :preferences, coder: YAML
end
Serialize the preferences
attribute using JSON
class User < ActiveRecord::Base
serialize :preferences, coder: JSON
end
Serialize the preferences
Hash
using YAML
class User < ActiveRecord::Base
serialize :preferences, type: Hash, coder: YAML
end
Serializes preferences
to YAML, permitting select classes
class User < ActiveRecord::Base
serialize :preferences, coder: YAML, yaml: { permitted_classes: [Symbol, Time] }
end
Serialize the preferences
attribute using a custom coder
class Rot13JSON
def self.rot13(string)
string.tr("a-zA-Z", "n-za-mN-ZA-M")
end
# Serializes an attribute value to a string that will be stored in the database.
def self.dump(value)
rot13(ActiveSupport::JSON.dump(value))
end
# Deserializes a string from the database to an attribute value.
def self.load(string)
ActiveSupport::JSON.load(rot13(string))
end
end
class User < ActiveRecord::Base
serialize :preferences, coder: Rot13JSON
end
📝 Source code
# File activerecord/lib/active_record/attribute_methods/serialization.rb, line 183
def serialize(attr_name, class_name_or_coder = nil, coder: nil, type: Object, yaml: {}, **options)
unless class_name_or_coder.nil?
if class_name_or_coder == ::JSON || [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
ActiveRecord.deprecator.warn(<<~MSG)
Passing the coder as positional argument is deprecated and will be removed in Rails 7.2.
Please pass the coder as a keyword argument:
serialize #{attr_name.inspect}, coder: #{class_name_or_coder}
MSG
coder = class_name_or_coder
else
ActiveRecord.deprecator.warn(<<~MSG)
Passing the class as positional argument is deprecated and will be removed in Rails 7.2.
Please pass the class as a keyword argument:
serialize #{attr_name.inspect}, type: #{class_name_or_coder.name}
MSG
type = class_name_or_coder
end
end
coder ||= default_column_serializer
unless coder
raise ArgumentError, <<~MSG.squish
missing keyword: :coder
If no default coder is configured, a coder must be provided to `serialize`.
MSG
end
column_serializer = build_column_serializer(attr_name, coder, type, yaml)
attribute(attr_name, **options) do |cast_type|
if type_incompatible_with_serialize?(cast_type, coder, type)
raise ColumnNotSerializableError.new(attr_name, cast_type)
end
cast_type = cast_type.subtype if Type::Serialized === cast_type
Type::Serialized.new(cast_type, column_serializer)
end
end
🔎 See on GitHub