JSON to ActiveRecord (deserialize)
Asked Answered
S

2

7

I'm building a web API with Ruby, Grape and ActiveRecord. Coming from ASP.NET Web API I'm used to having automatic model binding from JSON to a class object which then can be saved with Entity Framework. I have been searching a bit to find out if there is anything similiar to this when using ActiveRecord but haven't found anything which make me think that I'm missing something really obvious. What is the best way to deserialize JSON into ActiveRecord models?

UPDATE

Matts answer worked great for simple types but when I'm having associations I get the following error: ActiveRecord::AssociationTypeMismatch: Connector(#70297711668780) expected, got Hash(#70297701477140)

This is the code I'm running

class Card < ActiveRecord::Base
   has_many    :connectors, autosave: true
end

class Connector < ActiveRecord::Base
   has_one  :card
   has_one  :connected_card, :class_name => "Card", :foreign_key => "connected_card_id"
end

json = "{\"id\":\"1\", \"connectors\": [{\"id\": 1, \"card_id\":1}]}"
card = Card.new(ActiveSupport::JSON.decode(json))
Shalom answered 30/4, 2015 at 13:24 Comment(2)
api.rubyonrails.org/classes/ActiveSupport/JSON.htmlMenton
That will decode the JSON object into a generic hash object. Is there any way to cast it to an ActiveRecord type?Shalom
R
3

In your model Card you have created an association named connectors. This is an ActiveRecord::Relation method, and it will be expecting a bunch of models from the class Connector. You, otherwise, are passing an array of hashes.

{"id"=>"1", "connectors"=>[{"id"=>1, "card_id"=>1}]}

In to your #new method this becomes something like:

Card.new(id: 1, connectors: [{id: 1, card_id: 1}])

Once you get there, the connectors array should be filled with Connector instances, not hashes. And that's why is failing.

You have two options. First is to rename the key in a before_action in the controller and use nested attributes:

class Card < ActiveRecord::Base
  has_many :connectors
  accepts_nested_attributes_for :connectors
end

class CardsController < ApplicationController
  before_action :set_nested

  private
  def set_nested
    params["connectors_attributes"] = params.delete("connectors")
  end
end

Another option is to create a callback in the model to create the objects from the hash.

class Card < ActiveRecord::Base
  def initialize(params={})
    params["connectors"] = params["connectors"].map {|connector| Connector.new(connector)}
    super(params)
  end
end

Any of the above will fit to your needs. IMO the best one is the first, as you won't be redefining ActiveRecord#Methods

Ragouzis answered 30/4, 2015 at 14:27 Comment(2)
Great answer Jorge! :) I understood why I got the error but didn't know how to deal with it. A question regarding your second sugestion, if I where to redefine the intialize method, would that force me to always use a hash to create my models?Shalom
Both answers tell you how to deal with the error. Just copy the code and you should get at least a new error. You always need a hash to create the models. The initiaze(params={}) is active model defaults for initialize.Ragouzis
C
12

You can use standard JSON decoding from ActiveSupport, which will give you a hash.

You will need to then build a new ActiveRecord object from that hash.

my_object = Foo.new(ActiveSupport::JSON.decode(some_json))

Swap new for create if you want it to save in one line.

Chimerical answered 30/4, 2015 at 13:34 Comment(6)
Ok, that worked great when my model only have simple types like int or string. If my model includes associations to other ActiveRecord models I get the following error where Connector is the name of the associated ActiveRecord class: ActiveRecord::AssociationTypeMismatch: Connector(#70114314283600) expected, got Hash(#70114301340420)Shalom
@Shalom Could you include an example json in your question pleaseChimerical
Updated with an exampleShalom
That is because you are using connectors as a key in the hash and in the association, but you are not initializating it. Try renaming the connectors key to connectors_attributesRagouzis
@JorgedelosSantos Would that also require accepts_nested_attributes_for :connectors ?Chimerical
Yes that will be the best solution in order to make rails manage the associations.Ragouzis
R
3

In your model Card you have created an association named connectors. This is an ActiveRecord::Relation method, and it will be expecting a bunch of models from the class Connector. You, otherwise, are passing an array of hashes.

{"id"=>"1", "connectors"=>[{"id"=>1, "card_id"=>1}]}

In to your #new method this becomes something like:

Card.new(id: 1, connectors: [{id: 1, card_id: 1}])

Once you get there, the connectors array should be filled with Connector instances, not hashes. And that's why is failing.

You have two options. First is to rename the key in a before_action in the controller and use nested attributes:

class Card < ActiveRecord::Base
  has_many :connectors
  accepts_nested_attributes_for :connectors
end

class CardsController < ApplicationController
  before_action :set_nested

  private
  def set_nested
    params["connectors_attributes"] = params.delete("connectors")
  end
end

Another option is to create a callback in the model to create the objects from the hash.

class Card < ActiveRecord::Base
  def initialize(params={})
    params["connectors"] = params["connectors"].map {|connector| Connector.new(connector)}
    super(params)
  end
end

Any of the above will fit to your needs. IMO the best one is the first, as you won't be redefining ActiveRecord#Methods

Ragouzis answered 30/4, 2015 at 14:27 Comment(2)
Great answer Jorge! :) I understood why I got the error but didn't know how to deal with it. A question regarding your second sugestion, if I where to redefine the intialize method, would that force me to always use a hash to create my models?Shalom
Both answers tell you how to deal with the error. Just copy the code and you should get at least a new error. You always need a hash to create the models. The initiaze(params={}) is active model defaults for initialize.Ragouzis

© 2022 - 2024 — McMap. All rights reserved.