Configuration

Panko works with zero configuration. Two areas are tunable: auto-specialization, the per-record-class compilation described in Design Choices, and the size limit of the reused JSON writers (writer_pool_max_bytes).

Settings are process-global and read at serialization time. Set them once, in an initializer, before your app starts serializing:

# config/initializers/panko.rb
Panko.configure do |config|
  config.auto_specialization.capacity = 32
end

Panko.configure yields Panko::Config, so the block form above and direct assignment are equivalent:

Panko::Config.auto_specialization.capacity = 32

auto_specialization

When a serializer first sees a given ActiveRecord class, Panko compiles a variant of its generated code specialized for that model. Two settings control this:

Setting Default Meaning
enabled true Whether specialized variants are compiled at all. false routes every record class to the generic code path.
capacity 16 Maximum specialized variants kept per serializer class and output mode. Record classes seen past the cap use the generic path.

Assigning an invalid value raises ArgumentError immediately: enabled must be exactly true or false, and capacity a positive Integer.

capacity

Each serializer keeps at most capacity specialized variants per output mode (JSON and Hash count separately). When a serializer meets record class number capacity + 1, that class, and every later new class, is serialized through the generic path instead, and Panko warns once per serializer class:

UserSerializer auto-specialization capacity (16) reached at AdminUser;
further record classes use the generic path. Raise
Panko::Config.auto_specialization.capacity if this is intentional.

The output is identical either way: the generic path produces the same bytes, it just isn’t specialized. Raise capacity when one serializer legitimately serializes many record classes (a wide STI hierarchy, one serializer reused across many models) and you see the warning.

enabled

Setting enabled = false skips specialization entirely; every record class uses the generic path. Useful when debugging or benchmarking, to rule specialization in or out.

writer_pool_max_bytes

serialize_to_json writes into an Oj::StringWriter and reuses it on the next call on the same thread, which avoids allocating a writer per call. A writer keeps the buffer it grew to, so after a call whose JSON is larger than writer_pool_max_bytes the writer is dropped instead of reused, and the garbage collector frees its buffer.

Setting Default Meaning
writer_pool_max_bytes 1_048_576 (1 MB) Largest JSON output whose writer is kept for reuse.

Each thread keeps one writer (more only when a serializer calls serialize_to_json from inside another serialization), and a kept writer holds less than twice this limit. Lower it to cap per-thread memory further; outputs above the limit pay one writer allocation per call. It must be a positive Integer, and it is read on every call, so a change applies right away.

Panko::Config.writer_pool_max_bytes = 256 * 1024

Compiling at boot

Panko compiles a serializer the first time it is used. In a server that forks workers (Puma or Unicorn with preload_app), every worker repeats that work on its first requests. Panko.compile_all does it once, in the parent process, so forked workers inherit the compiled code.

Declare the record classes each serializer serializes with models:

class PostSerializer < Panko::Serializer
  models Post, Article
  attributes :id, :title
end

models is a hint: a record of any other class still serializes, and gets its specialized variant on first use. Subclasses inherit the list, and calling models again replaces it.

models also tells Panko what a child holds when its source is a plain method rather than an association. Panko learns a child’s class from the parent’s association reflection; a method has none, so without models the child compiles on the generic path:

class Post < ApplicationRecord
  has_many :comments
  has_many :notes

  def visible_comments = comments.reject(&:hidden)
  def attachments = comments.to_a + notes.to_a
end

class CommentSerializer < Panko::Serializer
  models Comment
  attributes :id, :body, :metadata
end

class AttachmentSerializer < Panko::Serializer
  models Comment, Note
  attributes :id, :metadata
end

class PostSerializer < Panko::Serializer
  has_many :visible_comments, serializer: CommentSerializer   # specialized for Comment
  has_many :attachments, serializer: AttachmentSerializer     # one body per model
end
  • One declared model: the child compiles for that class, the same as an association child.
  • Several: the child gets one body per model, picked per record by its exact class. Any other record gets the generic body.
  • A reflection wins over models when the source is an association.
  • A declared model that lacks one of the serializer’s fields is skipped; its records get the generic body.

The specialized body writes json and jsonb columns as their stored text, as 0.8.x did, and reads columns without the generic path’s per-record method calls.

Call compile_all after the app is eager loaded and before workers fork:

# config/initializers/panko.rb
Rails.application.config.after_initialize do
  Panko.compile_all if Rails.application.config.eager_load
end

It compiles every loaded Panko::Serializer subclass that declares a field, for both output modes (modes: [:json] limits it to one). It returns a Panko::CompileAllResult, where each serializer appears in exactly one field:

Field Holds
compiled serializers compiled for every declared model
without_models serializers with no models: the generic code is compiled, specialization happens on first use
not_specialized serializer to the declared models that got no specialized variant (not an ActiveRecord class, a model missing one of the serializer’s fields, a compile error, capacity reached, or specialization disabled); their records use the generic path
errors serializer to the error its compile raised

To require models everywhere, assert on the result in a test:

result = Panko.compile_all
expect(result.without_models).to be_empty
expect(result.not_specialized).to be_empty
expect(result.errors).to be_empty

If you warm serializers up before forking, do it after compile_all.

When the settings apply

The auto_specialization settings are read when a serializer meets a record class for the first time. Changing them later in the process doesn’t recompile or discard variants that already exist, which is why an initializer, before any serialization has happened, is the right place to set them.



Copyright © 2026 Panko Serializer. Distributed under the MIT License.

This site uses Just the Docs, a documentation theme for Jekyll.