Skip to content

Map Outputs to Their Inputs

A contract lists its inputs and its outputs as two separate lists. On their own, they say nothing about which input feeds which output, so for a contract that declares several outputs Invaract can only assume the worst case: every write must read every declared input. That is wrong for a job that, say, reads three datasets and builds one output from two of them and another from the third — the second write never touches the first two inputs, and would be rejected with a MISSING_INPUT for each.

derivedFrom on an output is how you say which inputs it is built from.

inputs:
- name: orders
location: bronze/orders
schema: { fields: [{ name: order_id, type: string }] }
- name: customers
location: bronze/customers
schema: { fields: [{ name: customer_id, type: string }] }
- name: events
location: bronze/events
schema: { fields: [{ name: event_id, type: string }] }
outputs:
- name: orders_summary
location: gold/orders_summary
derivedFrom: [orders, customers]
schema: { fields: [{ name: order_id, type: string }] }
- name: audit_log
location: gold/audit_log
derivedFrom: [events]
schema: { fields: [{ name: event_id, type: string }] }

The names are the names of the contract’s own inputs. The mapping is many-to-many: an input can appear under several outputs, and an output can list as many inputs as it needs.

Each write a contract governs is checked on its own. When that write lands on an output that declares derivedFrom, Invaract checks it against only that output’s inputs:

SituationResult
The write reads every input its output listsPasses the input checks.
The write skips an input its output listsMISSING_INPUT for exactly that input.
The write never reads an input that only a different output listsNothing — it is not expected here.
The write reads a declared input its output does not list, and rejectUndeclaredInputs is onUNDECLARED_INPUT, naming the input and the output.

For example, if audit_log were written by a job that also read bronze/orders, and rejectUndeclaredInputs is enabled, the write is rejected with:

plan reads 'bronze/orders' (input 'orders'), which this contract declares but not as a source of output 'audit_log' (its derivedFrom lists 'events')
You writeIt means
no derivedFromThe output is built from all declared inputs.
derivedFrom: [a, b]Built from a and b; other declared inputs are not expected of this write.
derivedFrom: []Built from no declared input (for example, generated rows). Any declared input the write reads is an UNDECLARED_INPUT under rejectUndeclaredInputs.

A single-output contract does not need derivedFrom; it only becomes useful once a contract governs more than one write. When a multi-output contract without derivedFrom reports a MISSING_INPUT, the violation’s remediation points you here.

You don’t have to write the mapping from scratch. Dry-run mode prints one single-output draft per write with derivedFrom already filled in from what that write actually read, and MergeDraftsCli combines the drafts into one contract: shared inputs are unified by location, and every output’s derivedFrom is rewritten to the merged input names. Review the result — the observed mapping is what the job did on that run, not necessarily what it should do.

Changing which inputs an output is built from changes what a valid write must look like, so ContractCompatibility classifies it as a breaking change. It compares the inputs an output is effectively built from — spelling out the full input list where it was previously implied is not a change.

derivedFrom lives in the contract file itself, so a platform team can enable it against a job whose source they don’t own by editing the contract that job is pointed at. It works the same on Spark and on Beam.