jurager/json-api

A Lightweight JSON:API Resource for Laravel

Installs: 61

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

pkg:composer/jurager/json-api

1.0.1 2026-01-27 06:17 UTC

This package is auto-updated.

Last update: 2026-01-27 06:18:38 UTC


README

Latest Stable Version Total Downloads PHP Version Require License

A Laravel package that enables seamless adherence to the JSON:API specification resources, featuring built-in support for sparse fieldsets, compound documents, and additional specification-driven capabilities.

Note

These docs are not designed to introduce you to the JSON:API specification and the associated concepts, instead you should head over and read the specification if you are not yet familiar with it. The documentation that follows only covers how to implement the specification via the package.

Requirements

PHP >= 8.2 and Laravel 11.x or higher

Installation

composer require jurager/resource

Getting started

The JsonApiResource class provided by this package is a specialisation of Laravel's Eloquent API resource. All public facing APIs are still accessible; in a controller, for example, you interact with a JsonApiResource as you would with Laravel's JsonResource class.

<?php

namespace App\Http\Controllers;

use App\Http\Resources\UserResource;
use App\Models\User;

class UserController
{
    public function index()
    {
        $users = User::with([/* ... */])->paginate();

        return UserResource::collection($users);
    }

    public function show(User $user)
    {
        $user->load([/* ... */]);

        return UserResource::make($user);
    }
}

As we make our way through the examples you will see that new APIs are introduced when interacting with the class internally, for example, the toArray() method is no longer used.

Creating resource

To get started, let's create a UserResource for our User model. In our user resource will expose the user's name, website, and twitter_handle in the response.

First we will create a new API resource that extends Jurager\JsonApi\JsonApiResource.

<?php

namespace App\Http\Resources;

use Jurager\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    //
}

Adding attributes

We will now create an $attributes property and list the model's attributes we want to expose.

<?php

namespace App\Http\Resources;

use Jurager\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    public $attributes = [
        'name',
        'website',
        'twitter_handle',
    ];
}

When making a request to an endpoint that returns the UserResource, for example:

Route::get('users/{user}', fn (User $user) => UserResource::make($user));

The following JSON:API formatted data would be returned:

{
  "data": {
    "type": "users",
    "id": "1234",
    "attributes": {
      "name": "John",
      "website": "https://johndoe.me",
      "twitter_handle": "@johndoe"
    }
  }
}

We will now dive into adding relationships to your resources, but if you would like to explore more complex attribute features you may like to jump ahead:

Adding relationships

Available relationships may be specified in a $relationships property, similar to the $attributes property, however you may use a key / value pair to provide the resource class that should be used for the given relationship.

We will make two relationships available on the resource:

  • $user->team: a "toOne" / HasOne relationship.
  • $user->posts: a "toMany" / HasMany relationship, etc.
<?php

namespace App\Http\Resources;

use Jurager\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    public $attributes = [
        'name',
        'website',
        'twitter_handle',
    ];

    public $relationships = [
        'team' => TeamResource::class,
        'posts' => PostResource::class,
    ];
}

Assuming the key / value pair follows the convention '{myKey}' => {MyKey}Resource::class, the class may be omitted to streamline things further.

<?php

namespace App\Http\Resources;

use Jurager\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    public $attributes = [
        'name',
        'website',
        'twitter_handle',
    ];

    public $relationships = [
        'team',
        'posts',
    ];
}
Example request and response

The client may now request these relationships via the include query parameter.

GET /users/74812?include=posts,team

Note Relationships are not exposed in the response unless they are requested by the calling client via the include query parameter. This is intended and is part of the JSON:API specification.

{
  "data": {
    "id": "74812",
    "type": "users",
    "attributes": {
      "name": "John",
      "website": "https://johndoe.me",
      "twitter_handle": "@johndoe"
    },
    "relationships": {
      "posts": {
        "data": [
          {
            "type": "posts",
            "id": "25240"
          },
          {
            "type": "posts",
            "id": "39974"
          }
        ]
      },
      "team": {
        "data": {
          "type": "teams",
          "id": "18986"
        }
      }
    }
  },
  "included": [
    {
      "id": "25240",
      "type": "posts",
      "attributes": {
        "title": "So what is `JSON:API` all about anyway?",
        "content": "...",
        "excerpt": "..."
      }
    },
    {
      "id": "39974",
      "type": "posts",
      "attributes": {
        "title": "Building an API with Laravel, using the `JSON:API` specification.",
        "content": "...",
        "excerpt": "..."
      }
    },
    {
      "id": "18986",
      "type": "teams",
      "attributes": {
        "name": "Laravel"
      }
    }
  ]
}

