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.