4,000 emails/month for free | Mailtrap sends real emails now!

Laravel OpenAPI Validator

Published on by

Laravel OpenAPI Validator image

Everyone loves someone who's always true to their word. When you open up a public API to the world from your application, it's equally important that you stay true to your word there, as well. What can other applications do with your API? And, how do they do it?

Chances are pretty good you're exposing an API specification, probably through documentation and/or something like an OpenAPI spec (if you're not, please please please drop everything you're doing and take care of that). This is a spectacular way to create a clear line of communication to your users on how you'd like them to interact. Even better yet, if you hold steadfast to your "word" (spec), you'll have a steady stream of followers pining to use your app!

If you're not already familiar with the OpenAPI 3 spec, you'll want to read up a bit on it first. You can find a great overview from Smartbear (the company behind the OpenAPI Specification now, formerly Swagger Specification) here.

Introducing Laravel OpenAPI Validator

Take your defined OpenAPI spec and automatically test your adherence to it in your PHPUnit tests. Behind the scenes this package connects the Laravel HTTP helpers to the PHP League's OpenAPI Validator. Best of all, it's (almost) plug-n-play.

Start by pulling in the package:

composer require kirschbaum-development/laravel-openapi-validator

And, in any feature/integration tests that you make an HTTP call to your API, simply apply the trait:

use Kirschbaum\OpenApiValidator\ValidatesOpenApiSpec;
 
class HttpTest extends TestCase
{
use ValidatesOpenApiSpec;
}

In most situations, that's all you need to do. The trait will tap into any HTTP calls (such as $this->putJson(...) or $this->get(...)) and hand the request and response over to the validator automatically.

What can it do?

Say you have a spec, with a few paths that looks something like this:

openapi: "3.0.0"
 
// …
 
paths:
/test:
get:
responses:
'200':
description: OK
/form:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
formInputInteger:
type: integer
formInputString:
type: string
required:
- formInputInteger
- formInputString
responses:
'200':
description: OK

In my test, I'm going to assert that my simple get endpoint returns a 200:

/**
* @test
*/
public function testGetEndpoint()
{
$response = $this->get('/test');
 
$response->assertStatus(200);
}

Voila! We've asserted that we get a 200! And, with the trait applied to this class, we're automatically checking adherence to our OpenAPI spec as well. In the implementation, let's break something to ensure it's working.

class TestController extends Controller
{
public function __invoke(Request $request)
{
// Break our original implementation
// return response()->json(status: 200);
return response()->json(status: 418); // Tea, anyone?
}
}

When we run our test again:

> ./vendor/bin/phpunit
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
 
.E 2 / 2 (100%)
 
Time: 00:00.138, Memory: 22.00 MB
 
There was 1 error:
 
1) Tests\Feature\SimpleGetTest::testBasicTest
League\OpenAPIValidation\PSR7\Exception\NoResponseCode: OpenAPI spec contains no such operation [/test,get,418]

Yep, sure enough, 418 was not a response we were expecting!

Ok, so it can check status codes, big whoop. That was already part of our assertions anyway! Let's try it out with something a bit more complex, like our form endpoint. Here's the spec (I've omitted any $refs and merged it into a single layer for easier reading):

openapi: "3.0.0"
 
// …
 
paths:
/test:
get:
responses:
'200':
description: OK
/form:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
formInputInteger:
type: integer
formInputString:
type: string
required:
- formInputInteger
- formInputString
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
isValid:
type: boolean
description: Is this a valid object?
howShiny:
type: integer
description: How shiny this object is.
required:
- isValid
- howShiny

So, a few high-level things we can expect, just from the spec:

1. It's a POST request

2. The request body is required, and has two properties, formInputInteger that's an integer, and formInputString that's a string (very descriptive names are important ;-) )

3. The response code is 200

4. The response body is a json object with two properties, isValid (bool) and howShiny (integer).

Here's our (simplified) test:

/**
* @test
*/
public function testFormPost()
{
$response = $this->postJson('/form', [
'formInputInteger' => 42,
'formInputString' => "Don't Panic"
]);
 
$response->assertStatus(200);
 
$this->assertTrue($response->json()['isValid'], true);
$this->assertEquals(10, $response->json()['howShiny']);
}

And a simple (naive) implementation

class FormController extends Controller
{
public function __invoke(Request $request)
{
// ...Validation and typing here...
if ($request->formInputInteger === 42
&& $request->formInputString === "Don't Panic") {
return response()->json([
'isValid' => true,
'howShiny' => 10,
]);
}
 
return response()->json([
'isValid' => false,
'howShiny' => 0,
]);
}
}

We run our test:

> ./vendor/bin/phpunit --filter testFormPost
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
 
. 1 / 1 (100%)
 
Time: 00:00.128, Memory: 22.00 MB
 
OK (1 test, 3 assertions)

And all is well! Looking at the spec, both the parameters on the request are required, so our OpenAPI validator should let us know that before it hits any kind of Laravel validation. Let's try that out. Modify the test and comment out one of the post fields:

