Skip to content

The HTTP API

The surface

Method Path Handler Purpose
GET /schemas Read Every published identifier
GET /schemas/{name}/versions Read The versions of one name
GET /schemas/{name}/{version} Read The schema, as its own bytes
PUT /schemas/{name}/{version} Write Publish

Under serve.Mount the read handler is at the root and the write handler is under /publish, so the publish path is PUT /publish/schemas/{name}/{version}.

Headers

Header Direction Meaning
X-Schema-Language Both jsonschema or protobuf
X-Schema-Root Both The root message of a protobuf descriptor set. Absent for JSON Schema

Reading a schema

$ curl -i https://schema.internal.example/schemas/orders.created/3
HTTP/1.1 200 OK
Content-Type: application/schema+json
X-Schema-Language: jsonschema

{"type":"object","required":["order"]}

Schemas are served as their own bytes, not wrapped in an envelope, so a JSON Schema is readable in a browser and a descriptor set is usable by anything that reads one. A descriptor set is served as application/octet-stream rather than mislabelled as JSON.

Listing

$ curl https://schema.internal.example/schemas
{"schemas":["urn:phpboyscout:schema:orders.created:1","urn:phpboyscout:schema:orders.created:3"]}

$ curl https://schema.internal.example/schemas/orders.created/versions
{"versions":[1,3]}

$ curl https://schema.internal.example/schemas/nothing.here/versions
{"versions":[]}

Both are ordered — by name then version — so two calls against an unchanged registry agree. A name with no versions is [], never null: JSON null forces every client to special-case a value that means the same thing as empty.

Publishing

$ curl -X PUT https://schema.internal.example/publish/schemas/orders.created/3 \
    -H 'X-Schema-Language: jsonschema' \
    --data-binary @orders.created.v3.json

?compatibility= accepts only none, and defaults to it. Bodies are capped at 1 MiB and an oversize one is 413; an unbounded read on a write path is a denial of service with extra steps.

Status codes

Each one is a statement about whose problem it is.

Code When
200 Read succeeded
201 Published, including a republication with identical bytes
400 The version is not canonical, the name breaks the URN grammar, the language is unknown, the body is empty, or a compatibility mode other than none was asked for
404 Nothing is published under that identifier
409 That identifier is already published with different bytes
413 The body exceeds 1 MiB
500 The registry's own storage failed

Versions must be canonical

/schemas/orders.created/3 resolves; /01, /007, /+1 and /1.0 are all 400.

The path segment goes into the URN grammar as it arrived rather than through strconv.Atoi, which accepts 007 and +1. Laundering it would give one schema unlimited URLs: a cache-key explosion on the read path, and on the write path a way past any proxy rule keyed on the URL — a policy denying PUT /publish/schemas/payments.captured/1 would be evaded with /01.

The name is validated on every endpoint that takes one, including /versions. That one is the unauthenticated surface and Store is a seam somebody will back with a filesystem.

Nothing the registry means returns a 500, so an alert on the 500 rate means something.

Why not Confluent's API

Confluent's Schema Registry is built on a subject/version model:

GET  /subjects
GET  /subjects/{subject}/versions
POST /compatibility/subjects/{subject}/versions/{version}

with a registry-assigned integer id and an application/vnd.schemaregistry.v1+json content type.

We match none of it. Their API is organised around a mutable subject whose versions accumulate and whose ids the registry assigns — the direct opposite of a permanent identifier chosen by the publisher and refused if it changes. Adopting the endpoints would inherit a compatibility model, an id-assignment scheme and a subject concept we deliberately replaced, and buy no interoperability in return: nothing speaking their protocol works against ours once the addressing differs.

One idea is taken, because it is right. Compatibility is an operation a caller asks for explicitly, rather than an implicit property of publishing. That is what the compatibility parameter is, and it is why it exists before anything checks it.