سؤال

  • Rails: 3.0.3
  • Ruby: 1.9.2

Trying to deserialize a very simple object using YAML.load or Marshal.load produces a corrupted object because the class which belongs to is not required on the deserializing process.

Example:

# app/models/my_model.rb
class MyModel
  attr_accessor :id
end

# test/unit/serializing_test.rb
require 'test_helper'

class SerializingTest < Test::Unit::TestCase
  def test_yaml_serialize_structure
    my_model = MyModel.new
    my_model.id = 'my model'

    File.open( "#{Rails.root}/tmp/object.yml" , 'w' ) do |f|
      YAML::dump(my_model, f)
    end
  end

  def test_yaml_deserialize_structure
    object = YAML.load_file "#{Rails.root}/tmp/object.yml"
    assert( object.instance_of? MyModel )
    assert_equal( 'my model', object.id )
  end
end

With this code we can run this shell console session without any error:

$ ruby -Itest test/unit/serializing_test.rb -n test_yaml_serialize_structure
$ ruby -Itest test/unit/serializing_test.rb -n test_yaml_deserialize_structure

But if I run the deserialization calls from a Rails console the object is not deserialized properly because the class is never required:

$ rails c
ruby-1.9.2-p0 > object = YAML.load_file "#{Rails.root}/tmp/object.yml"
 => #<Syck::Object:0x0000010322ea30 @class="MyModel", @ivars={"id"=>"my model"}> 

I know the only problem is that the class is not required because if I require it by hand everything works:

ruby-1.9.2-p0 > require "#{Rails.root}/app/models/my_model"
 => ["MyModel"] 
ruby-1.9.2-p0 > object = YAML.load_file "#{Rails.root}/tmp/object.yml"
 => #<MyModel:0x0000010320c8e0 @id="my model"> 

I have presented only the YAML examples but with Marshal is pretty the same.

Also say that although I'm reproducing the problem in a Rails console originally this problem was turning me crazy in a normal request to my application.

So the question is: How can I deserialize objects in Rails without have to require all my classes by hand?

Thanks

f.

هل كانت مفيدة؟

المحلول

Well, after read @tadman and a bunch of answers I have received in the spanish ror mailing list [1] I have collected a few hot tips when you have to deal with Ruby deserializing and class loading in Rails:

Super fast solution

Use config.cache_classes = true in your development.rb but you will lost the class auto-refreshing.

Better solution

Require all the classes that are gonna be deserialized but not with require but with require_dependency[2] so in development environment the class auto-refreshing will remain working.

Elegant solution

Monkey-patch the YAML and the Marshal gem to tell them to call require_dependency when they find a non-defined class to deserialize.

And @Xavi has sent me a proposition of monkey-patch Marshal (he says he wrote it on the air and it is not tested so use it in your own risk) [3]

نصائح أخرى

I described this "issue" on GitHub: https://github.com/rails/rails/issues/1585

To automatically require classes on YAML loading in the manner @fguillen suggests is elegant, I wrote this short monkey-patch.

It simply attempts to require_dependency any class the Psych ToRuby class resolves to classes.

Works for me in a serialised Active Record that stores a custom class instance, YMMV.

module Psych::Visitors
  ToRuby.class_eval do
    alias :resolve_class_without_autoload :resolve_class
    def resolve_class klassname
      begin
        require_dependency klassname.underscore 
      rescue NameError, LoadError
      end
      resolve_class_without_autoload klassname
    end
  end
end

I had to adapt @ben-patterson's answer a bit to make it work (using Rails 5.0.2):

module Psych::Visitors
    ToRuby.class_eval do
        def resolve_class(klassname)
            begin
                class_loader.load klassname
            rescue ArgumentError
                require_dependency klassname.underscore
                klassname.constantize
            end
        end
    end
end

As far as I know, both YAML and Marshal do not make use of the Rails autoloader. You must go ahead and pre-load any classes that might need to be deserialized.

It's a bit if a fuss, especially in the development environment where almost nothing is loaded before it is needed.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top