A service reads a response from another system. It needs two fields, but its deserializer models twenty. A harmless producer change adds a field, changes an unused field, or expands an enum that the consumer never acts on. The consumer still breaks because it accidentally depended on more of the contract than its job required.

A tolerant reader avoids that unnecessary coupling. It reads the smallest part of an external representation that the consumer needs and rejects changes only when they threaten assumptions the consumer actually relies on.

This does not mean accepting arbitrary malformed data. It means being strict about your requirements and deliberately unconcerned with irrelevant details.

This article explains that distinction, shows how to design a small reader, and covers the cases where tolerance helps or hides a real incompatibility.

Depend on meaning, not the whole representation

Suppose a shipping service receives this order document:

{
  "id": "ORD-42",
  "status": "paid",
  "customerName": "Mina",
  "marketingSource": "newsletter"
}

The shipping service only needs to know which order it is handling and whether the order is ready for fulfilment. Yet it may be tempting to create a local model that mirrors every field:

ExternalOrder:
    id
    status
    customerName
    marketingSource

That model looks complete, but completeness is not automatically useful. It creates local knowledge about fields the shipping service does not use.

A narrower input model expresses the real dependency:

ShippableOrderInput:
    id
    status

The mental model is simple:

A consumer’s effective contract is the subset of producer behavior that the consumer depends on.

If marketingSource changes, shipping should not care unless shipping has chosen to make a decision from that field.

Tolerance and validation solve different problems

The phrase tolerant reader can sound like “accept anything.” That is not the goal.

Consider this input:

{
  "id": "ORD-42",
  "status": "paid",
  "marketingSource": {"campaign": "spring"}
}

If shipping ignores marketingSource, changing its shape from a string to an object is irrelevant to shipping. Rejecting the entire document because of that unused field creates coupling without protecting a shipping invariant.

Now consider:

{
  "id": "",
  "status": "paid"
}

If shipping requires a non-empty order identifier, accepting this document would violate a real requirement. A tolerant reader should reject it.

The useful distinction is:

  • Be tolerant about information you do not use.
  • Be strict about information whose meaning your behavior depends on.

This is why tolerance belongs at a boundary. The reader can turn a broad external representation into a small trusted internal value.

Start with the smallest useful reader

Assume the shipping workflow only starts for paid orders. A boundary reader can perform three jobs:

readShippingInput(document):
    id = requireString(document, "id")
    status = requireString(document, "status")

    if id is empty:
        return error("order id must not be empty")

    if status not in ["paid", "cancelled", "pending"]:
        return error("unsupported order status")

    return ShippingInput(id, status)

This simplified pseudocode demonstrates an important boundary: the reader knows the external field names and representation, while the rest of the shipping code receives a deliberately smaller value.

Unknown top-level fields do not appear in the algorithm because the consumer has no reason to interpret them.

Production code may use a schema library, generated client, serializer configuration, or hand-written parser. The mechanism varies. The design question stays the same: which facts must this consumer understand to do its job correctly?

Unknown fields and unknown values are not the same

One of the easiest mistakes is to treat every kind of novelty as equally safe.

An unknown field can often be ignored when the consumer does not use it:

{
  "id": "ORD-42",
  "status": "paid",
  "warehouseHint": "east"
}

If shipping does not use warehouseHint, ignoring it may preserve compatibility.

An unknown value in a field you do use is different:

{
  "id": "ORD-42",
  "status": "awaiting_manual_review"
}

Shipping makes decisions from status, so silently treating an unfamiliar status as pending could produce incorrect behavior. The reader needs an explicit policy.

Valid policies depend on the domain. It might reject the message, map the value to an explicit UNKNOWN state that prevents action, or route it for later handling. What matters is that the fallback is safe for the consumer’s responsibility rather than merely convenient for parsing.

Preserve the distinction between absent and defaulted data

Tolerance can also become dangerous when missing data is silently replaced with a plausible default.

Suppose an older producer omitted priority, while a newer shipping rule uses it:

priority = document.get("priority", "normal")

That default is only correct if the contract explicitly defines absence to mean normal. If absence means “producer does not know” or if older producers used different semantics, the default invents information.

A safer model keeps meaningful states separate:

priority = optionalString(document, "priority")

if priority is absent:
    return PriorityNotProvided

The application can then decide what PriorityNotProvided means in its own workflow.

The rule is not “never default.” The rule is: default only when the missing case has a defined meaning that makes the substitution valid.

Keep external representations at the boundary

A tolerant reader is most useful when external data does not leak through the rest of the application.

Without a boundary, code may repeatedly access producer-shaped data:

orderJson["status"]
orderJson["id"]
orderJson["customerName"]

Now many modules know field names, optionality rules, and external value conventions. Even if the parser itself ignores unknown fields, the system remains tightly coupled to the producer’s representation.

Instead, translate once:

external document
      |
      v
shipping reader
      |
      v
ShippingInput(orderId, readiness)
      |
      v
shipping workflow

The internal type can use names and concepts that fit the consumer. For example, several producer statuses might map to a smaller internal concept such as READY or NOT_READY if that mapping is complete and correct for shipping.

This translation localizes change. When the producer representation evolves without changing shipping meaning, only the boundary reader may need attention.

Test the compatibility you intend to support

A reader is only tolerant if its behavior proves it.

Tests should cover both irrelevant evolution and meaningful breakage. For the shipping example, useful cases include:

accept: required fields plus an unknown extra field
accept: required fields in a different field order
reject: missing required id
reject: empty id
reject or safely contain: unknown status value

The exact cases depend on the serialization format and contract. Do not test properties the format itself already guarantees unless the test protects a decision in your code.

A particularly useful test starts with a valid producer example, adds an unrelated field, and verifies that the consumer result is unchanged. This directly checks the intended tolerance.

Conversely, tests should prove that malformed or semantically unsupported values do not pass merely because the parser can decode them.

Tolerance has limits

Tolerant reading works well for additive evolution, but it cannot make incompatible meanings compatible.

If status = "paid" changes from “payment is settled” to “payment has been initiated,” the same bytes now mean something different. Ignoring fields cannot protect the consumer from that semantic change.

Likewise, renaming a required field, changing its unit, changing identifier scope, or removing a value the consumer relies on may require coordinated evolution or explicit versioning.

This leads to a practical boundary:

  • representation changes outside the consumer’s dependency can often be tolerated;
  • changes to information the consumer uses require compatibility analysis;
  • changes to the meaning of that information require a new agreement, even if the serialized shape stays identical.

Tolerant readers reduce accidental coupling. They do not remove real coupling.

Common mistakes

Mirroring the producer’s entire model

A full mirror feels safe because every field is visible. In practice, it makes the consumer track details it may never use. Model the consumer’s needs unless another requirement genuinely needs the full representation.

Ignoring every unknown enum value

An enum value is not equivalent to an extra field. If behavior branches on that value, novelty may change the decision. Define a safe fallback or fail explicitly.

Converting missing values into convenient defaults

A default can erase the difference between “normal” and “not provided.” Use it only when the contract gives those states the same meaning.

Letting tolerant parsing hide observability

A consumer may correctly ignore new fields while operators still need to know that producer traffic is changing. Compatibility and observability are separate concerns. Metrics or sampled diagnostics can record unexpected input without turning harmless additions into failures.

Using tolerance instead of contract ownership

If teams repeatedly make semantic changes without communicating them, a forgiving parser is not a substitute for a clear contract. Tolerance should reduce coordination for changes that are genuinely irrelevant to a consumer, not excuse uncontrolled changes to shared meaning.

When a strict reader is the better choice

Some boundaries intentionally require an exact representation. Cryptographic verification, canonical serialization, regulatory records, import formats with fixed versions, and protocols where unknown fields alter interpretation may need strict parsing.

A strict reader can also be appropriate inside a tightly controlled component when accepting an unknown shape would make defects harder to detect and there is no independent deployment concern.

The decision should follow the compatibility requirement. Ask what kinds of producer evolution the consumer should survive independently. If the answer is “none,” strictness may be simpler and more informative.

A practical review method

When reviewing a consumer boundary, inspect each external field it models and ask:

  1. Does application behavior use this field?
  2. If not, why does the consumer need to parse or validate it?
  3. If yes, what assumptions does the consumer make about its presence, type, allowed values, units, and meaning?
  4. What should happen when the producer adds information the consumer does not know about?
  5. What should happen when information the consumer relies on becomes unknown or missing?

Those questions turn “be liberal in what you accept” into concrete engineering decisions. The goal is not maximum permissiveness. The goal is a contract no larger than the consumer actually needs.

Conclusion

A tolerant reader narrows an external contract to the facts a consumer depends on. It ignores irrelevant additions while validating the values and meanings that protect the consumer’s behavior.

The key design move is to separate unknown information from unsupported meaning. Extra data can often pass unnoticed. Missing identifiers, unfamiliar decision-driving values, changed units, or changed semantics usually need an explicit response.

Keep that translation at the boundary, test both tolerated evolution and meaningful failure, and make the consumer’s real dependencies visible. The result is not a consumer that accepts everything. It is a consumer that breaks for the right reasons.