skip to content

Why do web frameworks register one configured serializer for the whole application rather than one per handler?

level: middleimportance: must knowfreq 66%

answer

  1. policy lives on the instance
  2. one decision, every endpoint
  3. metadata caches are per instance
  4. error bodies still use the registered one

basics

~20 s

A serializer carries policy — naming, date and enum formats, null handling, unknown-key handling — plus per-type metadata caches. One registered instance settles those decisions for every endpoint; instances built per handler re-answer them from defaults and drift apart.

solid answer

~50 s

The framework converts request bytes and response values through a component chosen for the negotiated media type, and that component holds a configured serializer. Configure it once and naming, date and time form, enum representation, null omission and unknown-key policy become properties of the service rather than of whichever handler was written last. There is a performance argument too: serializers build per-type metadata on first use and cache it, so an instance constructed per request discards that work on every call. A useful tell is that framework-generated bodies, such as a validation-failure body, still go through the registered instance — so a handler-local serializer produces an endpoint whose success and error payloads disagree. When one endpoint genuinely needs another shape, use a per-endpoint option on the model, or a second explicitly named instance configured beside the first.

go deeper

for a junior

Know that the framework already holds a configured serializer at its boundary and that your handler should return a model and let that instance write it, rather than building a serializer yourself.

for a middle

Explain both costs of a per-handler instance: policy defaults that differ from the service's conventions, and the loss of per-type metadata caching that the shared instance amortises.

for a senior

Describe how you would detect existing drift and prevent it returning — a boundary test over stored documents plus a rule that fails the build when a serializer is constructed outside the configuration module.

for a principal

Decide when a second instance is a legitimate second contract versus a fracture in one, and where that configuration should live when many services must publish payloads that look like one estate.

## What "the framework's serializer" actually is At its boundary a web framework needs two conversions: incoming bytes into a typed input model, and a returned value into outgoing bytes. It performs both through a component chosen for the negotiated media type, and that component holds a **configured serializer instance**. How you reach that instance differs: some frameworks accept one you construct at startup, some expose a builder they own and let you adjust it, some take a converter or plugin registered per media type. The registration style is a product detail; the consequence is identical everywhere. There is one configured instance sitting behind the boundary, and almost everything that crosses it goes through that instance. ## What the instance carries A serializer is not a function; it is a bag of policy plus a cache: - the **naming strategy** that maps property names to keys; - the policy for **keys in the document that no property matches**; - whether **null or empty** members are written or omitted; - the **date and time** representation, including precision; - the **enum** representation; - the **registry of permitted subtypes** and the discriminator for polymorphic payloads; - **custom converters** for domain value types such as money or identifiers; - per-type **metadata** built by inspecting each model the first time it is used. Every item on that list is a decision about the service's payloads. A per-handler instance re-answers all of them by whatever the defaults happen to be. ## Two costs, one visible and one not **Drift** is the visible one. A handler that builds its own instance produces an endpoint whose keys, dates and null handling follow the defaults of the library version in use, not the conventions of the API. Nobody notices until a client integrates against two endpoints. **Cache loss** is the invisible one. Serializers build per-type metadata — property lists, accessors, constructors, resolved converters — on first use and keep it. An instance constructed per request throws that work away every time, so the cost is paid on every call and shows up as CPU and allocation pressure that is usually blamed on something else. There is a third effect worth naming, because it makes diagnosis easy: framework-generated bodies, such as the body produced for a validation failure or an unhandled error, are written through the **registered** instance. So a handler with a local serializer yields one endpoint whose success payload and error payload follow different conventions. A fourth is reach. The registered instance is also what serializes a model that appears *inside* another payload, so a local instance only fixes the one place it is called and leaves the same model looking different wherever it is embedded. Policy applied at the boundary applies once and everywhere; policy applied in a handler applies to exactly one write. ## When more than one instance is legitimate | Need | The right lever | |---|---| | One consumer requires a different key convention | a second, explicitly named instance, configured beside the first and injected where it is used | | One endpoint must expose a different set of fields | a per-endpoint option on the model, not a different serializer | | Calls to an upstream service with foreign conventions | a separate instance owned by the client code, kept out of the inbound boundary | | A one-off debug or admin dump | still the shared instance; if the shape is wrong, the shape is the problem | The distinction that matters: a second instance is legitimate when it serves a **different contract**, and illegitimate when it serves the same contract with different defaults. ## Making the rule stick 1. Configure the instance completely at startup and treat it as immutable afterwards. A configured instance is normally safe to share across concurrent requests; reconfiguring one while requests are in flight is not, and "adjust it for this response" is the most dangerous version of that mistake. 2. Forbid construction outside the configuration module with a static-analysis or architecture rule, so the review comment becomes a build failure. 3. Keep one boundary test that drives a representative model through the registered instance and compares the result with a stored document. It catches both a local instance and an accidental change to the shared configuration. The underlying reason this rule needs enforcing is that the wrong path is easy: a serializer is trivially constructible, the local instance works in the handler's own test, and nothing fails until two endpoints are read side by side.

  • Your service also calls an upstream API whose payload conventions are nothing like yours. Same instance?
    No. That is a different contract, so it gets its own configured instance owned by the client code. Keep it away from the inbound boundary, and configure it in the same place as the other one so both are visible as deliberate choices rather than local accidents.
  • How do you stop handler-local instances from reappearing over time?
    Make the wrong path fail the build: a static-analysis or architecture rule that forbids constructing a serializer outside the configuration module, plus one boundary test that serializes a representative model through the registered instance and compares it with a stored document. Review comments alone do not survive team turnover.
  • Is it safe to adjust the shared instance to change one response?
    No. A fully configured instance is normally safe to share across concurrent requests precisely because it stops changing; mutating it while requests are in flight makes one request's settings leak into another. Per-response variation belongs on the model or on a second instance.

saying these in an interview costs you the question

  • Builds a fresh serializer inside a handler because it is just a local object.
  • Thinks the only cost of a local instance is a little extra allocation.
  • Treats each endpoint's payload conventions as that endpoint's own business.
  • Reconfigures the shared instance at request time to shape one response.
  • Assumes framework-generated error bodies use whatever the handler used.