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.

Table 1. ODCS projection mapping
ODCS section Projected to

info.status

contract.{contractId}.status label (draft→DRAFT, active→STABLE, deprecated→DEPRECATED)

info.dataClassification

contract.{contractId}.classification label

team.name / team.domain / team.contact

contract.{contractId}.owner.team, .owner.domain, .support.contact labels

quality.accuracy rules

CEL contract rules prefixed odcs:{contractId}: in the contract_rules table

quality.completeness rules

contract.{contractId}.quality.completeness.{field} labels

quality.freshness

contract.{contractId}.quality.freshness.maxStaleness label

schemas[].fields[].pii / .tags

field-tag.{contractId}:{fieldPath}|{tagName} version labels

serviceLevel.*

contract.{contractId}.sla.* labels (stored, not enforced by registry)

ODCS accuracy rule thresholds determine what happens when a rule fails:

Table 2. Accuracy rule threshold mapping
Threshold Failure action Behavior

1.0 (100%)

ERROR

Zero tolerance. Non-compliant data is rejected.

0.95-0.99

DLQ

Near-100% tolerance. Violations are routed to a dead letter queue.

Below 0.95

NONE

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.

ODCS v3.1 contract
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:

  1. An ODCS_CONTRACT artifact is created to store the YAML.

  2. Namespaced labels are set on the orders/OrderEvent artifact: contract.orders-contract.status=STABLE, contract.orders-contract.owner.team=orders-team, and contract.orders-contract.classification=CONFIDENTIAL.

  3. Two CEL contract rules are created: odcs:orders-contract:positive-amount with onFailure=ERROR, and odcs:orders-contract:valid-email with onFailure=DLQ.

  4. Completeness thresholds are stored as labels, such as contract.orders-contract.quality.completeness.orderId=1.0.

  5. Field tags are set: field-tag.orders-contract:customerEmail|PII=EXTERNAL and field-tag.orders-contract:customerEmail|EMAIL=EXTERNAL.

  6. 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, artifact OrderEvent, 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 Address declares which of its fields are PII.

  • Changing Address does 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:

Table 3. Common field tags
Tag Meaning

PII

Personally Identifiable Information

SENSITIVE

Sensitive data requiring special handling

EMAIL

Email address fields

PHONE

Phone number fields

FINANCIAL

Financial data

HEALTH

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 tags and confluent:tags field properties.

{
  "type": "record",
  "name": "User",
  "fields": [{
    "name": "email",
    "type": "string",
    "tags": ["PII", "EMAIL"]
  }]
}
JSON Schema

Tags are extracted from x-tags and x-confluent-tags extension properties. Supports nested objects, arrays, additionalProperties, and allOf/oneOf/anyOf compositions.

{
  "type": "object",
  "properties": {
    "email": {
      "type": "string",
      "x-tags": ["PII", "EMAIL"]
    }
  }
}
Protobuf

Tags are extracted from documentation comments using the @tag: annotation. Also supports Confluent confluent.field_meta field 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-standalone library. Supports the full CEL specification, including has(), size(), contains(), startsWith(), parenthesized grouping, and ternary expressions.

JSONATA

