JSON serializers for Ruby, built on top of the powerful oj
library.
ActiveModel::Serializer
has a nice DSL, but it allocates many objects leading
to memory bloat, time spent on GC, and lower performance.
Oj::Serializer
provides a similar API, with better performance.
Learn more about how this library achieves its performance.
- Declaration syntax similar to Active Model Serializers
- Reduced memory allocation and improved performance
- Support for
has_one
,has_many
, andbelongs_to
; compose withflat_one
- Useful development checks to avoid typos and mistakes
- Integrates nicely with Rails controllers
- Caching
Add this line to your application's Gemfile:
gem 'oj_serializers'
And then run:
$ bundle install
You can define a serializer by subclassing Oj::Serializer
, and specify which
attributes should be serialized to JSON.
class AlbumSerializer < Oj::Serializer
attributes :name, :genres
attribute \
def release
album.release_date.strftime('%B %d, %Y')
end
has_many :songs, serializer: SongSerializer
end
Example Output
{
"name": "Abraxas",
"genres": [
"Pyschodelic Rock",
"Blues Rock",
"Jazz Fusion",
"Latin Rock"
],
"release": "September 23, 1970",
"songs": [
{
"track": 1,
"name": "Sing Winds, Crying Beasts",
"composers": [
"Michael Carabello"
]
},
{
"track": 2,
"name": "Black Magic Woman / Gypsy Queen",
"composers": [
"Peter Green",
"Gábor Szabó"
]
},
{
"track": 3,
"name": "Oye como va",
"composers": [
"Tito Puente"
]
},
{
"track": 4,
"name": "Incident at Neshabur",
"composers": [
"Alberto Gianquinto",
"Carlos Santana"
]
},
{
"track": 5,
"name": "Se acabó",
"composers": [
"José Areas"
]
},
{
"track": 6,
"name": "Mother's Daughter",
"composers": [
"Gregg Rolie"
]
},
{
"track": 7,
"name": "Samba pa ti",
"composers": [
"Santana"
]
},
{
"track": 8,
"name": "Hope You're Feeling Better",
"composers": [
"Rolie"
]
},
{
"track": 9,
"name": "El Nicoya",
"composers": [
"Areas"
]
}
]
}
To use the serializer, the recommended approach is:
class AlbumsController < ApplicationController
def show
album = Album.find(params[:id])
render json: AlbumSerializer.one(album)
end
def index
albums = Album.all
render json: { albums: AlbumSerializer.many(albums) }
end
end
If you are using Rails you can also use something closer to Active Model Serializers by adding sugar
:
require 'oj_serializers/sugar'
class AlbumsController < ApplicationController
def show
album = Album.find(params[:id])
render json: album, serializer: AlbumSerializer
end
def index
albums = Album.all
render json: albums, each_serializer: AlbumSerializer, root: :albums
end
end
It's recommended to create your own BaseSerializer
class in order to easily
add custom extensions, specially when migrating from active_model_serializers
.
In order to efficiently reuse the instances, serializers can't be instantiated directly. Use one
and many
to serialize objects or enumerables:
render json: {
favorite_album: AlbumSerializer.one(album),
purchased_albums: AlbumSerializer.many(albums),
}
You can use these serializers inside arrays, hashes, or even inside ActiveModel::Serializer
by using a method in the serializer.
Follow this discussion to find out more about the raw_json
extensions that made this high level of interoperability possible.
Attributes methods can be used to define which model attributes should be serialized to JSON. Each method provides a different strategy to obtain the values to serialize.
The internal design is simple and extensible, so creating new strategies requires very little code. Please open an issue if you need help 😃
Obtains the attribute value by calling a method in the object being serialized.
class PlayerSerializer < Oj::Serializer
attributes :full_name
end
Have in mind that unlike Active Model Serializers, it will not take into account methods defined in the serializer. Being explicit about where the attribute is coming from makes the serializers easier to understand and more maintainable.
Obtains the attribute value by calling a method defined in the serializer.
You may call serializer_attributes
or use the attribute
inline syntax:
class PlayerSerializer < Oj::Serializer
attribute \
def full_name
"#{player.first_name} #{player.last_name}"
end
end
Instance methods can access the object by the serializer name without the
Serializer
suffix, player
in the example above, or directly as @object
.
You can customize this by using object_as
.
Works like attributes
in Active Model Serializers, by calling a method in the serializer if defined, or calling read_attribute_for_serialization
in the model.
class AlbumSerializer < Oj::Serializer
ams_attributes :name, :release
def release
album.release_date.strftime('%B %d, %Y')
end
end
Should only be used when migrating from Active Model Serializers, as it's slower and can create confusion.
Instead, use attributes
for model methods, and the inline attribute
for serializer attributes. Being explicit makes serializers easier to understand, and to maintain.
Please refer to the migration guide for more information.
Very convenient when serializing Hash-like structures, this strategy uses the []
operator.
class PersonSerializer < Oj::Serializer
hash_attributes 'first_name', :last_name
end
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
# {"first_name":"Mary","last_name":"Watson"}
Reads data directly from attributes
in a Mongoid document.
By skipping type casting, coercion, and defaults, it achieves the best performance.
Although there are some downsides, depending on how consistent your schema is, and which kind of consumer the API has, it can be really powerful.
class AlbumSerializer < Oj::Serializer
mongo_attributes :id, :name
end
Use has_one
to serialize individual objects, and has_many
to serialize a collection.
The value for the association is obtained from a serializer method if defined, or by calling the method in the object being serialized.
You must specificy which serializer to use with the serializer
option.
class SongSerializer < Oj::Serializer
has_one :album, serializer: AlbumSerializer
has_many :composers, serializer: ComposerSerializer
# You can also compose serializers using `flat_one`.
flat_one :song, serializer: SongMetadataSerializer
end
The associations DSL is more concise and achieves better performance, so prefer to use it instead of manually definining attributes:
class SongSerializer < SongMetadataSerializer
attribute \
def album
AlbumSerializer.one(song.album)
end
attribute \
def composers
ComposerSerializer.many(song.composers)
end
end
You can use object_as
to create an alias for the serialized object to access it from instance methods:
class DiscographySerializer < Oj::Serializer
object_as :artist
# Now we can use `artist` instead of `object` or `discography`.
def latest_albums
artist.albums.desc(:year)
end
end
All the attributes and association methods can take an if
option to render conditionally.
class AlbumSerializer < Oj::Serializer
mongo_attributes :release_date, if: -> { album.released? }
has_many :songs, serializer: SongSerializer, if: -> { album.songs.any? }
# You can achieve the same by manually defining a method:
def include_songs?
album.songs.any?
end
end
Serializers are designed to be stateless so that an instanced can be reused, but sometimes it's convenient to store intermediate calculations.
Use memo
for memoization and storing temporary information.
class DownloadSerializer < Oj::Serializer
attributes :filename, :size
attribute \
def progress
"#{ last_event&.progress || 0 }%"
end
private
def last_event
memo.fetch(:last_event) {
download.events.desc(:created_at).first
}
end
end
The hashed form of the serializer can also be returned (does not impact string serialization).
class PersonSerializer < Oj::Serializer
attributes :first_name, :last_name
end
# person = Person.new(first_name: 'Mary', last_name: 'Watson')
PersonSerializer.one_as_hash(person)
# { first_name: 'Mary', last_name: 'Watson' }
# people = [...]
PersonSerializer.many_as_hash(people)
# [{ first_name:"Mary", last_name:"Watson" }, ...]
Use cached
to leverage key-based caching, which calls cache_key
in the object. You can also provide a lambda to cached_with_key
to define a custom key:
class CachedUserSerializer < UserSerializer
cached_with_key ->(user) {
"#{ user.id }/#{ user.current_sign_in_at }"
}
end
It will leverage fetch_multi
when serializing a collection with many
or has_many
, to minimize the amount of round trips needed to read and write all items to cache. This works specially well if your cache store also supports write_multi
.
Usually serialization happens so fast that turning caching on can be slower. Always benchmark to make sure it's worth it, and use caching only for time-consuming or deeply nested structures.
Unlike ActiveModel::Serializer
, which builds a Hash that then gets encoded to
JSON, this implementation uses Oj::StringWriter
to write JSON directly,
greatly reducing the overhead of allocating and garbage collecting the hashes.
It also allocates a single instance per serializer class, which makes it easy to use, while keeping memory usage under control.
ActiveModel::Serializer
instantiates one serializer object per item to be serialized.
Other libraries such as jsonapi-serializer
evaluate serializers in the context of
a class
instead of an instance
of a class. Although it is efficient in terms
of memory usage, the downside is that you can't use instance methods or local
memoization, and any mixins must be applied to the class itself.
panko-serializer
also uses Oj::StringWriter
, but it has the big downside of having to own the entire render tree. Putting a serializer inside a Hash or an Active Model Serializer and serializing that to JSON doesn't work, making a gradual migration harder to achieve. Also, it's optimized for Active Record but I needed good Mongoid support.
Oj::Serializer
combines some of these ideas, by using instances, but reusing them to avoid object allocations. Serializing 10,000 items instantiates a single serializer. Unlike panko-serializer
, it doesn't suffer from double encoding problems so it's easier to use.
As a result, migrating from active_model_serializers
is relatively straightforward because instance methods, inheritance, and mixins work as usual.
Even though most of the examples above use a single-line style to be succint, I highly recommend writing one attribute per line, sorting them alphabetically (most editors can do it for you), and always using a trailing comma.
class AlbumSerializer < Oj::Serializer
attributes(
:genres,
:name,
:release_date,
)
end
It will make things clearer, minimize the amount of git conflicts, and keep the history a lot cleaner and more meaningful when using git blame
.
This library wouldn't be possible without the wonderful and performant oj
library. Thanks Peter! 😃
Also, thanks to the libraries that inspired this one:
active_model_serializers
: For the DSLpanko-serializer
: For validating that usingOj::StringWriter
was indeed fast
The gem is available as open source under the terms of the MIT License.