Transmutation is a Ruby gem that provides a simple way to serialize Ruby objects into JSON.
It also adds an opinionated way to automatically find and use serializer classes based on the object's class name and the caller's namespace - it takes inspiration from the Active Model Serializers gem, but strips away adapters.
It aims to be a performant and elegant solution for serializing Ruby objects into JSON, with a touch of opinionated "magic" ✨.
Add the gem to your Gemfile and then run bundle install
to install the gem.
gem "transmutation"
-
Define a serializer class that inherits from
Transmutation::Serializer
and define the attributes to be serialized.class UserSerializer < Transmutation::Serializer attributes :id, :name, :email # You can define custom presentational attributes attribute :has_space_in_name do object.name.include?(" ") end belongs_to :organization # You can also use `has_one` or `association` to define "has one" relationships. has_many :posts # You can also use the `association` method to define "has many" relationships. end
-
Serialize an object using the serializer class.
user = User.new(id: 1, name: "John Doe", email: "[email protected]") user.organization = Organization.new(id: 1, name: "Example Inc.") user.posts = [ Post.new(id: 1, title: "My first post", body: "Sample body"), Post.new(id: 3, title: "This looks promising", body: "More content") ] UserSerializer.new(user).to_json # => "{\"id\":1,\"name\":\"John Doe\",\"has_space_in_name\":true,\"email\":\"[email protected]\",\"organization\":{\"id\":1,\"name\":\"Example Inc.\"},\"posts\":[{\"id\":1,\"title\":\"My first post\",\"body\":\"Sample body\"},{\"id\":3,\"title\":\"This looks promising\",\"body\":\"More content\"}]}"
Formatted JSON output:
{ "id": 1, "name": "John Doe", "has_space_in_name": true, "email": "[email protected]", "organization": { "id": 1, "name": "Example Inc." }, "posts": [ { "id": 1, "title": "My first post", "body": "Sample body" }, { "id": 3, "title": "This looks promising", "body": "More content" } ] }
As long as your object responds to the attributes defined in the serializer, it can be serialized.
Struct
User = Struct.new(:id, :name, :email)
Class
class User attr_reader :id, :name, :email def initialize(id:, name:, email:) @id = id @name = name @email = email end end
ActiveRecord
# == Schema Information # # Table name: users # # id :bigint # name :string # email :string class User < ApplicationRecord end
When you include the Transmutation::Serialization
module in your class, you can use the #serialize
method to serialize an object.
It will attempt to find a serializer class based on the object's class name along with the caller's namespace.
include Transmutation::Serialization
serialize(User.new) # => UserSerializer.new(User.new)
If no serializer class is found, it will return the object as is.
Under the hood, all association
methods, i.e. belongs_to
, has_one
, and has_many
, call #serialize
to find the best-suited serializer class. Once a serializer class has been found once, equal circumstances will use the in-memory cache to return the serializer.
When then Transmutation::Serialization
module is included in a Rails controller, it also extends your render
calls.
class Api::V1::UsersController < ApplicationController
include Transmutation::Serialization
def show
user = User.find(params[:id])
render json: user
end
end
This will attempt to bubble up the controller namespaces to find a defined serializer class:
Api::V1::UserSerializer
Api::UserSerializer
UserSerializer
This calls the #serialize
method under the hood.
If no serializer class is found, it will fall back to the default behavior of rendering the object as JSON.
You can disable this behaviour by passing serialize: false
to the render
method.
render json: user, serialize: false # => user.to_json
You can override the serialization lookup by passing the following options:
-
namespace
: The namespace to use when looking up the serializer class.render json: user, namespace: "V1" # => Api::V1::V1::UserSerializer
To prevent caller namespaces from being appended to the provided namespace, prefix the namespace with
::
.render json: user, namespace: "::V1" # => V1::UserSerializer
The
namespace
key is forwarded to the#serialize
method.render json: user, namespace: "V1" # => serialize(user, namespace: "V1")
-
serializer
: The serializer class to use.render json: user, serializer: "SuperUserSerializer" # => Api::V1::SuperUserSerializer
To prevent all namespaces from being appended to the serializer class, prefix the serializer class with
::
.render json: user, serializer: "::SuperUserSerializer" # => SuperUserSerializer
The
serializer
key is forwarded to the#serialize
method.render json: user, serializer: "SuperUserSerializer" # => serialize(user, serializer: "SuperUserSerializer")
If you follow the pattern outlined below, you can take full advantage of the automatic serializer lookup.
.
└── app/
├── controllers/
│ └── api/
│ ├── v1/
│ │ └── users_controller.rb
│ └── v2
│ └── users_controller.rb
├── models/
│ └── user.rb
└── serializers/
└── api/
├── v1/
│ └── user_serializer.rb
├── v2/
│ └── user_serializer.rb
└── user_serializer.rb
class Api::UserSerializer < Transmutation::Serializer
attributes :id, :name, :email
end
class Api::V1::UserSerializer < Api::UserSerializer
attributes :phone # Added in V1
end
class Api::V2::UserSerializer < Api::UserSerializer
attributes :avatar # Added in V2
end
To remove attributes, it is recommended to redefine all attributes and start anew. This acts as a reset and makes serializer inheritance much easier to follow.
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.