Skip to main content
Skip table of contents

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.

RUBY
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
RUBY
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.

RUBY
set_id { '' }

Url helpers

When using a link with a url helper there are two ways to load the URL helper(s):

RUBY
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

RUBY
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

RUBY
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:

RUBY
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

  1. Uses the serializer class as the example group name: ExampleSerializer

  2. The type of spec is specified: type: :serializer

  3. A factory object is stubbed. Only create if necessary

  4. The subject is at the to top and utilizes the serializer helper method serialize

  5. id and type are present

  6. Each top-level attribute is checked

  7. 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:

RUBY
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
RUBY
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:

RUBY
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:

RUBY
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

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.