Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Api Validator Laravel Package

twentytwo-labs/api-validator

Twentytwo Labs API Validator is a Laravel/PHP package for validating API requests and responses with reusable rules. It helps keep endpoints consistent, catches malformed payloads early, and makes validation errors easier to surface in your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small, single-purpose library: it takes a PSR-7 request plus an OpenAPI 2 (Swagger) or OpenAPI 3 document and tells you whether the request matches the contract. It is not a Laravel package — there is no service provider, no config file, no facade. Expect to write ~40 lines of glue yourself.

Install

composer require twentytwo-labs/api-validator nyholm/psr7

You almost certainly need a PSR-7/PSR-17 implementation as well; nyholm/psr7 is the lightest one that plays nicely with Laravel (nyholm/psr7-server if you need to build a request from globals). Under the hood this package delegates to league/openapi-psr7-validator (which in turn uses cebe/php-openapi and opis/json-schema), so most of the behaviour you'll fight with day-to-day comes from those two, not from the wrapper.

Where to look first

  1. README.md — usually one usage snippet, that's the whole documented API.
  2. src/ — find the single public entrypoint (typically a Validator/ApiValidator-style class plus a factory that takes a spec path). Grep for function validate and Exception.
  3. tests/ — the most reliable documentation. The functional tests will show you the exact constructor signature and the shape of the violation objects, which the README tends to omit.
  4. composer.json — check the pinned range of league/openapi-psr7-validator; that tells you which OpenAPI features you actually get.

First use case

Validate an incoming POST JSON body against the spec before it hits your controller, so malformed payloads never reach your FormRequest/action:

// app/Http/Middleware/ValidateAgainstOpenApi.php
public function handle($request, Closure $next)
{
    if (! app()->environment('local', 'staging')) {
        return $next($request);
    }

    $psrRequest = (new Psr17Factory())
        ->createRequest($request->method(), $request->fullUrl())
        ->withBody((new Psr17Factory())->createStream($request->getContent()));

    // exact class/method names vary by release — check src/
    $result = app(ApiValidator::class)->validate($psrRequest);

    if (! $result->isValid()) {
        throw new OpenApiViolationException($result->getErrors()); // renders as 400
    }

    return $next($request);
}

Register it globally or on an api route group only. Start with a single spec and a single route group; do not point it at every route on day one.

Implementation Patterns

Bind the validator as a singleton, compile once. Parsing YAML and resolving $refs on every request is the number-one performance mistake with this class of library. Register a singleton in AppServiceProvider that loads the spec once; the object is stateless and safe to reuse across requests (Octane-safe as long as you don't mutate it).

Validate in two directions, and care more about the response direction. Validating requests catches typos; validating responses in feature tests catches contract drift — the thing that actually breaks consumers. A cheap pattern:

// tests/Concerns/AssertsOpenApi.php
protected function assertMatchesSpec(TestResponse $response, string $specPath): void
{
    $psrResponse = /* convert $response to PSR-7 */;
    $result = $this->app->make(ApiValidator::class)->validateResponse($psrResponse);

    $this->assertTrue($result->isValid(), implode("\n", $result->getErrors()));
}

Call it from a base TestCase for every JSON assertion. This is where the package earns its keep in a Laravel codebase, because Laravel's own validation never checks that your response shape still matches what you promised in the spec.

Map violations to Laravel's error envelope. Don't leak raw library messages ("Value is not valid for schema at #/properties/email") to clients. Walk the violation list, convert JSON-pointer paths (/items/0/sku) into dot notation (items.0.sku), and re-emit as {"errors": {"items.0.sku": ["..."]}} so frontends can reuse the same error-rendering code they use for ValidationException.

One spec per route group / API version. If you serve /v1 and /v2, resolve the spec in the middleware from the route prefix rather than cramming everything into one document. A tiny SpecResolver (read the prefix → return the right path) is the natural extension point and keeps the middleware dumb.

Use it as a test-time guard, not just middleware. A --fail-on-spec-violation pre-deploy step (run the contract tests, fail the pipeline) is worth more than runtime validation, which costs latency and can only ever reject traffic you'd rather have seen in staging.

Fail-open by default in production. Gate the middleware behind an env flag. Runtime contract validation is a development and canary tool; in prod a spec bug becomes a 100% outage. Log-and-continue on isValid() === false in prod, throw in local/staging.

Gotchas and Tips

  • Path matching is the most common failure. The library matches the request URI path against the spec's servers/basePath. If Laravel strips a prefix via RouteServiceProvider or you're behind a proxy, the path it sees won't match the spec and you'll get "no route matched" rather than a validation error. Verify the matched operation before you trust any body validation result.
  • Query strings are always strings. ?page=2 will not satisfy type: integer under a strict schema. Either add a coercion/loose-type option if the version exposes one, or type query params as strings with a pattern. This bites everyone once.
  • Consume the body once. If your middleware reads $request->getContent() and hands the stream to the validator, make sure the stream is rewound (or that getContent()'s cached string is used) before the controller runs, or downstream code sees an empty body. Prefer passing the cached string over re-reading php://input.
  • $ref resolution is filesystem-relative. External file refs resolve relative to the spec document's directory, and remote (http://) refs will happily make an HTTP call per parse. Vendor your spec, keep it on disk, and disable remote resolution.
  • additionalProperties: false is where specs hurt most. Auto-generated specs frequently set it and then reject all the fields your existing clients send. Audit the spec before enabling enforcement, or you'll be rolling back on day two.
  • oneOf / discriminator produce useless error messages. A single missing field shows as "value does not match any of the schemas". Budget time for a translator layer that reports which branches failed.
  • Security schemes are validated for presence and shape, not credentials. A bearer in Authorization passes; whether the token is valid is your job. Don't let this replace actual auth middleware.
  • nullable, OpenAPI 3.1 (type: [string, "null"]), and exclusiveMinimum differ between OpenAPI versions and between spec parsers. Confirm which OpenAPI version the underlying parser supports before you convert your spec — 3.1 support has historically lagged.
  • Multipart/form-data and file uploads are poorly covered. If you accept uploads, exclude those routes from the middleware rather than fighting false positives.
  • Cache the compiled schema in APCu or a file cache keyed by the spec's mtime; re-parsing on every deploy is fine, on every request is not.
  • Maturity risk is real: 0 stars, 0 dependents, a single small maintainer, and a release date that looks wrong. Pin the exact version in composer.json, and be prepared to drop down to league/openapi-psr7-validator directly — the wrapper's value-add is thin enough that you lose very little by doing so, and you gain a package with actual users.
  • Debugging tip: log the matched operation ID alongside violations. Nine times out of ten a "validation failure" is really "matched the wrong operation because of path/prefix mismatch", and that one log line tells you which it is.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent