Serega Cheatsheet

Table of Contents

Your First Serializer

class UserSerializer < Serega
  attribute :name
  attribute :email
end

user = OpenStruct.new(name: "Felonious Gru", email: "gru@example.com")
UserSerializer.to_h(user)
# => {name: "Felonious Gru", email: "gru@example.com"}

Each attribute :name calls object.name and adds it to the hash.


Attribute Options

default β€” calls method of the same name

class UserSerializer < Serega
  attribute :first_name
end

UserSerializer.to_h(OpenStruct.new(first_name: "Felonious"))
# => {first_name: "Felonious"}

method: β€” call a different method

class UserSerializer < Serega
  attribute :name, method: :full_name
end

UserSerializer.to_h(OpenStruct.new(full_name: "Felonious Gru"))
# => {name: "Felonious Gru"}

block β€” nested serializer

A block defines a nested serializer for the attribute value. Requires a base serializer: the base_serializer: option or config.base_serializer (see Relations, Configuration).

class UserSerializer < Serega
  config.base_serializer = Serega

  attribute :statistics, method: :itself do
    attribute :likes_count
    attribute :comments_count
  end
end

UserSerializer.to_h(OpenStruct.new(likes_count: 10, comments_count: 3))
# => {statistics: {likes_count: 10, comments_count: 3}}

value: β€” callable

Signatures: (obj), (obj, ctx:), (obj, batches:), (obj, ctx:, batches:), (obj, context) (see Context, Batch Loading).

class UserSerializer < Serega
  attribute :quote, value: proc { |user, ctx| "#{user.first_name}: #{ctx[:line]}" }
end

UserSerializer.to_h(OpenStruct.new(first_name: "Felonious"),
  context: {line: "Light bulb!"})
# => {quote: "Felonious: Light bulb!"}

delegate: β€” object.to.method

class UserSerializer < Serega
  attribute :city, delegate: {to: :address, allow_nil: true}
end

UserSerializer.to_h(OpenStruct.new(address: nil))
# => {city: nil}

UserSerializer.to_h(OpenStruct.new(address: OpenStruct.new(city: "Suburbia")))
# => {city: "Suburbia"}

delegate: { to: :address, method: :town } calls object.address.town. delegate: { to: :address, method: :town, allow_nil: true } calls object.address&.town.

const: β€” fixed value

class UserSerializer < Serega
  attribute :role, const: "admin"
end

UserSerializer.to_h(OpenStruct.new)
# => {role: "admin"}

default: β€” replace nil

class UserSerializer < Serega
  attribute :nickname, default: "anonymous"
end

UserSerializer.to_h(OpenStruct.new(nickname: nil))
# => {nickname: "anonymous"}

UserSerializer.to_h(OpenStruct.new(nickname: "Vector"))
# => {nickname: "Vector"}

hide: β€” skip unless explicitly requested (see Field Selection)

class UserSerializer < Serega
  attribute :email
  attribute :phone, hide: true
end

UserSerializer.to_h(OpenStruct.new(email: "gru@example.com", phone: "555-1234"))
# => {email: "gru@example.com"}

UserSerializer.to_h(OpenStruct.new(email: "gru@example.com", phone: "555-1234"), with: :phone)
# => {email: "gru@example.com", phone: "555-1234"}

⚠️ Attribute names must match [a-zA-Z0-9_-~] (URL-safe). Disable with config.check_attribute_name = false.


Serialize

class UserSerializer < Serega
  attribute :name
end
user = OpenStruct.new(name: "Felonious Gru")

UserSerializer.to_h(user) # => {name: "Felonious Gru"}
UserSerializer.to_h([user]) # auto array detection => [{name: "Felonious Gru"}]
UserSerializer.to_data(user) # Ruby Data object => #<data name="Felonious Gru">
UserSerializer.to_struct(user) # Ruby Struct object => #<struct name="Felonious Gru">
UserSerializer.to_json(user) # JSON String => "{\"name\":\"Felonious Gru\"}"
UserSerializer.new(only: [:name]).to_h(user) # reuse β€” serialization plan built once

To serialize ids or other references, see Prepare Initial Objects.


Field Selection β€” :only :except :with

class UserSerializer < Serega
  attribute :name
  attribute :email
  attribute :phone, hide: true
end
user = OpenStruct.new(name: "Felonious", email: "gru@example.com", phone: "555-1234")

UserSerializer.to_h(user, only: [:name]) # => {name: "Felonious"}
UserSerializer.to_h(user, except: [:email]) # => {name: "Felonious"}
UserSerializer.to_h(user, with: [:phone]) # => {name: "Felonious", email: "gru@example.com", phone: "555-1234"}
# Nested
UserSerializer.to_h(user, only: [:name, {posts: [:title]}])
UserSerializer.to_h(user, only: "name,posts(title)") # one String, e.g. from GET ?fields=...
# Combined: :except wins, :only and :with both add fields
UserSerializer.to_h(user, only: [:name, :email], except: [:email]) # => {name: "Felonious"}
UserSerializer.to_h(user, only: [:name], with: [:phone]) # => {name: "Felonious", phone: "555-1234"}

Each relation can use its own option, see Combining options.

# Unknown attribute
UserSerializer.to_h(user, only: [:address])
# => raises Serega::AttributeNotExist

UserSerializer.to_h(user, only: [:address], check_initiate_params: false)
# => {}

Context

class UserSerializer < Serega
  attribute :email, value: proc { |user, ctx| user.email if ctx[:current_user] == user }
end
user = OpenStruct.new(email: "gru@example.com")

UserSerializer.to_h(user, context: {current_user: user}) # => {email: "gru@example.com"}
UserSerializer.to_h(user, context: {current_user: nil}) # => {email: nil}

Inside presenter methods context is accessed via __ctx__ (see Presenter).


Conditional Attributes β€” :if :unless

Option Sees Decides before
if: (object, ctx) computing value
unless: (object, ctx) computing value
if_value: (value, ctx) after value built
unless_value: (value, ctx) after value built
class UserSerializer < Serega
  attribute :email, if: proc { |user, ctx| ctx[:current_user] == user }
  attribute :nickname, unless_value: :empty?
end

user = OpenStruct.new(email: "gru@example.com", nickname: "")

UserSerializer.to_h(user, context: {current_user: nil}) # => {}
UserSerializer.to_h(user, context: {current_user: user}) # => {email: "gru@example.com"}

Symbol short form: if: :active? β†’ calls object.active?.

⚠️ if_value/unless_value cannot be combined with :serializer or a block β€” use if/unless there.

For unconditional hiding, prefer hide: true β€” see Attribute Options and Field Selection.


Formatters β€” format:

class AppSerializer < Serega
  formatter :iso8601, [:iso8601, 3], allow_nil: true # value&.iso8601(3)
  formatter :yes_no, ->(value) { value ? "yes" : "no" }
  formatter(:money) { |value, ctx| "$#{value / (10**ctx[:digits])}" }
end

class UserSerializer < AppSerializer
  attribute :balance, format: :money
  attribute :active, format: :yes_no
  attribute :role, format: [:to_s]                 # inline: value.to_s
  attribute :score, format: proc { |v| "#{v}%" } # inline
end

UserSerializer.to_h(OpenStruct.new(balance: 100_000, active: true, role: :admin, score: 87),
  context: {digits: 2})
# => {balance: "$1000", active: "yes", role: "admin", score: "87%"}

Relations

class CommentSerializer < Serega
  attribute :body
end

class PostSerializer < Serega
  attribute :title
  attribute :comments, serializer: CommentSerializer
end

post = OpenStruct.new(title: "How to Steal the Moon",
  comments: [OpenStruct.new(body: "Great post!")])
PostSerializer.to_h(post)
# => {title: "How to Steal the Moon", comments: [{body: "Great post!"}]}
# Cyclic references / lazy
attribute :comments, serializer: "CommentSerializer"
attribute :comments, serializer: -> { CommentSerializer }
attribute :comments, serializer: CommentSerializer, many: true # force collection
# Inline nested serializer via block. It inherits from `base_serializer:`
# option / `config.base_serializer` (attributes of the base, if any, are
# serialized too).
attribute :comments, base_serializer: AppSerializer do
  attribute :body
end

DB preloads for :serializer attributes β€” see Preloads. Force many: on every relation β€” see Required many:.


Batch Loading (N+1)

class UserSerializer < Serega
  batch :comments_count, ->(users) { users.to_h { |u| [u.id, u.id * 10] } }

  attribute :comments_count, batch: true
end

UserSerializer.to_h([OpenStruct.new(id: 1), OpenStruct.new(id: 2)])
# => [{comments_count: 10}, {comments_count: 20}]

Other forms:

attribute :comments_count, batch: :comments_count # named loader by name (same as batch: true above)
attribute :comments_count, batch: {use: :comments_count, id: :uuid} # hash form, only for sub-options
attribute :comments_count, batch: {use: ->(users) { Comment.where(user: users).group(:user_id).count }} # inline loader

# Multiple loaders β€” note the keyword is `batches:` (plural)
attribute :total_likes, batch: {use: [:fb_likes, :tw_likes]},
  value: proc { |user, batches:| batches[:fb_likes][user.id] + batches[:tw_likes][user.id] }

⚠️ Value-proc keyword is batches: (plural). config.batch_id_option = :id is the default ID method.

Pairs well with :preload to avoid N+1 inside loaders (see Preloads).


Preloads

class PostSerializer < Serega
  attribute :verified_comments, preload: :comments,
    value: proc { |post| post.comments.select(&:verified?) }
end

preload: value semantics

The value is passed to the preload handler exactly as written β€” :activerecord_preloads hands it straight to ActiveRecord::Associations::Preloader.

Value Effect
:assoc preloads the :assoc association onto the objects to serialize
[:a, :b] / {a: :b} / custom passed through as-is to the preload handler
nil preloads nothing (also blocks auto_preload)
false preloads nothing (also blocks auto_preload)
true invalid β€” raises when the attribute is defined
class AddressSerializer < Serega
  attribute :country, preload: :country
end
class UserSerializer < Serega
  attribute :address, serializer: AddressSerializer, preload: :address
end

:address is preloaded onto the users, and :country onto the addresses.

How preloads actually work

The loading is done by a handler registered with preload_with, called once per preloaded attribute with the gathered objects and that attribute’s :preload value:

class AppSerializer < Serega
  preload_with { |objects, preloads| MyORM.preload(objects, preloads) }
end

plugin :activerecord_preloads registers one for you (built on ActiveRecord::Associations::Preloader), so enabling it loads every declared association once, automatically β€” no N+1. For another ORM β€” or for plain non-ORM objects β€” register your own handler: it can load data from any source and attach it to the objects by your own rules. Declaring :preload with no registered handler raises an error.

The handler runs once per preloaded attribute β€” the same :preload value on several attributes calls it once for each, so a custom handler should check whether the data is already loaded before loading it again.

Because the :preload value is yours to choose (not necessarily an association name), it doubles as a discriminator β€” give attributes different :preload values and branch on preloads in the handler:

attribute :owner, serializer: UserSerializer, preload: :owner
attribute :author, serializer: UserSerializer, preload: :author

preload_with do |objects, preloads|
  case preloads
  when :owner then load_owners(objects)
  when :author then load_authors(objects)
  else MyORM.preload(objects, preloads)
  end
end

config.auto_preload saves you from writing preload: by hand β€” when enabled, attributes with :serializer or :delegate get a :preload inferred from that option’s target.


Serializing Hash Records (hash_access:)

# objects should implement #[] and #fetch β€” Hash already does
class UserSerializer < Serega
  # `true` is a shorthand for config.hash_access.default_mode (:symbol by default)
  attribute :name, hash_access: true     # record[:name]
  attribute :role, hash_access: :string  # record["role"]
end

UserSerializer.to_h({:name => "Kate", "role" => "admin"})
# => {name: "Kate", role: "admin"}
# allow_missing_key: nil instead of raising KeyError
class UserSerializer < Serega
  attribute :middle_name, hash_access: {allow_missing_key: true}
  attribute :status, hash_access: {allow_missing_key: true}, default: "active"
end

UserSerializer.to_h({})
# => {middle_name: nil, status: "active"}
# config.hash_access.default_mode / default_allow_missing_key set the defaults for every attribute
class UserSerializer < Serega
  config.hash_access.default_mode = :string
  config.hash_access.default_allow_missing_key = true

  attribute :middle_name, hash_access: true
end

UserSerializer.to_h({})
# => {middle_name: nil}
# delegate: configures hash access per step β€”
# to_hash_access: for the intermediate (:to) read, hash_access: for the final read
class UserSerializer < Serega
  attribute :city, delegate: {to: :address, hash_access: :symbol} # record.address[:city]