To learn about more complex relationship features you may like to jump ahead:

A note on eager loading

This package does not eager load Eloquent relationships. If a relationship is not eagerly loaded, the package will lazy load the relationship on the fly.

I recommend using Spatie's query builder package which will eager load your models against the JSON:API query parameter standards.

Spatie provide comprehensive documentation on how to use the package, but I will briefly give an example of how you might use this in a controller.

<?php

namespace App\Http\Controllers;

use App\Http\Resources\UserResource;
use App\Models\User;
use Spatie\QueryBuilder\QueryBuilder;

class UserController
{
    public function index()
    {
        $users = QueryBuilder::for(User::class)
            ->allowedIncludes(['team', 'posts'])
            ->paginate();

        return UserResource::collection($users);
    }

    public function show($id)
    {
        $user = QueryBuilder::for(User::class)
            ->allowedIncludes(['team', 'posts'])
            ->findOrFail($id);

        return UserResource::make($user);
    }
}

Digging deeper

We have now covered the basics of exposing attributes and relationships on your resources. We will now cover more advanced topics to give you even greater control.

Attributes

toAttributes()

As we saw in the adding attributes section, the $attributes property is the fastest way to expose attributes for a resource. In some scenarios you may need greater control over the attributes you are exposing. If that is the case, you may implement the toAttributes() method. This will grant you access to the current request and allow for conditional logic.

<?php

namespace App\Http\Resources;

use Jurager\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    /**
     * @param  \Illuminate\Http\Request  $request
     * @return array<string, mixed>
     */
    public function toAttributes($request)
    {
        return [
            'name' => $this->name,
            'website' => $this->website,
            'twitter_handle' => $this->twitter_handle,
            'email' => $this->when($this->email_is_public, $this->email, '<private>'),
            'address' => [
                'city' => $this->address('city'),
                'country' => $this->address('country'),
            ],
        ];
    }
}
Example response
{
  "data": {
    "id": "74812",
    "type": "users",
    "attributes": {
      "name": "John",
      "website": "https://johndoe.me",
      "twitter_handle": "@johndoe",
      "email": "<private>",
      "address": {
        "city": "Berlin",
        "country": "Germany"
      }
    }
  }
}

Sparse fieldsets

Sparse fieldsets are a feature of the JSON:API specification that allows clients to specify which attributes, for any given resource type, they would like to receive. This allows for more deterministic responses, while also improving server-side performance and reducing payload sizes. Sparse fieldsets work out of the box for your resources.

We will cover them briefly here, but we recommend reading the specification to learn more.

As an example, say we are building out an index page for a blog. The page will show each post's title and excerpt, and also the name of the post's author. If the client wishes, they may limit the response to only include the required attributes for each resource type, and exclude the other attributes, such as the post's content and the authors twitter_handle.

To achieve this we will send the following request.

GET /posts?include=author&fields[posts]=title,excerpt&fields[users]=name

Note The include query parameter key is author, while the sparse fieldset parameter key is users. This is because authors are users, e.g. the Eloquent author() relationship returns a User model.

Example response
{
  "data": [
    {
      "id": "25240",
      "type": "posts",
      "attributes": {
        "title": "So what is `JSON:API` all about anyway?",
        "excerpt": "..."
      },
      "relationships": {
        "author": {
          "data": {
            "type": "users",
            "id": "74812"
          }
        }
      }
    },
    {
      "id": "39974",
      "type": "posts",
      "attributes": {
        "title": "Building an API with Laravel, using the `JSON:API` specification.",
        "excerpt": "..."
      },
      "relationships": {
        "author": {
          "data": {
            "type": "users",
            "id": "74812"
          }
        }
      }
    }
  ],
  "included": [
    {
      "type": "users",
      "id": "74812",
      "attributes": {
        "name": "John"
      }
    }
  ]
}

Minimal attributes

Resources return a maximal attribute payload when sparse fieldsets are not in use i.e. all declared attributes on the resource are returned. If you prefer you can make the use of sparse fieldsets required in order to retrieve any attributes.

