ActiveRecord from_xml (and from_json) part 2

This post is an upgrade to the previous post about the unmarshalling of XML and JSON strings into rails objects, with arbitrarily deep object associations. As you may know if you are reading this, there doesn’t seem to be a way in rails to reverse to_xml and to_json when associations are included.

Example usage:

xml = firm.to_xml :include => [ :account, :clients ]  
firm = Firm.from_xml xml

firm.account and firm.clients will behave as intended (by default there does not seem to be a direct way in rails to unmarshall them.)

Here’s how to get from_xml and from_json defined for your ActiveRecord classes:

Create a file lib/extensions.rb with the following code:

module ActiveRecord
  class Base
 
    def self.from_hash( hash )
      h = hash.dup
      h.each do |key,value|
        case value.class.to_s
        when 'Array'
          h[key].map! { |e| reflect_on_association(
             key.to_sym ).klass.from_hash e }
        when /\AHash(WithIndifferentAccess)?\Z/
          h[key] = reflect_on_association(
             key.to_sym ).klass.from_hash value
        end
      end
      new h
    end
 
    def self.from_json( json )
      from_hash safe_json_decode( json )
    end
 
    # The xml has a surrounding class tag (e.g. ship-to),
    # but the hash has no counterpart (e.g. 'ship_to' => {} )
    def self.from_xml( xml )
      from_hash begin
        Hash.from_xml(xml)[to_s.demodulize.underscore]
      rescue ; {} end
    end
 
  end # class Base
end # module ActiveRecord
 
### Global functions ###
 
# JSON.decode, or return {} if anything goes wrong.
def safe_json_decode( json )
  return {} if !json
  begin
    ActiveSupport::JSON.decode json
  rescue ; {} end
end

At the bottom of config/environments.rb, insert the line:

require 'lib/extensions' # custom class extensions

You can name the “extensions.rb” file anything else you want; just update the environments.rb file accordingly.

This is an improvement over the previous post, in that it can work even when your associations use the :class_name parameter, you’re not editing the vendor/rails files directly, and it can deal with hashes of the rails class HashWithIndifferentAccess.

Share:
  • del.icio.us
  • Reddit
  • Technorati
  • Twitter
  • Facebook
  • Google Bookmarks
  • HackerNews
  • PDF
  • RSS
This entry was posted in rails and tagged , , , , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.
  • http://www.lastpiecesoftware.com Matt Brown

    Matt – Thanks for the code. Works great. You might want to mention that you have changed the paradigm of the from_xml. It currently is not a self method, so you need to create a object first. Your new method does not require this. I thought your code was not working at first until I noticed this.

  • Tiberiu Motoc

    Hi Matt,

    Thanks so much for this code. There is one modification that I had to do to make it work with my setup: I was trying to import from xml deep object associations exported with to_xml. For example, I could import something like:


    apples
    3

    oranges
    9

    But, an order with many items would be exported as:


    apples
    3

    oranges
    9

    Here’s the modifications that I had to do. Let me know if you spot any issues.

    def self.from_hash( hash )
    h = hash.dup
    h.each do |key,value|

    next if reflect_on_association(key.to_sym).nil?

    if reflect_on_association(key.to_sym).macro == :has_many
    h[key] = h[key][key.singularize].map! { |e|
    reflect_on_association(key.to_sym).klass.from_hash e
    }
    else
    h[key] = reflect_on_association(
    key.to_sym ).klass.from_hash value
    end
    end

    new h
    end

    Again, thanks for the code.

    Tiberiu Motoc

  • Billy Kimble

    Thanks for the snippet of code — it has helped me out tremendously. Unfortunately it did not work out of the box in Rails 2.3.5 Here is my slightly modified fix for it:

    module ActiveRecord
    class Base
    def self.from_hash(hash)
    h = hash.dup
    h.each do |key,value|
    h[key].map! { |e| reflect_on_association(key.to_sym ).klass.from_hash e } if value.is_a?(Array)
    h[key] = reflect_on_association(key.to_sym).klass.from_hash(value) if value.is_a?(Hash)
    end
    new h
    end

    def from_json( json )
    self.class.from_hash(ActiveSupport::JSON.safe_json_decode(json))
    end

    # The xml has a surrounding class tag (e.g. ship-to),
    # but the hash has no counterpart (e.g. ‘ship_to’ => {} )
    def from_xml( xml )
    from_hash begin
    Hash.from_xml(xml)[to_s.demodulize.underscore]
    rescue ; {} end
    end
    end # class Base
    end # module ActiveRecord

    module ActiveSupport
    module JSON
    def self.safe_json_decode(json)
    return {} if !json
    begin
    decode(json)
    rescue ; {} end
    end
    end
    end

  • Tee Parham

    Similarly, I wrote an “array_from_xml” method to deserialize the standard array xml serialization back into an array of objects:

    http://gist.github.com/329698

  • Tyler Collier

    Note: In case it helps anyone else, be sure to use [Your class name].from_xml, and not ActiveRecord::Base.from_xml. Took me a few minutes to figure that out. Was getting the error: “NoMethodError: undefined method `abstract_class?’ for Object:Class”. This was obvious from the example in the older post, but would have been nice to have here too. Matt, for people like me, it’d be great if you could update the old post with a link to this one.

    Thanks for the great code!

  • Tyler Collier

    Matt,

    When I use from_xml, I’m seeing that my objects are inflated correctly, which the exception of their id, which stays nil. Any ideas?

    Ty

  • Tyler Collier

    In case it helps anyone else, I figured out how to populate the id’s. In the “self.from_hash(hash)” method, replace the line “new h” with:

    inflated_object = new h
    if h.key?(‘id’)
    inflated_object.id = h['id']
    end
    inflated_object

    It makes me kind of nervous to be messing with base behavior like this…

  • http://www.xcombinator.com/2008/07/06/activerecord-from_json-and-from_xml/ ActiveRecord from_json and from_xml

    [...] (Edited 2 Sept 2011: Please see instead the updated post here: ActiveRecord from_xml (and from_json) part 2.) [...]

  • Anonymous

    This doesn’t work in Rails 4

    C:/ruby200/lib/ruby/gems/2.0.0/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:229:in `require’: cannot load such file — lib/extensions (LoadError)
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:229:in `block in require’
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:214:in `load_dependency’
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:229:in `require’
    from C:/Users/Chloe/workspace/Tyger/config/environment.rb:7:in `’
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:229:in `require’
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:229:in `block in require’
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:214:in `load_dependency’
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/activesupport-4.0.3/lib/active_support/dependencies.rb:229:in `require’
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/railties-4.0.3/lib/rails/application.rb:189:in `require_environment!’
    from C:/ruby200/lib/ruby/gems/2.0.0/gems/railties-4.0.3/lib/rails/commands.rb:61:in `’
    from bin/rails:4:in `require’
    from bin/rails:4:in `’