end

UserSerializer.to_h(OpenStruct.new(address: {city: "Paris"}))
# => {city: "Paris"}
class UserSerializer < Serega
  attribute :city, delegate: {to: :address, to_hash_access: :symbol, hash_access: :symbol} # record[:address][:city]
end

UserSerializer.to_h({address: {city: "Paris"}})
# => {city: "Paris"}

Sharing Setup via Inheritance

A child serializer inherits attributes, config, and plugins from its parent. Define a base serializer once, then put shared setup there.

class AppSerializer < Serega
  config.check_initiate_params = false # shared by every subclass
  config.base_serializer = self # nested serializers from attribute blocks inherit AppSerializer
end

class UserSerializer < AppSerializer
  attribute :name
end

class PostSerializer < AppSerializer
  attribute :title
end

Both UserSerializer and PostSerializer now skip unknown fields β€” no need to configure them twice.

Set defaults in the base class β€” see Configuration.


Prepare Initial Objects

class UserSerializer < Serega
  # OccamsRecord β€” faster, low-memory, read-only records
  prepare_initial_objects { |user_ids| OccamsRecord.query(User.where(id: user_ids)).run }

  attribute :first_name
end

UserSerializer.to_h(["17"]) # => [{first_name: "Ann"}]

Other forms:

# context as a second positional argument
prepare_initial_objects { |ids, ctx| OccamsRecord.query(User.where(id: ids, account: ctx[:account])).run }

# context as a :ctx keyword argument
prepare_initial_objects { |ids, ctx:| OccamsRecord.query(User.where(id: ids, account: ctx[:account])).run }

# any callable value
prepare_initial_objects UsersLoader

⚠️ Runs only for the objects given to the serializer, not for objects of nested serializers. Runs before :many is detected, so it may turn a single object into a collection.


Configuration

config.auto_preload β€” auto-add :preload from :delegate / :serializer

class UserSerializer < Serega
  config.auto_preload = true
  attribute :city, delegate: {to: :address, method: :city}
end
# `:address` is preloaded automatically (from the :delegate option)

Default false. true is sugar for both flags on. Pass a Hash to opt in to just one:

class UserSerializer < Serega
  config.auto_preload = {has_serializer_option: true, has_delegate_option: false}
  attribute :city, delegate: {to: :address, method: :city}   # no auto-preload
  attribute :posts, serializer: PostSerializer                     # auto-preloaded
end

See Preloads.

config.hide_by_default β€” hide attrs unless explicitly requested

class UserSerializer < Serega
  config.hide_by_default = :auto # hides attributes with preloads or batch loaders
  attribute :name
  attribute :stats, preload: :stats, value: proc { |u| u.stats }
end

UserSerializer.to_h(user)               # => {name: "Felonious"}        (stats skipped)
UserSerializer.to_h(user, with: :stats) # => {name: "Felonious", stats: 3}

Accepts false (default), true (hide everything) or :auto.

See Field Selection.

config.delegate_default_allow_nil β€” default :allow_nil for every :delegate

class UserSerializer < Serega
  config.delegate_default_allow_nil = true
  attribute :city, delegate: {to: :address}
end
UserSerializer.to_h(OpenStruct.new(address: nil))   # => {city: nil}

Default false β€” without this, a nil target raises NoMethodError.

config.batch_id_option β€” default method for batch IDs

class UserSerializer < Serega
  config.batch_id_option = :uuid
  batch :likes, ->(users) { users.to_h { |u| [u.uuid, 99] } }
  attribute :likes, batch: true   # uses :uuid instead of :id
end
UserSerializer.to_h([OpenStruct.new(uuid: "abc")])   # => [{likes: 99}]

Default :id. See Batch Loading.

config.base_serializer β€” parent for block-defined nested serializers

class AppSerializer < Serega
  config.base_serializer = self   # usually a settings-only serializer
end

class UserSerializer < AppSerializer
  attribute :statistics, method: :itself do  # inherits from AppSerializer
    attribute :likes_count
  end
end

No default β€” an attribute block raises an error when no base serializer is chosen. Can be set per attribute with the base_serializer: option (it wins over config). Attributes of the base, if any, are serialized too.

config.check_initiate_params β€” validate :only / :except / :with

class UserSerializer < Serega
  config.check_initiate_params = false
  attribute :name
