neomerx/json-api
Framework-agnostic PHP library implementing JSON:API v1.1. Builds compliant documents, relationships, compound includes, meta and errors. Parses/validates Accept/Content-Type and query params (pagination, sorting, sparse fields) to return proper 415/406 responses.
Install the package:
composer require neomerx/json-api
Define a resource schema (e.g., app/Schemas/PostSchema.php):
use Neomerx\JsonApi\Encoder\Encoder;
use Neomerx\JsonApi\Encoder\SchemaInterface;
use Neomerx\JsonApi\Encoder\ContextInterface;
class PostSchema implements SchemaInterface
{
public function getType(): string { return 'posts'; }
public function getId($post): ?string { return $post->id; }
public function getAttributes($post, ContextInterface $context): array
{
return [
'title' => $post->title,
'body' => $post->body,
];
}
public function getRelationships($post, ContextInterface $context): array
{
return [
'author' => [
SchemaInterface::RELATIONSHIP_DATA => $post->author,
],
];
}
}
Encode a resource (e.g., in a controller):
use Neomerx\JsonApi\Encoder\Encoder;
public function show(Post $post)
{
$encoder = Encoder::instance([
Post::class => PostSchema::class,
]);
return response()->json(
$encoder->encodeData($post),
200,
['Content-Type' => 'application/vnd.api+json']
);
}
Handle requests (e.g., validate Accept headers):
use Neomerx\JsonApi\Parser\Parser;
use Neomerx\JsonApi\Parser\ParserInterface;
public function index(Request $request)
{
$parser = Parser::instance();
$parser->parseAcceptHeader($request->header('Accept'));
if (!$parser->isAccepted('application/vnd.api+json')) {
return response('Unsupported media type', 415);
}
// Proceed with JSON API logic...
}
For a posts resource with author relationships:
// GET /posts/1?include=author
$encoder = Encoder::instance([
Post::class => PostSchema::class,
Author::class => AuthorSchema::class,
])
->withIncludedPaths(['author']); // Pre-configure includes
$post = Post::find(1);
return response()->json($encoder->encodeData($post));
Pattern: Group schemas by domain (e.g., app/Schemas/Api/Posts, app/Schemas/Api/Users).
Workflow:
SchemaInterface for each model.JsonApiServiceProvider):
public function register()
{
$this->app->singleton(Encoder::class, function ($app) {
return Encoder::instance([
Post::class => PostSchema::class,
Author::class => AuthorSchema::class,
]);
});
}
Encoder into controllers/services via Laravel’s DI.Tip: Use trait-based schemas for shared logic:
trait HasAuthorRelationship
{
public function getRelationships($model, ContextInterface $context): array
{
return [
'author' => [
SchemaInterface::RELATIONSHIP_DATA => $model->author,
],
];
}
}
Pattern: Parse and validate incoming requests before processing.
Steps:
Accept/Content-Type headers:
$parser = Parser::instance();
$parser->parseAcceptHeader($request->header('Accept'));
$parser->parseContentTypeHeader($request->header('Content-Type'));
if (!$parser->isAccepted('application/vnd.api+json')) {
return response('Unsupported media type', 415);
}
$queryParams = $parser->parseQueryParams($request->query());
$encoder->withIncludedPaths($queryParams->getIncludes());
Integration with Laravel: Use middleware to centralize parsing:
class ValidateJsonApiRequest
{
public function handle($request, Closure $next)
{
$parser = Parser::instance();
$parser->parseAcceptHeader($request->header('Accept'));
if (!$parser->isAccepted('application/vnd.api+json')) {
return response('Unsupported media type', 415);
}
return $next($request);
}
}
Pattern: Use lazy-loading or eager-loading with ContextInterface.
Example: Dynamically load relationships based on include paths:
class PostSchema implements SchemaInterface
{
public function getRelationships($post, ContextInterface $context): array
{
$includes = $context->getIncludedPaths();
$relationships = [];
if (in_array('author', $includes)) {
$relationships['author'] = [
SchemaInterface::RELATIONSHIP_DATA => $post->author,
];
}
if (in_array('comments', $includes)) {
$relationships['comments'] = [
SchemaInterface::RELATIONSHIP_DATA => $post->comments->load('user'),
];
}
return $relationships;
}
}
Circular References: The package handles them natively. No manual checks needed.
Pattern: Convert Laravel exceptions to JSON API errors.
Example:
try {
// Business logic
} catch (\Exception $e) {
$error = Error::jsonApiError(
'invalid_data',
'The provided data is invalid',
['source' => ['pointer' => '/data/attributes/title']],
422
);
return response()->json($error->toArray(), 422);
}
Global Exception Handler:
public function render($request, \Throwable $exception)
{
if ($exception instanceof \Illuminate\Validation\ValidationException) {
$errors = $exception->errors();
$errorCollection = new ErrorCollection();
foreach ($errors as $field => $messages) {
foreach ($messages as $message) {
$errorCollection->add(
Error::jsonApiError(
'validation_error',
$message,
['source' => ['pointer' => "/data/attributes/{$field}"]]
)
);
}
}
return response()->json($errorCollection->toArray(), 422);
}
return parent::render($request, $exception);
}
Pattern: Use BaseQueryParser to extract and apply query params.
Example:
$queryParams = $parser->parseQueryParams($request->query());
$page = $queryParams->getPage();
$perPage = $queryParams->getPerPage();
$sort = $queryParams->getSort();
$filter = $queryParams->getFilter();
$posts = Post::query();
if ($sort) {
$posts->orderBy($sort['field'], $sort['direction'] ?? 'asc');
}
if ($filter) {
$posts->where($filter['field'], $filter['operator'], $filter['value']);
}
$posts = $posts->paginate($perPage, ['*'], 'page', $page);
Integration with Laravel Pagination:
$encoder->withMeta([
'pagination' => [
'total' => $posts->total(),
'pages' => $posts->lastPage(),
'current_page' => $posts->currentPage(),
],
]);
ContextInterface to getAttributes()/getRelationships().
public function getAttributes($model, ContextInterface $context): array
public function getRelationships($post, ContextInterface $context): array
{
$includes = $context->getIncludedPaths();
if (in_array('comments', $includes)) {
return [
'comments' => [
SchemaInterface::RELATIONSHIP_DATA => $post->comments()->with('user')->get(),
],
];
}
return [];
}
How can I help you explore Laravel packages today?