You may call the useMinimalAttributes() method in an application service provider.

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Jurager\JsonApi\JsonApiResource;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        JsonApiResource::useMinimalAttributes();

        // ...
    }
}

Lazy attribute evaluation

For attributes that are expensive to calculate, it is possible to have them evaluated only when they are to be included in the response, i.e. they have not been excluded via sparse fieldsets or minimal attributes. This may be useful if you are interacting with a database or making HTTP requests in a resource.

As an example, let's imagine that we expose a base64 encoded avatar for each user. Our implementation downloads the avatar from our in-house avatar microservice.

<?php

namespace App\Http\Resources;

use Illuminate\Support\Facades\Http;
use Jurager\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    /**
     * @param  \Illuminate\Http\Request  $request
     * @return array<string, mixed>
     */
    public function toAttributes($request)
    {
        return [
            // ...
            'avatar' => Http::get("https://avatar.example.com/{$this->id}")->body(),
        ];
    }
}

The above implementation would make a HTTP request to our microservice even when the client is excluding the avatar attribute via sparse fieldsets or minimal attributes. To improve performance when this attribute is not being returned we can wrap the value in a Closure. The Closure will only be evaluated when the avatar is to be returned.

<?php

namespace App\Http\Resources;

use Illuminate\Support\Facades\Http;
use Jurager\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    /**
     * @param  \Illuminate\Http\Request  $request
     * @return array<string, mixed>
     */
    public function toAttributes($request)
    {
        return [
            // ...
            'avatar' => fn () => Http::get("https://avatar.example.com/{$this->id}")->body(),
        ];
    }
}

Relationships

toRelationships()

As we saw in the adding relationships section, the $relationships property is the fastest way to specify the available relationships for a resource. In some scenarios you may need greater control over the relationships you are making available. If that is the case, you may implement the toRelationships() method. This will grant you access to the current request and allow for conditional logic.

The value must always be wrapped in a Closure, which will only be called if the relationships is requested by the client.

<?php

namespace App\Http\Resources;

use Jurager\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    /**
     * @param  \Illuminate\Http\Request  $request
     * @return array<string, (callable(): \Jurager\JsonApi\JsonApiResource|\Jurager\JsonApi\JsonApiResourceCollection|\Illuminate\Http\Resources\PotentiallyMissing)>
     */
    public function toRelationships($request)
    {
        return [
            'team' => fn () => TeamResource::make($this->team),
            'posts' => fn () => $request->user()->is($this->resource)
                ? PostResource::collection($this->posts)
                : PostResource::collection($this->posts->where('published', true)),
        ];
    }
}

Customising the relationship resource class guessing

When you omit the resource class in the $relationships property, the package will attempt to guess the appropriate resource class. By default, it looks for classes in the following order:

  1. App\Http\Resources\{SingularStudly}Resource (e.g., postsPostResource)
  2. App\Http\Resources\{Studly}Resource (e.g., postsPostsResource)

For example, a relationship named posts will first look for App\Http\Resources\PostResource, and if not found, will look for App\Http\Resources\PostsResource.

If your application uses a different namespace or naming convention for resources, you can customise the guessing logic by calling guessRelationshipResourceUsing() in a service provider:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Jurager\JsonApi\JsonApiResource;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        JsonApiResource::guessRelationshipResourceUsing(function (string $relationship, JsonApiResource $resource): string {
            // Custom namespace for API resources
            $resourceClass = 'App\\Http\\Resources\\Api\\'.Str::of($relationship)->singular()->studly().'Resource';

            if (class_exists($resourceClass)) {
                return $resourceClass;
            }

            throw new \RuntimeException("Unable to find resource class for relationship [{$relationship}].");
        });
    }
}

The callback receives two arguments:

  • $relationship: The relationship name as defined in the $relationships property (e.g., 'posts', 'team')
  • $resource: The current JsonApiResource instance, which can be used to apply different logic based on the parent resource
Example: Namespace per module
JsonApiResource::guessRelationshipResourceUsing(function (string $relationship, JsonApiResource $resource): string {
    // Get the module namespace from the parent resource
    $parentNamespace = (new \ReflectionClass($resource))->getNamespaceName();

    $resourceClass = $parentNamespace.'\\'.Str::of($relationship)->singular()->studly().'Resource';

    if (class_exists($resourceClass)) {
        return $resourceClass;
    }

    // Fallback to default namespace
    return 'App\\Http\\Resources\\'.Str::of($relationship)->singular()->studly().'Resource';
});

