Skip to content

Instantly share code, notes, and snippets.

@fgrehm fgrehm/gist:1917450
Created Feb 26, 2012

Embed
What would you like to do?
Virtus custom attribute using coercion lib
require 'json'
Virtus::Coercion::String.class_eval do
def self.to_hash(value)
JSON.parse(value)
end
end
module MyApp
module Attributes
class JSON < Virtus::Attribute::Object
primitive ::Hash
coercion_method :to_hash
end
end
class User
include Virtus
attribute :info, Attributes::JSON
end
end
@user = MyApp::User.new(:info => '{"email":"john@domain.com"}')
@user.info.class # => Hash
@user.info = {'new' => 'info'} # => Doesn't throw an exception :)
@dkubb

This comment has been minimized.

Copy link

dkubb commented Feb 27, 2012

I might suggest doing something like:

def Virtus::Coercion::String.to_json(value)
  JSON.parse(value)
end

module MyApp
  module Attributes
    class JSON < Virtus::Attribute::Object
      coercion_method :to_json
    end
  end
end

By default Virtus::Attribute::Object allows you to use any ruby object, and since JSON can also be an Array, any string that is valid JSON could be coerced.

@dkubb

This comment has been minimized.

Copy link

dkubb commented Feb 27, 2012

Another option could be to push this even higher up and be able to coerce any object that responds to #to_json into JSON, not just a string, eg:

def Virtus::Coercion::Object.to_json(value)
  coerce_with_method(value, :to_json)
end

What this will do is call #to_json on the value if it responds to it, otherwise it will return the value as-is.

The only thing I'm not sure of, and this will require some testing, is how the ::String#to_json method is defined. I don't know if it calls JSON.parse(self) or not. I assume it does, but you probably want to test it to be sure.

@dkubb

This comment has been minimized.

Copy link

dkubb commented Feb 27, 2012

oh, woops. I realize I might be confusing #to_json with the fact that it's really being converted from json. sorry about that. lemme think about a more clear way to handle that.

@dkubb

This comment has been minimized.

Copy link

dkubb commented Feb 27, 2012

How about something like this:

def Virtus::Coercion::String.to_json(value)
  JSON.parse(value)
end

def Virtus::Coercion::Object.to_json(value)
  json = coerce_with_method(value, :to_json)
  json.equal?(value) ? value : String.to_json(json)
end

The implementation of this might work, but I'm having trouble with the proposed name of your Attribute class. I mean, if it really was a JSON field, then I would assume it was a JSON string, not an arbitrary ruby Object (or a Hash if you decide to restrict it that way). The naming is what threw me off above, and it still sticks out as something I might consider changing if I were modelling this.

With Virtus you're more focused around what the type of the attribute in the instance should be, not necessarily the serialization method used for the input. Sure, there are common coercions where it's unambiguous, like coercing "1" into 1 for an Integer attribute, but there's no way to say "this is a Hash, but it could come in as a Hash or as a String in JSON encoding".

I'm almost wondering if this is the kind of thing a form plugin would handle, like @emmanuel's conformitas. If I was writing a pure ruby object without Virtus or another framework I would almost certainly not handle deserialization inside the object, but I would have something that knows how to take the input and then create the object from it. It's often in bloated frameworks where the model handles this (and usually dozens of other responsibilities), but I'm not sure the core objects should know about that kind of thing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.