sixlive/laravel-json-schema-assertions
Validate JSON responses and payloads against JSON Schema definitions in your Laravel tests. Adds expressive assertions like assertJsonSchema so you can verify API structure and contracts with minimal setup.
sixlive/laravel-json-schema-assertions adds an assertJsonSchema() macro to Laravel's TestResponse, letting you validate a JSON HTTP response body against a JSON Schema document instead of hand-writing dozens of assertJsonStructure / assertJsonPath calls.
Install (dev only):
composer require --dev sixlive/laravel-json-schema-assertions
No manual provider registration is needed — package discovery wires up the service provider, and the macro is available on any TestResponse returned by $this->getJson(), $this->postJson(), $this->json(), etc.
First use case — contract-test an API endpoint. Drop a schema next to your tests:
// tests/schemas/users/index.json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": ["data"],
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "name", "email"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"created_at":{ "type": ["string", "null"] }
}
}
}
}
}
Then assert against it:
public function test_users_index_matches_schema(): void
{
$this->getJson('/api/users')
->assertOk()
->assertJsonSchema(base_path('tests/schemas/users/index.json'));
}
Where to look first: the package's test suite/README for the exact accepted argument forms (file path vs. inline schema string), and justinrainbow/json-schema docs — that library does the actual validation, so its draft support and error wording are what you'll be reading when an assertion fails.
Recommended rollout: pick one high-traffic read endpoint, write the schema from a real response ($response->json() pretty-printed), assert it, then expand to the rest of the resource. Schemas are cheap to keep in tests/schemas/ and act as living API documentation.
Layered assertion: status → schema → specific values. assertJsonSchema() only checks shape/type/constraints, so pair it with targeted assertions for business rules:
$response = $this->getJson('/api/users')
->assertOk()
->assertJsonSchema(base_path('tests/schemas/users/index.json'))
->assertJsonPath('data.0.email', '[email protected]');
Reusable schema fragments via $ref. Keep a tests/schemas/definitions.json with shared objects (user, pagination, error) and reference them locally from each endpoint schema. This is the single biggest maintainability win — one place to change when a resource gains a field.
Data providers for polymorphic responses. When an endpoint returns different shapes by role or state, drive the schema from a PHPUnit data provider so one test method covers admin, member, and guest payloads.
Snapshot the schema from a real response while writing tests. Temporarily dump($response->json()) in a failing test, paste into the schema file, then tighten constraints (required, format, enum, minimum). Start permissive ("type": "object") so CI stays green, then ratchet down.
Test the error envelope too. Most teams schema-test only happy paths. Error responses (404/422/403) are exactly where clients break — write one schema for your error body and assert it across all failure tests.
Pest integration works transparently because it's a macro:
it('returns a valid user collection', function () {
$this->getJson('/api/users')
->assertOk()
->assertJsonSchema(base_path('tests/schemas/users/index.json'));
});
CI wiring. Nothing special is required; the schemas are plain files committed with the tests. If you run tests inside a container, prefer base_path() / __DIR__ over relative paths so resolution doesn't depend on the working directory.
Complementary tooling. Use this package for response-shape assertions in feature tests, and keep a separate contract test (or OpenAPI validation) if your spec is the source of truth — the two can drift otherwise.
justinrainbow/json-schema. Draft-04 is the safe target; newer drafts (2019-09, 2020-12) and keywords from them (unevaluatedProperties, dependentRequired, modern $defs semantics) are not reliably supported. Stick to draft-04/06 keywords.format validation is opt-in in the underlying validator. "format": "email" / "uuid" may not be enforced by default depending on how the package constructs the validator. Don't assume format gives you enforcement — add a regex pattern if it matters.$response->json()), so integer/float coercion, null handling, and large-number precision behave as PHP does, not as the JSON text does. A schema requiring "type": "integer" can fail on a value that serialized as "1" in a badly-typed resource.assertOk() (or check content type) first so you're not debugging a schema error that's really a 500 HTML error page.$ref resolution. Local file refs are fine; remote HTTP $refs require network access and can be flaky/blocked in CI. Keep all refs local to the repo.base_path('tests/schemas/…')). Relative-path resolution depends on the process working directory, which differs between php artisan test, IDE runners, and CI containers.assertJsonStructure. assertJsonStructure is faster and gives cleaner diffs for simple shape checks; reach for JSON Schema when you need types, enums, ranges, and reusable definitions across many endpoints.^4.4; older 4.x pins will emit deprecations or fail to install. Keep it in require-dev so it never ships to production.assertJsonSchema() in your own base TestCase helper rather than forking the package — you keep upgrade paths open.How can I help you explore Laravel packages today?