Being tolerant of imperfect input can look helpful. A parser silently fixes an invalid value, an API treats an unknown option as a default, or a service accepts several spellings for the same field. The immediate caller succeeds instead of receiving an error.
The cost often appears later. Once clients discover that invalid input is accepted, they may depend on that behavior. Tightening validation then becomes a compatibility change, and different implementations may interpret the same malformed input differently.
A strict input contract accepts the forms a system intentionally supports and rejects inputs whose meaning is invalid or ambiguous. Strictness does not mean making an interface unnecessarily narrow. It means deciding what the contract is, validating at the boundary, and avoiding accidental promises about malformed data.
This article develops a practical mental model for input strictness, shows why silent repair can hide defects, and explains when normalization is part of a good contract rather than unwanted tolerance.
Treat acceptance as a promise
A useful mental model is simple:
If callers can repeatedly send an input and receive a useful result, some callers may treat that input as supported.
Imagine an order API whose documented status values are pending, paid, and cancelled:
createOrder({ status: "paid" })The implementation also happens to accept unknown values by replacing them with pending:
createOrder({ status: "PAID" })
stored status:
pendingThe request did not fail, but the system also did not preserve the caller’s likely intent. A client bug can remain hidden because every request receives a successful response.
If the API later starts rejecting "PAID", clients that accidentally relied on the fallback can break. What began as leniency has become observable behavior that is costly to remove.
Strict validation changes the feedback loop:
createOrder({ status: "PAID" })
error:
status must be one of: pending, paid, cancelledNow the mismatch is visible at the boundary where it enters the system.
Separate invalid input from normalizable input
Strictness does not require rejecting every variation. The important question is whether the transformation is an intentional part of the contract.
Suppose a registration form accepts an email address with surrounding whitespace. The service may define trimming as supported normalization:
input: " dev@example.com "
stored: "dev@example.com"That can be a clear contract: surrounding whitespace is ignored before validation.
Compare it with guessing a missing domain:
input: "dev"
guess: "dev@example.com"The second transformation invents information. Unless the product has a specific rule that makes the domain unambiguous, the system cannot know whether the guess represents the user’s intent.
A practical distinction is:
- Normalization maps explicitly equivalent representations to one canonical form.
- Repair tries to infer what an invalid input was supposed to mean.
Normalization can make an interface easier to use when the equivalence is deliberate and documented. Silent repair is riskier because the system may turn a detectable error into valid but incorrect data.
Validate where meaning becomes known
Validation is most useful at the boundary that understands the contract.
Consider a service that receives a percentage as an integer from 0 through 100. A transport layer can check that the field is present and numeric, but the application boundary knows the meaningful range:
setDiscount({ percentage: 20 }) -> accepted
setDiscount({ percentage: 150 }) -> rejectedClamping 150 to 100 may appear convenient:
min(150, 100) -> 100But the caller asked for 150 percent and the service applied 100 percent. The operation succeeded with different semantics.
Rejecting the value keeps the disagreement visible. The caller can then correct its assumption instead of building further behavior on a hidden conversion.
This principle also helps decide where checks belong. Syntax checks can happen while decoding input. Domain checks should live where domain meaning is understood. Duplicating the same rule across unrelated layers makes it easier for those layers to disagree.
Make failures specific enough to correct
Strict contracts are frustrating when rejection gives callers no useful information.
A response such as this identifies only that something failed:
invalid requestA more useful boundary identifies the violated rule without exposing implementation details:
field: retryCount
error: must be an integer from 0 through 5The caller now knows what must change. The service still controls how the rule is implemented internally.
For programmatic interfaces, stable machine-readable error categories can be useful when clients need to react differently to different failures. Human-readable messages can provide context, but clients should not have to parse prose unless the interface explicitly defines that as the contract.
The key is consistency. If invalid input sometimes fails and sometimes receives a guessed interpretation, callers cannot reliably distinguish accepted data from silently repaired data.
Strictness reduces ambiguity, not change
A strict contract does not make an interface immutable. Supported inputs can still evolve.
Suppose the order API adds a new refunded status. Older clients may not send it, while newer clients can:
pending
paid
cancelled
refundedThat is an intentional expansion of the accepted input set.
The design question is different from whether the service should accept misspellings such as refundd. Adding a defined value changes the contract deliberately. Accepting an undefined value and guessing its meaning expands the contract accidentally.
This distinction matters during versioning. Teams can reason about deliberate additions, document them, test them, and coordinate them. Accidental acceptance is harder to inventory because it may never appear in specifications or tests.
Be careful with permissive readers
Sometimes a consumer intentionally ignores data it does not understand. For example, a configuration reader may use only fields relevant to its version and ignore additional fields so producers can add optional information without breaking older readers.
That policy can be reasonable, but it should be designed rather than assumed.
Consider two different cases:
{
"name": "report",
"description": "monthly totals",
"newOptionalMetadata": "..."
}Ignoring an unknown optional field may preserve forward compatibility if the format explicitly allows extensions.
Now consider a misspelled known field:
{
"name": "report",
"descripton": "monthly totals"
}If all unknown fields are silently ignored, the typo can pass unnoticed and the description disappears. The same permissive rule that supports extensions also hides mistakes.
A better contract can distinguish extension points from ordinary fields. For example, it may allow unknown keys only inside a dedicated metadata object while rejecting unknown keys in the core schema.
The broader lesson is that tolerance should serve a specific compatibility goal. It should not be a substitute for deciding which inputs are valid.
Do not confuse strict input with strict output
Input and output compatibility have different pressures.
A service controls what it accepts, so rejecting malformed input can expose caller defects early. A consumer of another system’s output has less control over what it receives and may need an explicit strategy for compatible evolution.
For example, an enum sent across a service boundary creates a design choice. A producer may add a value in the future. A consumer that cannot safely interpret unknown values should fail clearly or route them to an explicit unsupported state. A consumer that can safely treat future values as other may define that behavior deliberately.
Neither choice is universally correct. The important point is to specify the behavior instead of accidentally depending on whatever the parser happens to do.
Strict input contracts therefore do not imply that every reader must reject every future extension. They imply that accepted and rejected forms should follow the compatibility model of the interface.
Avoid validation that is stricter than the real requirement
Over-validation creates a different problem: it rejects values that the application could correctly handle.
Suppose a display name is only stored and shown back to the user. Requiring it to contain exactly two words because current examples look like first and last names invents a domain rule that may not exist.
Likewise, a numeric identifier represented as text should not be forced into a machine integer if the system never performs arithmetic on it. Such validation can reject legitimate future values without protecting any real invariant.
Strictness should therefore come from meaning, not preference.
Ask:
- What property must hold for the system to process this value correctly?
- Which variations are intentionally equivalent?
- Which invalid forms would require guessing the caller’s intent?
- Which future extensions does the contract deliberately allow?
Validate the first category, normalize the second when useful, reject the third, and design an explicit policy for the fourth.
Migration requires observing current tolerance
Tightening an existing permissive interface is different from designing a new strict one. Existing callers may already rely on undocumented acceptance.
Changing from silent repair to immediate rejection can therefore create an outage even when the new rule is conceptually better.
A safer migration often separates discovery from enforcement:
1. detect inputs that violate the intended contract
2. measure and identify affected callers
3. help callers correct their requests
4. begin rejecting after usage has been removedThe exact mechanism depends on the system. It may involve logs, metrics, warnings, dry-run validation, or a versioned endpoint. The important engineering point is that existing behavior has compatibility weight even when it was accidental.
Do not collect raw sensitive input merely to measure violations. Record only the information needed to understand the problem, following the system’s privacy and security requirements.
Use strict contracts when ambiguity is expensive
Strict input contracts are especially useful when malformed values could produce persistent state, trigger external effects, cross team boundaries, or hide defects in automated clients.
They are less valuable when the interface intentionally exists to interpret flexible human input. A search box, for example, may reasonably normalize case, whitespace, or common punctuation because forgiving interpretation is part of the product behavior.
Even there, the same principle applies: define the tolerated forms deliberately. A user-facing parser can be flexible without making every downstream service equally permissive.
The goal is not maximum rejection. It is a boundary whose behavior is predictable.
Conclusion
Every accepted input teaches callers something about an interface. If a system silently accepts malformed or ambiguous data, that tolerance can hide defects today and become a compatibility obligation tomorrow.
Design strict input contracts by defining valid forms, normalizing only deliberate equivalents, rejecting cases that require guessing, and making failures specific enough to correct. Allow extensibility where the compatibility model requires it, but make that extensibility explicit.
A useful final question at any input boundary is: if callers repeat this input for years, are we willing to keep supporting its meaning? If the answer is no, rejecting it early is often cheaper than turning an accident into a contract.