Serega Ruby Serializer
The Serega Ruby Serializer provides easy and powerful DSL to describe your objects and to serialize them to Hash or JSON.
📌 Serega does not depend on any gem and works with any framework
It has some great features:
- Manually select serialized fields
- Secure from malicious queries with depth_limit plugin
- Solutions for N+1 problem (via batch, preloads or activerecord_preloads plugins)
- Built-in object presenter (presenter plugin)
- Adding custom metadata (via metadata or context_metadata plugins)
- Value formatters (formatters plugin) helps to transform time, date, money, percentage and any other values same way keeping code dry
- Conditional attributes - (if plugin)
- Auto camelCase keys - camel_case plugin
Installation
bundle add serega
Define serializers
Most apps should define base serializer with common plugins and settings to not repeat them in each serializer.
Serializers will inherit everything (plugins, config, attributes) from their superclasses.
class AppSerializer < Serega
# plugin :one
# plugin :two
# config.one = :one
# config.two = :two
end
class UserSerializer < AppSerializer
# attribute :one
# attribute :two
end
class CommentSerializer < AppSerializer
# attribute :one
# attribute :two
end
Adding attributes
class UserSerializer < Serega
# Regular attribute
attribute :first_name
# Option :method specifies method that must be called on serialized object
attribute :first_name, method: :old_first_name
# Block is used to define attribute value
attribute(:first_name) { |user| user.profile&.first_name }
# Option :value can be used with proc or callable object to define attribute value
attribute :first_name, value: UserProfile.new # must have #call method
attribute :first_name, value: proc { |user| user.profile&.first_name }
# Option :delegate can be used to define attribute value.
# Sub-option :allow_nil by default is false
attribute :first_name, delegate: { to: :profile, allow_nil: true }
# Option :delegate can be used with :method sub-option, so method chain here
# is user.profile.fname
attribute :first_name, delegate: { to: :profile, method: :fname }
# Option :const specifies attribute with specific constant value
attribute(:type, const: 'user')
# Option :hide specifies attributes that should not be serialized by default
attribute :tags, hide: true
# Option :serializer specifies nested serializer for attribute
# We can specify serializer as Class, String or Proc.
# Use String or Proc if you have cross references in serializers.
attribute :posts, serializer: PostSerializer
attribute :posts, serializer: "PostSerializer"
attribute :posts, serializer: -> { PostSerializer }
# Option `:many` specifies a has_many relationship
# Usually it is defined automatically by checking `is_a?(Enumerable)`
attribute :posts, serializer: PostSerializer, many: true
# Option `:preload` can be specified when enabled `:preloads` plugin
# It allows to specify associations to preload to attribute value
attribute(:email, preload: :emails) { |user| user.emails.find(&:verified?) }
# Options `:if`, `:unless`, `:if_value`, `:unless_value` can be specified
# when enabled `:if` plugin. They hide attribute key and value from response.
# See more usage examples in :if plugin section.
attribute :email, if: proc { |user, ctx| user == ctx[:current_user] }
attribute :email, if_value: :present?
# Option `:format` can be specified when enabled `:formatters` plugin
# It changes attribute value
attribute :created_at, format: :iso_time
attribute :updated_at, format: :iso_time
# Option `:format` also can be used as Proc
attribute :created_at, format: proc { |time| time.strftime("%Y-%m-%d")}
end
⚠️ Attribute names are checked to include only "a-z", "A-Z", "0-9", "_", "-", "~" characters.
We allow ONLY this characters as we want to be able to use attributes names in URLs without escaping.
This check can be disabled this way:
# Disable globally
Serega.config.check_attribute_name = false
# Disable for specific serializer
class SomeSerializer < Serega
config.check_attribute_name = false
end
Serializing
We can serialize objects using class methods .to_h, .to_json, .as_json and
same instance methods #to_h, #to_json, #as_json.
to_h method is also aliased as call.
user = OpenStruct.new(username: 'serega')
class UserSerializer < Serega
attribute :username
end
UserSerializer.to_h(user) # => {username: "serega"}
UserSerializer.to_h([user]) # => [{username: "serega"}]
UserSerializer.to_json(user) # => '{"username":"serega"}'
UserSerializer.to_json([user]) # => '[{"username":"serega"}]'
UserSerializer.as_json(user) # => {"username":"serega"}
UserSerializer.as_json([user]) # => [{"username":"serega"}]
If you always serialize same attributes it will make sense to save instance of serializer and reuse this instance, it will be a bit faster (fields will be prepared only once).
# Example with all fields
serializer = UserSerializer.new
serializer.to_h(user1)
serializer.to_h(user2)
# Example with custom fields
serializer = UserSerializer.new(only: [:username, :avatar])
serializer.to_h(user1)
serializer.to_h(user2)
⚠️ When you serialize Struct object, specify manually many: false. As Struct
is Enumerable and we check object.is_a?(Enumerable) to detect if we should
return array.
UserSerializer.to_h(user_struct, many: false)
Selecting Fields
By default all attributes are serialized (except marked as hide: true).
We can provide modifiers to select only needed attributes:
- only - lists attributes to serialize;
- except - lists attributes to not serialize;
- with - lists attributes to serialize additionally (By default all attributes
are exposed and will be serialized, but some attributes can be hidden when
they are defined with
hide: trueoption, more on this below.withmodifier can be used to expose such attributes).
Modifiers can be provided as Hash, Array, String, Symbol or their combinations.
With plugin string_modifiers we can provide modifiers as
single String with attributes split by comma , and nested values inside
brackets (), like: username,enemies(username,email). This can be very useful
to accept list of fields in GET requests.
When provided non-existing attribute, Serega::AttributeNotExist error will be
raised. This error can be muted with check_initiate_params: false parameter.
class UserSerializer < Serega
plugin :string_modifiers # to send all modifiers in one string
attribute :username
attribute :first_name
attribute :last_name
attribute :email, hide: true
attribute :enemies, serializer: UserSerializer, hide: true
end
joker = OpenStruct.new(
username: 'The Joker',
first_name: 'jack',
last_name: 'Oswald White',
email: 'joker@mail.com',
enemies: []
)
bruce = OpenStruct.new(
username: 'Batman',
first_name: 'Bruce',
last_name: 'Wayne',
email: 'bruce@wayneenterprises.com',
enemies: []
)
joker.enemies << bruce
bruce.enemies << joker
# Default
UserSerializer.to_h(bruce)
# => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
# With `:only` modifier
fields = [:username, { enemies: [:username, :email] }]
fields_as_string = 'username,enemies(username,email)'
UserSerializer.to_h(bruce, only: fields)
UserSerializer.new(only: fields).to_h(bruce)
UserSerializer.new(only: fields_as_string).to_h(bruce)
# =>
# {
# :username=>"Batman",
# :enemies=>[{:username=>"The Joker", :email=>"joker@mail.com"}]
# }
# With `:except` modifier
fields = %i[first_name last_name]
fields_as_string = 'first_name,last_name'
UserSerializer.new(except: fields).to_h(bruce)
UserSerializer.to_h(bruce, except: fields)
UserSerializer.to_h(bruce, except: fields_as_string)
# => {:username=>"Batman"}
# With `:with` modifier
fields = %i[email enemies]
fields_as_string = 'email,enemies'
UserSerializer.new(with: fields).to_h(bruce)
UserSerializer.to_h(bruce, with: fields)
UserSerializer.to_h(bruce, with: fields_as_string)
# =>
# {
# :username=>"Batman",
# :first_name=>"Bruce",
# :last_name=>"Wayne",
# :email=>"bruce@wayneenterprises.com",
# :enemies=>[
# {:username=>"The Joker", :first_name=>"jack", :last_name=>"Oswald White"}
# ]
# }
# With not existing attribute
fields = %i[first_name enemy]
fields_as_string = 'first_name,enemy'
UserSerializer.new(only: fields).to_h(bruce)
UserSerializer.to_h(bruce, only: fields)
UserSerializer.to_h(bruce, only: fields_as_string)
# => raises Serega::AttributeNotExist, "Attribute 'enemy' not exists"
# With not existing attribute and disabled validation
fields = %i[first_name enemy]
fields_as_string = 'first_name,enemy'
UserSerializer.new(only: fields, check_initiate_params: false).to_h(bruce)
UserSerializer.to_h(bruce, only: fields, check_initiate_params: false)
UserSerializer.to_h(bruce, only: fields_as_string, check_initiate_params: false)
# => {:first_name=>"Bruce"}
Using Context
Sometimes you can decide to use some context during serialization, like current_user or any.
class UserSerializer < Serega
attribute(:email) do |user, ctx|
user.email if ctx[:current_user] == user
end
end
user = OpenStruct.new(email: 'email@example.com')
UserSerializer.(user, context: {current_user: user})
# => {:email=>"email@example.com"}
UserSerializer.new.to_h(user, context: {current_user: user}) # same
# => {:email=>"email@example.com"}
Configuration
This is initial config options, other config options can be added by plugins
class AppSerializer < Serega
# Configure adapter to serialize to JSON.
# It is `JSON.dump` by default. When Oj gem is loaded then default is
# `Oj.dump(data, mode: :compat)`
config.to_json = ->(data) { Oj.dump(data, mode: :compat) }
# Configure adapter to de-serialize JSON.
# De-serialization is used only for `#as_json` method.
# It is `JSON.parse` by default.
# When Oj gem is loaded then default is `Oj.load(data)`
config.from_json = ->(data) { Oj.load(data) }
# Disable/enable validation of modifiers params `:with`, `:except`, `:only`
# By default it is enabled. After disabling,
# when provided not existed attribute it will be just skipped.
config.check_initiate_params = false # default is true, enabled
# Stores in memory prepared `plans` - list of serialized attributes.
# Next time serialization happens with same modifiers (`only, except, with`),
# we will reuse already prepared `plans`.
# This defines storage size (count of stored `plans` with different modifiers).
config.max_cached_plans_per_serializer_count = 50 # default is 0, disabled
end
Plugins
Plugin :preloads
Allows to define :preloads to attributes and then allows to merge preloads
from serialized attributes and return single associations hash.
Plugin accepts options:
auto_preload_attributes_with_delegate- defaultfalseauto_preload_attributes_with_serializer- defaultfalseauto_hide_attributes_with_preload- defaultfalse
This options are very handy if you want to forget about finding preloads manually.
Preloads can be disabled with preload: false attribute option option.
Also automatically added preloads can be overwritten with manually specified
preload: :another_value.
Some examples, please read comments in the code below
class AppSerializer < Serega
plugin :preloads,
auto_preload_attributes_with_delegate: true,
auto_preload_attributes_with_serializer: true,
auto_hide_attributes_with_preload: true
end
class UserSerializer < AppSerializer
# No preloads
attribute :username
# `preload: :user_stats` added manually
attribute :followers_count, preload: :user_stats,
value: proc { |user| user.user_stats.followers_count }
# `preload: :user_stats` added automatically, as
# `auto_preload_attributes_with_delegate` option is true
attribute :comments_count, delegate: { to: :user_stats }
# `preload: :albums` added automatically as
# `auto_preload_attributes_with_serializer` option is true
attribute :albums, serializer: 'AlbumSerializer'
end
class AlbumSerializer < AppSerializer
attribute :images_count, delegate: { to: :album_stats }
end
# By default preloads are empty, as we specify `auto_hide_attributes_with_preload`
# so attributes with preloads will be skipped so nothing should be preloaded
UserSerializer.new.preloads
# => {}
UserSerializer.new(with: :followers_count).preloads
# => {:user_stats=>{}}
UserSerializer.new(with: %i[followers_count comments_count]).preloads
# => {:user_stats=>{}}
UserSerializer.new(
with: [:followers_count, :comments_count, { albums: :images_count }]
).preloads
# => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
SPECIFIC CASE #1: Serializing same object as association
For example you decided to show your current user as "user" and "user_stats".
Where stats rely on user fields and some other associations.
You should specify preload: nil to preload nested associations, if any, to "user".
class AppSerializer < Serega
plugin :preloads,
auto_preload_attributes_with_delegate: true,
auto_preload_attributes_with_serializer: true,
auto_hide_attributes_with_preload: true
end
class UserSerializer < AppSerializer
attribute :username
attribute :user_stats,
serializer: 'UserStatSerializer',
value: proc { |user| user },
preload: nil
end
SPECIFIC CASE #2: Serializing multiple associations as single relation
For example "user" has two relations - "new_profile", "old_profile", and also
profiles have "avatar" association. And you decided to serialize profiles in one
array. You can specify preload_path: [[:new_profile], [:old_profile]] to
achieve this:
class AppSerializer < Serega
plugin :preloads,
auto_preload_attributes_with_delegate: true,
auto_preload_attributes_with_serializer: true
end
class UserSerializer < AppSerializer
attribute :username
attribute :profiles,
serializer: 'ProfileSerializer',
value: proc { |user| [user.new_profile, user.old_profile] },
preload: [:new_profile, :old_profile],
preload_path: [[:new_profile], [:old_profile]] # <--- like here
end
class ProfileSerializer < AppSerializer
attribute :avatar, serializer: 'AvatarSerializer'
end
class AvatarSerializer < AppSerializer
end
UserSerializer.new.preloads
# => {:new_profile=>{:avatar=>{}}, :old_profile=>{:avatar=>{}}}
SPECIFIC CASE #3: Preload association through another association
attribute :image,
preload: { attachment: :blob }, # <--------- like this one
value: proc { |record| record. },
serializer: ImageSerializer,
preload_path: [:attachment] # or preload_path: [:attachment, :blob]
In this case we don't know if preloads defined in ImageSerializer, should be
preloaded to attachment or blob, so please specify preload_path manually.
You can specify preload_path: nil if you are sure that there are no preloads
inside ImageSerializer.
📌 Plugin :preloads only allows to group preloads together in single Hash, but
they should be preloaded manually.
There are only activerecord_preloads plugin that can be used to preload this associations automatically.
Plugin :activerecord_preloads
(depends on preloads plugin, that must be loaded first)
Automatically preloads associations to serialized objects.
It takes all defined preloads from serialized attributes (including attributes from serialized relations), merges them into single associations hash and then uses ActiveRecord::Associations::Preloader to preload associations to objects.
class AppSerializer < Serega
plugin :preloads,
auto_preload_attributes_with_delegate: true,
auto_preload_attributes_with_serializer: true,
auto_hide_attributes_with_preload: false
plugin :activerecord_preloads
end
class UserSerializer < AppSerializer
attribute :username
attribute :comments_count, delegate: { to: :user_stats }
attribute :albums, serializer: AlbumSerializer
end
class AlbumSerializer < AppSerializer
attribute :title
attribute :downloads_count, preload: :downloads,
value: proc { |album| album.downloads.count }
end
UserSerializer.to_h(user)
# => preloads {users_stats: {}, albums: { downloads: {} }}
Plugin :batch
Must be used to omit N+1 when loading attributes values.
User must provide batch loader object to attribute -
attribute :foo, batch: {loader: SomeLoader, id_method: :id}.
Result must be returned as Hash, where each key is one of provided ids.
class AppSerializer
plugin :batch
end
class UserSerializer < AppSerializer
attribute :comments_count,
batch: { loader: SomeLoader, id_method: :id }
attribute :company,
batch: { loader: SomeLoader, id_method: :id },
serializer: CompanySerializer
end
Option :loader
Loaders can be defined as a Proc, a callable value or a named Symbol
Named loaders should be predefined with
config.batch.define(:loader_name) { |ids| ... })
Loader can accept 1 to 3 arguments:
- List of ids (each id will be found by using
:id_methodoption) - Context
- PlanPoint - a special object containing information about current
attribute and all children and parent attributes. It can be used to preload
required associations to batch values.
See example how
to find required preloads when using with
:preloadsplugin.
class AppSerializer < Serega
plugin :batch, id_method: :id
end
class UserSerializer < Serega
# Define loader as callable object
attribute :comments_count,
batch: { loader: CountLoader }
# Define loader as a Proc
attribute :comments_count,
batch: { loader: proc { |ids| CountLoader.call(ids) } }
# Define loader as a Symbol
config.batch.define(:comments_count_loader) { |ids| CountLoader.call(ids }
attribute :comments_count, batch: { loader: :comments_count_loader }
end
class CountLoader
def self.call(user_ids)
Comment.where(user_id: user_ids).group(:user_id).count
end
end
Option :id_method
Batch plugin can be added with global :id_method option. It can be a Symbol,
Proc or any callable value, which can accept current object and current context.
class SomeSerializer
plugin :batch, id_method: :id
end
class UserSerializer < AppSerializer
attribute :comments_count,
batch: { loader: CommentsCountBatchLoader } # no :id_method here anymore
attribute :company,
batch: { loader: UserCompanyBatchLoader }, # no :id_method here anymore
serializer: CompanySerializer
end
However, global id_method option can be overwritten via config.batch.id_method=
method or in specific attributes with id_method option.
class SomeSerializer
plugin :batch, id_method: :id # global id_method is `:id`
end
class UserSerializer < AppSerializer
# :user_id will be used as default `id_method` for all batch attributes
config.batch.id_method = :user_id
# id_method is :user_id
attribute :comments_count,
batch: { loader: CommentsCountBatchLoader }
# id_method is :user_id
attribute :company,
batch: { loader: UserCompanyBatchLoader }, serializer: CompanySerializer
# id_method is :uuid
attribute :points_amount,
batch: { loader: PointsBatchLoader, id_method: :uuid }
end
Option :default
The default value for attributes without found value can be specified via
:default option. By default attributes without found value will be
serialized as nil. Attribute marked as many: true will be
serialized as empty array []
class UserSerializer < AppSerializer
# Missing values become empty arrays, as `many: true` option specified
attribute :companies,
batch: {loader: proc {}},
serializer: CompanySerializer,
many: true
# Missing values become `0` as specified directly
attribute :points_amount,
batch: { loader: proc {}, default: 0 }
end
Batch attributes can be marked as hidden by default if plugin specified with
auto_hide option. Also auto_hide option can be changed with
config.batch.auto_hide= method.
Look at select serialized fields for more information about hiding/showing attributes.
class AppSerializer
plugin :batch, auto_hide: true
end
class UserSerializer < AppSerializer
config.batch.auto_hide = false
end
⚠️ ATTENTION: Batch plugin must be added to all serializers that have
:batch attributes inside nested serializers. For example when you serialize
User -> Album -> Song and Song has batch attribute, then
batch plugin must be added to the User serializer also.
Best way would be to create one parent AppSerializer < Serega serializer
and add :batch plugin once to this parent serializer.
Plugin :root
Allows to add root key to your serialized data
Accepts options:
- :root - specifies root for all responses
- :root_one - specifies root for single object serialization only
- :root_many - specifies root for multiple objects serialization only
Adds additional config options:
- config.root.one
- config.root.many
- config.root.one=
- config.root_many=
Default root is :data.
Root also can be changed per serialization.
Also root can be removed for all responses by providing root: nil.
In this case no root will be added to response, but you still can to add it per
serialization
#@example Define :root plugin with different options
class UserSerializer < Serega
plugin :root # default root is :data
end
class UserSerializer < Serega
plugin :root, root: :users
end
class UserSerializer < Serega
plugin :root, root_one: :user, root_many: :people
end
class UserSerializer < Serega
plugin :root, root: nil # no root by default
end
# @example Change root per serialization:
class UserSerializer < Serega
plugin :root
end
UserSerializer.to_h(nil) # => {:data=>nil}
UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
UserSerializer.to_h(nil, root: nil) # => nil
Plugin :metadata
Depends on: :root plugin, that must be loaded first
Adds ability to describe metadata and adds it to serialized response
Added class-level method :meta_attribute, to define metadata, it accepts:
*path[Array of Symbols] - nested hash keys.**options[Hash]:const- describes metadata value (if it is constant):value- describes metadata value as any#callableinstance:hide_nil- does not show metadata key if value is nil,falseby default:hide_empty, does not show metadata key if value is nil or empty,falseby default
&block[Proc] - describes value for current meta attribute
class AppSerializer < Serega
plugin :root
plugin :metadata
(:version, const: '1.2.3')
(:ab_tests, :names, value: ABTests.new.method(:names))
(:meta, :paging, hide_nil: true) do |records, ctx|
next unless records.respond_to?(:total_count)
{
page: records.page,
per_page: records.per_page,
total_count: records.total_count
}
end
end
AppSerializer.to_h(nil)
# => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=> ... }}
Plugin :context_metadata
Depends on: :root plugin, that must be loaded first
Allows to provide metadata and attach it to serialized response.
Accepts option :context_metadata_key with name of keyword that must be used to
provide metadata. By default it is :meta
Key can be changed in children serializers using config
config.context_metadata.key=(value)
class UserSerializer < Serega
plugin :root, root: :data
plugin :context_metadata, context_metadata_key: :meta
# Same:
# plugin :context_metadata
# config.context_metadata.key = :meta
end
UserSerializer.to_h(nil, meta: { version: '1.0.1' })
# => {:data=>nil, :version=>"1.0.1"}
Plugin :formatters
Allows to define formatters and apply them on attribute values.
Config option config.formatters.add can be used to add formatters.
Attribute option :format can be used with name of formatter or with
callable instance.
Formatters can accept up to 2 parameters (formatted object, context)
class AppSerializer < Serega
plugin :formatters, formatters: {
iso8601: ->(value) { time.iso8601.round(6) },
on_off: ->(value) { value ? 'ON' : 'OFF' },
money: ->(value, ctx) { value / 10**ctx[:digits) }
date: DateTypeFormatter # callable
}
end
class UserSerializer < Serega
# Additionally we can add formatters via config in subclasses
config.formatters.add(
iso8601: ->(value) { time.iso8601.round(6) },
on_off: ->(value) { value ? 'ON' : 'OFF' },
money: ->(value) { value.round(2) }
)
# Using predefined formatter
attribute :commission, format: :money
attribute :is_logined, format: :on_off
attribute :created_at, format: :iso8601
attribute :updated_at, format: :iso8601
# Using `callable` formatter
attribute :score_percent, format: PercentFormmatter # callable class
attribute :score_percent, format: proc { |percent| "#{percent.round(2)}%" }
end
Plugin :presenter
Helps to write clear code by adding attribute names as methods to Presenter
class UserSerializer < Serega
plugin :presenter
attribute :name
attribute :address
class Presenter
def name
[first_name, last_name].compact_blank.join(' ')
end
def address
[country, city, address].join("\n")
end
end
end
Plugin :string_modifiers
Allows to specify modifiers as strings.
Serialized attributes must be split with , and nested attributes must be
defined inside brackets (, ).
Modifiers can still be provided old way using nested hashes or arrays.
PostSerializer.plugin :string_modifiers
PostSerializer.new(only: "id,user(id,username)").to_h(post)
PostSerializer.new(except: "user(username,email)").to_h(post)
PostSerializer.new(with: "user(email)").to_h(post)
# Modifiers can still be provided old way using nested hashes or arrays.
PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
Plugin :if
Plugin adds :if, :unless, :if_value, :unless_value options to
attributes so we can remove attributes from response in various ways.
Use :if and :unless when you want to hide attributes before finding
attribute value, and use :if_value and :unless_value to hide attributes
after finding final value.
Options :if and :unless accept currently serialized object and context as
parameters. Options :if_value and :unless_value accept already found
serialized value and context as parameters.
Options :if_value and :unless_value cannot be used with :serializer option,
as serialized objects have no "serialized value".
Use :if and :unless in this case.
See also a :hide option that is available without any plugins to hide
attribute without conditions.
Look at select serialized fields for :hide usage examples.
class UserSerializer < Serega
attribute :email, if: :active? # translates to `if user.active?`
attribute :email, if: proc {|user| user.active?} # same
attribute :email, if: proc {|user, ctx| user == ctx[:current_user]}
attribute :email, if: CustomPolicy.method(:view_email?)
attribute :email, unless: :hidden? # translates to `unless user.hidden?`
attribute :email, unless: proc {|user| user.hidden?} # same
attribute :email, unless: proc {|user, context| context[:show_emails]}
attribute :email, unless: CustomPolicy.method(:hide_email?)
attribute :email, if_value: :present? # if email.present?
attribute :email, if_value: proc {|email| email.present?} # same
attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]}
attribute :email, if_value: CustomPolicy.method(:view_email?)
attribute :email, unless_value: :blank? # unless email.blank?
attribute :email, unless_value: proc {|email| email.blank?} # same
attribute :email, unless_value: proc {|email, context| context[:show_emails]}
attribute :email, unless_value: CustomPolicy.method(:hide_email?)
end
Plugin :camel_case
By default when we add attribute like attribute :first_name this means:
- adding a
:first_namekey to resulted hash - adding a
#first_namemethod call result as value
But its often desired to response with camelCased keys.
By default this can be achieved by specifying attribute name and method directly
for each attribute: attribute :firstName, method: first_name
This plugin transforms all attribute names automatically.
We use simple regular expression to replace _x to X for the whole string.
We make this transformation only once when attribute is defined.
You can provide your own callable transformation when defining plugin,
for example plugin :camel_case, transform: ->(name) { name.camelize }
For any attribute camelCase-behavior can be skipped when
camel_case: false attribute option provided.
This plugin transforms only attribute keys,
without affecting root, metadata, context_metadata plugins keys.
If you wish to select serialized fields, you should provide them camelCased.
class AppSerializer < Serega
plugin :camel_case
end
class UserSerializer < AppSerializer
attribute :first_name
attribute :last_name
attribute :full_name, camel_case: false,
value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
end
require "ostruct"
user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
UserSerializer.to_h(user)
# => {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
UserSerializer.new(only: %i[firstName lastName]).to_h(user)
# => {firstName: "Bruce", lastName: "Wayne"}
Plugin :depth_limit
Helps to secure from malicious queries that require to serialize too much or from accidental serializing of objects with cyclic relations.
Depth limit is checked when constructing a serialization plan, that is when
#new method is called, ex: SomeSerializer.new(with: params[:with]).
It can be useful to instantiate serializer before any other business logic
to get possible errors earlier.
Any class-level serialization methods also check depth limit as they also instantiate serializer.
When depth limit is exceeded Serega::DepthLimitError is raised.
Depth limit error details can be found in additional
Serega::DepthLimitError#details method
Limit can be checked or changed with next config options:
config.depth_limit.limitconfig.depth_limit.limit=
There are no default limit, but it should be set when enabling plugin.
class AppSerializer < Serega
plugin :depth_limit, limit: 10 # set limit for all child classes
end
class UserSerializer < AppSerializer
config.depth_limit.limit = 5 # overrides limit for UserSerializer
end
Plugin :explicit_many_option
Plugin requires to add :many option when adding relationships (relationships are attributes with :serializer option specified)
Adding this plugin makes it clearer to find if relationship returns array or single object
class BaseSerializer < Serega
plugin :explicit_many_option
end
class UserSerializer < BaseSerializer
attribute :name
end
class PostSerializer < BaseSerializer
attribute :text
attribute :user, serializer: UserSerializer, many: false
attribute :comments, serializer: PostSerializer, many: true
end
Errors
Serega::SeregaErroris a base error raised by this gem.Serega::AttributeNotExisterror is raised when validating attributes in:only, :except, :withmodifiers
Release
To release a new version, read RELEASE.md.
Development
bundle install- install dependenciesbin/console- open irb console with loaded gemsbundle exec rspec- run testsbundle exec rubocop- check code standardsyard stats --list-undoc --no-cache- view undocumented codeyard server --reload- view code documentation
Contributing
Bug reports, pull requests and improvements ideas are very welcome!
License
The gem is available as open source under the terms of the MIT License.