JSONata expressions for data transformation. Supports both CONDITION (truthy check) and TRANSFORM (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.

Additional resources

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:

Migration rule example
{
  "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:

Table 4. SerDes contract rule properties
Property Default Description

apicurio.registry.contract-rules.enabled

false

Enable contract rule execution during serialize/deserialize

apicurio.registry.contract-rules.fail-on-error

true

When true, rule violations throw a RuntimeException. When false, violations are logged but serialization proceeds (for DLQ routing by the application)

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 any CONDITION rule with onFailure=ERROR fails, 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=false to allow serialization to proceed even when rules fail. Your application can inspect the violations and route the message to a dead letter queue.

Producer with contract rules example
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 DRAFT to STABLE

  • 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
Response
[
  {
    "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.

Table 5. Contract search query parameters
Parameter Description

status

Filter by lifecycle status (DRAFT, STABLE, DEPRECATED)

ownerTeam

Filter by owner team name

classification

Filter by data classification (for example, CONFIDENTIAL or PUBLIC)

groupId

Filter by group

artifactId

Filter by artifact

limit / offset

Pagination (default limit: 20, max: 500)

orderby / order

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:

  1. Global rules apply to all artifacts and are set through /admin/contracts/ruleset.

  2. Artifact rules apply to a specific artifact and are set through /groups/{g}/artifacts/{a}/contract/ruleset.

  3. 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.

Table 6. ODCS contract REST API endpoints
Method Endpoint Description

POST

/groups/{groupId}/contracts

Submit an ODCS contract YAML. Creates the contract artifact and projects onto referenced schema artifacts.

GET

/groups/{groupId}/contracts?limit=&offset=

List ODCS contracts in a group (paginated, default 20, max 500)

GET

/groups/{groupId}/contracts/{contractId}

Retrieve the ODCS contract YAML

PUT

/groups/{groupId}/contracts/{contractId}

Update an ODCS contract (creates a new version and re-projects)

DELETE

/groups/{groupId}/contracts/{contractId}

Delete an ODCS contract artifact

GET

/groups/{groupId}/artifacts/{artifactId}/contract/export

Export an artifact’s contract data as ODCS YAML

Table 7. Contract metadata REST API endpoints
Method Endpoint Description

GET

/groups/{groupId}/artifacts/{artifactId}/contract/metadata

Get contract metadata for an artifact

PUT

/groups/{groupId}/artifacts/{artifactId}/contract/metadata

Create or update contract metadata

Table 8. Contract ruleset REST API endpoints
Method Endpoint Description

GET

/groups/{groupId}/artifacts/{artifactId}/contract/ruleset

Get the contract ruleset for an artifact

PUT

/groups/{groupId}/artifacts/{artifactId}/contract/ruleset

Create or replace the contract ruleset

DELETE

/groups/{groupId}/artifacts/{artifactId}/contract/ruleset

Delete the contract ruleset

Version-level rulesets are also available at …​/versions/2024.Q2/contract/ruleset.

Table 9. Contract status transition endpoint
Method Endpoint Description

POST

/groups/{groupId}/artifacts/{artifactId}/contract/status

Change lifecycle status with transition validation

Table 10. Governance REST API endpoints
Method Endpoint Description

POST

/groups/{groupId}/artifacts/{artifactId}/contract/promote

Promote the contract stage from DEV to STAGE to PROD. Body: {"contractId": "…​", "targetStage": "STAGE"}

GET

/groups/{groupId}/artifacts/{artifactId}/contract/quality?contractId=

Get quality score (overall, completeness, compliance, stability)

Table 11. Rule execution REST API endpoints
Method Endpoint Description

POST

/groups/{groupId}/artifacts/{artifactId}/versions/2024.Q2/contract/execute

Execute contract rules against a data record. Body: {"mode": "WRITE", "record": {…​}}

POST

/groups/{groupId}/artifacts/{artifactId}/contract/migrate

Migrate a record between versions. Body: {"sourceVersion": "1", "targetVersion": "3", "record": {…​}}

Table 12. Contract audit log REST API endpoint
Method Endpoint Description

GET

/groups/{groupId}/artifacts/{artifactId}/contract/audit

Get contract audit log entries for an artifact

Table 13. Compatibility group REST API endpoints
Method Endpoint Description

GET

/groups/{groupId}/artifacts/{artifactId}/contract/compatibility-group

Get the compatibility group for an artifact

PUT

/groups/{groupId}/artifacts/{artifactId}/contract/compatibility-group

Set the compatibility group for an artifact

Table 14. Contract search REST API endpoint
Method Endpoint Description

GET

/search/contracts?status=&ownerTeam=&classification=&limit=&offset=

Search contracts across the registry

Table 15. Global contract ruleset REST API endpoints
Method Endpoint Description

GET

/admin/contracts/ruleset

Get the global contract ruleset

PUT

/admin/contracts/ruleset

Set the global contract ruleset

DELETE

/admin/contracts/ruleset

Delete the global contract ruleset

Additional resources

REST API examples

Common curl examples for the Data Contracts REST API, including submitting, listing, exporting, and promoting contracts.

Submitting an ODCS contract
curl -X POST \
  http://localhost:8080/apis/registry/v3/groups/my-group/contracts \
  -H 'Content-Type: application/x-yaml' \
  --data-binary @my-contract.yaml
Response
{
  "contractId": "orders-contract",
  "version": "1.0.0",
  "projection": {
    "rulesApplied": 2,
    "labelsApplied": 8,
    "tagsApplied": 3,
    "warnings": []
  }
}
Listing contracts
curl "http://localhost:8080/apis/registry/v3/groups/my-group/contracts?limit=10&offset=0"
Exporting as ODCS
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.

Promoting a contract
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"}'
Getting quality score
curl "http://localhost:8080/apis/registry/v3/groups/my-group/artifacts/my-artifact/contract/quality?contractId=orders-contract"
Response
{"overall": 0.85, "completeness": 0.83, "compliance": 1.0, "stability": 0.67}
Executing contract rules
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"}}}'
Response
{"passed": true, "violations": [], "executedRules": 2, "failedRules": 0}
Setting contract metadata directly
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"
  }'
Transitioning contract status
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_log table 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.

Additional resources