To reset to the default guessing behaviour, pass null:

JsonApiResource::guessRelationshipResourceUsing(null);

Filtering

The package provides support for filtering according to the JSON:API specification recommendations. Filters are defined in the model using the Filterable trait.

Basic filtering

Add the Filterable trait to your model and define allowed filters in the $filterable property:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Jurager\JsonApi\Concerns\Filterable;

class User extends Model
{
    use Filterable;

    protected array $filterable = [
        'status',
        'role',
    ];
}

Use the filtered() scope in your controller:

<?php

namespace App\Http\Controllers;

use App\Http\Resources\UserResource;
use App\Models\User;

class UserController
{
    public function index()
    {
        return UserResource::collection(
            User::filtered()->paginate()
        );
    }
}

Clients can now filter data using the filter query parameter:

GET /users?filter[status]=active&filter[role]=admin

Only filters declared in the $filterable property will be applied. Any unknown filters are ignored (whitelist approach).

allowedFilters()

For more control over filtering, override the allowedFilters() method in your model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Jurager\JsonApi\Concerns\Filterable;
use Jurager\JsonApi\Filter;

class User extends Model
{
    use Filterable;

    public function allowedFilters(): array
    {
        return [
            'status' => Filter::exact(),
            'name' => Filter::partial(),
            'created_at' => Filter::operators('gte', 'lte'),
            'role' => Filter::operators('eq', 'in'),
            'active' => Filter::scope(),
        ];
    }
}

Filter types

The Filter class provides several filter types:

use Jurager\JsonApi\Filter;

// Exact match (WHERE column = value)
Filter::exact()

// Partial match (WHERE column LIKE %value%)
Filter::partial()

// Model scope (calls scopeName on the model)
Filter::scope()

// Custom callback
Filter::callback(function ($query, $value, $property) {
    $query->where('name', 'like', "%{$value}%")
          ->orWhere('email', 'like', "%{$value}%");
})

// Operators filter (supports: eq, not, gt, gte, lt, lte, like, in, not_in)
Filter::operators('gte', 'lte', 'eq')

Filter modifiers

Filters can be customized with modifiers:

// Map filter name to a different column
Filter::exact()->column('user_status')

// Allow null values
Filter::exact()->nullable()

// Restrict allowed operators
Filter::operators('gte', 'lte')->allowOperators('eq', 'gt')

Operators

The package supports the following operators when using Filter::operators():

Operator SQL Example
eq = ?filter[status][eq]=active
not != ?filter[status][not]=deleted
gt > ?filter[age][gt]=18
gte >= ?filter[age][gte]=21
lt < ?filter[price][lt]=100
lte <= ?filter[price][lte]=50
like LIKE %val% ?filter[name][like]=john
in IN (...) ?filter[status][in]=active,pending
not_in NOT IN (...) ?filter[status][not_in]=deleted,archived

Example request with operators:

GET /posts?filter[created_at][gte]=2024-01-01&filter[created_at][lte]=2024-12-31
GET /users?filter[status][in]=active,pending

Filtering by relationships

Use dot notation to filter by related model attributes:

class Post extends Model
{
    use Filterable;

    protected array $filterable = [
        'author.name',
        'category.slug',
    ];

    // Or with allowedFilters() for more control
    public function allowedFilters(): array
    {
        return [
            'author.name' => Filter::exact(),
            'author.email' => Filter::partial(),
            'category.slug' => Filter::exact(),
        ];
    }

    public function author()
    {
        return $this->belongsTo(User::class);
    }

    public function category()
    {
        return $this->belongsTo(Category::class);
    }
}

Example request:

GET /posts?filter[author.name]=John&filter[category.slug]=technology

Multiple values

Pass multiple values using array syntax:

GET /users?filter[status][]=active&filter[status][]=pending

This will apply WHERE status IN ('active', 'pending')

Combining with other query methods

The filtered() scope can be combined with other query methods:

User::filtered()
    ->where('is_admin', false)
    ->orderBy('created_at', 'desc')
    ->paginate()

