How to create a serializer
Last Updated:
A serializer is a class that transform's an object, usually a model, into JSON format for an API response. In Vets API, we use a serialization gem called jsonapi-serializer. This gem serializes the object according to the JSON:API specifications: https://jsonapi.org.
In rare cases you may not be able to follow the JSON:API specs, so you'll need to create a custom serializer. More information on that can be found near the bottom of the page.
Building a JSON:API serializer
The required components of the serializer are the module inclusion, the id
and type
. By default the id
is Object#id
and the type is based on the class name of the serializer, in this case "Movie" is the default. These values can be overridden with set_type
and set_id
.
class MovieSerializer
include JSONAPI::Serializer
set_type :motion_picture
set_id :owner_id
attributes :name
attribute :released_in_year, &:year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
end
class MovieController
def index
movies = Movie.all
render json: MovieSerializer.new(movies)
end
def show
movie = Movie.find(params[:id])
render json: MovieSerializer.new(movie)
end
end
Visit the gem's GitHub repo for more documentation: https://github.com/jsonapi-serializer/jsonapi-serializer
Best practices
Naming the serializer
Before you start listing the attributes, you need to name your serializer. It's ALMOST always the class name (or ancestor) of the object you're serializing. For example, if I want to add a SavedClaim
then the serializer should be named SavedClaimSerializer
. If the object class is RatedDisabilitiesResponse
then the serializer should be named RatedDisabilitiesResponseSerializer
. By naming the serializer based on the object you'll avoid unintended confusion and difficulty understanding what's being serialized.
If in the rare situation you're serializing a Hash, you can name the serializer whatever you feel is appropriate.
set_id
In some cases there isn't an id or appropriate alternative. Because jsonapi-serializer requires a not-null id
, you can use the following to set the id
to a blank string.
set_id { '' }
Url helpers
When using a link with a url helper there are two ways to load the URL helper(s):
class MessagesSerializer
include JSONAPI::Serializer
singleton_class.include Rails.application.routes.url_helpers
link :download do |object|
v0_message_attachment_url(object.message_id, object.id)
end
end
or
class FolderSerializer
include JSONAPI::Serializer
link :self do |object|
Mobile::UrlHelper.new.v0_folder_url(object.id)
end
end
Guard statements in blocks
When using a Guard Statement it's best to use next
instead of return
attribute :account_type do |object|
next nil if object.account.deposit_type.blank?
object.account.deposit_type == 'C' ? 'Checking' : 'Savings'
end
Writing a unit test
Here's a template you can use to create a serializer spec:
describe ExampleSerializer, type: :serializer do
subject { serialize(example, serializer_class: described_class) }
let(:example) { build_stubbed(:example_factory) }
let(:data) { JSON.parse(subject)['data'] }
let(:attributes) { data['attributes'] }
let(:links) { data['links'] }
let(:relationships) { data['relationships'] }
it 'includes :id' do
expect(data['id']).to eq example.id
end
it 'includes :type' do
expect(data['type']).to eq 'examples'
end
it 'includes :attribute_name' do
expect(attributes['attribute_name']).to eq example.attribute_name
end
it 'includes :time_attribute' do
expect_time_eq(attributes['time_attribute'], example.time_attribute)
end
it 'includes :download link' do
expected_url = v0_example_url(example.id)
expect(links['download']).to eq expected_url
end
it 'includes :attachments' do
expect(relationships['attachments']['data'].size).to eq example.attachments.size
end
end
Key components of a spec
Uses the serializer class as the example group name:
ExampleSerializer
The type of spec is specified:
type: :serializer
A factory object is stubbed. Only create if necessary
The subject is at the to top and utilizes the serializer helper method
serialize
id
andtype
are presentEach top-level attribute is checked
Time-based checks should use the
expect_time_eq
helper
Custom serializer
jsonapi-serializer can be somewhat rigid with it's implementation. If some required functionality is not available in jsonapi-serializer or you can’t use JSON:API specification, you can create a custom serializer class. Here's a straightforward and flexible template:
class ExampleSerializer
def initialize(resource)
@resource = resource
end
# this is needed for ActionController `render json:`
def to_json(*)
Oj.dump(serializable_hash, mode: :compat, time_format: :ruby)
end
def serializable_hash
if collection?(@resource)
serialize_collection(@resource)
else
serialize_resource(@resource)
end
end
private
def collection?(resource)
resource.is_a?(Enumerable) && !resource.is_a?(:each_pair)
end
def serialize_collection(resources)
{
data: resources.map { |resource| serialize_resource(resource) }
}
end
# customize your json here
def serialize_resource(resource)
{
id: resource.id.to_s
another_attribute: resource.another_attribute
custom_attribute: customer_attribute(resource)
some_association: some_association(resource)
}
end
def customer_attribute(resource)
# some complex business logic related to serialization suc
end
def some_association(resource)
{
id: resource.association.id,
associated_attribute: resource.associated_attribute.id
}
end
end
class ExamplesController
def index
examples = Example.all
render json: ExampleSerializer.new(examples)
end
def show
example = Example.find(params[:id])
render json: ExampleSerializer.new(example)
end
end
Another example
You could use a custom serializer to exclude object associations in a collection, but not for a single object like this:
def serialize_collection(resources)
{
data: resources.map { |resource| serialize_resource(resource, is_collection: true)[:data] }
}
end
def serialize_resource(resource, is_collection: false)
serialized_data = {
id: resource.id.to_s,
another_attribute: resource.another_attribute,
custom_attribute: customer_attribute(resource)
}
serialized_data[:some_association] = some_association(resource) unless is_collection
{ data: serialized_data }
end
Simpler custom serializer
If you want an even simpler serializer for a single object, you can use this template:
class SimplestSerializer
def initialize(resource)
@resource = resource
end
def to_json(*)
Oj.dump(serializable_hash, mode: :compat, time_format: :ruby)
end
def serializable_hash
{
foo: @resource.foo,
bar: @resource.bar,
baz: @resource.baz
}
end
end
Help and feedback
Get help from the Platform Support Team in Slack.
Submit a feature idea to the Platform.