{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/webhook-schema-codegen","markdown_url":"https://test.abhinandan.one/webhook-schema-codegen.md","article":{"id":"58f9a43b-8e60-4846-98a1-9b0948f43a88","article_slug":"webhook-schema-codegen","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:55:08.101868+00:00","keywords":["email-received-event.schema.json","webhook schema codegen","generate_schema_module.py","AJV standalone validator","ValidateEmailReceivedEvent","make node-generate python-generate go-generate"],"meta_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.","og_image_url":null,"source_file_paths":[],"recording_id":null,"replayable":false,"task_name":"Webhook Schema Codegen","category":"API Core / OpenAPI Generation","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"## What this pipeline produces\n\nThis 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](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.\n\nEach language keeps the exact same webhook payload contract, but the generated shape looks different per runtime:\n\n| Language | Runtime validation | Generated artifacts |\n|---|---|---|\n| Node | Generated AJV standalone validator | Webhook schema module, TypeScript types, validator module |\n| Python | Schema-driven validation plus generated Pydantic models | Packaged webhook schema copy, generated webhook models |\n| Go | Embedded schema plus Go validation helpers | Embedded webhook schema source, Go validation functions |\n\nThis is distinct from the [OpenAPI spec normalization pipeline](openapi-spec-normalization), 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](monorepo-and-releases).\n\n<Note>\n\nThe 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](webhook-events) for the forward-compatibility contract across event families.\n\n</Note>\n\n## Regenerate after a schema change\n\nRun this whenever `json-schema/email-received-event.schema.json` changes, or after pulling a change that touched it.\n\n<Steps>\n\n<Step title=\"Update the canonical schema\">\n\nEdit `json-schema/email-received-event.schema.json` directly. This file, not any generated output, is what you change by hand.\n\n</Step>\n\n<Step title=\"Regenerate all three SDKs from the repo root\">\n\n```bash\nmake node-generate python-generate go-generate\n```\n\nOr regenerate one language at a time while iterating locally, then run all three before the final commit:\n\n```bash\nmake node-generate\nmake python-generate\nmake go-generate\n```\n\nInside `sdk-python`, that target runs the schema and model scripts directly:\n\n```bash\nuv run python scripts/generate_schema_module.py\nuv run python scripts/generate_models.py\n```\n\n</Step>\n\n<Step title=\"Verify each SDK still builds and its tests pass\">\n\n```bash\nmake node-check python-check go-check\n```\n\n</Step>\n\n<Step title=\"Update shared fixtures if the behavioral contract changed\">\n\nIf 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](monorepo-and-releases) for how the shared-fixture contract fits into the release flow.\n\n</Step>\n\n<Step title=\"Run the full check suite and commit everything together\">\n\n```bash\nmake check\n```\n\nCommit the schema change and every regenerated file in the same PR. Regenerated output is never hand-edited afterward.\n\n</Step>\n\n</Steps>\n\n## What each language generates\n\n### Node: AJV standalone validator plus TypeScript types\n\nNode 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.\n\nThe 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](node-sdk-contract-module)).\n\n### Python: packaged schema copy plus generated Pydantic models\n\nPython'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).\n\nField 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.\n\n`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.\n\n### Go: embedded schema plus generated validation helpers\n\nGo 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](go-webhook-schema-validation) for their exact signatures and error shapes.\n\nAs 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.\n\n<Tip>\n\nNeed the parity behavior these generators are tested against, not the generator internals? [Webhook Events Overview](webhook-events) documents the shared contract (signature verification, event catalog, forward compatibility) that the shared `test-fixtures/` suite enforces across all three generated outputs.\n\n</Tip>\n\n## Common pitfalls\n\n<Warning>\n\nNever 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.\n\n</Warning>\n\n- **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.\n- **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.\n- **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](codegen-workflow).\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Webhook Events Overview\" href=\"webhook-events\">\n\nThe shared webhook contract these generated validators enforce: signature verification, the event catalog, and forward compatibility.\n\n</Card>\n\n<Card title=\"Webhook Payload Schema Validation\" href=\"go-webhook-schema-validation\">\n\nThe Go SDK's ValidateEmailReceivedEvent and SafeValidateEmailReceivedEvent, generated by this pipeline.\n\n</Card>\n\n<Card title=\"OpenAPI Spec Normalization and Codegen Artifacts\" href=\"openapi-spec-normalization\">\n\nThe parallel codegen pipeline for the HTTP API surface, normalized from openapi/primitive-api.yaml.\n\n</Card>\n\n<Card title=\"Monorepo Structure and Release Process\" href=\"monorepo-and-releases\">\n\nHow schema and OpenAPI contract changes flow through make targets, shared fixtures, and per-language releases.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":null,"raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Webhook+Schema+Codegen&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fwebhook-schema-codegen","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}