Sorting

The package provides support for sorting according to the JSON:API specification. Sorts are defined in the model using the Sortable trait.

Basic sorting

Add the Sortable trait to your model and define allowed sorts in the $sortable property:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Jurager\JsonApi\Concerns\Sortable;

class User extends Model
{
    use Sortable;

    protected array $sortable = [
        'name',
        'created_at',
    ];
}

Use the sorted() scope in your controller:

<?php

namespace App\Http\Controllers;

use App\Http\Resources\UserResource;
use App\Models\User;

class UserController
{
    public function index()
    {
        return UserResource::collection(
            User::sorted()->paginate()
        );
    }
}

Clients can now sort data using the sort query parameter:

GET /users?sort=name
GET /users?sort=-created_at
GET /users?sort=name,-created_at
  • Ascending sort: ?sort=name
  • Descending sort (with - prefix): ?sort=-name
  • Multiple sorts (comma-separated): ?sort=name,-created_at

Only sorts declared in the $sortable property will be applied. Any unknown sorts are ignored (whitelist approach).

allowedSorts()

For more control over sorting, override the allowedSorts() method in your model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Jurager\JsonApi\Concerns\Sortable;
use Jurager\JsonApi\Sort;

class User extends Model
{
    use Sortable;

    public function allowedSorts(): array
    {
        return [
            'name' => Sort::field(),
            'email' => Sort::field()->column('email_address'),
            'popularity' => Sort::scope(),
            'relevance' => Sort::callback(function ($query, $direction) {
                $query->orderByRaw('MATCH(name, bio) AGAINST(?) ' . $direction, [request('q')]);
            }),
        ];
    }
}

Sort types

The Sort class provides several sort types:

use Jurager\JsonApi\Sort;

// Simple field sort (ORDER BY column direction)
Sort::field()

// Model scope (calls scopeName on the model)
Sort::scope()

// Custom callback
Sort::callback(function ($query, $direction, $field) {
    $query->orderByRaw('LENGTH(name) ' . $direction);
})

Sort modifiers

Sorts can be customized with modifiers:

// Map sort name to a different column
Sort::field()->column('user_name')

Combining filtering and sorting

Both traits can be used together:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Jurager\JsonApi\Concerns\Filterable;
use Jurager\JsonApi\Concerns\Sortable;

class User extends Model
{
    use Filterable, Sortable;

    protected array $filterable = ['status', 'role'];
    protected array $sortable = ['name', 'created_at'];
}

In your controller:

User::filtered()->sorted()->paginate()

Example request:

GET /users?filter[status]=active&sort=-created_at

Resource Identification

https://jsonapi.org/format/#document-resource-object-identification

We have defined a sensible default for you so you can hit the ground running without having to fiddle with the small stuff.

The "id" and "type" of a resource is automatically resolved for you under-the-hood if you are using resources solely with Eloquent models.

"id" is resolved by calling the $model->getKey() method and the "type" is resolved by using a camel case of the model's table name, e.g. blog_posts becomes blogPosts.

You can customise how this works to support other types of objects and behaviours, but that will follow in the advanced usage section.

Nice. Well that was easy, so let's move onto...

Resource Links

https://jsonapi.org/format/#document-resource-object-links

To provide links for a resource, you can implement the toLinks($request) method...

<?php

use Jurager\JsonApi\Link;

class UserResource extends JsonApiResource
{
    public function toLinks($request): array
    {
        return [
            Link::self(route('users.show', $this->resource)),
            'related' => 'https://example.com/related'
        ];
    }
}

Resource Meta

https://jsonapi.org/format/#document-meta

To provide meta information for a resource, you can implement the toMeta($request) method...

<?php

class UserResource extends JsonApiResource
{
    public function toMeta($request): array
    {
        return [
            'resourceDeprecated' => true,
        ];
    }
}

Error Objects

https://jsonapi.org/format/#error-objects

The package provides a JsonApiError class for returning error responses that comply with the JSON:API specification. Error objects can be returned directly from controllers, just like resources.

Basic usage

<?php

use Jurager\JsonApi\JsonApiError;

