Validation JSON Schema
Studio\Gesso\JsonValidationResultRenderer::render() turns any OpenApiValidationResult into a machine-readable JSON document for consumers that need more than the flat assertion text — CI ingestion, IDE annotations, scripted triage, or AI-assisted remediation. This page documents the schema so downstream consumers can rely on a stable shape.
A sample document is committed at samples/validation.json.
use Studio\Gesso\JsonValidationResultRenderer;
$json = JsonValidationResultRenderer::render($result);
// Optionally embed a reproduction command (redact secrets first — the
// built-in curl reproduction output is already redacted by default):
$json = JsonValidationResultRenderer::render($result, $curlCommand);The document is deliberately timestamp-free: rendering the same result twice produces byte-identical output, so snapshot tests and CI diffing work without masking.
Selecting json failure output in adapters
Every framework adapter (Laravel, Symfony, Pest, PSR-7) can emit this document in its assertion failure message instead of the plain text shape. One process-wide switch selects the mode everywhere, resolved in priority order:
the
GESSO_VALIDATION_FORMATenvironment variable (text|json);ValidationOutput::use(ValidationOutputFormat::Json)— call it from your test bootstrap, or set thevalidation_outputparameter on the PHPUnit extension, which calls it for you:xml<extensions> <bootstrap class="Studio\Gesso\PHPUnit\OpenApiCoverageExtension"> <parameter name="spec_base_path" value="openapi/dist"/> <parameter name="validation_output" value="json"/> </bootstrap> </extensions>text(the default — failure output is unchanged unless you opt in).
An unrecognised value warns on STDERR and never changes the selection: resolution falls through to the next source (so an invalid validation_output parameter keeps a format already selected via ValidationOutput::use()).
In json mode a failing assertion message is one human-readable header line followed by this document (the curl reproduction moves into reproduce_command, so no separate Reproduce: line is emitted):
OpenAPI schema validation failed for GET /v1/pets (spec: front):
{
"schema_version": 1,
...
}Everything after the first line parses as JSON. The one exception is the PSR-7 exchange assertion (assertPsr7ExchangeMatchesOpenApiSchema()), which validates two results at once: it emits one [request] / [response] label line plus document per failing side, each block parseable on its own and all blocks sharing the same reproduce_command.
Top level
| Field | Type | Description |
|---|---|---|
schema_version | integer | Bumped on incompatible contract changes. The current version is 1. Consumers SHOULD reject unknown values. |
tool | object | { "name": "studio-design/gesso", "version": "<composer version or 'unknown'>" }. "unknown" is emitted when Composer's InstalledVersions metadata is unavailable; the field is always a string. |
outcome | string | One of "success", "failure", "skipped" — the result's outcome() enum value. |
matched | object | Operation context the validator resolved. See matched fields. |
skip_reason | string | null | Human-readable reason for "skipped" outcomes; null otherwise. |
reproduce_command | string | null | The command passed as the second render() argument, verbatim; null when omitted. The renderer performs no redaction of its own — pass a pre-redacted command (the built-in curl formatter redacts sensitive headers, cookies, and query values by default). |
issues | array | One entry per structured issue, in the same order as errors(). Empty for "success" and "skipped" outcomes. See Issue. |
Matched fields
| Field | Type | Description |
|---|---|---|
path | string | null | Matched spec path template (e.g. /v1/pets/{petId}), or null when no path matched. |
status_code | string | null | Spec response key ("200", "5XX", "default") or the literal status for skipped responses; null for request-side results and unresolved lookups. |
content_type | string | null | Spec media-type key (spec author's casing) the body was checked against; null when no body lookup occurred. |
Issue
Each entry serialises one ValidationIssue field-for-field (snake_case). The category slugs and null-ness semantics are the same as the PHP API — see api-reference.md and versioning.md.
| Field | Type | Description |
|---|---|---|
category | string | Stable slug naming the producing validator (request.body, response.header, …). Results built by code that predates the structured API derive "unknown". |
message | string | Exact human-readable error text. Not a compatibility surface — assert on category and context instead. |
instance_path | string | null | RFC 6901 JSON Pointer: "" is the document root and "/" is the property whose name is the empty string. (The human-readable message prefix renders the root as [/] for historical reasons; instance_path is the unambiguous form.) Set on schema violations — for body issues it points into the validated body, for parameter / response-header issues into the named value; null for structural and spec-malformation errors. |
keyword | string | null | JSON Schema keyword that failed (type, required, enum, …). Set on schema violations, and additionally as a synthetic violation kind: "required" when a required parameter / header / security credential is missing, "format" when a credential is present but unusable, "parse" when the PSR-7 adapter could not read or JSON-decode a body. null otherwise. |
parameter | string | null | Name of the spec object a non-body issue is about: the request parameter (request.parameter.*), response header (response.header), or security scheme (request.security). null for body issues and for errors not attributable to one named object (structural spec errors, error-boundary captures). |
method | string | null | HTTP method of the validated operation. |
path | string | null | Matched spec path template. |
status_code | string | null | Spec response key. Always null on request-side issues. |
content_type | string | null | Resolved spec media-type key. Set only on body issues and on the response.content_type note (where it is still the key the body was validated against — the undeclared Content-Type that triggered the note appears in message). |
Versioning
The document shape is covered by the versioning policy:
- Adding a new field is a minor change — consumers MUST ignore fields they do not recognise.
- Removing or renaming a field, changing a field's type, or changing the meaning of an existing value is a major change and bumps
schema_version. - New
categoryslugs and newkeywordvalues may appear in minor releases.
Security notes
- The renderer itself adds no filesystem paths or environment details — every field is derived from the validation result plus the caller-supplied
reproduce_command.messageandreproduce_commandare emitted verbatim, so anything they embed (request/response fragments, absolute paths in test payloads or commands) passes through: treat the document with the same sensitivity as your test payloads and redact before rendering if needed. - Invalid UTF-8 byte sequences inside messages are replaced with U+FFFD so the document always renders instead of masking the original validation failure.