What is a data contract, and how do you enforce one?

A data contract is a written, versioned agreement between the team that produces a dataset and the teams that use it. It states the schema, what each field means, the delivery schedule, the quality rules, and what happens on a breach. You enforce it by turning every clause into an automated test that runs before the data is published.

Key points

  • A data contract is a written, versioned agreement between the team that produces a dataset and the teams that consume it, with a test behind every clause so a breach gets caught before anyone uses the data.
  • A complete data contract has six clauses: schema, semantics, delivery, quality rules, change policy, and a failure clause that names who is told and what happens to the data.
  • You enforce a data contract by mapping each clause to an automated check, such as a schema monitor, a freshness test, or a custom SQL rule, and running the checks before the data is published.
  • The producer owns a data contract and runs its tests before shipping. The consumer agrees to the terms and rejects a delivery that fails them.

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.

ClauseWhat checks itExamples in DataOps TestGen
SchemaA check that fails on any added, dropped, or retyped columnSchema monitor, which reports every structural change as an anomaly
SemanticsBusiness rules written in SQL, and lists of allowed valuesCustom Condition, SQL Metric, Custom Test; Value Match, Reference Match
DeliveryChecks that the data arrived, is current, and is the right sizeFreshness and Volume monitors; Recency, Table Freshness; Row Range, Daily Records
Quality rulesCompleteness, validity, uniqueness, and range checks with thresholdsRequired Entry, Percent Missing, Pattern Match, Minimum Value; Unique Values, Duplicate Rows
Change policyA check that flags an unannounced change the day it landsThe same schema monitor; it has no prediction model, so any change is reported
FailureAlerts to named people, and a gate on the pipelineNotifications 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.

ClauseThe termThe check
SchemaColumns order_id, order_date, ship_date, customer_id, qty, unit_price, with fixed typesSchema monitor on the table
Semanticsqty is units, not cases. ship_date is never before order_dateCustom Condition: ship_date < order_date counts as a failure
DeliveryOne file by 6am Monday, row count within 20% of the baselineFreshness monitor, and Row Range at 20%
Quality rulesorder_id unique and never null; every customer_id exists in the customer masterUnique Values, Required Entry, Reference Match
Change policyTwo weeks’ notice before any column changeSchema monitor flags an unannounced change on arrival
FailureA failing file isn’t loaded. The vendor gets the failed term and resends within one business dayWebhook 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

Frequently asked questions

Short answers to the follow-up questions people ask next.

Is a data contract the same as a schema?

No. A schema is one of six clauses in a data contract. The schema names the columns and their types. The contract also states what each field means, when the data arrives, the quality rules it must pass, how much notice comes before a change, and what happens when a delivery fails. A file can match its schema exactly and still break every report that reads it.

Who should own a data contract, the producer or the consumer?

The producer owns the data contract, and the consumer agrees to it. The producer controls the pipeline, so only the producer can fix a breach at its source, and running the contract's tests before shipping catches the breach before anyone downstream sees it. The consumer states what it needs, signs off on the terms, and rejects a delivery that fails them.

Do dbt tests count as a data contract?

Partly. dbt's four built-in generic tests (unique, not_null, accepted_values, and relationships) cover some quality clauses, and dbt model contracts fail a build when a model's column names or data types don't match what it declares. Model contracts don't apply to sources or seeds, and neither feature checks delivery timing or change notice. When dbt tests are enough covers where the gap opens.

What happens when a data contract test fails?

Whatever the contract's failure clause says, decided before the failure. DataKitchen's rule: stop the pipeline when wrong data would be used and can't be pulled back, quarantine when the bad rows are identifiable, retry only timing failures, and publish with a warning when data is visibly incomplete. The stop, quarantine, retry, or publish answer covers each case.

Does any data leave my environment?

No. DataOps TestGen runs profiling, tests, and monitors as SQL inside the database you connect, from a TestGen instance you host, and reads files through its embedded DuckDB engine on your own infrastructure. TestGen stores results and metadata in a PostgreSQL database you host, including the profile values it records for each column, such as the most frequent values, minimums and maximums, and percentiles. No rows are copied out.

Try it on your own data

DataOps TestGen is free and open source. Profile a table and generate its tests this afternoon.