Better Ruby serialization into JSON

JsonDumper

Serialize Ruby into JSON. Cleaner. Faster.

Rails is at its best when it comes to convention over configuration. Responders gem provides a nice way to serialize ruby objects into json using Object#as_json. Unfortunately this style of serialization crumbles when your app needs to present the same resource in different views, to different users with different permissions.
Of course we have jBuilder. But if you ask me its DSL looks a bit ugly and not intuitive. Also it cannot eager load needed associations so you have to handle this burden yourself.

In this article I am going to explain how to work with JsonDumper gem which hopefully solves former problems.
Its main goals are:

  • help serialize Ruby objects and ActiveRecord objects into json
  • help organize your code
  • solve N+1 query problem

Usage

Let's say you want to serialize Human object, which has many Cars.
First define a class that is going to serialize Human:


class HumanJson < JsonDumper::Base
  def preview
    {
      id: id,
      first_name: first_name,
      last_name: last_name,
      born_at: created_at.strftime('%m/%d/%Y')
    }
  end
end

you can call it like that:


john = Human.create(
  first_name: 'John',
  last_name: 'Doe',
  created_at: 20.years.ago
)

json = HumanJson.preview(john)
json == {
  id: 1,
  first_name: 'John',
  last_name: 'Doe',
  born_at: '09/19/1997'
}

Whenever you invoke a method on a JsonDumper::Base instance and it is missing a similar method is invoked on the object you passed to the serializer. For example in the snippet above a method id is going to be called on john object.


{
  id: id,
  ...
}

Let's introduce an association into the mix:


class CarJson < JsonDumper::Base
  def preview
    {
      id: id,
      name: name,
    }
  end
end

class HumanJson < JsonDumper::Base
  # ...
  def details
    preview.merge(
      car: CarJson.preview(car)
    )
  end
end

ferrari = Car.create(
  name: 'Ferrari',
)
john.car = ferrari

json = HumanJson.details(john)
json == {
  id: 1,
  first_name: 'John',
  last_name: 'Doe',
  born_at: '09/19/1997',
  car: {
    id: 1,
    name: 'Ferrari'
  }
}

This structure provides a very clean way to specify dependencies for ActiveRecord preloader:


class HumanJson < JsonDumper::Base
  def preview
    # ...
  end

  def preview_preload
    {}
  end

  def details
    # ...
  end

  def details_preload
    preview_preload.merge(car: [])
  end
end

Furthermore you can omit defining preview_preload because JsonDumper returns empty hashes ({}) whenever a method does not exist and its name ends with _preload.
You can utilize it in the following way:


preloader = ActiveRecord::Base::Preloader.new
preloader.preload(john, HumanJson.details_preload)
json = HumanJson.details(john)

Another cool feature that you can now do is to do both preloading and serialization in a single command via fetch_METHOD_NAME. This creates a special JsonDumper::Delayed object which delays its execution until it's time to render. This allows to do preloading at render time.
Since this is a common operation you can include JsonDumper::Base in your controller.


class HumansController < ActionController::Base
  include JsonDumper::Helper

  def show
    human = Human.find(params[:id])
    json = dumper_json(
      my_human: HumanJson.fetch_details(human)
    )
    render json: json
  end

  # OR

  def show
    human = Human.find(params[:id])
    render_dumper_json(
      my_human: HumanJson.fetch_details(human)
    )
  end
end

# going to render:
{
  myHuman: {
    id: 1,
    firstName: 'John',
    lastName: 'Doe',
    bornAt: '09/19/1997',
    car: {
      id: 1,
      name: 'Ferrari'
    }
  }
}

Take a note that dumper_json also camelizes your keys.

Usage with Gon

This gem also provides a seamless integration with Gon gem.
The above example could be rewritten in the following way:


class HumansController < ActionController::Base
  def show
    human = Human.find(params[:id])
    gon.my_human = HumanJson.fetch_details(human)
  end
end

Later in your javascript:


console.log(gon.myHuman);

If you have any questions / suggestions feel free to contact me.
Full code of JsonDumper can be found here.

Popular posts from this blog

HTTP server in Ruby 3 - Fibers & Ractors

Next.js: restrict pages to authenticated users

Migration locks for TypeORM