Skip to content

JSON Schema + Events

Event schemas evolve. A field gets removed, a required property appears, an enum value is renamed, and downstream consumers break silently. In event-driven systems this is especially painful because consumers are often decoupled services that are not controlled.

This recipe covers schema governance at the pull request layer, before any code ships.


For teams using EventBridge, SNS, SQS, or any message broker, define event payloads as standalone JSON Schemas.

contractual.yaml
contracts:
- name: order-created-event
type: json-schema
path: ./events/order-created.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/events/order-created.schema.json",
"title": "OrderCreated",
"description": "Published to the order-events SNS topic when an order is placed.",
"type": "object",
"required": ["orderId", "customerId", "totalAmount", "createdAt"],
"additionalProperties": false,
"properties": {
"orderId": {
"type": "string",
"description": "Unique order identifier"
},
"customerId": {
"type": "string",
"description": "Identifier of the customer who placed the order"
},
"totalAmount": {
"type": "number",
"minimum": 0,
"description": "Order total in the smallest currency unit"
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code",
"default": "USD"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp"
}
}
}

Add a new required field regionCode to an existing payload:

Terminal window
contractual breaking
Checking for breaking changes...
✗ order-created-event events/order-created.schema.json
BREAKING /required
Added required field: regionCode
Existing messages that omit this field will fail validation
against the updated schema.
1 breaking change detected.
Exit code: 1

ChangeBreaking?Reason
Adding a required fieldYesExisting messages that omit it become invalid
Removing a propertyYesConsumers reading that field receive undefined
Narrowing a type (numberinteger)YesValues previously valid may no longer be
Removing an enum valueYesMessages with that value become invalid
Tightening a constraint (minimum: 0minimum: 1)YesValues in range before are now out of range
Setting additionalProperties: falseYesMessages with extra fields are now rejected
Adding an optional fieldNoExisting messages remain valid
Relaxing a type (integernumber)NoMore values are valid, not fewer
Adding an enum valueNoExisting values remain valid

For runtime validation without schema parsing overhead, compile the JSON Schema to a standalone validator:

contractual.yaml
contracts:
- name: order-created-event
type: json-schema
path: ./events/order-created.schema.json
outputs:
- name: compiled-validator
command: "ajv compile -s {spec} -o ./dist/validators/order-created.js --code-es5"
const validate = require("./dist/validators/order-created.js");
function handleOrderCreated(message: unknown): void {
if (!validate(message)) {
console.error("Invalid event:", validate.errors);
throw new Error("Event validation failed");
}
// message is valid
processOrder(message);
}

.github/workflows/event-schema-governance.yml
name: Event Schema Governance
on:
pull_request:
paths:
- 'events/**'
permissions:
contents: write
pull-requests: write
jobs:
check:
name: Check event schemas
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- uses: contractual-dev/action@v1
with:
mode: pr-check
github-token: ${{ secrets.GITHUB_TOKEN }}
fail-on-breaking: true

Contractual and a schema registry address different failure modes:

  • Contractual catches structural breaking changes at the pull request. No code has shipped.
  • A schema registry (Confluent, AWS Glue, Apicurio) enforces compatibility at runtime.

The registry is the safety net. Contractual is the prevention step that means the safety net rarely triggers.