A producer changes an order amount from cents to dollars. The column name and SQL type stay the same, so a schema check passes. A downstream report then treats the new values as cents. A useful data contract makes the unit part of the agreement, alongside the structure that software can validate.
Data contracts describe what producers will deliver and what consumers may depend on. They need both a checkable definition and an agreed process for changes. A document that nobody validates or maintains offers little protection when a dataset evolves.
What belongs in a data contract?
Start with one dataset that another team actually consumes. Identify its producer and consumers, then write down the expectations that would cause a problem if they changed.
| Area | Example question to settle |
|---|---|
| Identity and ownership | Which dataset is covered, and who maintains it? |
| Structure | Which fields, types, and keys are expected? |
| Meaning | Is an amount gross or net, and which units does it use? |
| Quality | Which missing values, duplicate keys, or invalid states are unacceptable? |
| Delivery | When should data arrive, and what interval does a delivery cover? |
| Permitted use | Which consumers and uses does the agreement cover? |
| Change process | How are changes reviewed, announced, and retired? |
The Open Data Contract Standard provides a shared specification for describing data contracts. If you adopt it, use a specific version and validate your document against that version. The worksheet below is a planning example, not an ODCS document or an ActionBox API payload.
Dataset: analytics.orders_daily
Producer: orders data team
Consumers: revenue reporting and support analytics
Row grain: one row per order
Key: order_id, unique and non-null
Amount: amount_cents, integer USD cents, excluding tax
Unknown amount: null; never converted silently to zero
Delivery: agreed daily cutoff, recorded with a timezone
Quality checks: key uniqueness, required fields, allowed status values
Changes requiring review: units, key, meaning, removals, delivery commitment
Migration plan: replacement version, consumer checks, retirement date
Owner for failed delivery: named role and escalation pathReplace the example commitments with ones the producer can meet and consumers can use. Avoid a freshness promise that has no owner or measurement behind it.
Separate a schema from the wider agreement
A schema describes structure. A contract can also describe meaning, delivery expectations, and responsibilities. Automated checks cover only the parts you have implemented.
In dbt, an enforced model contract checks the model's column names and types. Constraint support depends on the adapter and data platform. A declared constraint may not be enforced by the warehouse, so check the dbt model-contract documentation for your setup. Data tests provide a separate way to check model contents.
A model contract therefore should not be presented as proof that revenue is defined correctly or that every expected source record arrived. Use source freshness checks for delivery recency and data reconciliation to compare records across systems. Each check supplies different evidence.
Run a structural check, then expose a semantic failure
The following Python example implements only the worksheet's field names, integer types, nullable amount, and unique order key. It is a teaching validator, not an implementation of ODCS or a substitute for your warehouse's checks.
def validate_orders(rows):
"""Validate only the example's exact fields, types, and unique key."""
seen = set()
for row in rows:
if not isinstance(row, dict) or set(row) != {"order_id", "amount_cents"}:
raise ValueError("Expected exactly order_id and amount_cents")
key = row["order_id"]
if type(key) is not int or key in seen:
raise ValueError("order_id must be a unique non-null integer")
amount = row["amount_cents"]
if amount is not None and type(amount) is not int:
raise ValueError("amount_cents must be an integer or null")
seen.add(key)
validate_orders([{"order_id": 1, "amount_cents": 12000}])
validate_orders([{"order_id": 1, "amount_cents": 120}])Both calls succeed. The validator cannot tell whether the second record means $1.20 or whether a producer accidentally sent $120 in dollars under a cents field. The data still has the expected shape.
Use an independently established business fixture to test that meaning. For example, if a test order is known to be $120 excluding tax, its expected value is 12,000 cents. Do not calculate that expected value using the same transformation you are trying to test.
| Test input | Structural result | Interpretation |
|---|---|---|
| Unique integer ID and integer amount | Pass | Fields and types meet these rules |
| Null amount | Pass | This example explicitly allows unknown amounts |
| Repeated or null ID | Reject | Matching-key requirement is violated |
| Boolean or decimal in the amount field | Reject | This example requires integer cents or null |
| Extra or missing field | Reject | This example requires an exact shape |
| 120 instead of the known fixture's 12,000 | Pass structurally | A separate business expectation is needed |
The exact-field rule deliberately treats an added column as incompatible for this example. Another consumer may allow extra fields. Write that policy explicitly rather than treating this validator's choice as universal.
Download the complete Python example suite and run:
python3 data_quality_examples.pyThe suite uses Python 3.10 or later and the standard library. Its 13 tests include both articles' examples and demonstrate that the unit-change fixture passes structural validation. It does not validate an ODCS document, enforce a delivery promise, or prove compatibility with every consumer.
Decide how validation affects publication
Choose the point where a failing check prevents a consumer from receiving incompatible data. Running validation after an export has already been delivered can detect a problem, but it cannot prevent that delivery.
For a proposed change, record the candidate dataset or artifact, contract version, and validation results together. Make the publishing job use that same candidate. If the data or definition changes after validation, run the checks again.
A practical release sequence is:
- Prepare the proposed dataset and contract change.
- Run the structural and quality checks required for that version.
- Identify consumer-facing changes that need review.
- Confirm the migration plan with the affected owners.
- Publish the reviewed candidate and record the outcome.
Failing validation should produce a specific explanation. "Contract failed" forces someone to reconstruct the problem; the field, expected rule, observed result, and affected version make the next step clearer.
Assess compatibility from the consumer's perspective
A syntactically small change can have a large effect. Review compatibility against actual consumers rather than treating every added column as harmless or every version increment as sufficient notice.
| Proposed change | What to investigate |
|---|---|
| Remove or rename a field | Queries, exports, and applications that reference it |
| Change a field's type | Parsing, precision, joins, and downstream casts |
| Change units or business meaning | Calculations that still run but now produce a different answer |
| Add a field | Consumers expecting an exact shape or positional column order |
| Change delivery timing | Jobs and reports that depend on the previous cutoff |
For the cents-to-dollars example, choose a transition that makes the difference explicit. A separately named field or version can give consumers a migration path. Test known calculations against both representations, and agree when the old form will stop being delivered.
A version label identifies a change; it does not perform the migration or establish that consumers are ready.
Use human review for the decision software cannot make
Automated checks can compare a candidate with declared rules. A person may still need to decide whether a business-definition change is acceptable, whether a consumer can migrate in time, or whether a limited exception is justified.
Give that reviewer the contract diff, affected consumers, validation evidence, proposed release window, and recovery plan. Define whose approval is required before building the release path. Avoid substituting a generic team acknowledgement for the decision of the owner responsible for the affected use.
ActionBox can hold the review request and decision; your validation tools and publishing system continue to enforce the contract and release the data. The ActionBox architecture describes this separation. If a callback resumes publication, follow the verification and decision-binding rules so the response applies to the version that was reviewed.
A rejection should return the candidate for revision or stop its release. An unanswered request should follow the agreed deadline policy. Neither outcome should become permission to publish because the scheduled job is running late.
Keep the contract useful after release
Assign someone to update the contract when the producer or consumer changes. Review repeated exceptions: they may indicate that the commitment is unrealistic or the consumer's needs have changed. Keep temporary exceptions tied to a scope and deadline instead of weakening a rule indefinitely.
Begin with one important shared dataset and a small set of measurable expectations. Rehearse a breaking change and a failed delivery before expanding the process. If an approved model change requires historical rebuilding, the dbt full-refresh guide covers execution planning; this contract should identify why the change is needed and who depends on it.
Verification notes
Examples checked September 7, 2026 with Python 3.11.9 and SQLite 3.45.1 using synthetic fixtures. The downloadable suite reports 13 passing tests. No production datasets, warehouse performance measurements, or customer results are represented here. Linked official documentation supports the tool-specific behavior; the workflow recommendations are this article's design guidance.
