skip to content

Which parts of a wire contract can an IDL's generated types enforce, and which must still be checked by hand?

level: middleimportance: nice to knowfreq 26%

answer

  1. shape, not meaning
  2. well-formed is not valid
  3. the type system is the limit
  4. cross-field rules are hand-written
  5. state the rule in the contract file

basics

~10 s

Generated types enforce shape: declared fields, declared types, and that undeclared fields cannot be set through the generated surface. Ranges, cross-field rules, units, referential validity and state legality are invariants no generator checks.

solid answer

~50 s

Code generation gives a consumer a type whose fields match the contract and an encoder that can only emit what the contract declares — so shape errors become compile errors. What it cannot give is meaning. An amount that must be positive, a rule that exactly one of two funding fields is populated, an identifier that must refer to an existing record, a unit convention, or a transition that is only legal from certain states are all invariants the generated type will happily violate. They live in hand-written validation on both ends, and the rule itself belongs in the contract file's comments so every consumer implements the same one. Some IDL families let you attach constraint annotations, but the generated code does not enforce them unless extra tooling is wired in, and ecosystems differ in how far that tooling goes.

code

pseudocode · 13 lines
pseudocode
# generated from the contract: shape only
record Payment:
    amount_minor : integer
    card_token   : string
    account_ref  : string

# hand-written: the invariants the contract cannot state
function validate(p):
    if p.amount_minor <= 0:
        reject("amount must be positive")
    if is_set(p.card_token) == is_set(p.account_ref):
        reject("exactly one funding source is required")
    return ok

go deeper

for a junior

Remember that a generated type checks the shape of a message — which fields exist and their types — and not whether the values inside it make sense.

for a middle

Explain the boundary precisely: what the contract's type system can express, why cross-field and range rules fall outside it, and where those rules belong instead.

for a senior

Show the discipline: validate on both ends, keep the rule in the contract file so every consumer implements the same one, and treat constraint annotations as inert until tooling consumes them.

for a principal

Decide how far the organisation invests in machine-checkable constraints against plain review conventions, knowing that support differs across the ecosystems you must keep equivalent.

## Shape against meaning A generator turns a contract into types. That gives real, checkable guarantees, and they are all about **shape**: - every declared field exists on the generated type with the declared type; - a field the contract does not declare cannot be set through the generated surface; - the encoder emits only what the contract describes, and the decoder parses it under the same rules; - adding or removing a field changes the generated surface, so a consumer that uses a removed field fails to compile. Everything a generator knows, it knows from the contract's type system. What the contract's type system cannot say, the generated code cannot check. That second set is where most production incidents actually live, because those are the rules a reader assumed and the writer never guaranteed. ## The invariants generation does not carry | Rule | Expressible in the type system? | Where it must live | |---|---|---| | Amount must be greater than zero | no | validation on both ends | | Exactly one of two fields populated | no | validation, plus a comment in the contract | | Identifier refers to an existing record | no | validation against a store, on receipt | | Value is in minor units, not major | no | contract comment and a review convention | | A shipped record carries a dispatch timestamp | no | validation tied to the state machine | | Field is declared as a record of this shape | yes | the generated type | The asymmetry is worth stating out loud in an interview: generation eliminates a class of error that was never the expensive one, and leaves the expensive class untouched. That is not an argument against generation; it is an argument against believing it did more than it did. ## Where the rest of the contract lives 1. **Written into the contract file.** The declaration is the only artefact every consumer reads, so a rule stated nowhere else will be implemented differently by each of them. A one-line comment on the field is the cheapest correction available. 2. **Validated on receipt, by the consumer.** Every consumer validates, because a consumer cannot assume the producer's version is the one it expects, and because a message may have been relayed by an intermediary. 3. **Validated before send, by the producer.** Cheaper to diagnose and keeps invalid records out of queues and archives entirely. 4. **Shared as a validation module where the ecosystem permits it**, so the rule is implemented once — but ecosystems differ in whether a shared module is even possible across all consumers, and the contract comment remains the fallback. ## Constraint annotations, and their limit Some IDL families allow constraint metadata on a field — a range, a pattern, a required-together group. This is genuinely useful: it puts the rule in the artefact everyone reads, and with the right tooling a generator or a companion library can emit checks from it. Two cautions belong in the answer. First, **the metadata is inert unless something consumes it**; a contract full of unenforced annotations is documentation with a false air of enforcement. Second, **support is uneven across targets**, so a rule enforced automatically in one ecosystem may be enforced by hand or not at all in another — which is precisely the inconsistency the contract exists to prevent. ## The failure this prevents The characteristic incident is a consumer that trusted the generated type. The field decoded, the type was right, and the value was nonsense — a negative amount, both funding fields set, a reference to a record that was deleted. The consumer had no validation because the compiler had been satisfied, and the bad record propagated into a store or an archive where it now has to be corrected retroactively. The short form to say out loud: **generated types tell you the message is well-formed, not that it is valid.** Well-formedness is the generator's job; validity is yours, on both ends, with the rule written where every consumer can read it.

  • If the rule cannot be generated, why write it in the contract file at all?
    Because the contract is the only artefact every consumer reads. A rule that lives solely in the producer's validation code will be reimplemented differently, or not at all, by each consumer — and by the next one, who never sees that code. Writing it on the field makes the same rule available to everyone who generates from it.
  • Should the producer validate as well, or is validating on receipt enough?
    Both. Validating on receipt is non-negotiable, because a consumer cannot assume which version wrote the message or whether an intermediary touched it. Producer-side validation is still worth having: it keeps invalid records out of queues and archives, where they are far more expensive to correct than at the point they were created.

saying these in an interview costs you the question

  • Believes a generated type guarantees the values are valid
  • Skips validation because the message decoded successfully
  • Assumes constraint annotations are enforced without extra tooling
  • Keeps the invariant only in the producer's validation code
  • Validates only on send and trusts every received record