# Relationships
# Relationship Fields
In addition to the variety of attributes we've already discussed, Laravel
JSON:API has full support for all of Laravel's Eloquent relationships. To
add a relationship to a schema, we can simply add it to the schema's
fields
method.
Our relationship fields take care of querying and hydrating relationships.
To create a relationship, we use the static make
method, providing the
JSON:API field name as the first argument. For example, if our Post
schema had author
and tags
relationships:
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsTo;
use LaravelJsonApi\Eloquent\Fields\Relations\HasMany;
/**
* @inheritDoc
*/
public function fields(): array
{
return [
ID::make(),
// ...attribute fields
BelongsTo::make('user'),
HasMany::make('tags'),
];
}
# Model Method Names
By default, the field will expect the method for the relationship on the
Eloquent model to be the camel-case form of the JSON:API field name.
For example, for a relationship field name of blog-author
, the
expected method on the Eloquent model would be blogAuthor
.
To use a different method name than the default, provide the method name
as the second argument to the make
method:
BelongsTo::make('author', 'blogAuthor');
# URI Name
By default we dasherize the JSON:API field name when it appears in
relationship URLs. For example, if the field name was
blogAuthor
, the links would be:
{
"links": {
"self": "http://localhost/api/v1/posts/123/relationships/blog-author",
"related": "http://localhost/api/v1/posts/123/blog-author"
}
}
If you wanted to keep blogAuthor
as-is, use the retainFieldName
method:
BelongsTo::make('blogAuthor')->retainFieldName()
Otherwise, if you want to use a different convention, provide the URI
fragment to the withUriFieldName
method:
BelongsTo::make('blogAuthor')->withUriFieldName('blog_author')
# Inverse Type
Every relationship must have an inverse resource type. This is the JSON:API resource type that is returned by the relationship.
For relationships that return zero-to-one related resources (known as to-one relations), we assume the inverse type is the dasherized and pluralized form of the Eloquent method name. For example:
BelongsTo::make('blogPost') // assumed 'blog-posts'
BelongsTo::make('author', 'user') // assumed 'users'
For relationships that return zero-to-many related resources (known as to-many relations), we assume the inverse type is the dasherized form of the Eloquent method name. For example:
HasMany::make('postTags') // assumed 'post-tags'
HasMany::make('tags', 'postTags') // assumed 'post-tags'
In either case, to use a different inverse resource type, just call the
type()
method. For example if our Post
schema has an author
relationship
field, but the inverse type was users
:
BelongsTo::make('author')->type('users')
# Eager Loading
By default, all relationship field types are allowed as include paths (opens new window). This is the equivalent of Eloquent's eager loading capability.
If you do not want to allow a relationship field to be an include path,
use the cannotEagerLoad
method:
BelongsTo::make('author')->cannotEagerLoad()
For more detail, see the Eager Loading chapter.
# Sparse Fields
By default, all relationship field types are allowed as
sparse fields (opens new window).
If you do not want to allow an relationship to be a sparse field, you should
use the notSparseField
method:
BelongsTo::make('author')->notSparseField()
# Read-Only Fields
The majority of our relationship fields are writeable, with the exception
of the HasOneThrough
and HasManyThrough
relationships which are always
read-only.
For the other relationship fields, there are times when you may want to allow
the client to only create or update certain relationships on the resource.
You can do this by using the readOnly
method, which will prevent the field
from being filled
BelongsTo::make('author')->readOnly()
If you need a relationships to only be filled in certain circumstances,
pass a closure to the readOnly
method. It will receive the current
request as the first argument:
BelongsTo::make('author')->readOnly(
static fn($request) => !$request->user()->isAdmin()
)
If you only want to set the relationship to read only when creating or
updating resources, you may use the readOnlyOnCreate
or
readOnlyOnUpdate
methods:
BelongsTo::make('author')->readOnlyOnCreate()
BelongsTo::make('author')->readOnlyOnUpdate()
DANGER
The read-only methods on relationships only affect the relationship when
it is being filled as a result of a POST
or PATCH
request for a
resource type. For example, the author
field on a posts
resource
will have its read-only state checked when the client has submitted a request
to create or update a posts
resource, i.e. a request to
POST /api/v1/posts
or PATCH /api/v1/posts/123
The read-only methods do not affect changing relationships via relationship
end-points: e.g. PATCH /api/v1/posts/123/relationships/author
. There is
no need for us to check the read-only status in this circumstance because
this relationship route should not be registered with the Laravel router
if the relationship is read-only.
# Countable Fields
Sometimes a client may want to count the number of related resources for a given relationship without actually loading the related resources. Our to-many fields allow this using our Countable Relationships feature.
# Relationship Serialization
Schemas are used to convert models to JSON:API resource objects.
Each relationship you define will appear in the resource JSON.
Each relationship is serialized as a
JSON:API relationship object (opens new window).
These relationship objects must contain at least one of links
,
data
or meta
.
By default, relationship objects are serialized with the links
member, and will include the self
and related
links. The data
member will only be included if the relationship data has been
requested by the client using an
Include Path. (opens new window)
To customise the relation serialization, use the serializeUsing
method. This receives the JSON:API relation as its first argument:
BelongsTo::make('author')->serializeUsing(
static fn($relation) => $relation->withoutSelfLink()
)
# Removing Links
If you do not want the relationship object to have a self
link,
use the withoutSelfLink
method:
BelongsTo::make('author')->serializeUsing(
static fn($relation) => $relation->withoutSelfLink()
)
Likewise, if you do not want the relationship object to have a related
link, use the withoutRelatedLink
method:
BelongsTo::make('author')->serializeUsing(
static fn($relation) => $relation->withoutRelatedLink()
)
If you want to remove both the self
and related
links, you can use the
withoutLinks
method:
BelongsTo::make('author')->serializeUsing(
static fn($relation) => $relation->withoutLinks()
)
# Showing Data
By default we only show the data
member of the relationship if the
client has requested it using an include path. This is a sensible
default because the whole point of the JSON:API specification is to
give the client complete control over what it wants include in the
response JSON. If the server made decisions over what is included,
then it will increase the size of the response payload when
the client may have no intent to use the extra JSON content.
If you must override this, we provide two methods on the relation.
Firstly, the showDataIfLoaded
method will include the relationship
data
member if the relation is loaded on the model:
BelongsTo::make('author')->serializeUsing(
static fn($relation) => $relation->showDataIfLoaded()
)
Secondly we provide the alwaysShowData
member to force the data
member to be shown. If you are using the alwaysShowData
method,
you will need to consider adding default eager load paths to your
schema to avoid "N+1" database query problems.
BelongsTo::make('author')->serializeUsing(
static fn($relation) => $relation->alwaysShowData()
)
WARNING
If you use the alwaysShowData
method, you must ensure that the
relationship is always eager loaded. See the
Eager Loading chapter for details.
# Hiding Fields
When serializing relationships to JSON, you may want to omit a field from
the JSON. To do this, use the hidden
method:
BelongsTo::make('createdBy')->hidden()
To only hide the field in certain circumstances, provide a closure to
the hidden
method. It will receive the current request as the first
argument:
BelongsTo::make('createdBy')->hidden(
static fn($request) => !$request->user()->isAdmin()
)
Note that if you use JSON:API resources outside of HTTP requests -
for example, queued broadcasting - then your closure should handle
the $request
parameter being null
.
TIP
If you have complex logic for determining what relationships should appear in the resource's JSON, you should use our resource classes which give you complete control over the serialization.
# Relationship Types
Laravel JSON:API ships with field types that work with all of the Eloquent relationships. The types are:
JSON:API Type | Eloquent Type(s) | Writeable |
---|---|---|
Has One | hasOne , morphOne | Yes |
Has Many | hasMany , morphMany | Yes |
Has One Through | hasOneThrough | No |
Has Many Through | hasManyThrough | No |
Belongs To | belongsTo | Yes |
Belongs To Many | belongsToMany , morphToMany , morphedByMany | Yes |
Morph To | morphTo | Yes |
Morph to Many | None | Yes |
# Has One
The HasOne
field corresponds to either a hasOne
or morphOne
Eloquent
relationship. For example, let's assume a User
model hasOne
Address
model. We may add the relationship to our User
schema like so:
use LaravelJsonApi\Eloquent\Fields\Relations\HasOne;
HasOne::make('address')
As described above, this will assume the method name on the model is address
.
If this is not the case, the method name can be provided as the second
argument:
HasOne::make('address', 'postalAddress')
The inverse resource type is assumed to be the pluralized and dasherized form
of the Eloquent method name. If this is not the case, use the type
method:
HasOne::make('address') // assumed 'addresses'
HasOne::make('address')->type('user-addresses')
# Detaching Has-One Models
When modifying this relationship, you will need to consider how you want to
handle a model being detached from the relationship. For example, if a User
model already had one Address
and the client changes the relationship to
null
or a different Address
, how should the existing Address
model be
detached?
The default detach behaviour is to set the relationship columns on the Address
model to null
. I.e. the Address
model is retained and detached from the
User
model.
In some cases, you might actually want the Address
model to be deleted if
it is detached from the relationship. You can configure this behaviour
using the deleteDetachedModel()
or forceDeleteDetachedModel()
methods
on the HasOne
field. For example:
HasOne::make('address')->deleteDetachedModel()
// or
HasOne::make('address')->forceDeleteDetachedModel()
# Has Many
The HasMany
field corresponds to a hasMany
or a morphMany
Eloquent
relationship. For example, let's assume that our Video
model hasMany
Comment
models. We may add the relationship to our Video
schema like so:
use LaravelJsonApi\Eloquent\Fields\Relations\HasMany;
HasMany::make('comments')
As described above, this will assume the method name on the model is comments
.
If this is not the case, the method name can be provided as the second
argument:
HasMany::make('comments', 'userComments')
The inverse resource type is assumed to be the dasherized form of the Eloquent
method name. If this is not the case, use the type
method:
HasMany::make('comments') // assumed 'comments'
HasMany::make('comments')->type('video-comments')
# Detaching Has-Many Models
When modifying this relationship, you will need to consider how you want to
handle models that are detached from the relationship. For example, when
updating the Video
model's comments
relationship, Comment
models may be
detached (removed).
The default detach behaviour is to set the relationship columns on the Comment
model to null
. I.e. each Comment
model removed from the relationship
is retained and detached from the Video
model.
In some cases, you might actually want Comment
models to be deleted when
they are detached from the relationship. You can configure this behaviour
using the deleteDetachedModels()
or forceDeleteDetachedModels()
methods
on the HasMany
field. For example:
HasMany::make('comments')->deleteDetachedModels()
// or
HasMany::make('comments')->forceDeleteDetachedModels()
# Has One Through
The HasOneThrough
field corresponds to a hasOneThrough
Eloquent
relationship. For example, let's assume a Mechanic
model has one Car
,
and each Car
may have one Owner
. While the Mechanic
and the Owner
has no direct connection, the Mechanic
can access the Owner
through
the Car
itself.
You can add this relationship to your Mechanic
schema as follows:
use LaravelJsonApi\Eloquent\Fields\Relations\HasOneThrough;
HasOneThrough::make('owner')
As described above, this will assume the method name on the model is owner
.
If this is not the case, the method name can be provided as the second
argument:
HasOneThrough::make('owner', 'carOwner');
The inverse resource type is assumed to be the pluralized and dasherized form
of the Eloquent method name. If this is not the case, use the type
method:
HasOneThrough::make('owner') // assumed 'owners'
HasOneThrough::make('owner')->type('car-owners')
WARNING
The HasOneThrough
field cannot be hydrated (filled). This is because
the intermediary resource should be created or updated instead.
Using our example, a JSON:API client can create or update a cars
resource,
and its related owner and mechanic. That would change the mechanics
has-one-through owner
relation.
# Has Many Through
The HasManyThrough
field corresponds to a hasManyThrough
Eloquent
relationship. For example, a Country
model might have many Post
models
through an intermediate User
model. In this example, you could easily
gather all blog posts for a given country.
You can add this relationship to your Country
schema as follows:
use LaravelJsonApi\Eloquent\Fields\Relations\HasManyThrough;
HasManyThrough::make('posts')
As described above, this will assume the method name on the model is posts
.
If this is not the case, the method name can be provided as the second
argument:
HasManyThrough::make('posts', 'blogPosts')
The inverse resource type is assumed to be the dasherized form of the Eloquent
method name. If this is not the case, use the type
method:
HasManyThrough::make('posts') // assumed 'posts'
HasManyThrough::make('posts')->type('blog-posts')
WARNING
The HasManyThrough
field cannot be hydrated (filled). This is because
the intermediary resource should be created or updated instead.
Using our example, the contents of the Country
's posts
relationship is
generated by the User
models that are linked to Post
models. Therefore a
JSON:API client would create or update a posts
resource, and/or change the
country linked to a users
resource, to amend the country's posts
relationship.
# Belongs To
The BelongsTo
field corresponds to a belongsTo
Eloquent relationship.
For example. let's assume a Post
model belongsTo
a User
model. We
may add the relationship to our Post
schema like so:
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsTo;
BelongsTo::make('user')
As described above, this will assume the method name on the model is user
.
If this is not the case, the method name can be provided as the second
argument:
BelongsTo::make('author', 'user')
The inverse resource type is assumed to be the pluralized and dasherized form
of the Eloquent method name. If this is not the case, use the type
method:
BelongsTo::make('author', 'user') // assumed 'users'
BelongsTo::make('author')->type('users')
# Belongs To Many
The BelongsToMany
field corresponds to a belongsToMany
, morphToMany
or morphedByMany
Eloquent relationship. For example, let's assume a
User
model belongsToMany
Role
models. We may add the relationship to
our User
schema like so:
use LaravelJsonApi\Eloquent\Fields\Relations\BelongsToMany;
BelongsToMany::make('roles');
As described above, this will assume the method name on the model is roles
.
If this is not the case, the method name can be provided as the second
argument:
BelongsToMany::make('roles', 'accessRoles')
The inverse resource type is assumed to be the dasherized form of the Eloquent
method name. If this is not the case, use the type
method:
BelongsToMany::make('roles') // assumed 'roles'
BelongsToMany::make('roles')->type('access-roles')
# Pivot Table
If your BelongsToMany
relationship interacts with additional pivot columns
that are stored on the intermediate table of the relationship, you may also
attach these to the relationship.
TIP
The JSON:API specification does not allow attributes to be sent with relationship identifiers. It is therefore important to understand that the follow examples only allow us to add server-calculated values to a pivot table.
If you need the client to provide values for a pivot table, then you should
use a pivot model for the intermediary table, and add that model to your API
as its own resource type. So in the following example, we would have a
RoleUser
model for the intermediary role_user
table, and have a JSON:API
resource called role-users
.
For example, let's assume our User
model belongsToMany
Role
models.
On our role_user
intermediate table, let's imagine that we have an
approved
column that contains a flag as to whether an administrator
created the relationship between the User
and the Role
. We can
store this information by providing a callback to the fields
method
on the relationship:
BelongsToMany::make('roles')->fields(static fn($parent, $related) => [
'approved' => \Auth::user()->admin,
]);
TIP
The callback receives two arguments: the parent of the relationship, and
the models that are being attached. In this example, the parent would be
the User
model, and the related models would be Role
models.
To provide values that require no calculation, pass an array
to the fields
method:
BelongsToMany::make('roles')->fields(['approved' => false]);
Of course we could also add this pivot data on the inverse of the relationship.
So, if we define the BelongsToMany
field on the User
schema, we would
also define its inverse on the Role
schema:
BelongsToMany::make('users')->fields(static fn() => [
'approved' => \Auth::user()->admin,
]);
Since defining the field on both ends of the relationship can cause some
code duplication, we allow you to pass an invokable object to the fields
method:
BelongsToMany::make('roles')->fields(new ApprovedPivot())
In this example, the ApprovedPivot
could be a simple, invokable class that
returns the array of pivot fields:
namespace App\JsonApi\Pivots;
use Illuminate\Support\Facades\Auth;
use function boolval;
use function optional;
class ApprovedPivot
{
/**
* Get the pivot attributes.
*
* @param $parent
* @param $related
* @return array
*/
public function __invoke($parent, $related): array
{
return [
'approved' => boolval(optional(Auth::user())->admin),
];
}
}
# Morph To
The MorphTo
field corresponds to a morphTo
Eloquent relationship.
For example, let's assume a Comment
model has a polymorphic relationship
with both the Post
and Video
models. We may add the relationship to our
Comment
schema like so:
use LaravelJsonApi\Eloquent\Fields\Relations\MorphTo;
MorphTo::make('commentable')->types('posts', 'videos')
As you can see from the above example, the types
method is used to
instruct the MorphTo
field what its inverse JSON:API resource types are.
As described above, this will assume the method name on the model is
commentable
. If this is not the case, the method name can be provided as the
second argument:
MorphTo::make('commentable', 'items')
# Morph To Many
The JSON:API MorphToMany
field helps you construct polymorphic to-many
relationships. It's important to note that there is not an equivalent relationship
type in Eloquent. (The Eloquent morphToMany
relationship type is handled by
the BelongsToMany
JSON:API field.)
Adding polymorphic to-many relations involves a number of complexities and limitations. As such, there is a chapter covering exactly this topic, where you can find instructions on how to add this relationship to your schema.