data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes
-
27-06-2021 - |
Question
I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Solution 2
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
@attributes ||= []
@attributes.concat vars
super(*vars)
end
def attributes
@attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base
class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.
OTHER TIPS
Datamapper has it’s own serialization library, dm-serializer
, that provides a to_json
method for any Datamapper resource. If you require Datamapper with require 'data_mapper'
in your code, you are using the data_mapper
meta-gem that requires dm-serializer as part of it’s set up.
The to_json
method provided by dm-serializer
only serializes the Datamapper properties of your object (i.e. those you’ve specified with property
) and not the “normal” properties (that you’ve defined with attr_accessor
). This is why you get id
and first_name
but not last_name
.
In order to avoid using dm-serializer
you need to explicitly require those libraries you need, rather than rely on data_mapper
. You will need at least dm-core
and maybe others.
The “normal” json
library doesn’t include any attributes in the default to_json
call on an object, it just uses the objects to_s
method. So in this case, if you replace require 'data_mapper'
with require 'dm-core'
, you will get something like "\"#<Person:0x000001013a0320>\""
.
To create json representations of your own objects you need to create your own to_json
method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json
method you could still call require 'data_mapper'
, your to_json
will replace the one provided by dm-serializer
. In fact dm-serializer
also adds an as_json
method that you could use to create the combined to_json
method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end