Skip to content

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.

Each message has two files, named by its fully-qualified name:

FileUnknown fields
fora.v1.ResourceResponse.schema.jsonAccepted. A reader that must accept messages from a newer protocol version uses this variant.
fora.v1.ResourceResponse.schema.strict.jsonRefused: 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.

The schemas ship in all three published packages, at the version you pin:

PackageAccess
Go module github.com/FORA-Protocol/protocolThe 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 PyPIwire.schemas.load("fora.v1.ResourceResponse", strict=True) returns the schema as a dict. wire.schemas.names() lists the messages.
@fora-protocol/sdk on npmThe export path @fora-protocol/sdk/jsonschema/*, for example @fora-protocol/sdk/jsonschema/fora.v1.ResourceResponse.schema.strict.json.

Python, with the jsonschema library:

from jsonschema import Draft202012Validator
from wire import schemas
validator = Draft202012Validator(schemas.load("fora.v1.ResourceResponse", strict=True))
validator.validate(body) # raises ValidationError on an unknown field or a constraint violation

TypeScript, 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.

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.

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.

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 offerGroups is an unknown field, so the strict variant refuses it.
  • google.protobuf.Struct stays open. Every ext field 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 int64 as a string; the SDKs send a JSON number. A bound applies to both forms: Quota.limit must be at least 1, so both 0 and "0" fail.
  • Other numbers are JSON numbers. Proto-JSON parsers also accept an int32 or a double as 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 is required, because proto-JSON omits a zero value, so an omitted field is that zero value. A repeated.unique field carries uniqueItems.
  • 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).

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

  • 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 protovalidate on every case of the conformance corpus, in Python (with jsonschema) and in TypeScript (with Ajv).