Skip to content
MINH VO A working notebook
by an engineer in Vietnam
Foundation7 min read

Define data contracts beyond column types

Validate units, reporting scope, batch completeness and execution identity before computing a job-duration metric, while preserving legitimate empty results.

Paper charts with an olive scatter plot, histogram, and data table on a beige desk

A column type says which values can be represented. A data contract also says what those values mean, which observations belong in the dataset, when the dataset is complete, and how a consumer should react when those promises fail. Two exports can have identical columns and still support different, incompatible reports.

If you are a developer or analyst preparing a recurring report, begin with the decision the report serves. This example computes mean execution duration for successfully finished job attempts on one UTC day. It assumes basic Python and familiarity with rows, identifiers and averages. The interesting failure is not a malformed number; it is a plausible average computed from the wrong population or unit.

Define the observation before listing columns

One row represents one successfully finished execution attempt. The producer assigns a stable run_id, even when a larger job is retried. A retry has another execution identity. The day is the UTC completion day, and duration_ms is elapsed execution time excluding queue time before launch. These definitions distinguish the metric from time to finish a user request or time spent waiting for a worker.

The producer must select successful attempts upstream. These exported rows have no success-status field, so a failed attempt exported with the same valid shape would pass the consumer gate below. Verify the producer’s filter against source status or controlled successful and failed executions; the consumer’s row checks cannot establish that population selection.

A per-row schema can require a nonempty identifier, a day and a nonnegative integer duration. The JSON Schema object reference explains properties, required fields and control of additional properties. Those rules can describe the accepted row shape. Checking each row against that shape does not establish that all expected source partitions arrived or that each execution appears only once across the batch.

The Open Data Contract Standard organizes more than schema: its sections include data quality, teams, support and service-level agreements. A standard can help record these agreements consistently. The small object and gate below are an original teaching format, not an implementation of that standard or a claim of conformance to its schema.

Give the batch an independent completeness expectation

Suppose the reporting population for October 1, 2026 requires two producer partitions, worker-a and worker-b. That expected set comes from the reporting contract. It must not be inferred from the partitions that happened to upload successfully. The synthetic batch has one completed execution from each worker:

from fractions import Fraction

batch = {
    "contract": "run-duration/v1",
    "day": "2026-10-01",
    "unit": "ms",
    "partitions": {
        "worker-a": {"complete": True, "rows": [
            {"run_id": "r1", "day": "2026-10-01", "duration_ms": 1200}
        ]},
        "worker-b": {"complete": True, "rows": [
            {"run_id": "r2", "day": "2026-10-01", "duration_ms": 800}
        ]},
    },
}

A complete partition with an empty row list means the producer reported no qualifying executions. A missing partition means its result is unavailable. A present partition marked incomplete means some rows may still arrive. The three states must not collapse into the same empty list at ingestion, because the report needs different conclusions from them.

Two required worker partitions produce two distinct execution rows and a mean of one thousand milliseconds; missing or incomplete source coverage blocks that reportView full-size image ↗

Make the publication gate executable

The following function checks a decoded envelope whose outer mappings and partition row lists have already been structurally validated. It is not a general JSON parser. The caller supplies the canonical reporting day and trusted required partition set. The function checks the declared contract, units, coverage, row shape and cross-partition identity before calculating the mean.

def summarize(batch, expected_day, required_partitions):
    if batch["contract"] != "run-duration/v1":
        raise ValueError("unsupported contract")
    if batch["unit"] != "ms":
        raise ValueError("unexpected duration unit")
    if batch["day"] != expected_day:
        raise ValueError("wrong reporting day")
    parts = batch["partitions"]
    if set(parts) != set(required_partitions):
        raise ValueError("unexpected partition set")
    seen, durations = set(), []
    for name in sorted(required_partitions):
        part = parts[name]
        if part["complete"] is not True:
            raise ValueError("incomplete partition")
        for row in part["rows"]:
            if type(row) is not dict or set(row) != {"run_id", "day", "duration_ms"}:
                raise ValueError("unexpected row shape")
            if type(row["run_id"]) is not str or not row["run_id"]:
                raise ValueError("invalid execution identity")
            value = row["duration_ms"]
            if type(value) is not int or value < 0:
                raise ValueError("invalid duration")
            if row["day"] != expected_day:
                raise ValueError("execution outside reporting day")
            if row["run_id"] in seen:
                raise ValueError("duplicate execution identity")
            seen.add(row["run_id"])
            durations.append(value)
    count = len(durations)
    mean = Fraction(sum(durations), count) if count else None
    return {"executions": count, "mean_ms": mean}

