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
endExample 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
endIf 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
endIt'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
endHave 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
endInstance 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
endShould 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
endUse 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
endThe 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
endYou 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
endAll 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
endSerializers 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
endThe 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 }"
}
endIt 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,
)
endIt 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::StringWriterwas indeed fast
The gem is available as open source under the terms of the MIT License.