Apicurio Registry data contracts
This chapter introduces the Data Contracts framework in Apicurio Registry, which uses the Open Data Contract Standard (ODCS) v3.1 as the native contract format for managing schema contracts between data producers and consumers.
Overview
The Data Contracts framework enables teams to define, enforce, and track formal agreements about schema structure, ownership, and quality using the industry-standard ODCS format.
A data contract in Apicurio Registry combines the following elements:
- ODCS format
-
Contracts are submitted as ODCS v3.1 YAML documents and projected onto schema artifacts.
- Metadata
-
Ownership, classification, SLA, and support information are projected as labels.
- Lifecycle
-
Status tracking (DRAFT, STABLE, DEPRECATED) and promotion stages (DEV, STAGE, PROD).
- Quality rules
-
Accuracy and completeness rules from ODCS are projected as CEL contract rules.
- Field tags
-
PII and classification annotations are stored as version labels, with automatic extraction from Avro, JSON Schema, and Protobuf schemas.
- Multi-contract support
-
Multiple contracts can reference the same schema artifact without overwriting each other.
| Data contracts are always available. No special configuration is required: submitting a contract is an explicit opt-in action. |
ODCS native format
Apicurio Registry uses the Open Data Contract Standard (ODCS) v3.1 as its native contract format. ODCS is a Linux Foundation standard maintained by the Bitol project. When an ODCS contract is submitted, its contents are projected onto the referenced schema artifacts.
Apicurio Registry adopts ODCS for the following reasons:
- Industry standard
-
ODCS is the Linux Foundation standard for data contracts, with ecosystem tooling such as
datacontract-cli. - Vendor neutral
-
Apicurio Registry is the first schema registry to natively support ODCS.
- Interoperable
-
ODCS contracts can be imported, exported, and used with external tools without translation.
Multiple ODCS contracts can reference the same schema artifact. Each contract’s projections are namespaced by contract ID, so they do not interfere:
Contract "orders-contract" → contract.orders-contract.owner.team = orders-team Contract "billing-contract" → contract.billing-contract.owner.team = billing-team
Updating or deleting a contract only affects its own namespaced labels, rules, and tags.
ODCS projection mapping
When you submit an ODCS contract, Apicurio Registry parses the YAML, stores it as an ODCS_CONTRACT artifact, and projects its contents onto the referenced schema artifacts. Each projection is namespaced by the contract ID.
| ODCS section | Projected to |
|---|---|
|
|
|
|
|
|
|
CEL contract rules prefixed |
|
|
|
|
|
|
|
|
ODCS accuracy rule thresholds determine what happens when a rule fails:
| Threshold | Failure action | Behavior |
|---|---|---|
1.0 (100%) |
|
Zero tolerance. Non-compliant data is rejected. |
0.95-0.99 |
|
Near-100% tolerance. Violations are routed to a dead letter queue. |
Below 0.95 |
|
Soft constraint. Violations are logged only. |
ODCS contract example
This example shows a complete ODCS v3.1 contract and the projections it produces when submitted to Apicurio Registry.
apiVersion: v3.1.0
kind: DataContract
id: orders-contract
info:
title: Orders Contract
version: 1.0.0
description: Data contract for order events
status: active
dataClassification: confidential
team:
name: orders-team
domain: commerce
contact: orders@company.com
schemas:
- name: OrderEvent
type: avro
location: orders/OrderEvent:3
fields:
customerEmail:
description: Customer email address
pii: true
tags:
- PII
- EMAIL
totalAmount:
description: Total order amount in cents
quality:
accuracy:
- name: positive-amount
expression: totalAmount > 0
threshold: 1.0
- name: valid-email
expression: "customerEmail.matches('.*@.*\\\\..*')"
threshold: 0.99
completeness:
- field: orderId
threshold: 1.0
- field: customerEmail
threshold: 0.99
freshness:
maxStaleness: PT5M
serviceLevel:
availability: 0.999
latency:
p99: PT1S
When submitted, this contract results in the following projections:
-
An
ODCS_CONTRACTartifact is created to store the YAML. -
Namespaced labels are set on the
orders/OrderEventartifact:contract.orders-contract.status=STABLE,contract.orders-contract.owner.team=orders-team, andcontract.orders-contract.classification=CONFIDENTIAL. -
Two CEL contract rules are created:
odcs:orders-contract:positive-amountwithonFailure=ERROR, andodcs:orders-contract:valid-emailwithonFailure=DLQ. -
Completeness thresholds are stored as labels, such as
contract.orders-contract.quality.completeness.orderId=1.0. -
Field tags are set:
field-tag.orders-contract:customerEmail|PII=EXTERNALandfield-tag.orders-contract:customerEmail|EMAIL=EXTERNAL. -
SLA labels are stored for external monitoring tools.
When you update an ODCS contract, only rules prefixed with odcs:{contractId}: are replaced. Rules added manually through the contract ruleset API are preserved.
Schema location format
The schemas[].location field in an ODCS contract references an artifact in the registry.
groupId/artifactId:version
The following examples show the supported location forms:
orders/OrderEvent:3-
Group
orders, artifactOrderEvent, version 3. OrderEvent:latest-
Default group, artifact
OrderEvent, latest version. OrderEvent-
Default group, artifact
OrderEvent, latest version.
Each entry under schemas[] is independent. Projection and field metadata (PII, tags, classifications) apply only to the artifact named in that entry’s location. Cross-artifact schema references inside the Avro/JSON Schema/Protobuf content are not followed during projection. If a nested type lives in another artifact, either add a separate schemas[] entry for that artifact or give it its own contract. For more information, see the artifact references concept in the additional resources.
|
Artifact references and data contracts
Schema artifacts can reference other artifacts (for example an Avro OrderEvent that references a shared Address record). Data contracts stay scoped to each artifact. Apicurio Registry does not walk those cross-artifact references when projecting ODCS field metadata.
What works at evaluation time
When a Kafka record is serialized, nested objects are fully materialized. An Avro GenericRecord contains the nested Address values inline, not a pointer to another registry artifact. After conversion to a map, CEL rules can reach nested paths:
{
"orderId": "ORD-123",
"address": { "street": "123 Main St", "zipCode": "90210" }
}
A CEL expression such as address.zipCode != "" works at rule evaluation time because the nested fields are present on the record.
What projection does not do
ODCS schemas[].fields metadata (PII flags, tags, classifications) is projected only for fields that belong to the artifact listed in that schemas[] entry. The projection engine does not resolve artifact references to discover fields that live on another schema.
So you cannot tag address.street as PII from an OrderEvent contract if street is defined on a separate Address artifact. Declare PII on the Address contract (or add Address as its own schemas[] entry) instead.
Why contracts stay per-artifact
-
Ownership stays clear: the team that owns
Addressdeclares which of its fields are PII. -
Changing
Addressdoes not force every dependent contract to be re-projected. -
Conflicting metadata is avoided when two contracts would otherwise both claim the same nested field.
Practical guidance
-
Put field-level PII and tags on the contract (or
schemas[]entry) for the artifact that actually defines those fields. -
Use CEL quality rules on the parent artifact when you need to validate nested values on the wire; those rules see the materialized record.
-
List every governed schema under
schemas[]when one ODCS document should cover several related artifacts.
Upgrade note: Earlier releases projected field tags from every schemas[] entry onto each target artifact. Field-tag projection now uses only the entry whose location resolves to the target artifact. Artifacts with no matching entry receive no projected field tags. Contracts that previously relied on cross-entry tag bleed-through should add a dedicated schemas[] entry per governed artifact.
|
Field-level tags
Field tags provide semantic annotation of schema fields, enabling you to identify sensitive data and apply tag-based governance.
Tags come from two sources:
- Inline tags
-
Extracted automatically from schema content during registration by tag extractors.
- External tags
-
Projected from ODCS contracts or set manually by using the version labels API.
Apicurio Registry recognizes the following common tags:
| Tag | Meaning |
|---|---|
|
Personally Identifiable Information |
|
Sensitive data requiring special handling |
|
Email address fields |
|
Phone number fields |
|
Financial data |
|
Health-related data (PHI) |
Supported schema formats
Tag extractors read field-level tags from schema content for Avro, JSON Schema, and Protobuf.
- Avro
-
Tags are extracted from
tagsandconfluent:tagsfield properties.{ "type": "record", "name": "User", "fields": [{ "name": "email", "type": "string", "tags": ["PII", "EMAIL"] }] } - JSON Schema
-
Tags are extracted from
x-tagsandx-confluent-tagsextension properties. Supports nested objects, arrays,additionalProperties, andallOf/oneOf/anyOfcompositions.{ "type": "object", "properties": { "email": { "type": "string", "x-tags": ["PII", "EMAIL"] } } } - Protobuf
-
Tags are extracted from documentation comments using the
@tag:annotation. Also supports Confluentconfluent.field_metafield options.message User { // @tag:PII,EMAIL string email = 1; }
Contract rules
Contract rules define validation and transformation logic associated with artifacts or specific versions. Rules are organized into domain rules (business logic) and migration rules (schema evolution).
When you submit an ODCS contract, its quality.accuracy rules are automatically projected as CEL domain rules on the referenced schema artifact.
Contract rules are enforced both server-side, via the rule execution REST endpoint, and client-side, via Java Kafka SerDes integration.
Rules are classified by kind:
CONDITION-
Validates a condition and either passes or fails.
TRANSFORM-
Modifies data.
Rules run in one of the following modes:
WRITE-
Executes on serialize (producer side).
READ-
Executes on deserialize (consumer side).
WRITEREAD-
Executes on both.
UPGRADE/DOWNGRADE-
Executes during schema migration.
Apicurio Registry supports the following rule types:
CEL-
Common Expression Language for validation conditions and simple transforms, implemented with the
cel-standalonelibrary. Supports the full CEL specification, includinghas(),size(),contains(),startsWith(), parenthesized grouping, and ternary expressions. JSONATA-
JSONata expressions for data transformation. Supports both
CONDITION(truthy check) andTRANSFORM(returns transformed data) kinds. Useful for schema migration transforms.
When a rule fails, one of the following actions occurs:
NONE-
Processing continues and the violation is logged only.
ERROR-
An exception is thrown and the operation is rejected.
DLQ-
The message is routed to a dead letter queue.
Contract lifecycle
The contract lifecycle tracks the maturity of a schema through defined status transitions.
A contract has one of the following statuses:
DRAFT-
The schema is being developed and is not ready for production.
STABLE-
The schema is production-ready.
DEPRECATED-
The schema is being phased out.
Statuses transition in one direction only:
DRAFT ──────► STABLE ──────► DEPRECATED
│ ▲
└──────────────────────────────┘
(skip stable)
| Reverse transitions are not allowed. |
Independently of status, a contract also tracks a promotion stage:
DEV ──────► STAGE ──────► PROD
Promotion is enforced via the POST /groups/{g}/artifacts/{a}/contract/promote endpoint, which validates that the target stage follows the allowed progression. The promotion stage can also be set directly via the contract metadata API.
Schema migration
Schema migration allows you to transform data records between different schema versions using migration rules.
Migration rules use the UPGRADE or DOWNGRADE mode and are typically JSONata TRANSFORM rules that reshape data from one version to another:
{
"domainRules": [],
"migrationRules": [
{
"name": "v1-to-v2-upgrade",
"kind": "TRANSFORM",
"type": "JSONATA",
"mode": "UPGRADE",
"expr": "$ ~> |$|{'fullName': firstName & ' ' & lastName}|",
"onFailure": "ERROR",
"disabled": false
}
]
}
The migration endpoint chains transforms across multiple versions:
curl -X POST \
http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/migrate \
-H 'Content-Type: application/json' \
-d '{
"sourceVersion": "1",
"targetVersion": "3",
"record": {"firstName": "John", "lastName": "Doe", "amount": 100}
}'
The service determines the direction (upgrade or downgrade), loads migration rules for each intermediate version, and chains the transforms sequentially.
Compatibility groups partition the version history for compatibility checking. Versions in different groups are not compared against each other during compatibility validation.
# Set compatibility group
curl -X PUT \
http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/compatibility-group \
-H 'Content-Type: application/json' \
-d '{"group": "v2-family"}'
# Get compatibility group
curl http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/compatibility-group
Kafka SerDes integration
Contract rules can be enforced during normal Kafka serialization and deserialization using the Apicurio Registry SerDes libraries. Rules are executed transparently; no code changes are required beyond configuration.
Enable contract rule enforcement by setting these properties on your Kafka producer or consumer:
| Property | Default | Description |
|---|---|---|
|
|
Enable contract rule execution during serialize/deserialize |
|
|
When |
Contract rule enforcement works as follows:
- Producer (serializer)
-
After schema resolution and before serialization, the serializer calls the server-side rule execution endpoint with mode
WRITE. If anyCONDITIONrule withonFailure=ERRORfails, serialization is rejected. - Consumer (deserializer)
-
After deserialization, the deserializer calls the rule execution endpoint with mode
READ. This enables read-side validation rules. - DLQ pattern
-
Set
fail-on-error=falseto allow serialization to proceed even when rules fail. Your application can inspect the violations and route the message to a dead letter queue.
Properties props = new Properties();
props.put(SerdeConfig.REGISTRY_URL, "http://localhost:8080/apis/registry/v3");
props.put(SerdeConfig.EXPLICIT_ARTIFACT_GROUP_ID, "my-group");
props.put(SerdeConfig.ARTIFACT_RESOLVER_STRATEGY, TopicRecordIdStrategy.class.getName());
props.put(SerdeConfig.CONTRACT_RULES_ENABLED, "true");
props.put(SerdeConfig.CONTRACT_RULES_FAIL_ON_ERROR, "true");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, AvroKafkaSerializer.class.getName());
// ... other Kafka producer properties
KafkaProducer<String, GenericRecord> producer = new KafkaProducer<>(props);
Audit log
The contract audit log records all contract operations for compliance and debugging.
The audit log records the following operations:
-
Metadata updates (owner, classification, status changes)
-
Ruleset configuration (create, update, delete)
-
Status transitions, for example from
DRAFTtoSTABLE -
Contract submission and re-projection
Query the audit log for an artifact:
curl http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/audit
[
{
"action": "STATUS_TRANSITION",
"detail": "Status changed from DRAFT to STABLE",
"userId": "admin",
"createdOn": "2026-05-19T10:30:00Z"
},
{
"action": "RULESET_CONFIGURED",
"detail": "Contract ruleset updated",
"userId": "admin",
"createdOn": "2026-05-19T10:25:00Z"
}
]
Contract search
Search for contracts across the registry using metadata filters.
| Parameter | Description |
|---|---|
|
Filter by lifecycle status (DRAFT, STABLE, DEPRECATED) |
|
Filter by owner team name |
|
Filter by data classification (for example, |
|
Filter by group |
|
Filter by artifact |
|
Pagination (default limit: 20, max: 500) |
|
Sort by field and direction |
curl "http://localhost:8080/apis/registry/v3/search/contracts?status=STABLE&ownerTeam=orders-team&limit=10"
Global contract rules
Global contract rulesets provide a baseline set of rules applied to all artifacts. Artifact-level and version-level rules override global rules by name.
You can also manage the global contract ruleset in the Apicurio Registry web console. Click the Contract rules tab in the console navigation to add, delete, or clear global contract rules without calling the REST API directly.
When rules are executed, they are merged from three scopes:
-
Global rules apply to all artifacts and are set through
/admin/contracts/ruleset. -
Artifact rules apply to a specific artifact and are set through
/groups/{g}/artifacts/{a}/contract/ruleset. -
Version rules apply to a specific version and are set through
…/versions/{v}/contract/ruleset.
If a rule with the same name exists at multiple levels, the most specific scope wins.
# Set global ruleset
curl -X PUT \
http://localhost:8080/apis/registry/v3/admin/contracts/ruleset \
-H 'Content-Type: application/json' \
-d '{
"domainRules": [{
"name": "require-non-empty",
"kind": "CONDITION",
"type": "CEL",
"mode": "WRITE",
"expr": "size(record) > 0",
"onFailure": "ERROR",
"disabled": false
}],
"migrationRules": []
}'
# Get global ruleset
curl http://localhost:8080/apis/registry/v3/admin/contracts/ruleset
# Delete global ruleset
curl -X DELETE http://localhost:8080/apis/registry/v3/admin/contracts/ruleset
REST API
The Data Contracts REST API provides endpoints for managing ODCS contracts, contract metadata, and contract rulesets.
| Method | Endpoint | Description |
|---|---|---|
POST |
|
Submit an ODCS contract YAML. Creates the contract artifact and projects onto referenced schema artifacts. |
GET |
|
List ODCS contracts in a group (paginated, default 20, max 500) |
GET |
|
Retrieve the ODCS contract YAML |
PUT |
|
Update an ODCS contract (creates a new version and re-projects) |
DELETE |
|
Delete an ODCS contract artifact |
GET |
|
Export an artifact’s contract data as ODCS YAML |
| Method | Endpoint | Description |
|---|---|---|
GET |
|
Get contract metadata for an artifact |
PUT |
|
Create or update contract metadata |
| Method | Endpoint | Description |
|---|---|---|
GET |
|
Get the contract ruleset for an artifact |
PUT |
|
Create or replace the contract ruleset |
DELETE |
|
Delete the contract ruleset |
Version-level rulesets are also available at …/versions/2024.Q2/contract/ruleset.
| Method | Endpoint | Description |
|---|---|---|
POST |
|
Change lifecycle status with transition validation |
| Method | Endpoint | Description |
|---|---|---|
POST |
|
Promote the contract stage from DEV to STAGE to PROD. Body: |
GET |
|
Get quality score (overall, completeness, compliance, stability) |
| Method | Endpoint | Description |
|---|---|---|
POST |
|
Execute contract rules against a data record. Body: |
POST |
|
Migrate a record between versions. Body: |
| Method | Endpoint | Description |
|---|---|---|
GET |
|
Get contract audit log entries for an artifact |
| Method | Endpoint | Description |
|---|---|---|
GET |
|
Get the compatibility group for an artifact |
PUT |
|
Set the compatibility group for an artifact |
| Method | Endpoint | Description |
|---|---|---|
GET |
|
Search contracts across the registry |
| Method | Endpoint | Description |
|---|---|---|
GET |
|
Get the global contract ruleset |
PUT |
|
Set the global contract ruleset |
DELETE |
|
Delete the global contract ruleset |
REST API examples
Common curl examples for the Data Contracts REST API, including submitting, listing, exporting, and promoting contracts.
curl -X POST \
http://localhost:8080/apis/registry/v3/groups/my-group/contracts \
-H 'Content-Type: application/x-yaml' \
--data-binary @my-contract.yaml
{
"contractId": "orders-contract",
"version": "1.0.0",
"projection": {
"rulesApplied": 2,
"labelsApplied": 8,
"tagsApplied": 3,
"warnings": []
}
}
curl "http://localhost:8080/apis/registry/v3/groups/my-group/contracts?limit=10&offset=0"
curl http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/export
Returns the reconstructed ODCS v3.1 YAML from the artifact’s current contract labels, rules, and field tags.
curl -X POST \
http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/promote \
-H 'Content-Type: application/json' \
-d '{"contractId": "orders-contract", "targetStage": "STAGE"}'
curl "http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/quality?contractId=orders-contract"
{"overall": 0.85, "completeness": 0.83, "compliance": 1.0, "stability": 0.67}
curl -X POST \
http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/versions/1/contract/execute \
-H 'Content-Type: application/json' \
-d '{"mode": "WRITE", "record": {"message": {"totalAmount": 100, "status": "active"}}}'
{"passed": true, "violations": [], "executedRules": 2, "failedRules": 0}
curl -X PUT \
http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/metadata \
-H 'Content-Type: application/json' \
-d '{
"status": "DRAFT",
"ownerTeam": "Platform Team",
"ownerDomain": "payments",
"supportContact": "platform@example.com",
"classification": "CONFIDENTIAL",
"stage": "DEV"
}'
curl -X POST \
http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/status \
-H 'Content-Type: application/json' \
-d '{ "status": "STABLE" }'
Configuration
Data contracts are always available; no special configuration is required. Submitting a contract is an explicit opt-in action that does not affect existing registry functionality.
Contract rule enforcement in Kafka client applications is controlled by the SerDes properties described in the Kafka SerDes integration reference.
Data contracts work across all storage variants:
- SQL (PostgreSQL)
-
Full support with immediate consistency. The
contract_audit_logtable is created automatically (database upgrade 106). - KafkaSQL
-
Full support. Label writes go through the Kafka journal for multi-node consistency.
- GitOps / KubernetesOps
-
Read-only safe. Write operations are rejected by the read-only decorator.
