Most data contracts are a spreadsheet in somebody’s inbox. The vendor sent it at signing, nobody has opened it in eight months, and the data has drifted three ways since. That’s the file interchange agreement in our post The Data Contract Is Your Receipt, and it’s the normal case.
A contract you don’t test is a description. Enforcing it takes a test behind every clause, and that is the part most teams skip.
What a data contract contains
Six clauses. Leave one out and you will find out which one the hard way.
The schema clause lists the columns, their types, the key, and which columns can be null. Everyone writes this one, because it’s the easiest to write down.
The semantics clause says what each field means. It names the unit (units or cases, dollars or cents) and the grain of a row (one row per order line, or per order). It also states the rules that hold between fields, such as a ship date that never comes before its order date.
The delivery clause sets the cadence, the arrival window, and how late counts as late. Add the expected row count, and how the producer signals a restatement of data it already sent.
The quality rules cover completeness, validity, uniqueness, ranges, and references to other tables, each with a threshold. “Mostly complete” is a wish. “No more than 2% null” is a term.
The change policy says how much notice comes before a breaking change, and how the contract’s version moves when the data does.
The failure clause names who gets told when a delivery breaks a term, what happens to the data, and how fast the producer sends a fix.
You don’t need to invent a file format for this. The Open Data Contract Standard (ODCS) defines a YAML contract with sections for schema, data quality, service levels, support channels, and team roles. dbt has a narrower idea under the same name: a dbt model contract fixes a model’s column names and types and fails the build when they don’t match.
Why most data contracts fail
They get written once and never tested. Six weeks of meetings produce a PDF. The PDF goes into a wiki. From then on, whoever reads the data discovers each breach, which is the exact outcome the contract existed to prevent.
They specify shape and skip meaning. The column is still called revenue and still holds a decimal. It also started including refunds two weeks ago, and every schema check passes. A model reading that column won’t notice. Neither will the dashboard.
They have no consequence clause. A contract that says what the data should look like and nothing about what happens when it doesn’t is a decoration. The consequence (a stopped load, a rejected file, an alert with a name on it) is what makes it a contract.
How you enforce a data contract
Map every clause to a check that runs on its own, on every delivery, before anyone downstream reads the data. Here is the mapping, with the TestGen test and monitor types that do each job. The test types reference describes each one.
| Clause | What checks it | Examples in DataOps TestGen |
|---|---|---|
| Schema | A check that fails on any added, dropped, or retyped column | Schema monitor, which reports every structural change as an anomaly |
| Semantics | Business rules written in SQL, and lists of allowed values | Custom Condition, SQL Metric, Custom Test; Value Match, Reference Match |
| Delivery | Checks that the data arrived, is current, and is the right size | Freshness and Volume monitors; Recency, Table Freshness; Row Range, Daily Records |
| Quality rules | Completeness, validity, uniqueness, and range checks with thresholds | Required Entry, Percent Missing, Pattern Match, Minimum Value; Unique Values, Duplicate Rows |
| Change policy | A check that flags an unannounced change the day it lands | The same schema monitor; it has no prediction model, so any change is reported |
| Failure | Alerts to named people, and a gate on the pipeline | Notifications by email, Slack, Microsoft Teams, or HTTPS webhook |
Draft the quality clauses from a profile of the real data. A profile tells you what the data does today: which columns are never null, which hold five codes, how far back the dates go. Then you decide which of those facts are promises. Test generation works this way. TestGen writes a Required Entry test when profiling finds every value in a column filled, and a Value Match test when a code column holds 2 to 10 distinct values. Each generated test is a draft term. Keep it, tighten it, or delete it.
Then run the checks between landing and publishing. A pipeline can submit a test run through the API and poll for the result, and a webhook notification can gate a pipeline. What the pipeline does with a failure is the failure clause, and the stop, quarantine, retry, or publish answer covers the choices.
A worked example: a weekly vendor file
Say a distributor sends orders.csv every Monday. Here are the six clauses for that file, with the check beside each.
| Clause | The term | The check |
|---|---|---|
| Schema | Columns order_id, order_date, ship_date, customer_id, qty, unit_price, with fixed types | Schema monitor on the table |
| Semantics | qty is units, not cases. ship_date is never before order_date | Custom Condition: ship_date < order_date counts as a failure |
| Delivery | One file by 6am Monday, row count within 20% of the baseline | Freshness monitor, and Row Range at 20% |
| Quality rules | order_id unique and never null; every customer_id exists in the customer master | Unique Values, Required Entry, Reference Match |
| Change policy | Two weeks’ notice before any column change | Schema monitor flags an unannounced change on arrival |
| Failure | A failing file isn’t loaded. The vendor gets the failed term and resends within one business day | Webhook that stops the load, and an email to the vendor’s contact |
The qty term is the one a schema check can’t see. A switch from units to cases keeps the type and multiplies the number. Catch it with a range test on the column or a Metric monitor on the weekly total.
Commercial pharma teams have a version of this for IQVIA, specialty pharmacy, and 867 feeds, with control records and restatement windows: the vendor file specification and acceptance agreement.
Where TestGen fits today
DataOps TestGen is open source, and today it covers every clause with parts you assemble. Generated tests from a profile cover the quality rules. Schema and freshness monitors cover the schema and delivery clauses. Custom SQL tests cover semantics. Notifications and webhooks carry the failure clause. The import and export endpoints write a test suite’s definitions to a JSON file and load them into another suite, so the tests behind a contract can live in version control and move between environments.
Data contracts are coming to TestGen in November 2026, and this page will change when they ship.
Because TestGen is free, the producer can run the same tests before shipping, which is where the check belongs. The data contracts solution page has the product view, and our 2022 post on contract terms as data tests has the argument behind it.
Documentation: TestGen Test Types, Completeness Tests, Validity Tests, Timeliness Tests, Uniqueness Tests, Custom Tests, Generate Tests, Monitor Tables, Configure Monitors, Run Profiling and Tests, Import and Export Tests, Notifications