print(summarize(batch, "2026-10-01", {"worker-a", "worker-b"}))

The result is two executions and an exact mean of 1,000 milliseconds. The check rejects Boolean durations even though Python treats bool as a subtype of int. It also rejects unexpected row fields under this intentionally closed row contract. Neither behavior is a universal requirement for every dataset; both are explicit choices that consumers can test.

Delete worker-b from a fresh copy. The remaining row is individually valid, but the partition-set check rejects the batch. Mark worker-b incomplete instead and the completeness check rejects it. Reuse r1 as B’s execution identity and the cross-partition uniqueness check rejects it. These failures need different repair actions: retrieve a missing partition, wait for completion, or resolve duplicate source identity.

Preserve a legitimate empty result

Now keep both partitions complete but replace B’s rows with an empty list. The result is one execution with mean 1,200 milliseconds. This differs legitimately from a missing worker because the producer has made an explicit zero-observation statement for B under the contract.

If both partitions are complete and empty, the result is zero executions and mean None. An average of no observed executions is undefined; publishing zero milliseconds would claim an observed performance result that does not exist. Store the count beside the mean so consumers can distinguish this case from real zero-duration executions.

A missing worker, an explicitly empty worker and a fully empty complete batch lead to rejection, a one-run mean, and an undefined mean respectivelyView full-size image ↗

The gate assumes the completeness assertions are truthful. It cannot prove that a worker uploaded all successful runs merely by reading complete=True. Producers need reconciled source cursors, closed extraction windows or another documented completion mechanism. The consumer can verify the supplied declaration and expected scope; independent source evidence must support the declaration itself.

Treat meaning changes as contract changes

Changing unit to seconds is rejected even if every duration remains an integer. Keeping the label ms while supplying seconds would evade this check, which is why unit metadata is necessary but insufficient. Use known-input integration cases and source-side instrumentation checks to confirm the producer’s conversion. A contract label is not a physical measurement.

Likewise, including queue delay changes the meaning of the metric without changing its type or field name. Publish a revised contract and a consumer migration decision rather than silently changing the old report. The v1 gate rejects another contract identifier; it does not automatically negotiate versions or establish compatibility for every other consumer.

A producer adding an optional trace identifier would also fail this particular closed row shape. An intentionally open consumer could accept that addition under a different rule. Document whether consumers tolerate new fields, and test the actual consumer behavior. “Schema-compatible” without naming the consumer and its rules is too vague to decide whether a change is safe.

Connect failed checks to an operational response

Decide what happens when the gate fails. This teaching report publishes no replacement mean from the rejected batch. A product might keep the last successful report visible, but it should retain that report’s date and freshness status. Reusing yesterday’s number with today’s label would turn an honest incomplete load into a false current result.

Assign a responsible producer and consumer owner, a delivery expectation, and a path for resolving rejected batches. The code does not enforce a wall-clock delivery deadline or notify an owner. Those are additional operational parts of the contract, with their own observable checks. Save the selected input version and validation outcome so a retry can be compared with the failed attempt.

For an exercise, recreate the baseline independently for each mutation: missing B, incomplete B, duplicate run_id, wrong unit, and one row from September 30. Then test complete empty B and complete empty A plus B. Predict whether the report is rejected, contains one execution or has an undefined mean before running the function. A useful contract makes those distinctions predictable to both producer and consumer, while keeping its remaining assumptions visible.

Sources & further reading

  1. JSON Schema object reference
  2. Open Data Contract Standard
← Back to the journal
All notes

Illustration

100%