class UserController
{
    public function show($id)
    {
        $user = User::find($id);

        if (!$user) {
            return JsonApiError::make('Not Found', 'User with the specified ID was not found')
                ->withStatus('404')
                ->withCode('USER_NOT_FOUND');
        }

        return UserResource::make($user);
    }
}
Example response
{
  "errors": [
    {
      "status": "404",
      "code": "USER_NOT_FOUND",
      "title": "Not Found",
      "detail": "User with the specified ID was not found"
    }
  ]
}

Error properties

The JsonApiError class supports all properties defined by the JSON:API specification:

JsonApiError::make('Validation Error', 'The email field is required')
    ->withId('550e8400-e29b-41d4-a716-446655440000')  // Unique error identifier
    ->withStatus('422')                               // HTTP status code
    ->withCode('VALIDATION_ERROR')                    // Application-specific error code
    ->withPointer('/data/attributes/email')           // JSON Pointer to the source
    ->withParameter('filter[status]')                 // Query parameter that caused the error
    ->withHeader('Authorization')                     // Header that caused the error
    ->withMeta(['timestamp' => now()->toIso8601String()])
    ->withLinks([Link::related('https://docs.example.com/errors/validation')]);

Multiple errors

You can return multiple errors at once using the collection() method. It accepts both arrays and JsonApiError objects.

From arrays
return JsonApiError::collection([
    ['title' => 'Validation Error', 'detail' => 'The email field is required', 'pointer' => '/data/attributes/email'],
    ['title' => 'Validation Error', 'detail' => 'The name must be at least 3 characters', 'pointer' => '/data/attributes/name'],
])->withStatus(422);
From objects
return JsonApiError::collection([
    JsonApiError::make('Validation Error', 'The email field is required')
        ->withPointer('/data/attributes/email'),
    JsonApiError::make('Validation Error', 'The name must be at least 3 characters')
        ->withPointer('/data/attributes/name'),
])->withStatus(422);
Mixed format
return JsonApiError::collection([
    ['title' => 'Validation Error', 'detail' => 'The email is required', 'pointer' => '/data/attributes/email'],
    JsonApiError::make('Custom Error', 'Something went wrong')->withCode('CUSTOM_CODE'),
])->withStatus(422);
Example response
{
  "errors": [
    {
      "status": "422",
      "title": "Validation Error",
      "detail": "The email field is required",
      "source": {
        "pointer": "/data/attributes/email"
      }
    },
    {
      "status": "422",
      "title": "Validation Error",
      "detail": "The name must be at least 3 characters",
      "source": {
        "pointer": "/data/attributes/name"
      }
    }
  ]
}

From exceptions

The JsonApiError class can automatically convert exceptions to JSON:API error responses.

use Jurager\JsonApi\JsonApiError;

try {
    // Some operation that may throw
} catch (Throwable $e) {
    return JsonApiError::fromException($e);
}

For ValidationException, errors are automatically mapped to the appropriate source pointers:

// This is handled automatically when using fromException()
return JsonApiError::fromValidationException($validationException);

Exception Handler integration

You can integrate JsonApiError with Laravel's exception handler to automatically convert all exceptions to JSON:API format.

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (Throwable $e, Request $request) {
        if ($request->wantsJson()) {
            return \Jurager\JsonApi\JsonApiError::fromException($e);
        }

        return null;
    });
})

Advanced usage

Resource Identification

Customising the resource "id"

You can customise the resolution of the id by specifying an id resolver in your service provider.

<?php

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        JsonApiResource::resolveIdUsing(function (mixed $resource, Request $request): string {
            // your custom resolution logic...
        });
    }
}

Although it is not recommended, you can also override the toId(Request $request): string method on a resource by resource basis.

Customising the resource "type"

You can customise the resolution of the type by specifying a type resolver in your service provider.

<?php

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        JsonApiResource::resolveTypeUsing(function (mixed $resource, Request $request): string {
            // your custom resolution logic...
        });
    }
}

Although it is not recommended, you can also override the toType(Request $request): string method on a resource by resource basis.

Resource Relationships

https://jsonapi.org/format/#fetching-includes

Relationships can be resolved deeply and also multiple relationship paths can be included. Of course you should be careful about n+1 issues, which is why we recommend using this package in conjunction with Spatie's Query Builder.

# Including deeply nested relationships
/api/posts/8?include=author.comments

# Including multiple relationship paths
/api/posts/8?include=comments,author.comments
  • Using "whenLoaded is an anti-pattern"