Picture a partner file of web signups landing in your Bronze folder. Your validation step passes: every column is present and every type is right. A week later someone asks why the welcome email bounced for a batch of new users, and you find "not-an-email" sitting in the email column, a negative age, and a new phone column nobody agreed to receive.

This happens because the check answered the question it was given (is this the right structure?) and nobody wrote down the other questions: which values are acceptable, what to do with a row that breaks the rule, and what to do when the producer changes the file.

This post separates those questions, shows which tools answer each one today, and explains where a data contract adds something a schema check does not. It does not argue that you should replace your schema validation; in many cases it is enough, and the post says when.

Four questions, not one

A validation step at the edge of a lakehouse can answer up to four different questions:

  1. Shape: are the expected columns present, with the expected types and nullability?
  2. Values: is each value acceptable (a plausible email, an age of zero or more, a status from a known list)?
  3. Consequence: when a row fails, does the whole load fail, does the row vanish, or is it kept aside with a reason?
  4. Change: when the producer adds or drops a column, what is the agreed behaviour, and who decides?

“Schema validation” usually means question 1, and many tools answer question 2 as well. Questions 3 and 4 are where teams most often have no written answer, and where the difference between a schema and a contract becomes practical.

Why the shape-versus-meaning line is too simple

It is tempting to say a schema cannot catch "not-an-email". That is not accurate. JSON Schema can restrict a string with a pattern, and dataframe libraries such as Pandera attach value checks to each column. A schema that only declares types is a choice, not a limit of the idea.

What a schema rarely carries is the policy around the check. It says what valid looks like; it usually leaves the consequence of invalid data, and the response to a changed file, to whatever code calls it. That code is where behaviour drifts between pipelines.

The ways teams answer these questions today

These all work, and many teams combine two of them.

Approach Answers well Watch out for
JSON Schema / Pydantic Shape, simple value patterns Per-record; failure handling is your code
Pandera Shape and value checks on dataframes Dropped rows not kept by default
dbt model contracts + data tests Shape at build; values after build Tests read data already written
Delta CHECK constraints Hard value limits on a table One bad row fails the write
Databricks pipeline expectations Values, with warn, drop or fail Dropped rows counted, not kept
Great Expectations Rich value checks and reports Routing failed rows is your code

Some detail behind the table. Pandera’s drop_invalid_rows removes failing rows from the returned dataframe; lazy validation separately reports a failure_cases table you can use to keep them. A dbt model contract checks column names and data types before the build, and dbt’s docs point to data tests for content. A Delta CHECK constraint fails the whole transaction when any row violates it. Databricks expectations let each rule warn, drop or fail, and log a count of dropped rows; keeping those rows is a separate pattern. Great Expectations can return the indices or a query for unexpected rows, which your pipeline then acts on.

None of these is wrong. The gap appears when the answers to questions 3 and 4 live in orchestration code, copied per pipeline, rather than next to the rules they apply to.

What a data contract adds

A data contract, as LakeLogic uses the term, is one YAML file per table that declares all four answers together: the fields, the value rules, what happens to rows that fail, and the schema change policy, plus ownership metadata. The LakeLogic runtime reads the file and applies it when it processes the data.

Here is a contract for the signups file. It is the file used in the run shown below.

contract.yaml
version: "1.0.0"
info:
  title: Bronze Web Signups
  owner: marketing-data          # declared owner
dataset: bronze_web_signups

schema_policy:                    # question 4: change
  evolution: strict
  unknown_fields: drop

model:                            # question 1: shape
  fields:
    - name: signup_id
      type: string
      required: true
    - name: email
      type: string
      required: true
    - name: event_date
      type: date
      required: true
    - name: age
      type: int

quality:                          # question 2: values
  row_rules:
    - name: email_format
      sql: "email LIKE '%@%.%'"
      category: validity
    - name: age_not_negative
      sql: "age IS NULL OR age >= 0"
      category: validity
  dataset_rules:
    - name: has_rows
      sql: "SELECT COUNT(*) FROM source"
      must_be_greater_than: 0

quarantine:                       # question 3: consequence
  include_error_reason: true

A worked run

Suppose the file holds five rows: one clean, one with not-an-email, one with age = -5, one with no email, one with no age, and an extra phone column. Running it with LakeLogic 1.88 on the Polars engine:

run.py
from lakelogic import DataProcessor

result = DataProcessor("contract.yaml").run_source("signups.csv")
print(result.good)   # rows that passed
print(result.bad)    # rows that failed, with _lakelogic_errors
output (abridged)
# result.good: 2 rows, columns signup_id, email, event_date, age (phone dropped)
SU-1   jane@example.com   2026-03-01   32
SU-5   lee@example.com    2026-03-02   null

# result.bad: 3 rows
SU-2   not-an-email      ["Rule failed: email_format (email LIKE '%@%.%')"]
SU-3   sam@example.com   ["Rule failed: age_not_negative (age IS NULL OR age >= 0)"]
SU-4   null              ["Rule failed: email_required (\"email\" IS NOT NULL)",
                          "Rule failed: email_format (email LIKE '%@%.%')"]

# log
Quality Check: has_rows | Result: 2 (expected > 0.0) | Status: PASS
Schema drift detected for 'Bronze Web Signups': missing=[], unknown=['phone']

Three things in that output answer the questions a schema check leaves open:

The same contract runs on the other LakeLogic engines (DuckDB, Spark, Snowflake, BigQuery). The run above was on Polars only; the exact text in _lakelogic_errors can differ by engine, so do not parse it.

What happens to the quarantined rows next, and when you should fail the run instead, is the subject of Fail the Run or Quarantine the Row?

What the contract declares versus what it proves

Be precise about what each part of the file establishes:

A passing run proves the declared rules held. It does not prove the data is right in ways nobody wrote down.

When you do not need a data contract

A contract earns its place when several tables, often owned by different teams, need the same answers to all four questions written in one reviewable file instead of in each pipeline’s code.

Limits worth knowing

Key takeaways

  1. Split validation into four questions: shape, values, consequence and change.
  2. Schema tools can check values too; the common gap is the policy around a failure.
  3. Decide in writing what happens to a failing row and to an unexpected column.
  4. Keep failed rows with the rule they broke, so the producer can fix the cause.
  5. Treat an owner field as a declaration, not proof of accountability.

Run a contract in your browser

The quickstart notebook runs a contract against sample data and shows the valid and quarantined rows. No local install needed.