skip to content

In a web framework's output serializer, how do you keep a field such as a password hash out of the response body?

level: juniorimportance: must knowfreq 72%

answer

  1. what the mapper may write out
  2. output is opt-out by default
  3. deny lists forget the next field
  4. write-only: accepted in, never out
  5. exclusion on the type, not per handler

basics

~20 s

Declare the exclusion on the model the mapper serializes: mark the field write-only or ignored on output, or better, use a response model that has no such field at all. Do not strip it per handler.

solid answer

~40 s

Output is produced by a mapper walking the object it was handed, and most mappers write every readable property unless told otherwise, so a secret leaks the moment it exists on a serialized type. There are three levers. Field-level metadata marks a property as excluded from output. A direction marker makes a property asymmetric: bound from the request body but never written back (`write-only`), or the reverse (`read-only`). The structural fix is a dedicated response model that simply never carries the secret, so there is nothing to forget. Prefer the model-level fix, because a per-handler "remove this key before responding" is a deny list that the next field added to the type will silently escape. Then assert on the serialized body in a test, not on the object you passed in.

go deeper

for a junior

Remember that the response body is generated from the object you return, and by default it contains everything readable on that type. Know the words write-only and read-only for direction-limited fields.

for a middle

Be able to explain the mapper's default opt-out behaviour, the difference between excluding a field and never putting it on the output type, and why nested types and map-shaped properties escape simple exclusion.

for a senior

Show how you would stop this class of leak in a running system: a response model per endpoint shape, tests that assert on serialized keys, and the same policy applied to error bodies and logs.

for a principal

Frame it as a default: output is allow-listed by construction so a new domain field is inert at the boundary, and the cost of that discipline is extra types and mapping that the team must be willing to pay.

## What the boundary actually writes A response body is not "the object" — it is whatever the **output mapper** decided to write from the object a handler returned. Most mappers work by walking the value reflectively: they enumerate the readable properties of the type, ask each for its value, and emit a key/value pair per property, recursing into nested objects and collections. The important consequence is the default direction of the policy: **it is usually opt-out, not opt-in**. Nobody lists what may be written; the mapper writes what it can see, minus whatever you excluded. That is why a leaked credential is almost always a serialization bug rather than a handler bug. No one wrote `response.put("passwordHash", ...)`. Somebody added a column, added a property to a type that was already being serialized, and the boundary faithfully published it. Visibility in the language does not help either: mappers that read through accessor methods will happily publish a `private` field that has a public getter. ## The mechanisms, from weakest to strongest 1. **Per-response stripping.** The handler builds the object, deletes or blanks the key, then returns it. Works once, protects nothing later. 2. **Field-level exclusion metadata.** Declarative metadata on the property tells the mapper to skip it in every serialization of that type. One declaration, applies to every endpoint that returns the type. 3. **Direction asymmetry (write-only / read-only).** A property can be bound *from* an incoming body but omitted *from* outgoing ones, or the reverse — a server-assigned identifier that is written out but ignored if a client sends it. 4. **A dedicated response model.** The type the boundary serializes carries only fields that are allowed to leave the process. The secret is not excluded; it is absent. 5. **View or group selection**, where one model is tagged per field and an endpoint picks which tag set is active — useful for shaping, but it is field selection, not a secrecy boundary, because the field still exists on the serialized type. | Mechanism | Protects against | Fails when | |---|---|---| | Stripping in a handler | Nothing structural | A second endpoint returns the same type | | Field-level exclusion | Any endpoint returning that type | Someone adds a *new* sensitive property | | Write-only marker | Echoing back a value the client sent | The same type is reused for an admin view | | Dedicated response model | New domain fields, entirely | Mapping code is hand-written and drifts | ## Why a deny list leaks and an allow list does not Exclusion metadata and hand-stripping are both **deny lists**: they name what must not go out, and everything unnamed goes out. A response model is an **allow list**: it names what may go out, and everything unnamed stays in. The difference only shows up over time — the day a token, an internal flag, or an audit column is added to the domain type. With a deny list the new field appears in every response that already returned that type, silently and immediately. Watch particularly for: - **Nested objects.** Exclusion applies to the type it is declared on; a secret one level down travels unless that nested type is protected too. - **Dynamic property bags.** A map-typed or open-ended property serializes whatever keys it happens to hold, so no static exclusion covers it. - **Error paths.** A validation failure that echoes the submitted model back to the caller can publish exactly the input field you were careful to exclude from the success path. - **Logging.** The same object is often written to a log by the same mapper; exclusion configured only for the HTTP boundary does not protect the log. ## Proving it, not asserting it Test the **bytes**, not the object. A test that inspects the returned object proves nothing about what the mapper wrote. Useful checks: - Serialize each response type and compare the produced key set against an approved list, so an added property fails the build. - Hit the endpoint in a test and assert the body does not contain the sensitive key, including nested paths. - Generate API schema documentation from the actual response type; if the schema is generated from the type the mapper really serializes, a reviewer can see the leak in a diff. ## What interviewers listen for The strong answer says "whatever is on the serialized type goes out by default", names the write-only/read-only asymmetry, and prefers a response model over an exclusion marker — then closes the loop by testing the serialized body rather than the object.

  • Why is a dedicated response model considered stronger than a field-level exclusion marker?
    Exclusion is a deny list: everything not named is published, so the next property added to the type leaks automatically. A response model is an allow list — a new domain field cannot appear in a body unless someone deliberately adds it to the output type and maps it. The protection survives changes nobody reviewed.
  • A field is excluded from the HTTP response but still appears in application logs. Why?
    Exclusion is usually configured on a mapper instance or on the type as read by that mapper. If logging serializes the same object through a different mapper, or via a default string conversion of the type, the exclusion policy is simply not in that path. The durable fix is to keep the secret off the object that crosses either boundary.
  • How can a validation error response leak a field the success response carefully hides?
    Error bodies often echo the rejected input back to help the caller. If that echo serializes the bound input model, and the secrecy rule was applied only to the success-path response model, the value is published on the failure path. Error shaping needs the same output policy as the success shape.

saying these in an interview costs you the question

  • Thinks a field the client never displays is not in the body
  • Strips the key inside each handler instead of on the type
  • Assumes a private field is skipped by a reflective mapper
  • Believes encrypted transport makes an extra internal field harmless
  • Adds a property to a serialized type and expects the body to be unchanged
  • Tests the returned object rather than the serialized bytes