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). 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, 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.
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 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.jsondirectly. This file, not any generated output, is what you change by hand. - 2
Regenerate all three SDKs from the repo root#
make node-generate python-generate go-generateOr regenerate one language at a time while iterating locally, then run all three before the final commit:
make node-generate make python-generate make go-generateInside
sdk-python, that target runs the schema and model scripts directly: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#
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, andparseWebhookEvent/handleWebhookparity stay aligned across all three languages. See Monorepo Structure and Release Process for how the shared-fixture contract fits into the release flow. - 5
Run the full check suite and commit everything together#
make checkCommit 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).
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 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.
Need the parity behavior these generators are tested against, not the generator internals? Webhook Events Overview documents the shared contract (signature verification, event catalog, forward compatibility) that the shared test-fixtures/ suite enforces across all three generated outputs.
Common pitfalls#
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-generatebefore opening a PR, Python and Go silently fall behind. Runmake 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.
Next steps#
The shared webhook contract these generated validators enforce: signature verification, the event catalog, and forward compatibility.
Webhook Payload Schema ValidationThe Go SDK's ValidateEmailReceivedEvent and SafeValidateEmailReceivedEvent, generated by this pipeline.
OpenAPI Spec Normalization and Codegen ArtifactsThe parallel codegen pipeline for the HTTP API surface, normalized from openapi/primitive-api.yaml.
Monorepo Structure and Release ProcessHow schema and OpenAPI contract changes flow through make targets, shared fixtures, and per-language releases.
Was this page helpful?