Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested Attributes get not serialized correctly #60

Open
kaelumania opened this issue May 27, 2020 · 10 comments
Open

Nested Attributes get not serialized correctly #60

kaelumania opened this issue May 27, 2020 · 10 comments

Comments

@kaelumania
Copy link

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.

@DmitryTsepelev
Copy link
Owner

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

@DmitryTsepelev
Copy link
Owner

Closing the issue for now

@rmckayfleming
Copy link

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.

@DmitryTsepelev
Copy link
Owner

Yeah, I re–checked it quickly, and looks like a regular #save calls only cast/cast_value

@flop
Copy link

flop commented Oct 2, 2021

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.

@DmitryTsepelev DmitryTsepelev reopened this Oct 3, 2021
@hallelujah
Copy link

hallelujah commented Nov 7, 2021

Yes this is because of

when Hash, @model_klass
ActiveSupport::JSON.encode(value)

TL;DR It calls directly as_json instead of serializing the values as defined.

@makikata
Copy link

makikata commented Nov 8, 2021

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

@23tux
Copy link

23tux commented Apr 21, 2022

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.

DmitryTsepelev added a commit that referenced this issue Jun 14, 2022
@DmitryTsepelev
Copy link
Owner

@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?

@23tux
Copy link

23tux commented Jun 2, 2024

@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

decoded = ActiveSupport::JSON.decode(value) rescue nil
model_instance(decoded) unless decoded.nil?

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants