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?

Was it helpful?

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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top