JSON Schemas
Every release publishes one JSON Schema (draft 2020-12) for each message of the wire
contract, fora.v1 and fora.admin.v1. An implementer or a conformance harness can
check the shape of a FORA message against them, in any language, without the SDK.
The schemas are generated from the proto by the same pinned generator the SDK models are
built from (bufbuild/protoschema-plugins protoc-gen-jsonschema v0.6.0), so they carry
the per-field buf.validate constraints. CI regenerates them on every change and fails
if the committed files differ.
Two variants
Section titled “Two variants”Each message has two files, named by its fully-qualified name:
| File | Unknown fields |
|---|---|
fora.v1.ResourceResponse.schema.json | Accepted. A reader that must accept messages from a newer protocol version uses this variant. |
fora.v1.ResourceResponse.schema.strict.json | Refused: additionalProperties is false on every message object, at every depth. A conformance check uses this variant, so an unknown or misspelled field fails. |
Each file is self-contained. The message is the document root, and every message it
references is inlined under $defs, keyed by its fully-qualified name. One file is
enough to validate a message, with no schema registry and no network access.
Where to get them
Section titled “Where to get them”The schemas ship in all three published packages, at the version you pin:
| Package | Access |
|---|---|
Go module github.com/FORA-Protocol/protocol | The gen/jsonschema/ directory. Package github.com/FORA-Protocol/protocol/gen/jsonschema embeds it: jsonschema.Load("fora.v1.ResourceResponse", true) returns the strict variant, and jsonschema.FS holds every file. |
fora-protocol on PyPI | wire.schemas.load("fora.v1.ResourceResponse", strict=True) returns the schema as a dict. wire.schemas.names() lists the messages. |
@fora-protocol/sdk on npm | The export path @fora-protocol/sdk/jsonschema/*, for example @fora-protocol/sdk/jsonschema/fora.v1.ResourceResponse.schema.strict.json. |
Validate a message
Section titled “Validate a message”Python, with the jsonschema library:
from jsonschema import Draft202012Validatorfrom wire import schemas
validator = Draft202012Validator(schemas.load("fora.v1.ResourceResponse", strict=True))validator.validate(body) # raises ValidationError on an unknown field or a constraint violationTypeScript, with Ajv, which @fora-protocol/sdk already depends on:
import Ajv2020 from "ajv/dist/2020.js";import schema from "@fora-protocol/sdk/jsonschema/fora.v1.ResourceResponse.schema.strict.json" with { type: "json" };
const validate = new Ajv2020({ validateFormats: false }).compile(schema);if (!validate(body)) console.log(validate.errors);Go reads the file with jsonschema.Load and passes the bytes to any draft 2020-12
validator.
Strict decoding in the SDKs
Section titled “Strict decoding in the SDKs”The Python and TypeScript clients use the strict variant themselves. A client built with
ClientConfig(strict=True) (Python) or strict: true (TypeScript) validates every
successful answer against the strict schema of its response message before it decodes
it, and then applies the cross-field rules the schemas cannot express, which the SDKs
already carry. A FORA server emits unpopulated fields, so a null member of a message
object is read as absent before the check, as proto-JSON reads it. An unknown field at
any depth, a field-level violation or a cross-field violation refuses the answer as
malformed. The Go client, connect.WithStrictDecoding(), applies the same contract from
the compiled descriptor: it refuses unknown fields and runs protovalidate.
An error answer is checked in strict decoding too. The ErrorDetail a Connect error
carries is validated against fora.v1.ErrorDetail.schema.strict.json and the cross-field
rules: the binary details[].value once it is decoded, with an unknown field in the
binary encoding refused as well, and the lowerCamelCase debug projection read under the
proto field names. The schema does not express oneof exclusivity, so a projection that
sets two reason members is refused separately. The envelope around the detail is checked
against the Connect protocol: only code, message and details, a known Connect code,
and well-formed details entries. A refusal is malformed and keeps the peer’s Connect code.
Checking a message or a document yourself
Section titled “Checking a message or a document yourself”The check strict decoding runs is public, so a harness can apply it to anything it read
itself: check_strict("fora.v1.WellKnownManifest", payload) in Python (from fora_sdk
or fora_sdk.client) takes the decoded value and raises StrictViolationError;
checkStrict(message, payload) in TypeScript (from @fora-protocol/sdk/client) does the
same and throws StrictViolation; helpers.CheckStrict(name, payload) in Go takes the
proto-JSON bytes and returns an error wrapping helpers.ErrStrictViolation. Python
checks any message with a published schema. TypeScript bundles the schemas of the
messages the SDK reads and takes any other message’s strict schema as a third argument.
The document readers apply the same check to what a party publishes over HTTPS: the
well-known manifest (fora.v1.WellKnownManifest), the WBA key directory
(fora.v1.WBAFile), the key revocation list (fora.v1.KeyRevocationList), and the
License whose document they fetch and verify against uri_digest. See
the agent SDK overview
for the readers in each language.
The Python client applies the check to what it sends as well, when it is built with
ClientConfig(strict_requests=True). Each request, as the client stamped it and just
before it is signed, must pass the strict schema of its request message and the
cross-field rules, so an unknown member is refused rather than sent and discarded by the
server, and a verb refuses a plain dict in place of its generated request model.
What the schemas accept
Section titled “What the schemas accept”The schemas describe canonical proto-JSON as the FORA wire carries it. The two variants differ only in the unknown-field policy; everything below applies to both.
- Field names are the proto field names, in snake_case. That is the FORA wire
naming: servers render proto-JSON with the proto names, the SDKs send them, and the
canonical signing form uses them. A lowerCamel key such as
offerGroupsis an unknown field, so the strict variant refuses it. google.protobuf.Structstays open. Everyextfield is a Struct: a JSON object whose keys the contract does not define. The strict variant never closes a Struct, at any depth. A map field keeps its value schema.- 64-bit integers accept a decimal string or a JSON integer. Canonical proto-JSON, and
so a FORA server, emits an
int64as a string; the SDKs send a JSON number. A bound applies to both forms:Quota.limitmust be at least 1, so both0and"0"fail. - Other numbers are JSON numbers. Proto-JSON parsers also accept an
int32or adoubleas a string, but no FORA sender emits that form, and accepting it would let a string such as"1000"past a numeric bound. - Constraints. String patterns and lengths, numeric bounds, enum restrictions and item
counts are carried from
buf.validate. A field whose zero value its own rule rejects isrequired, because proto-JSON omits a zero value, so an omitted field is that zero value. Arepeated.uniquefield carriesuniqueItems. - Enums accept the value name, which FORA senders emit, or the number of a permitted value, which proto-JSON parsers also accept.
For example, the strict variant of Quota accepts this message:
{ "limit": "5", "metric": "accesses", "window": "QUOTA_WINDOW_DAILY" }It refuses the same message with "limit": "0" (the bound), without metric (a required
field), or with an added "metricName": "accesses" (an unknown field).
What the schemas do not check
Section titled “What the schemas do not check”- Cross-field rules. A rule that relates two fields, such as which pricing fields a pricing model requires, cannot be expressed per field. Servers enforce these rules, and the SDKs apply them too, including in strict decoding.
- Oneof exclusivity. A schema does not refuse two members of the same oneof.
- Formats.
format(date-time,duration) is an annotation unless your validator is configured to assert formats.
A message that passes the strict schema has the right shape and field-level values; a server can still refuse it for a cross-field rule, a signature or the state of the transaction.
How the schemas are checked
Section titled “How the schemas are checked”- The drift gate regenerates the schemas from the proto and compares the files byte for byte, as it does for the generated code.
- A Go conformance test checks the file set, the field names and the unknown-field policy against the compiled proto descriptors.
- Both variants must reach the verdict of Go
protovalidateon every case of the conformance corpus, in Python (withjsonschema) and in TypeScript (with Ajv).