ActiveModelSerializerPlus

Enhances the standard ActiveModel::Serializers::JSON and ActiveModel::Serializers::Xml modules by adding a default #attributes= method that implements the normal loop used to assign values to attributes. The loop makes use of the #attribute_types hash to convert sub-hashes to objects of the right class and to parse strings into values of the right type (eg. to insure a string containing a time value is converted into a Time object). This allows for automatic deserialization of serialized objects without needing to write code for it.

Installation

Add this line to your application's Gemfile:

gem 'active_model_serializer_plus'

And then execute:

$ bundle

Or install it yourself as:

$ gem install active_model_serializer_plus

Usage

Add the gem's module to your source file:

require 'active_model_serializer_plus`

Then in the class you need to serialize/deserialize you include the Assignment module after including all of the ActiveModel modules you need:

include ActiveModelSerializerPlus::Assignment

The #attributes= method this module provides will raise an ArgumentError with a descriptive message if a problem occurs.

The class will need to implement the #attribute_types method which should return a hash consisting of attribute names and the name of the type/class of the attribute. You only need to include those attributes that are themselves a serializable class and that you want turned back into objects rather than being left as hashes, or attributes that aren't automatically converted from strings back into the correct type (eg. Date, Time, DateTime).

The value can be a string, symbol or class name to specify the type. For containers (arrays or hashes) it is a 2-element array with the first element being the type of the container (array or hash) and the second element specifying the type of the elements in the container. You can also specify the type of the container as just 'Container' or :Container and the code will create a container of the same type (array or hash) as occurs in the JSON. Due to the fact that the JSON doesn't indicate the type of elements in a container there's no straightforward way to have containers contain elements of different types. It's also not possible to declare a container whose elements are containers, elements have to be of a non-container class.

This would be an example:

require 'active_model_serializer_plus'

class Example
    include ActiveModel::Model
    include ActiveModel::Serializers::JSON
    include ActiveModelSerializerPlus::Assignment

    attr_accessor :integer_field
    attr_accessor :time_field
    attr_accessor :string_field
    attr_accessor :object_field
    attr_accessor :array_field

    def attributes
    {
        'integer_field' => nil,
        'time_field' => nil,
        'string_field' => nil,
        'object_field' => nil,
        'array_field' => nil
    }

    def attribute_types
    {
        'time_field' => 'Time',
        'object_field' => 'SomeSerializableClass',
        'array_field' => [ 'Array', 'Integer' ]
    }

    def initialize( i, t, s, o )
    {
        integer_field = i
        time_field = t
        string_field = s
        object_field = o
        array_field = [ 1, 2, 3, 4, 5 ]
    }

end

Examples of using this class to serialize an object to a JSON string:

obj = Example.new( 5, Time.now, 'xyzzy abc', SomeSerializableClass.new )
json_string = obj.to_json

json_string now contains:

{ "integer_field" => 5, "time_field" => "2015-09-26T19:04:07-07:00", "string_field" => "xyzzy abc",
  "object_field" => { ... }, "array_field" => [ 1, 2, 3, 4, 5 ] }

And deserializing that string back to an object:

new_obj = Example.new.from_json( json_string )

new_obj should now be identical to obj, including having time_field being a Time object and object_field being a SomeSerializableClass object initialized from the hash in 'object_field'.

For ActiveModel classes the types could have been included in the #attributes hash, but that would conflict with the use of #attributes in ActiveRecord classes and could cause confusion.

Adding information about new types

In the translations.rb file there are some functions defined on the ActiveModelSerializerPlus module itself. The ones you'll probably find useful are #add_xlate and #add_type which add information about a type to the hashes that control formatting and parsing. You'll need to read the documentation on the hashes themselves for details, but the short form is that #add_type takes the name of a type/class, a Proc that takes and object and formats it into a string, a Proc that takes a string and parses it and initializes an object from it, and a Proc that takes a deserialized hash and constructs an object from it. #add_xlate takes the name of a type and the name of it's pseudo-parent class and adds an entry to the type name translation table. The lookup routines check for a translation first and see if Procs exist for the translated name (the pseudo-parent type). Most of the time you'd omit any new translations and let the lookup routines walk up the normal class inheritance chain to find the correct formatting and parsing Procs. You'd only fill in translations if you had classes that can be treated as derived from a common class but that don't actually derive from any common class. TrueClass and FalseClass are a good example. They don't need a formatting class because they already serialize as true and false which is convenient, but you'll\ find a parsing Proc for the pseudo-class Boolean that correctly parses both of those strings back to true and false values. To set this up you'd do:

add_type('Boolean', nil, Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip.downcase) }, nil)
add_xlate('TrueClass', 'Boolean')
add_xlate('FalseClass', 'Boolean')

which would create the parsing proc entry for Boolean and add the translation entries so that TrueClass and FalseClass act as if derived from an imaginary Boolean class for formatting and parsing purposes.

Adding information about new containers

In assignment.rb there are some functions defined on the ActiveModelSerializersPlus module for adding new containers. The basic one is #add_container which takes 3 arguments: the container type name, a proc for iterating through elements from that kind of container and a proc for adding an item to that kind of container. The procs paper over the differences in containers, without that there'd need to be a separate bit of code for every permutation of source container in the JSON and new destination container. The procs for Hash-like containers are the basis for the code. The iterator proc for an Array-like container uses the array index as the key when calling the adder proc, and the adder proc for Array-like containers ignores the key and just appends the new item to the array. I don't expect a need to add containers often, and since most new containers will act like either Arrays or Hashes there are two convenience functions #add_arraylike_container and #add_hashlike_container that take just the container type name and use the matching standard iterator and adder procs.

Contributing

This project uses git-flow, where new features are developed along feature branches based off the develop branch rather than master. Avoid naming your branches master, develop, hotfix-* or release-* as those conflict with standard branches for hotfixes and releases. You should fork and branch off of the develop branch instead of master, and merge back to develop before creating your pull request.