public function testFormPost()
{
$response = $this->postJson('/form', [
'formInputInteger' => 42,
// 'formInputString' => "Don't Panic"
]);
 
$response->assertStatus(200);
 
$this->assertTrue($response->json()['isValid'], true);
$this->assertEquals(10, $response->json()['howShiny']);
}

Run the tests again and see:

> ./vendor/bin/phpunit --filter testFormPost
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
 
F 1 / 1 (100%)
{
"formInputInteger": 42
}
Keyword validation failed: Required property 'formInputString' must be present in the object
Key: formInputString
 
Time: 00:00.107, Memory: 22.00 MB
 
There was 1 failure:
 
1) Tests\Feature\SimpleTest::testFormPost
Body does not match schema for content-type "application/json" for Request [post /form]
 
[...stack trace here...]
 
FAILURES!
Tests: 1, Assertions: 1, Failures: 1\.

Failure, as expected! The validator lets us know that formInputString was a requirement on the request, and it wasn't specified. In other words, the "Body does not match schema...for Request."

Let's uncomment that line again and modify our response this time, and only return that it's shiny:

class FormController extends Controller
{
public function __invoke(Request $request)
{
// ...Validation and typing here...
if ($request->formInputInteger === 42
&& $request->formInputString === "Don't Panic") {
return response()->json([
// 'isValid' => true,
'howShiny' => 10,
]);
}
 
return response()->json([
'isValid' => false,
'howShiny' => 0,
]);
}
}

Now we get a similar error, but on the response:

> ./vendor/bin/phpunit --filter testFormPost
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
 
F 1 / 1 (100%)
{
"howShiny": 10
}
Keyword validation failed: Required property 'isValid' must be present in the object
Key: isValid
 
Time: 00:00.126, Memory: 24.00 MB
 
There was 1 failure:
 
1) Tests\Feature\SimpleTest::testFormPost
Body does not match schema for content-type "application/json" for Response [post /form 200]
 
[...stack trace here...]
 
FAILURES!
Tests: 1, Assertions: 1, Failures: 1\.

The package lets us know that our "body does not match schema...for Response". Our required property isValid (from the spec) wasn't specified in our response, so it failed our test for us.

Enjoy your valid fun!

We hope you enjoy this package, it definitely simplifies life a lot when you're working with an API. It gives you a nice layer of accountability that keeps you true to your word! Take a look at the repo for more usage info and let us know how it goes for you.

Zack Teska photo

Zack, Senior Developer at Kirschbaum, began his career as a web application developer almost 20 years ago when he started tinkering with PHP in his basement. Since then, his passion for learning and experimentation has driven his work with companies across several industries. He enjoys identifying business issues and discovering impactful, elegant solutions.

Having studied dance at St. Olaf College, Zack finds opportunities to incorporate wellness and the creative beauty of movement in both the digital and analogue worlds. Zack specializes in PHP, Laravel, VueJS, and Javascript, as well as Docker, Kubernetes, and CI/CD pipelines; and is always interested in learning something new.

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Battle Ready Laravel

The ultimate guide to auditing, testing, fixing and improving your Laravel applications so you can build better apps faster and with more confidence.

Visit Battle Ready Laravel
Curotec logo

Curotec

World class Laravel experts with GenAI dev skills. LATAM-based, embedded engineers that ship fast, communicate clearly, and elevate your product. No bloat, no BS.

Curotec
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $3200/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
Cut PHP Code Review Time & Bugs into Half with CodeRabbit logo

Cut PHP Code Review Time & Bugs into Half with CodeRabbit

CodeRabbit is an AI-powered code review tool that specializes in PHP and Laravel, running PHPStan and offering automated PR analysis, security checks, and custom review features while remaining free for open-source projects.

Cut PHP Code Review Time & Bugs into Half with CodeRabbit
Get expert guidance in a few days with a Laravel code review logo

Get expert guidance in a few days with a Laravel code review

Expert code review! Get clear, practical feedback from two Laravel devs with 10+ years of experience helping teams build better apps.

Get expert guidance in a few days with a Laravel code review
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Harpoon: Next generation time tracking and invoicing logo

Harpoon: Next generation time tracking and invoicing

The next generation time-tracking and billing software that helps your agency plan and forecast a profitable future.

Harpoon: Next generation time tracking and invoicing
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit

The latest

View all →
"Don't Remember" Form Helper Added in Inertia.js 2.3.7 image

"Don't Remember" Form Helper Added in Inertia.js 2.3.7

Read article
Fast Laravel Course Launch image

Fast Laravel Course Launch

Read article
Laravel News Partners With Laracon India image

Laravel News Partners With Laracon India

Read article
A new beta of Laravel Wayfinder just dropped image

A new beta of Laravel Wayfinder just dropped

Read article
Ben Bjurstrom: Laravel is the best Vibecoding stack for 2026 image

Ben Bjurstrom: Laravel is the best Vibecoding stack for 2026

Read article
Laravel Altitude - Opinionated Claude Code agents and commands for TALL stack development image

Laravel Altitude - Opinionated Claude Code agents and commands for TALL stack development

Read article