Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /webhook-schema-codegen.md.
Verified · 8/11/2026

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:

LanguageRuntime validationGenerated artifacts
NodeGenerated AJV standalone validatorWebhook schema module, TypeScript types, validator module
PythonSchema-driven validation plus generated Pydantic modelsPackaged webhook schema copy, generated webhook models
GoEmbedded schema plus Go validation helpersEmbedded 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.

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 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. 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. 2

    Regenerate all three SDKs from the repo root#

    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:

    make node-generate
    make python-generate
    make go-generate
    

    Inside 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. 3

    Verify each SDK still builds and its tests pass#

    make node-check python-check go-check
    
  4. 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 for how the shared-fixture contract fits into the release flow.

  5. 5

    Run the full check suite and commit everything together#

    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).

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.

Tip

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#

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.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer