# Attributes
# Introduction
The attributes
member on a resource object represents information about
the model that it is serializing. Typically this will be the values of
database columns, excluding those that represent relationships.
Attributes may contain any JSON value.
Complex data structures involving JSON objects and array are allowed as
attribute values. However, any object that constitutes or is contained
in an attribute must not contain a relationships
or links
member.
# Field Names
The members of a resource's attributes
and relationships
objects are
collectively called a resource object's fields. Fields share common
namespace with each other and the type
and id
members. In other words,
a resource cannot have an attribute or relationship with the same name, nor
can it have an attribute or relationship named type
or id
.
# Field Name Case
The JSON:API specification places hard restrictions (opens new window)
on what names you are allowed to use for the attributes
member names.
To summarize, you are allowed snake-case, camel-case and dash-case member name. However, the specification does recommend that camel-case is used. (opens new window)
# Defining Attributes
To add attributes to your resource object, list them in the attributes
method. For example, a posts
resource might look like this:
namespace App\JsonApi\V1\Posts;
use LaravelJsonApi\Core\Resources\JsonApiResource;
class PostResource extends JsonApiResource
{
/**
* Get the resource's attributes.
*
* @param \Illuminate\Http\Request|null $request
* @return iterable
*/
public function attributes($request): iterable
{
return [
'content' => $this->content,
'createdAt' => $this->created_at,
'slug' => $this->slug,
'synopsis' => $this->synopsis,
'title' => $this->title,
'updatedAt' => $this->updated_at,
];
}
// ...
}
Notice that we can access model properties from the $this
variable. This
is because a resource class will automatically proxy property and method
access down to the underlying model for convenient access.
# Conditional Attributes
Sometimes you may wish to only include an attribute in a resource response if a given condition is met. For example, you may wish to only include a value if the current user is an "administrator". Our API resource class provides a variety of helper methods to assist you in this situation.
The when
method may be used to conditionally add an attribute to a
resource response. In the following example, the secret
member will only
be serialized in the attributes
of a users
resource if the
authenticated user's isAdmin
method returns true
:
public function attributes($request): iterable
{
return [
'createdAt' => $this->created_at,
'email' => $this->email,
'name' => $this->name,
'secret' => $this->when($request->user()->isAdmin(), 'secret-value'),
'updatedAt' => $this->updated_at,
];
}
The when
method also accepts a Closure
as its second argument, allowing
you to only calculate the resulting value if the given condition is true
:
'secret' => $this->when($request->user()->isAdmin(), fn() => 'secret-value'),
# Merging Conditional Attributes
Sometimes you may have several attributes that should only be included
in the resource object based on the same condition. In this case,
you may use the mergeWhen
method to include the attributes
only when the given condition is true
:
public function attributes($request): iterable
{
return [
'createdAt' => $this->created_at,
'email' => $this->email,
'name' => $this->name,
$this->mergeWhen($request->user()->isAdmin(), [
'firstSecret' => 'value',
'secondSecret' => 'value',
]),
'updatedAt' => $this->updated_at,
];
}
Again, if the given condition is false
, these attributes will not appear
in the serialized JSON:API resource object.
The mergeWhen
method also allows values to be Closure
instances, allowing
you to only calculate values if they will be in the serialized resource
object:
$this->mergeWhen($request->user()->isAdmin(), [
'firstSecret' => fn() => 'value',
'secondSecret' => 'value',
]),
And finally, the mergeWhen
method also accepts a Closure
as its
second argument:
$this->mergeWhen($request->user()->isAdmin(), fn() => [
'firstSecret' => 'value',
'secondSecret' => 'value',
]),
# Array Attributes
In JSON there is a distinction between array lists (zero-indexed arrays), and JSON objects (PHP associative arrays). It is important for API clients that values are returned in a consistent format.
# Array Lists
Always ensure that array lists are serialized as zero-indexed arrays.
If you use Laravel's Collection
class to process arrays when serializing
them, it is important to remember that some methods, such as filter
or
reject
, may result in the array having non-sequential keys.
For example, let's say our users
resource has a permissions
attribute
that we want to be a JSON array of string permissions. Given the following:
'permissons' => collect($this->permissions)
->reject('bar')
->all(),
If the value of permissions was ['foo', 'bar', 'baz']
, the above example
would result in a JSON object: {"0":"foo","2":"baz"}
. This would be
unexpected for the client, that expects a zero-indexed JSON array.
To fix, we should call the values
method before all
:
'permissons' => collect($this->permissions)
->reject('bar')
->values()
->all(),
# Associative Arrays
When serializing PHP associative arrays, there are two things to consider: avoiding empty values and being consistent with the key case.
To illustrate both, let's say our users
resource has an options
attribute. In PHP this is an associative array stored in a JSON database
column.
# Empty Values
If the array stored in our database column is empty, then the following attribute:
'options' => $this->options,
Would actually be serialized as an empty JSON array, i.e. []
. This
would be an unexpected value for the client, which is expecting the options
attribute to be a JSON object.
To fix, we should cast the value to null
if it is empty:
'options' => $this->options ?: null,
# Key Case
When serializing associative arrays, you should also consider the key case that is used.
Say for example our users
resource follows the JSON:API recommendation of
camel-case member names. It would make sense for the options
JSON object
to also use camel-case keys. However, when storing the options
we might
be following the Eloquent convention of snake-casing key names.
In such a scenario we would need to transform the keys of the options
array when serializing it. Luckily we provide some array helpers to
recursively transform array keys. For example:
use LaravelJsonApi\Core\Support\Arr;
'options' => Arr::camelize($this->options) ?: null,
Our Arr
helper class has the following methods to recursively transform
keys:
camelize
: recursively camel-case all keys in the provided array.dasherize
: recursively dash-case all keys in the provided array.underscore
: recursively convert camel-case and dash-case keys to underscore (snake) case.
TIP
The underscore
method differs from Laravel's snake
case, in that it will
convert both camel-cased and dasherized strings to snake case with
a _
delimiter.
Our Arr
helper also forwards calls to the Illuminate\Support\Arr
class,
meaning you do not need to import both classes if you want to use methods
on both.