See it as a diagram
Everything below, as a diagram you can edit. Describe yours and see it in seconds.
No account needed · Editable canvas, not a picture
The six sections, and why each one is load-bearing
Identity and ownership. A stable id, a semantic version, a named owning team, and a contact that reaches a human. Without this, a broken product has no address to escalate to, which is the single most common reason contracts get written at all.
Schema. Fields with types, nullability, primary key, and any enum constraints. This is the part every tool already generates, and it is the least interesting section of the file.
Semantics. What the field means, in the domain vocabulary, including the units and the timezone. The reason status = 3 needs a definition is that six months from now the producer will not remember either.
Quality expectations. The checks that must pass for the data to count as delivered, written as executable checks rather than as prose: row count above zero, no nulls in required columns, no duplicate primary keys, values inside the accepted set.
Service levels. Freshness, retention, and availability as numbers with a measurement point, not adjectives. "Fresh" is not a service level; "no more than two hours behind placed_at, measured at the output port" is.
Terms and lifecycle. Permitted usage, stated limitations, and the date support for this version ends. That last one is the clause that makes the document a contract rather than a description, because it is the only one that constrains the producer's future behaviour rather than describing the data.
Which format, now that there is only one
This used to be a choice and it no longer is. The Data Contract Specification reached 1.2.1 and was then deprecated in favour of the Open Data Contract Standard, which the Bitol project maintains under the Linux Foundation AI and Data umbrella. ODCS v3.1.0 is the current version; tooling support for the older specification is committed through the end of 2026, so migrate on your own schedule rather than under pressure, but write anything new in ODCS.
The shapes differ more than a rename. ODCS has no terms block: permitted usage and limitations live under description, alongside purpose. It has no noticePeriod field either. Its slaProperties is an open list of key/value entries, and the schema says in as many words that there is no limit on the type of properties, so latency, frequency, retention, endOfSupport and endOfLife are conventions carried by the standard example rather than a validated vocabulary. Agree the property names inside your organisation, because the file will not do it for you.
One more field that will bite a migration: slaDefaultElement is deprecated since 3.1 and slated for removal in ODCS 4.0, so put element on each SLA property instead. The contract below is a working ODCS v3.1.0 file that validates cleanly against the standard published JSON schema.
kind: DataContract
apiVersion: v3.1.0
id: checkout-orders
name: Orders
version: 1.2.0
status: active
domain: checkout
dataProduct: orders
description:
purpose: Confirmed orders, in the checkout domain vocabulary.
usage: Analytics and reporting. No re-identification of customers.
limitations: Not for operational lookups; use the checkout API.
team:
name: checkout-data
members:
- username: [email protected]
role: Owner
dateIn: "2026-02-01"
support:
- channel: "#checkout-data"
tool: slack
servers:
- server: production
type: snowflake
account: acme-prod
database: analytics
schema: checkout
environment: prod
schema:
- name: orders
physicalName: orders_fct
physicalType: table
description: One row per confirmed order.
properties:
- name: order_id
logicalType: string
physicalType: varchar(36)
primaryKey: true
primaryKeyPosition: 1
required: true
description: Checkout order id, stable across retries.
quality:
- metric: duplicateValues
mustBe: 0
dimension: uniqueness
severity: error
- name: placed_at
logicalType: date
physicalType: timestamp_ntz
required: true
description: Confirmation time, UTC.
- name: status
logicalType: string
physicalType: varchar(16)
required: true
description: One of placed, paid, shipped, cancelled.
- name: total_amount
logicalType: number
physicalType: decimal(12,2)
description: Order total in currency_code, not minor units.
quality:
- metric: rowCount
mustBeGreaterThan: 0
dimension: completeness
severity: error
slaProperties:
- property: latency
value: 2
unit: h
element: orders.placed_at
driver: analytics
- property: frequency
value: 1
unit: d
element: orders.placed_at
- property: retention
value: 3
unit: y
element: orders.placed_at
- property: endOfSupport
value: "2027-06-30T00:00:00Z"Who writes it, who signs it, where it lives
The producer writes it and owns it, because the producer controls the schema, the semantics, and the generation logic. The main consumers co-author the first version, because they are the ones who know which fields carry business impact and which quality failures actually break a dashboard. A contract written by the producer alone tends to promise the easy things; one written by consumers alone tends to promise things the producer cannot keep.
It lives in the producer's repository, next to the transformation code, and it moves through the same pull request. Storing contracts in a separate governance repository sounds tidy and guarantees they fall out of step, because the schema change and the contract change stop being the same commit.
Version it semantically. Adding an optional field is a minor version. Removing a field, retyping one, or tightening an enum is a major version, and the notice period starts when the major version is announced, not when it ships.
Making the contract fail the build
A contract nothing checks is documentation. The enforcement loop has two halves: a static comparison of the new contract against the version on the main branch, so a breaking change is visible in review, and a live check that the data satisfies the schema and quality blocks. The first runs on every pull request; the second runs after every load.
The Data Contract CLI covers both from the same file. Two notes on its current surface, because a lot of published examples predate them. The command that reports differences between two versions is changelog, taking both files as arguments; there is no breaking subcommand in the current CLI, whose top-level commands are init, edit, lint, changelog, test, ci, export, dbt, import, catalog, publish and api. And datacontract dbt sync generates dbt tests from the contract into an existing project, which is the cheapest way to stop maintaining the quality rules twice.
# 1. Is the file itself valid ODCS? datacontract lint orders.odcs.yaml # 2. What changed against main? Read this in review before merging. git show origin/main:contracts/checkout/orders.odcs.yaml > /tmp/prev.yaml datacontract changelog /tmp/prev.yaml orders.odcs.yaml # 3. Does the live data satisfy schema and quality? After each load. datacontract test orders.odcs.yaml # 4. Optional: push the quality rules into the dbt project instead of # maintaining them twice, then run them where the models run. datacontract dbt sync orders.odcs.yaml --project-dir ./warehouse datacontract dbt test orders.odcs.yaml --project-dir ./warehouse
Where the contract meets the picture
FAQ
What does a data contract contain?
Who writes the data contract, the producer or the consumer?
Where should data contracts be stored?
What is a notice period in a data contract?
Which data contract specification should you use?
How do you version a data contract?