Skip to content

Schema Generation

Protean can generate standard JSON Schema (Draft 2020-12) documents for all data-carrying domain elements: aggregates, entities, value objects, commands, events, and projections.

Generated schemas are useful for:

  • Contract testing — validate that payloads conform to expected shapes
  • Schema registries — publish schemas to Confluent Schema Registry, AWS Glue, or similar
  • OpenAPI integration — JSON Schema Draft 2020-12 is natively supported by OpenAPI 3.1
  • Documentation — auto-generate API docs from schema definitions
  • Code generation — generate client SDKs in other languages from schemas

Generating schemas

Use the protean schema generate command to produce JSON Schema files for every data-carrying element in your domain.

From a live domain

protean schema generate --domain=my_app.domain

This loads the domain, builds the IR, and writes schemas to .protean/schemas/.

From an IR file

protean schema generate --ir=domain-ir.json

If you already have a serialized IR (from protean ir show), you can generate schemas directly from it.

Custom output directory

protean schema generate --domain=my_app.domain --output=build

Other output formats

By default protean schema generate emits JSON Schema. Pass --format to emit other contract formats — json, avro, protobuf, or all (which writes JSON Schema, Avro, and Protobuf into one tree):

protean schema generate --domain=my_app.domain --format all

The Avro schema carries an aliases entry for any field declared with renamed_from, so an external Avro consumer resolves the old field name on the wire. See the protean schema CLI reference for every flag, and Evolving Events Over Time for how these formats fit the wider event-evolution workflow.


Inspecting a single schema

Use protean schema show to print the JSON Schema for a specific element:

# By short name
protean schema show OrderPlaced --domain=my_app.domain

# By fully qualified name
protean schema show my_app.ordering.OrderPlaced --domain=my_app.domain

# Raw JSON for piping
protean schema show OrderPlaced --domain=my_app.domain --raw | jq .

If multiple elements share the same short name, the command lists all matching FQNs and asks you to disambiguate.


Output structure

Schemas are organized by aggregate cluster:

.protean/
├── ir.json
└── schemas/
    ├── Order/
    │   ├── aggregates/
    │   │   └── Order.v1.json
    │   ├── commands/
    │   │   └── PlaceOrder.v1.json
    │   ├── entities/
    │   │   └── LineItem.v1.json
    │   ├── events/
    │   │   └── OrderPlaced.v2.json
    │   └── value_objects/
    │       └── Money.v1.json
    └── projections/
        └── OrderDashboard.v1.json

Filenames include the element version (v1, v2, etc.) for events and commands. Other element types default to v1.


Schema structure

Each generated schema is a standard JSON Schema object with Protean-specific metadata in x-protean-* extension fields:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "OrderPlaced",
  "type": "object",
  "properties": {
    "order_id": { "type": "string" },
    "customer_name": { "type": "string" },
    "total": { "type": "number" }
  },
  "required": ["customer_name", "order_id", "total"],
  "x-protean-element-type": "event",
  "x-protean-fqn": "my_app.ordering.OrderPlaced",
  "x-protean-aggregate": "my_app.ordering.Order",
  "x-protean-version": 1,
  "x-protean-type": "Ordering.OrderPlaced.v1"
}

Extension fields

Field Description
x-protean-element-type Element kind: aggregate, entity, value_object, command, event, projection
x-protean-fqn Fully qualified name in the domain
x-protean-aggregate FQN of the owning aggregate
x-protean-version Event/command version number
x-protean-type Message type string
x-protean-published true if the event is part of the published contract
x-protean-is-fact-event true if auto-generated from aggregate changes
x-protean-auto-generated true if generated by the framework
x-protean-identity-field Identity field name (aggregates only)
x-protean-is-event-sourced true if the aggregate uses event sourcing

Nested structures

Value objects and entities referenced by an aggregate are resolved as $defs with $ref pointers:

{
  "properties": {
    "shipping_address": { "$ref": "#/$defs/ShippingAddress" }
  },
  "$defs": {
    "ShippingAddress": {
      "type": "object",
      "properties": {
        "street": { "type": "string", "maxLength": 255 },
        "city": { "type": "string", "maxLength": 100 }
      },
      "required": ["city", "street"]
    }
  }
}

Optional fields

Optional fields use the anyOf pattern with null:

{
  "total": {
    "anyOf": [
      { "type": "number", "minimum": 0.0 },
      { "type": "null" }
    ]
  }
}

Validating payloads

Generated schemas work with any JSON Schema validator. For example, using Python's jsonschema library:

import json
import jsonschema

with open(".protean/schemas/Order/events/OrderPlaced.v1.json") as f:
    schema = json.load(f)

payload = {
    "order_id": "order-123",
    "customer_name": "Alice",
    "total": 99.99,
}

jsonschema.validate(payload, schema)  # Passes

Design decisions

  • IR-first generation — schemas are built from IR field metadata, not Pydantic's model_json_schema(). See ADR-0005.
  • Standard JSON Schema with x- extensions — not a custom envelope. See ADR-0006.