---
title: "Webhook Schema Codegen"
canonical: "https://test.abhinandan.one/webhook-schema-codegen"
markdown_url: "https://test.abhinandan.one/webhook-schema-codegen.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "API Core / OpenAPI Generation"
description: "The email-received-event.schema.json file is the single source of truth compiled into TypeScript types, an AJV validator, and Go/Python model modules."
keywords: ["email-received-event.schema.json", "webhook schema codegen", "generate_schema_module.py", "AJV standalone validator", "ValidateEmailReceivedEvent", "make node-generate python-generate go-generate"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:08.101868+00:00"
sections:
  - {anchor: "what-this-pipeline-produces", title: "What this pipeline produces"}
  - {anchor: "regenerate-after-a-schema-change", title: "Regenerate after a schema change"}
  - {anchor: "step-update-the-canonical-schema", title: "Update the canonical schema"}
  - {anchor: "step-regenerate-all-three-sdks-from-the-repo-root", title: "Regenerate all three SDKs from the repo root"}
  - {anchor: "step-verify-each-sdk-still-builds-and-its-tests-pass", title: "Verify each SDK still builds and its tests pass"}
  - {anchor: "step-update-shared-fixtures-if-the-behavioral-contract-changed", title: "Update shared fixtures if the behavioral contract changed"}
  - {anchor: "step-run-the-full-check-suite-and-commit-everything-together", title: "Run the full check suite and commit everything together"}
  - {anchor: "what-each-language-generates", title: "What each language generates"}
  - {anchor: "node-ajv-standalone-validator-plus-typescript-types", title: "Node: AJV standalone validator plus TypeScript types"}
  - {anchor: "python-packaged-schema-copy-plus-generated-pydantic-models", title: "Python: packaged schema copy plus generated Pydantic models"}
  - {anchor: "go-embedded-schema-plus-generated-validation-helpers", title: "Go: embedded schema plus generated validation helpers"}
  - {anchor: "common-pitfalls", title: "Common pitfalls"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# Webhook Schema Codegen

Traces how json-schema/email-received-event.schema.json becomes typed webhook models, an AJV validator, and Go/Python parsing code across all three SDKs, and when you need to re-run the generators.

## What this pipeline produces

This pipeline compiles one JSON Schema into each SDK's native webhook validation and typing layer: an AJV standalone validator plus TypeScript types in Node, generated Pydantic models in Python, and an embedded schema plus validation helpers in Go. `json-schema/email-received-event.schema.json` is the single source of truth for the shape of an `email.received` webhook payload (the `EmailReceivedEvent` object described in [Inbound and Outbound Email Model](https://test.abhinandan.one/email-model.md)). Every SDK compiles that one file into its own native validation and typing layer, so a schema change propagates identically to Node, Python, and Go without any SDK hand-writing its own webhook types.

Each language keeps the exact same webhook payload contract, but the generated shape looks different per runtime:

| Language | Runtime validation | Generated artifacts |
|---|---|---|
| Node | Generated AJV standalone validator | Webhook schema module, TypeScript types, validator module |
| Python | Schema-driven validation plus generated Pydantic models | Packaged webhook schema copy, generated webhook models |
| Go | Embedded schema plus Go validation helpers | Embedded webhook schema source, Go validation functions |

This is distinct from the [OpenAPI spec normalization pipeline](https://test.abhinandan.one/openapi-spec-normalization.md), which compiles `openapi/primitive-api.yaml` into the generated HTTP API clients. The webhook schema and the OpenAPI spec are two separate contracts with two separate codegen passes, tied together only by the shared [monorepo structure](https://test.abhinandan.one/monorepo-and-releases.md).

> **Note:** The schema defines *known* event shapes strictly. Event types the schema doesn't recognize are intentionally preserved as `UnknownEvent` rather than rejected, so a webhook consumer built against an older SDK version doesn't break when Primitive ships a new event type. See [Webhook Events Overview](https://test.abhinandan.one/webhook-events.md) for the forward-compatibility contract across event families.

## Regenerate after a schema change

Run this whenever `json-schema/email-received-event.schema.json` changes, or after pulling a change that touched it.

### 1. Update the canonical schema

Edit `json-schema/email-received-event.schema.json` directly. This file, not any generated output, is what you change by hand.

### 2. Regenerate all three SDKs from the repo root

```bash
make node-generate python-generate go-generate
```

Or regenerate one language at a time while iterating locally, then run all three before the final commit:

```bash
make node-generate
make python-generate
make go-generate
```

Inside `sdk-python`, that target runs the schema and model scripts directly:

```bash
uv run python scripts/generate_schema_module.py
uv run python scripts/generate_models.py
```

### 3. Verify each SDK still builds and its tests pass

```bash
make node-check python-check go-check
```

### 4. Update shared fixtures if the behavioral contract changed

If the schema change alters validation outcomes (a field became required, an enum gained a value, etc.), update the shared compatibility fixtures in `test-fixtures/` so schema validation, signature verification, auth classification, and `parseWebhookEvent`/`handleWebhook` parity stay aligned across all three languages. See [Monorepo Structure and Release Process](https://test.abhinandan.one/monorepo-and-releases.md) for how the shared-fixture contract fits into the release flow.

### 5. Run the full check suite and commit everything together

```bash
make check
```

Commit the schema change and every regenerated file in the same PR. Regenerated output is never hand-edited afterward.

## What each language generates

### Node: AJV standalone validator plus TypeScript types

Node compiles the schema into three pieces: a webhook schema module (the schema JSON re-exported as `emailReceivedEventJsonSchema` from `@primitivedotdev/sdk/webhook`), generated TypeScript types (`EmailReceivedEvent` and its nested shapes), and a **generated AJV standalone validator**, AJV compiles the schema ahead-of-time into plain JavaScript validation code, avoiding a runtime `new Function()` compile step on every SDK import.

The validator backs `validateEmailReceivedEvent` and `safeValidateEmailReceivedEvent`, exported from `@primitivedotdev/sdk/webhook`. `handleWebhook(...)` and `receive(...)` call `validateEmailReceivedEvent` internally, so most SDK consumers never touch these directly; reach for them when you're validating a payload you built yourself (a test fixture, a producer building `email.received` payloads programmatically, see [Building Webhook Payloads](https://test.abhinandan.one/node-sdk-contract-module.md)).

### Python: packaged schema copy plus generated Pydantic models

Python's `scripts/generate_schema_module.py` packages a copy of the JSON Schema into the installed `primitive` package, and `scripts/generate_models.py` generates Pydantic model classes from it (`EmailReceivedEvent`, `Delivery`, `Smtp`, `Headers`, `Content`, `Email`, and the rest of the nested shapes, all importable from the top-level `primitive` package).

Field names follow Python convention (`snake_case`), and the wire's mixed-case fields are carried as Pydantic aliases, so `event.model_dump()` and `event.model_dump_json()` round-trip back to the exact wire shape: the dumped payload carries `from` (not `from_`), `dmarcPolicy`, and `dkimSignatures` as the wire spells them.

`validate_email_received_event(...)` in `primitive.webhook` runs the generated Pydantic validation and raises `WebhookValidationError` (code `SCHEMA_VALIDATION_FAILED`) on a schema mismatch. `handle_webhook(...)` and `receive(...)` call it internally.

### Go: embedded schema plus generated validation helpers

Go embeds the schema source directly into the compiled binary (no filesystem read at runtime) and pairs it with Go validation helpers. `ValidateEmailReceivedEvent` and `SafeValidateEmailReceivedEvent` run that validation against a raw decoded payload, see [Webhook Payload Schema Validation](https://test.abhinandan.one/go-webhook-schema-validation.md) for their exact signatures and error shapes.

As in the other two languages, `HandleWebhook(...)` and `Receive(...)` call the embedded validator internally, so you only reach for the validation functions directly when handling a payload you didn't get from those entry points.

> **Tip:** Need the parity behavior these generators are tested against, not the generator internals? [Webhook Events Overview](https://test.abhinandan.one/webhook-events.md) documents the shared contract (signature verification, event catalog, forward compatibility) that the shared `test-fixtures/` suite enforces across all three generated outputs.

## Common pitfalls

> **Warning:** Never hand-edit a generated file (the AJV validator module, the Pydantic model file, or the Go embedded schema/model source). The next `make *-generate` run overwrites it silently, and your fix disappears without a diff to explain why.

- **Regenerating only one language before committing**: if you edit the schema and run only `make node-generate` before opening a PR, Python and Go silently fall behind. Run `make node-generate python-generate go-generate` (all three) for the commit that ships the schema change, even if you iterated on one language locally first.
- **Schema change with no fixture update**: a field that becomes required, or an enum that gains a value, changes what the shared compatibility suite expects. Skipping the `test-fixtures/` update means CI passes locally per-language but the cross-language parity check (`make shared-check`) can still diverge undetected until another SDK's behavior is exercised.
- **Confusing this pipeline with the OpenAPI codegen pipeline**: this page is about `json-schema/email-received-event.schema.json` (webhook payload shapes only). The generated HTTP API clients (`PrimitiveApiClient`, `sdk-go/api`, `primitive.api`) come from a completely separate spec and generator, see [Regenerating SDK Code from the OpenAPI Spec](https://test.abhinandan.one/codegen-workflow.md).
