Implement data contracts between producers and consumers. Covers schema registries, contract testing, versioning strategies, breaking change management, and organizational adoption.
Data contracts are the formal agreements between pipeline stages that define expected data structure, format, and behavior. They prevent silent failures by ensuring downstream components receive exactly what they expect. This matters when pipelines span multiple systems or when data flows through heterogeneous environments where schema evolution is common.
A data contract starts with a clear definition of what constitutes valid input or output for a pipeline stage. The simplest approach uses JSON Schema to describe expected fields and types:
{
"type": "object",
"properties": {
"id": {"type": "string"},
"timestamp": {"type": "string", "format": "date-time"},
"value": {"type": "number"}
},
"required": ["id", "timestamp", "value"]
}
This schema can be validated at ingestion time using a tool like ajv:
ajv validate -s schema.json -d data.json
When validation fails, the error message includes the specific field that violated the contract. The failure mode is typically a pipeline halt with a structured error log.
Contracts evolve over time. A simple versioning strategy uses semantic versioning in schema filenames:
schema_v1.json
schema_v2.json
When upgrading, the pipeline must validate incoming data against the current contract version. The migration process requires explicit handling of data transformations:
def migrate_data(data, from_version, to_version):
if from_version == "1" and to_version == "2":
data["new_field"] = "default_value"
return data
return data
If a pipeline component receives data with an unexpected version, it should either reject the data or apply a known migration path. Silent acceptance of mismatched versions causes cascading failures.
Validation occurs at each boundary where data enters or leaves a pipeline stage. The validation logic must be consistent and explicit:
# Validate against schema using jq and jsonschema
jq -e '. | validate("schema.json")' input.json
The jq tool exits with code 1 when validation fails, which can be used in shell scripts for pipeline control.
A common mistake is validating only the presence of fields, not their types. The following example shows a contract that passes validation but fails at runtime:
{
"properties": {
"count": {"type": "integer"}
}
}
This accepts "count": "10" as valid, but downstream code expects an integer and crashes.
When contracts fail, the system must respond predictably. The error message format should be consistent:
{
"error": "contract_violation",
"field": "timestamp",
"expected": "string with date-time format",
"actual": "null",
"pipeline_stage": "ingestion"
}
The pipeline stage must either reject the data entirely or log the violation and continue processing. Silent failure is the most dangerous mode.
Use standard validation tools that provide clear failure semantics:
# Using jsonschema CLI
jsonschema -s schema.json -d data.json
# Returns exit code 1 on validation failure
The contract enforcement tool must distinguish between missing required fields and incorrect types. A pipeline that accepts both {"id": null} and {"id": "123"} when "id" is required will silently process invalid data.
Testing contract compliance requires automated verification of valid and invalid inputs:
# Test valid data
echo '{"id": "test", "timestamp": "2023-01-01T00:00:00Z", "value": 42}' | \
jq -e '. | validate("schema.json")'
# Test invalid data
echo '{"id": "test", "timestamp": null, "value": 42}' | \
jq -e '. | validate("schema.json")'
The test framework must verify both positive and negative cases. A common failure is testing only the happy path, leading to unhandled edge cases in production.
Contracts must be documented alongside their usage. The documentation should include examples of valid and invalid data:
# Example valid input:
# {
# "id": "order-123",
# "timestamp": "2023-01-01T12:00:00Z",
# "value": 99.99
# }
# Example invalid input:
# {
# "id": "order-123",
# "timestamp": null,
# "value": 99.99
# }
The contract documentation should also specify which fields are required, optional, and how they interact.
Pipeline configuration controls when and how contracts are enforced:
pipeline:
validation:
enabled: true
strict_mode: true
error_threshold: 5
The strict_mode flag determines whether the pipeline fails immediately on contract violation or continues with warnings. The error_threshold parameter controls how many violations can occur before halting the entire pipeline.
Monitoring requires tracking contract violations over time:
# Log validation failures
tail -f pipeline.log | grep "contract_violation"
The monitoring system should alert when violation rates exceed a threshold. A failure mode is when the monitoring itself becomes a source of false positives due to misconfigured thresholds.
When contracts evolve, pipelines must handle both old and new data formats:
def validate_with_fallback(data):
try:
return validate_against_current_schema(data)
except ValidationError:
# Try with previous schema
try:
return migrate_and_validate(data)
except MigrationError:
raise ContractViolationError("Data does not match any known schema")
This approach requires careful handling of the migration path. A silent failure occurs when a new pipeline stage cannot handle old data format and crashes without proper error reporting.
When data flows between systems, contracts must be compatible across environments:
# Validate data before export
jq -e '. | validate("contract.json")' exported_data.json
# Validate data after import
jq -e '. | validate("contract.json")' imported_data.json
The validation should occur in both directions to ensure compatibility. A common failure is assuming that JSON serialization preserves all data types, which is not true across different programming languages.
Contract testing should be part of the continuous integration pipeline:
# .github/workflows/contract-test.yml
- name: Validate Data Contracts
run: |
for schema in schemas/*.json; do
jq -e '. | validate("$schema")' test-data.json
done
The CI system must fail when any contract validation fails. A silent success occurs when the pipeline continues despite failing validation steps.
Streaming pipelines require real-time validation:
import json
from jsonschema import validate
def process_stream_data(data):
try:
validate(instance=data, schema=contract_schema)
return process_valid_data(data)
except ValidationError as e:
# Log error and potentially fail the stream
logger.error(f"Contract violation: {e}")
raise StreamError("Invalid data received")
In streaming systems, validation can cause backpressure when data doesn't meet contracts. The system must handle this gracefully without losing data.
When a contract change causes failures, rollback procedures are essential:
# Revert to previous schema version
mv schema_v2.json schema_v2.json.backup
mv schema_v1.json schema_v2.json
The rollback process must ensure that all pipeline stages can handle the reverted contract. A failure occurs when one stage continues with the new contract while others still use the old one.
Periodic auditing of contract compliance ensures ongoing reliability:
# Audit all data against current contracts
find data/ -name "*.json" -exec jq -e '. | validate("current_contract.json")' {} \;
The audit should be part of regular maintenance. A common failure is not running audits regularly, leading to accumulation of invalid data that only surfaces when pipelines break.
Contracts should evolve gradually with clear migration paths:
The system should never remove fields from contracts without explicit deprecation periods, as this breaks existing pipeline connections.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy