module ActiveRecord::AttributeMethods::Serialization::ClassMethods
def build_column_serializer(attr_name, coder, type, yaml = nil)
def build_column_serializer(attr_name, coder, type, yaml = nil) # When ::JSON is used, force it to go through the Active Support JSON encoder # to ensure special objects (e.g. Active Record models) are dumped correctly # using the #as_json hook. coder = Coders::JSON if coder == ::JSON if coder == ::YAML || coder == Coders::YAMLColumn Coders::YAMLColumn.new(attr_name, type, **(yaml || {})) elsif coder.respond_to?(:new) && !coder.respond_to?(:load) coder.new(attr_name, type) elsif type && type != Object Coders::ColumnSerializer.new(attr_name, coder, type) else coder end end
def serialize(attr_name, class_name_or_coder = nil, coder: nil, type: Object, yaml: {}, **options)
end
serialize :preferences, coder: Rot13JSON
class User < ActiveRecord::Base
end
end
ActiveSupport::JSON.load(rot13(string))
def self.load(string)
# Deserializes a string from the database to an attribute value.
end
rot13(ActiveSupport::JSON.dump(value))
def self.dump(value)
# Serializes an attribute value to a string that will be stored in the database.
end
string.tr("a-zA-Z", "n-za-mN-ZA-M")
def self.rot13(string)
class Rot13JSON
===== Serialize the +preferences+ attribute using a custom coder
end
serialize :preferences, coder: YAML, yaml: { permitted_classes: [Symbol, Time] }
class User < ActiveRecord::Base
===== Serializes +preferences+ to YAML, permitting select classes
end
serialize :preferences, type: Hash, coder: YAML
class User < ActiveRecord::Base
===== Serialize the +preferences+ +Hash+ using YAML
end
serialize :preferences, coder: JSON
class User < ActiveRecord::Base
===== Serialize the +preferences+ attribute using JSON
end
serialize :preferences, coder: YAML
class User < ActiveRecord::Base
===== Serialize the +preferences+ attribute using YAML
==== Examples
=> "#
>> JSON.parse(JSON.dump(Struct.new(:foo)))
silently cast unsupported types to +String+:
For instance the +JSON+ serializer provided in the standard library will
data is deserialized.
silently casting them to other types. This can cause bugs when the
Some serialization methods may accept some types they don't support by
===== Ensure serialization stability
to evolve the format in a backward compatible way.
This pattern allows to be more deliberate about what is serialized, and
end
serialize :address, coder: Address
class User < ActiveRecord::Base
end
end
@line, @city, @country = line, city, country
def initialize(line, city, country)
end
)
"country" => address.country,
"city" => address.city,
"line" => address.line,
YAML.safe_dump(
def self.dump(address)
end
new(data["line"], data["city"], data["country"])
data = YAML.safe_load(payload)
def self.load(payload)
attr_reader :line, :city, :country
class Address
primitives of the serialization format, for example:
As such, it is heavily recommended to instead convert these objects into
change its internal representation without notice.
from a dependency which doesn't expect to be serialized this way and may
old attributes. This problem is even worse when the serialized type comes
instances that were persisted before the change will be loaded with the
In the above example, if any of the +Address+ attributes is renamed,
end
end
@line, @city, @country = line, city, country
def initialize(line, city, country)
class Address
as some database records still contain these serialized types.
that type serialization remains backward and forward compatible as long
This can lead to unexpected types being serialized, and it is important
+Marshal+ or +YAML+ are capable of serializing almost any Ruby object.
only expected types will be serialized. For instance some serializer like
When serializing data in a column, it is heavily recommended to make sure
===== Avoid accepting arbitrary types
another format later on can be difficult.
evaluate the properties of a serializer before using it, as migrating to
While any serialization format can be used, it is recommended to carefully
==== Choosing a serializer
be used. Otherwise, the default will be +nil+.
this option is not passed, the previous default value (if any) will
* +:default+ - The default value to use when no value is provided. If
==== Options
* +:unsafe_load+ - Unsafely load YAML blobs, allow YAML to load any class.
* +:permitted_classes+ - +Array+ with the permitted classes.
* +yaml+ - Optional. Yaml specific options. The allowed config is:
will set to +type.new+
* If the column is +NULL+ or starting from a new record, the default value
ActiveRecord::SerializationTypeMismatch error.
* Attempting to serialize another type will raise an
* +type+ - Optional. What the type of the serialized object should be.
+dump+ method may return +nil+ to serialize the value as +NULL+.
deserialized using the coder's load(string) method. The
using the coder's dump(value) method, and will be
* The attribute value will be serialized
* +coder+ The serializer implementation to use, e.g. +JSON+.
* +attr_name+ - The name of the attribute to serialize.
==== Parameters
domain objects, consider using the ActiveRecord::Attributes API.
For more complex cases, such as conversion to or from your application
case.
objects transparently. There is no need to use #serialize in this
converted between JSON object/array syntax and Ruby +Hash+ or +Array+
for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be
Keep in mind that database adapters handle certain serialization tasks
custom coder class.
The serialization format may be YAML, JSON, or any custom format using a
will be handled automatically.
then specify the name of that attribute using this method and serialization
serialized object, and retrieved by deserializing into the same object,
If you have an attribute that needs to be saved to the database as a
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
def type_incompatible_with_serialize?(cast_type, coder, type)
def type_incompatible_with_serialize?(cast_type, coder, type) cast_type.is_a?(ActiveRecord::Type::Json) && coder == ::JSON || cast_type.respond_to?(:type_cast_array, true) && type == ::Array end