Skip to content

Embed your schemas

This is the path nearly every service wants. The network resolver is for the case a service meets a URN it does not hold, which is the interesting case and the rare one.

Lay the files out by identifier

schemas/
  orders.created.v1.json
  orders.created.v3.json
  orders.shipped.v1.binpb

Naming the files after the URN is not required and is strongly worth doing: a schema whose filename and identifier disagree is a mismatch nothing detects.

Register them at start-up

//go:embed schemas/orders.created.v3.json
var ordersCreated []byte

//go:embed schemas/orders.shipped.v1.binpb
var ordersShipped []byte

reg, err := schema.Embed(
    schema.Schema{
        URN:      urn.Schema{Name: "orders.created", Version: 3},
        Language: schema.JSONSchema,
        Bytes:    ordersCreated,
    },
    schema.Schema{
        URN:      urn.Schema{Name: "orders.shipped", Version: 1},
        Language: schema.Protobuf,
        Bytes:    ordersShipped,
        Root:     "uk.phpboyscout.OrderShipped",
    },
)

Embed registers a no-op checker per language it sees, so nothing has to know which to wire.

Embedding refuses a contradiction

Two schemas embedded under one identifier with different bytes is ErrImmutable, at start-up. That is a build that shipped two different schemas under one name, and refusing loudly at boot beats serving one of them for a fortnight.

Wire a guard

guard := schema.NewGuard(reg, []schema.Validator{
    jsonschema.New(),
    protobuf.New(),
})

Register only the validators you need. A schema whose language has no validator fails open and is counted, which is correct but is not what you wanted.

Keep old versions

Keep every version you have ever consumed, not just the one you emit. A consumer reading a JetStream stream or an object store is reading messages written months ago, and a dataschema that resolves to nothing is a message that goes through unvalidated.

Deleting an embedded schema is the commonest way to make the fail-open counter start moving.

Add the network as a fallback later

resolver, err := client.New("https://schema.internal.example")
if err != nil {
    return err
}

guard := schema.NewGuard(resolver, []schema.Validator{jsonschema.New()})

Doing that makes the registry reachable, not required — the guard still fails open when it is not. See Run the registry.