Documentation
¶
Overview ¶
Package conformance is the schema conformance gate for provider wire fixtures. It answers one question, on every test run, before a fixture is allowed anywhere near a Looprig decoder: is this byte string actually a legal payload for the provider API it claims to come from?
The reason is epistemic rather than defensive. A fixture is only evidence about Looprig's correctness if the fixture itself is something the provider could really have sent. A decoder test that feeds itself an invented payload proves a property of a fiction. The gate makes that failure mode impossible to reach silently: MustValidate fails the test, loudly and with the exact violating instance path, rather than letting a malformed fixture through.
Where the gate lives, and why here ¶
It lives in the inference module, beside the codecs, and it is exported.
It used to live in llm, one tier up, behind an internal/. That put it out of reach of exactly the tests it matters most to: inference/CLAUDE.md requires that "every encode path must hold its encoded body against the format's official request schema in tests", and the encode paths are here. The gate could only be applied by the provider clients that compose these codecs, which is a tier late — a provider test that rejects a body has already lost the ability to say which encoder produced it. Moving it down means the codec that builds a body can validate that body, and llm keeps every call site it had.
Requests and responses ¶
Kinds come in both directions, and the request half is the more valuable of the two. A response schema tells us whether we understood the provider; a request schema tells us whether the provider will understand us, and it says so before the bytes leave the process rather than after a 400. Request schemas are also the stricter half of every specification: they carry the required lists and the additionalProperties:false closures that response schemas routinely omit, which is exactly where an encoder bug hides. Anthropic closes 83 of the 84 object shapes in its request body and 5 of the 49 in its response.
Use MustValidateRequest on an encoded request body and MustValidateResponse on a received payload. Non-test callers use ValidateRequest and ValidateResponse. All four take the bytes on the wire and refuse a kind whose direction does not match, because an encoded request held against a response schema would appear to pass while proving nothing.
What backs the gate ¶
The schema/ tree holds standalone JSON Schema 2020-12 documents derived from the four official upstream API descriptions — OpenAI's own openapi.yaml, the OpenAPI document Anthropic's own SDK points at, Google's production discovery endpoint, and AWS's own Smithy model distribution. They are checked in as real files, never fetched at test time, and every reference is rebased into a local $defs so each document validates offline and on its own. schema/provenance.json records where each source came from, when, and its SHA-256; schema/unenforced.json records, per document, everything the derived schema does NOT enforce. Regenerate with cmd/schemagen; never edit the tree by hand.
What the gate does not catch ¶
The honest limits, all enumerated per document in schema/unenforced.json:
- Unknown properties are allowed wherever the specification allows them. The gate never adds a closure of its own — providers add response fields without notice, and a closure we invented would reject tomorrow's legal payload. Where the specification closes an object itself the closure is kept, because the provider really will reject the extra field. That is overwhelmingly a request-side phenomenon (Anthropic closes 83 of 84 request shapes and 5 of 49 response shapes). Everywhere else a payload may carry a field that does not exist.
- Some unions are relaxed from oneOf to anyOf. Both OpenAI documents offer overlapping branches in places — an ordinary Responses input message matches both EasyInputMessage and Item — where oneOf would reject a payload the live API accepts. Only unions with a demonstrated overlap are allowlisted for relaxation, each is listed in schema/unenforced.json, and every discriminated union keeps its oneOf, so an undefined variant is still caught.
- "format" is an annotation, not an assertion. A malformed date-time or uri passes. This matches the draft 2020-12 default and is measured against the official suite in testdata/format-assertion-support.json.
- Semantic coherence is out of scope. Index continuity across streaming deltas, token counts that add up, ids that match between frames — the schema sees each payload in isolation.
- Google's discovery format declares almost no required properties, in either direction, so the gemini documents constrain types, enums and nesting but cannot catch a missing field.
- AWS marks only modelId as required on ConverseRequest, and modelId travels in the URI path rather than the body. The Bedrock request document consequently requires nothing at the top level; a body with no messages passes.
- Where OpenAPI 3.0's "nullable" survives in a 3.1 document, the gate widens the affected schema to admit null. That under-constrains those positions rather than falsely rejecting a legal null.
The gate's strength is exactly: structure, required properties, types, enums, and string/number/array constraints, as the provider itself publishes them.
Trusting the validator ¶
Assertions are delegated to github.com/santhosh-tekuri/jsonschema/v6. There is no first-party Go implementation to prefer: the JSON Schema organisation publishes a language-agnostic conformance suite and no Go validator, so every Go option is third-party and hand-rolling one would defeat the point of a correctness gate. Instead of trusting the library, suite_test.go runs the organisation's official draft2020-12 suite against it from a checked-in copy on every run. See that file for the required/optional split.
github.com/google/jsonschema-go is the one Go validator with an institutional owner, and it is already in this workspace as an indirect dependency of the MCP Go SDK, so "consolidate on it" is the obvious question to ask. It was measured rather than argued, against the criteria this package actually cares about:
- Official required draft2020-12 suite: v0.4.3 fails 3 of 1299 cases, all in vocabulary.json ($vocabulary and custom metaschemas). santhosh-tekuri passes all 46 files with no skips. It also omits content.json, format.json and vocabulary.json from its own vendored copy of the suite, so those keywords are not self-verified upstream.
- Our schemas: all 14 provider documents load and resolve under both.
- Our verdicts: a differential over the checked-in provider fixture corpus, every fixture and SSE frame against every (format, kind) gate, agreed on 6664 of 6664 comparisons.
So the swap is behaviourally free on today's workload and costs the gate's one headline guarantee — 46/46, zero skips — for a pre-1.0 API. None of the three failures touch a keyword our schemas use, so this is a judgement call and not a defect finding; revisit it if google/jsonschema-go reaches 1.0 with the vocabulary cases passing, at which point the institutional-owner and one-fewer-distinct-dependency arguments carry it.
Index ¶
- Constants
- func Formats() []string
- func Kinds(format string) []string
- func MustValidate(t testing.TB, format, kind string, payload []byte)
- func MustValidateRequest(t testing.TB, format, kind string, body []byte)
- func MustValidateResponse(t testing.TB, format, kind string, payload []byte)
- func MustValidateStream(t testing.TB, format, kind string, body []byte) int
- func SchemaFS() fs.FS
- func Validate(format, kind string, payload []byte) error
- func ValidateRequest(format, kind string, body []byte) error
- func ValidateResponse(format, kind string, payload []byte) error
- type Entry
- type Failure
- type Frame
- type Index
- type Provenance
- type Source
- type Union
Constants ¶
const ( // DirectionRequest marks a kind that describes an outbound request body. // Validating one catches an encoder bug before the bytes reach a live API, // which is why request kinds exist at all. DirectionRequest = "request" // DirectionResponse marks a kind that describes an inbound provider // message. DirectionResponse = "response" )
const ( // UnionStyleProperty selects a member by the value of a shared property, // as OpenAI and Anthropic stream events do with "type". UnionStyleProperty = "property" // UnionStyleMemberKey selects a member by which single property is // present, as the Smithy union encoding does. UnionStyleMemberKey = "member-key" )
Variables ¶
This section is empty.
Functions ¶
func MustValidate ¶
MustValidate fails t unless payload is a legal message of the given kind for the given api-format. The payload is always the encoded body: the bytes on the wire, not a Go value. Call it on every fixture before the fixture reaches a Looprig decoder, and on every encoded request before it reaches a live API.
On failure it reports the violating instance path and the schema keyword that rejected it, not a generic "does not validate": a fixture suite is only maintainable if a rejection tells you which byte to fix.
func MustValidateRequest ¶
MustValidateRequest fails t unless body is a legal request body of the given kind. body is what the encoder produced — the marshalled HTTP body, with any values the provider carries outside it (a Bedrock modelId in the URI path, for instance) already excluded, exactly as the schema models it.
It is the same check as MustValidate with one addition: it refuses a kind that is not a request. Holding an encoded request against a response schema would appear to pass while proving nothing, so the direction mismatch is reported as the mistake it is.
func MustValidateResponse ¶
MustValidateResponse is MustValidateRequest's counterpart for inbound provider messages.
func MustValidateStream ¶
MustValidateStream fails t unless every data frame of an SSE body is a legal message of the given kind. It returns the number of frames validated.
Frames are validated one at a time, against the event union, so a suite can assert that each individual chunk a provider would emit is legal rather than only that the concatenation parses. A body with no frames fails: an empty fixture is not evidence of anything.
func SchemaFS ¶
SchemaFS exposes the embedded schema tree so tests can assert on the files themselves without reaching outside the package directory.
func Validate ¶
Validate is the non-testing form of MustValidate. It returns nil when the payload conforms and a diagnostic error otherwise.
func ValidateRequest ¶
ValidateRequest validates an outbound request against a request schema and the semantic constraints that JSON Schema cannot express. Semantic checks run only after schema validation so the more precise instance-path diagnostic is never masked.
func ValidateResponse ¶
ValidateResponse validates an inbound provider payload against a response schema and refuses request kinds.
Types ¶
type Entry ¶
type Entry struct {
// Document is the slash-separated path of the schema document inside the
// schema/ tree.
Document string `json:"document"`
// Direction is DirectionRequest for what Looprig sends and
// DirectionResponse for what the provider returns.
Direction string `json:"direction"`
// Root is the JSON pointer of the entry subschema within that document,
// always of the form "#/$defs/Name".
Root string `json:"root"`
// Union is set when the kind is a union of message shapes, such as a
// stream event. It lets the gate report a violation against the one member
// the payload claims to be instead of against every branch at once.
Union *Union `json:"union,omitempty"`
}
Entry locates the schema for one (api-format, message-kind) pair.
type Failure ¶
type Failure struct {
Format string
Kind string
Document string
Pointer string
// Selected names the union member the payload claimed to be, when the gate
// narrowed the check to it.
Selected string
Union *Union
Err *jsonschema.ValidationError
}
Failure is a schema violation rendered for a test log.
type Frame ¶
type Frame struct {
// Event is the value of the "event:" field, empty when absent.
Event string
// Data is the concatenated "data:" payload.
Data []byte
}
Frame is one server-sent event.
func ParseSSE ¶
ParseSSE splits a server-sent-event body into its data frames. It implements only what provider streams use — "event:", "data:", blank-line separation and comment lines — and rejects anything it does not understand rather than guessing, so a malformed stream fixture fails the gate instead of silently validating a subset of its frames.
type Provenance ¶
type Provenance struct {
Comment string `json:"comment"`
Sources map[string]*Source `json:"sources"`
}
Provenance records where every input document came from. It is serialised as schema/provenance.json and asserted by provenance_test.go.
func LoadProvenance ¶
func LoadProvenance() (Provenance, error)
LoadProvenance reads the checked-in provenance record.
type Source ¶
type Source struct {
// URL is the exact document the schemas were derived from.
URL string `json:"url"`
// File is the name it is stored under in the generator's -specs directory.
File string `json:"file"`
// Dialect names the conversion applied to it.
Dialect string `json:"dialect"`
// Publisher records who publishes the bytes.
Publisher string `json:"publisher"`
// Hosting is empty for first-party-hosted sources, and otherwise states
// the chain of custody and why no first-party option exists.
Hosting string `json:"hosting,omitempty"`
// Retrieved is the UTC date the recorded bytes were last observed. It is
// carried forward unchanged when a refresh produces identical bytes.
Retrieved string `json:"retrieved"`
// Bytes and SHA256 identify the exact document consumed.
Bytes int `json:"bytes"`
SHA256 string `json:"sha256"`
// CanonicalSHA256 is the hash of a key-sorted re-encoding, present only for
// sources whose serialisation is not byte-stable across fetches.
CanonicalSHA256 string `json:"canonical_sha256,omitempty"`
CanonicalNote string `json:"canonical_note,omitempty"`
// Fields holds a pointer document's parsed key/value pairs.
Fields map[string]string `json:"fields,omitempty"`
// PointerSource, PointerHash and PointerHashNote record the first-party
// document that names this source, for sources that are not first-party
// hosted.
PointerSource string `json:"pointer_source,omitempty"`
PointerHash string `json:"pointer_hash,omitempty"`
PointerHashNote string `json:"pointer_hash_note,omitempty"`
}
Source is the provenance of one upstream API description.
type Union ¶
type Union struct {
// Style is UnionStyleProperty or UnionStyleMemberKey.
Style string `json:"style"`
// Property is the discriminating property name, for UnionStyleProperty.
Property string `json:"property,omitempty"`
// Members maps a discriminator value (or, for UnionStyleMemberKey, a
// member property name) to that member's pointer within the document.
Members map[string]string `json:"members"`
// Ambiguous lists discriminator values that more than one member claims.
// They are deliberately absent from Members: focusing on one of them would
// be a guess, so such payloads are validated against the whole union.
Ambiguous []string `json:"ambiguous,omitempty"`
}
Union describes how a payload selects its member of a union kind.