end
UserSerializer.to_h(user, only: [:nope])   # => {}        (silently skipped)
# default `true` would raise Serega::AttributeNotExist

Common pattern: false in production, true in dev/test.

config.check_attribute_name β€” URL-safe attribute names

class UserSerializer < Serega
  config.check_attribute_name = false
  attribute :"weird.name"   # would otherwise raise
end

Default true β€” names must match [a-zA-Z0-9_-~].

config.depth_limit β€” deepest nesting of fields

class CommentSerializer < Serega
  config.depth_limit = 2
  attribute :text
  attribute :replies, serializer: -> { CommentSerializer }, many: true, hide: true
end

CommentSerializer.new(with: :replies)              # depth 2
CommentSerializer.new(with: {replies: :replies})
# => raises Serega::DepthLimitError (#details has the path)

Default nil β€” no limit. The root serializer’s limit applies. Checked at .new β€” instantiate before business logic to fail fast on malicious ?with= queries.

config.max_cached_plans_per_serializer_count β€” plan cache

Caches prepared serialization plans by :only / :except / :with signature so repeated requests with the same modifiers skip rebuilding. Default 20 (0 disables it); the plan without modifiers is always cached. No effect on output β€” purely a performance tuning knob.


Presenter

class UserSerializer < Serega
  attribute :full_name
  attribute :role

  presenter do # methods of the wrapped object are available directly
    def full_name
      "#{first_name} #{last_name}"
    end

    def role
      (id == __ctx__[:current_user_id]) ? :self : :other # context via __ctx__
    end
  end
end

UserSerializer.to_h(OpenStruct.new(first_name: "Felonious", last_name: "Gru", id: 1),
  context: {current_user_id: 1})
# => {full_name: "Felonious Gru", role: :self}

__getobj__ returns the wrapped object. __ctx__ exposes the serialization context (see Context).


ActiveRecord Preloads

class AppSerializer < Serega
  plugin :activerecord_preloads
end

UserSerializer.to_h(user) # AR Preloader runs declared preloads automatically

Needs preloads declared on attributes β€” see Preloads.


Required many:

class AppSerializer < Serega
  config.require_many = true
  config.base_serializer = self # for relations defined with a block
end

class PostSerializer < AppSerializer
  attribute :author, serializer: -> { UserSerializer }
end
# => raises Serega::SeregaError: Attribute option :many [Boolean] must be provided

# Fix:
attribute :author, serializer: -> { UserSerializer }, many: false
attribute :comments, serializer: -> { CommentSerializer }, many: true

# Same for relations defined with a block:
attribute :comments, many: true do
  attribute :text
end

Root Key and Metadata

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 :root, root: :users # change for all
plugin :root, root_one: :user, root_many: :users # different keys per shape
plugin :root, root: nil # no wrapping by default

# Per-side override:
config.root.one = nil
config.root.many = nil
class UserSerializer < Serega
  plugin :root
  plugin :metadata
  meta_attribute(:version, const: "1.2.3")
end

UserSerializer.to_h(nil)
# => {data: nil, version: "1.2.3"}
class UserSerializer < Serega
  plugin :root
  plugin :context_metadata # default key :meta β€” merges into top level
end

UserSerializer.to_h(nil, meta: {page: 1})
# => {data: nil, page: 1}

Combined (root_one/root_many + metadata + context_metadata):

ResponseSerializer.to_h(user, meta: {page: 1})
# => {user: {first_name: "Felonious"}, show: "Despicable Me", page: 1}

ResponseSerializer.to_h([walter, lucy], meta: {page: 1})
# => {users: [{first_name: "Felonious"}, {first_name: "Lucy"}], show: "Despicable Me", page: 1}

πŸ’‘ Recommendation: instead of mixing :root, :metadata and :context_metadata, build an envelope serializer with plain :data and :meta attributes β€” then every response field plays by the same rules (field selection, formatters, if/unless):

class ResponseSerializer < AppSerializer
attribute :data, serializer: -> { ... }
attribute :meta # via :value or a nested serializer block
end

Errors

Error When
Serega::SeregaError base class for everything
Serega::AttributeNotExist unknown attr in [Field Selection][field-selection] β€” #serializer, #attributes
Serega::DepthLimitError [config.depth_limit][depth-limit] breach β€” #details