skip to content

In a web framework, how can a value reach persistence unvalidated even though the input model declares a constraint on that field?

level: seniorimportance: should knowfreq 56%

answer

  1. the guarantee is on a path
  2. declaration without a trigger is inert
  3. raw reads and hand-built models bypass
  4. mutation after the check
  5. a check after the write protects nothing

basics

~20 s

Declared constraints only run where the framework binds that model and evaluates it. Reading the raw request, building the model in code, binding a laxer second model, or mutating a field after the check all reach the write unprotected.

solid answer

~40 s

A constraint is enforced at a **point**, not everywhere the value exists. The point is the framework's validation of a bound input model, and every path to the write that avoids it is unprotected. The common ones: a handler that reads raw body bytes or parameters itself and assembles the object in code; a route that binds the model but was never marked as validated, where that marking is what triggers the step; a second endpoint binding a laxer model onto the same storage; a handler that validates and then mutates the object before writing. The cure is structural rather than a stronger rule — one binding shape per operation, validation triggered by configuration rather than per route, and no write reachable except through a bound, validated model.

go deeper

for a junior

Take away the core fact: a constraint runs where the framework validates a bound model, so code that builds the object by hand gets no checking at all.

for a middle

Be able to list the bypass paths — raw request reads, unmarked bindings, a second laxer model, post-check mutation — and say why each one still looks correct in the model source.

for a senior

Show the diagnosis: start from the bad row, enumerate writers, verify the trigger rather than the declaration, and prove rejection happens before the side effect.

for a principal

Argue the structural fix — one validated binding per operation, validation on by configuration rather than per-route marking — and where storage-level rules earn their place as the last line.

## The guarantee is about a path, not about a field It is tempting to read a constraint declared on a field as "this value can never be illegal". What it actually means is narrower: *when the framework binds this model for a request and evaluates its declared rules, an illegal value is rejected there.* The guarantee is attached to a **path through the request**. A value that reaches the same write by any other path carries no guarantee at all, and the field still shows a declaration when you read the model, which is why this bug survives code review. ## The paths that skip the check - **The handler reads the raw request itself.** Pulling raw body bytes, a form field, or a query parameter and constructing the object in code produces an instance with all its metadata and nothing to evaluate it. - **Nothing triggers the validation step.** Where a framework validates only marked binding sites or only routes that opt in, an unmarked one binds the model and calls the handler with whatever arrived. - **A second entry point binds a laxer model.** An administrative endpoint, an import job, or an older version of the API writes the same storage through a different model whose rules were never aligned. - **Mutation after the check.** The model is validated on entry, then the handler sets a field from a header, a computed default, or another service's response, and that value was never anyone's declared input. - **Partial updates.** When absent means "leave unchanged", presence rules are deliberately weakened on the update model, and a rule that exists on the create model quietly does not exist on the update path. - **Deferred consumption.** A body consumed as a stream or handed to background work after the response is not the same thing the validation step saw, unless the validated model is what gets carried forward. ## Why "validate later" is not a fallback A check that runs after the effect has been applied does not protect the effect. Returning an error response afterwards changes what the caller is told, not what the store now contains, and a compensating delete is a second operation that can itself fail. The ordering requirement is simple to state and is the whole content of the leaf's warning: **the check must sit between the value arriving and the value being used**. | Where the check sits | What it protects | |---|---| | Before the write, on the bound model | The write itself — the illegal value never happens | | Inside the write's transaction | The invariant, at the cost of doing the work to find out | | After the write, before responding | Nothing about the stored state; only the response text | | In a later batch or report | Detection, not prevention | Storage-level rules — required columns, length limits, uniqueness — are a genuine last line and worth having, but they fail late, fail with an error shaped by the store rather than the contract, and cannot express most of what a boundary rule can. ## How to diagnose this in a live system 1. Start from the illegal row and ask **which writes can produce it**, not which endpoint was supposed to. 2. For each writer, ask whether its input came from a bound model and whether that binding is actually validated — check the configuration or marking, not the declaration on the model. 3. Look for mutation between the validated model and the write, including defaults filled in by mapping code. 4. Check for a second, older or internal, entry point onto the same storage. 5. Reproduce with the field omitted entirely, sent as an explicit empty value, and sent with the wrong type: these three take different paths and often produce different outcomes. ## How to make the class of bug impossible Keep one bound input model per operation and let nothing else reach the write. Prefer validation triggered by framework configuration over per-route marking, so forgetting a marker is not a silent hole. Treat any handler that reads the raw request as requiring a reason. And when a value is computed rather than supplied, validate it where it is computed, because no input declaration covers it.

  • How would you prove, rather than assume, that a route's input model is being validated?
    Send a request that violates a declared rule and confirm rejection before any side effect — no row written, no downstream call made. A passing unit test on the model proves the rule exists, not that the route runs it.
  • Are storage-level constraints a reasonable substitute for boundary rules?
    They are a valuable last line, not a substitute. They fail after the work is done, express only a narrow set of rules, and surface errors in the store's vocabulary rather than the contract's, so callers get a poor answer even when the data stays clean.
  • A handler validates its model, then fills in a field from a request header before writing. What is the risk?
    That field was never declared input, so no constraint covers it. Either bind the header into the validated model so its rules apply, or check the computed value explicitly at the point it is produced.

saying these in an interview costs you the question

  • Believes a declared constraint protects every path that writes the value
  • Thinks returning an error after the write undoes the write
  • Assumes every bound model is validated automatically in every framework
  • Relies on storage constraints as the primary input rule set
  • Ignores fields the handler fills in after the model was checked