store_model
store_model copied to clipboard
Nested Attributes get not serialized correctly
Hello,
I was wondering, why during serialization my custom types don't get used to determine the json format:
class DateOrTimeType < ActiveRecord::Type::Value
def type
:json
end
def cast_value(value)
case value
when String
decoded = ActiveSupport::JSON.decode(value) rescue nil
build_from(decoded)
when Hash
build_from(value)
when Date, Time, DateTime
value
end
end
def serialize(value)
case value
when Date
ActiveSupport::JSON.encode(date: value)
when Time, DateTime
ActiveSupport::JSON.encode(datetime: value)
else
super
end
end
def changed_in_place?(raw_old_value, new_value)
cast_value(raw_old_value) != new_value
end
private
def build_from(hash)
if hash['date'].present?
Date.parse(hash['date'])
else
Time.parse(hash['datetime'])
end
end
end
class Appointments::Schedule
include StoreModel::Model
attribute :from, DateOrTimeType.new
attribute :to, :date_or_time
but during serialization, the default json serialisation of the given type is used.
Hi @kaelumania! Could you please provide the example of what format you're trying to get and what you see instead? Gist or failing spec would be really helpful
Closing the issue for now
I’m running into a similar issue. As far as I can tell, serialize and deserialize are never called on attributes with custom types. The only method called is cast.
Yeah, I re–checked it quickly, and looks like a regular #save
calls only cast
/cast_value
I just ran into this problem. Serialize is not called on save. Here is a simplified version of what I'm trying to do :
class Shipment < ActiveRecord::Base
attribute :recipient, Shipment::Recipient.to_type
end
class Shipment::Recipient
include StoreModel::Model
attribute :country, :country
end
require 'countries' # https://github.com/countries/countries
class CountryType < ActiveModel::Type::Value
def cast(value)
ISO3166::Country.new(value)
end
def serialize(value)
value.alpha2
end
end
shipment = Shipment.create!(recipient: {country: 'FR'})
This should save {"country": ''FR'} in the recipient attribute in the database and shipment.recipient.country should be a ISO3166::Country. But instead the gem is trying to save a hash of all the data from ISO3166::Country without calling serialize.
The CountryType works correctly when used with a direct string attribute.
Yes this is because of https://github.com/DmitryTsepelev/store_model/blob/44071d2f5b3367ab19529438dd56fe02f2149a77/lib/store_model/types/one.rb#L49-L50
TL;DR It calls directly as_json
instead of serializing the values as defined.
I worked it around somehow including ActiveModel::Serializers::JSON
and overriding read_attribute_for_serialization
class Message < ApplicationRecord
attribute :payload, MessagePayload.to_type
end
class MessagePayload
include StoreModel::Model
# Workaround for: https://github.com/DmitryTsepelev/store_model/issues/60#issuecomment-962668573
# `as_json` will read values from `read_attribute_for_serialization`
include ActiveModel::Serializers::JSON
attribute :user, ActiveModel::Type::GlobalId.new
def read_attribute_for_serialization(attribute)
attribute_types[attribute].serialize(super)
end
end
module ActiveModel
module Type
# An ActiveModel::Type that serializes and deserializes GlobalID object
class GlobalId < Value
def type
:global_id
end
def serialize(value)
value&.to_gid.try(:to_s)
end
def cast_value(value)
value.is_a?(::String) ? GlobalID::Locator.locate(value) : value
end
def assert_valid_value(value)
value.nil? || value.respond_to?(:to_gid) || value.to_s.start_with?('gid://')
end
end
end
end
Is there any news on this? I'm running into a similar problem, when trying to build a type that encrypts data when stored to the DB and encrypts it on the fly when read.
@23tux @flop @kaelumania could you please take a look at #60? I created a custom type with the serialization method and it seems to work. What am I missing?
@DmitryTsepelev I just stumbled upon this old issue, as I'm trying to implement some encryption handling into StoreModel (again).
My approach seems to be fine for non-OneOf use cases. I tried to make a ready to use file that can be run with ruby debug.rb
:
# frozen_string_literal: true
ENV["BUNDLE_GEMFILE"] = ""
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails", "7.1.3.3"
gem "sqlite3", "1.7.3"
gem "store_model", "3.0.0"
end
require "active_record"
require "active_support/all"
require "store_model"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
create_table :dummies, force: true do |t|
t.json :duck
t.json :pond
t.json :water
end
end
class EncryptedString < ActiveRecord::Type::String
def deserialize(value)
return unless value = super
Rails::Secrets.decrypt(value)
end
def serialize(value)
return unless value = super
Rails::Secrets.encrypt(value)
end
end
class Duck
include StoreModel::Model
attribute :name, EncryptedString.new
end
class Pond
include StoreModel::Model
attribute :model, :string
attribute :duck, Duck.to_type
end
class Lake
include StoreModel::Model
attribute :model, :string
attribute :duck, Duck.to_type
end
Water = StoreModel.one_of { Lake }
class Dummy < ActiveRecord::Base
attribute :duck, Duck.to_type
attribute :pond, Pond.to_type
attribute :water, Water.to_type
end
dummy = Dummy.create(
duck: { name: "Steve" },
pond: { duck: { name: "John" } },
water: { duck: { name: "Bob" } }
)
dummy.reload
puts dummy.duck.inspect
=> #<Duck name: "Steve">
puts dummy.pond.inspect
=> #<Pond duck: #<Duck name: "John">>
puts dummy.water.inspect
=> #<Lake duck: #<Duck name: "ucN9gjlKAIHdcHrdhw==--N8nHKnRBj6NL4X6a--SFwr8nj1MfiPZlqykMgRww==">>
As you can see, the water
attribute, which has a OneOf
configuration, did NOT successfully decrypt the data. Can you help me find out why? Why isn't the #deserialize
method called from my custom type?
Edit: It seems that it boils down to these lines
https://github.com/DmitryTsepelev/store_model/blob/842a98c78ac47dd9e80b1ca1084f90d51903d33b/lib/store_model/types/one_base.rb#L51-L52
Here, value
is just the json from the DB and it get's decoded and then passed to Lake.new
which does not involve #deserialize
anymore. Is this intended?