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.
JSON Schema message payload
Section titled “JSON Schema message payload”For teams using EventBridge, SNS, SQS, or any message broker, define event payloads as standalone JSON Schemas.
Configure the contract
Section titled “Configure the contract”contracts: - name: order-created-event type: json-schema path: ./events/order-created.schema.jsonExample JSON Schema
Section titled “Example JSON Schema”{ "$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" } }}Breaking change detection
Section titled “Breaking change detection”Add a new required field regionCode to an existing payload:
contractual breakingChecking 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: 1What counts as breaking for event schemas
Section titled “What counts as breaking for event schemas”| Change | Breaking? | Reason |
|---|---|---|
| Adding a required field | Yes | Existing messages that omit it become invalid |
| Removing a property | Yes | Consumers reading that field receive undefined |
Narrowing a type (number → integer) | Yes | Values previously valid may no longer be |
| Removing an enum value | Yes | Messages with that value become invalid |
Tightening a constraint (minimum: 0 → minimum: 1) | Yes | Values in range before are now out of range |
Setting additionalProperties: false | Yes | Messages with extra fields are now rejected |
| Adding an optional field | No | Existing messages remain valid |
Relaxing a type (integer → number) | No | More values are valid, not fewer |
| Adding an enum value | No | Existing values remain valid |
Compiling validators
Section titled “Compiling validators”For runtime validation without schema parsing overhead, compile the JSON Schema to a standalone validator:
Configure output generation
Section titled “Configure output generation”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"Use the compiled validator
Section titled “Use the compiled validator”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 Action workflow
Section titled “GitHub Action workflow”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: trueCombining with a schema registry
Section titled “Combining with a schema registry”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.
Next steps
Section titled “Next steps”- Breaking Change Detection: How the structural differ works
- Classifications: Full list of what Contractual considers breaking
- Enforcing No Breaking Changes: Branch protection and changeset overrides