TinySerializer
Simple DSL for converting objects to Hashes, which is mostly API-compatible with ActiveModel::Serializers. Not quite a drop-in replacement, but facilitates migrating away from it. The resulting Hashes can be rendered as JSON by Rails using the ActiveSupport::JSON module.
Unlike AMS, it does not integrate with Rails for automatic usage with the render
function,
which has proven to be one of the biggest sources of confusion and complexity for me.
render json: @object
will continue to work exactly as it does in a base Rails setup.
Use if you have heavily invested in active_model_serializers, but have started experiencing the same frustrations I had with it and can't transition to jsonapi-rb or fast_jsonapi.
Benefits:
- Extremely simple and deterministic behavior; no adapter classes and other complications. Just turns Objects into Hashes.
- Easy to understand; uses
#as_json
to serialize attributes. - Simple and explicit invocation.
- ~200 lines of code, give or take.
- Seems pretty darn fast, at least as fast as
public_send
,Hash#[]=
, andHash#to_json
can be.
Downsides:
- Uses
#as_json
to serialize attributes (maybe unintended consequences, especially with complex objects). - Requires ActiveSupport.
Usage
Serializer definition:
MyObject = Struct.new(:id, :first_name, :last_name, :date) do
def parent; nil; end
def sub_record; nil; end
def related_items; []; end
end
class MyObjectSerializer < TinySerializer
attributes :id,
:first_name,
:last_name,
:date
belongs_to :parent, serializer: ParentSerializer
has_one :sub_record # serializer will be inferred to be SubRecordSerializer
has_many :related_items, serializer: RelatedItemSerializer
attribute :full_name do |object|
"#{object.first_name} #{object.last_name}"
end
end
Notes on blocks:
The object
parameter for blocks is optional, as blocks are executed
in the context of the serializer instance. It just makes it easier
to use the fast_jsonapi gem later if you want.
Usage:
object = MyObject.new(...)
# Several ways to invoke the serializer are available:
MyObjectSerializer.new(object).serialize
MyObjectSerializer.serialize(object)
Produces:
{
"id": 1,
"first_name": "Fred",
"last_name": "Flintstone",
"date": "2000-01-01",
"full_name": "Fred Flintstone",
"parent": null,
"sub_record": null,
"related_items": []
}
Usage in Rails:
In Rails, calling .serialize
is optional because TinySerializer implements all the 'magic' methods (as_json
, to_json
, and serializable_hash
), although I'm not the biggest fan (it loses some explicitness):
@model = MyObject.new(1, "Fred", "Flintstone", Date.new(2000, 1, 1))
render json: MyObjectSerializer.new(@model)
Under the Hood
Takes every attribute you define, and uses it to call #public_send
on the object to serialize,
then uses #as_json
to serialize the resulting value. If you define the attribute with a block, it will call the block to get the value instead.
Usage with Collections:
Collections are handled automatically:
class MyObjectSerializer < TinySerializer
attributes :id, :first_name
end
items = [
MyObject.new(1, "Fred"),
MyObject.new(2, "Wilma")
]
MyObjectSerializer.new(items).serialize
Produces:
[
{"id": 1, "name": "Fred"},
{"id": 2, "name": "Wilma"}
]
Defining Associations and Serializing Sub-Objects
The DSL methods belongs_to
, has_one
, and has_many
are all synonyms for "use this serializer to serialize this property". They are basically syntax sugar for:
class MySerializer < TinySerializer
attribute :parent do |object|
ParentSerializer.new(object.parent).serialize
end
end
These methods all optionally take a block, which you can use to customize the
object or collection. For example, to avoid loading every single item in a
has_many
relation:
class MySerializer < TinySerializer
has_many :items, serializer: ItemSerializer do |object|
object.items.limit(100)
end
end
Links and URL's
When used with Rails, serializer instances have the #url_helpers
method available.
class ItemSerializer < TinySerializer
attribute :link do
url_helpers.item_path(object)
end
end
Notes on ID's.
If you don't want ID's to be serialized as numeric types, you can have them automatically detected and coerced to Strings.
class MyObjectSerializer < TinySerializer
self.coerce_ids_to_string = true
end
To enable globally, but opt out for a specific attribute:
class MyObjectSerializer < TinySerializer
self.coerce_ids_to_string = true
attribute :id, is_id: false
end
Passing options and additional arguments to the serializer
If you pass in an options object as the second argument to the serializer, it will be available for use within the serializer. For example:
class MyObjectSerializer < TinySerializer
attribute :id do |object, options|
options[:prefix] + object.id
end
end
MyObjectSerializer.new(object, { prefix: "prefix_" }).serialize
# Produces:
{
id: "prefix_id"
}
Attribute Inheritance
Attributes are inherited from parent classes, but can be extended.
This example works as you would expect from active_model_serializers
(the name
attribute will only appear if you use MyObjectSerializer::WithName.serialize(my_object)
):
class MyObjectSerializer < TinySerializer
attribute :id
class WithName < MyObjectSerializer
attribute :name
end
end
Installation
Add this line to your application's Gemfile:
gem 'tiny_serializer', '~> 2.0.0'
And then execute:
$ bundle
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/wmakley/tiny_serializer.
License
The gem is available as open source under the terms of the MIT License.