Attributes
Attribute Fields
Fields are used to describe the attributes of an Eloquent resource. To add an attribute to a schema, we can simply add it to the schema's fields
method.
To create an attribute, we use the static make
method, providing the JSON:API field name as the first argument. For example, if we had a tags
resource that had createdAt
, updatedAt
and name
attributes:
use LaravelJsonApi\Eloquent\Fields\DateTime;
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Str;
/**
* @inheritDoc
*/
public function fields(): array
{
return [
ID::make(),
DateTime::make('createdAt'),
Str::make('name'),
DateTime::make('updatedAt'),
];
}
Column Names
By default, the field will expect the column name of the attribute to be the snake case form of the JSON:API field name. So in the example above, the column names will be created_at
, updated_at
and name
.
To use a different column name than the default, provide the column name as the second argument to the make
method:
Str::make('name', 'display_name')
Attribute Hydration
On every create or update request, the attribute's corresponding model attribute will automatically be filled. The value will be the value from the request JSON.
Given the following request, the name
field will be filled with the string "laravel"
:
POST /api/v1/tags HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "tags",
"attributes": {
"name": "laravel"
}
}
}
If you want to perform any conversion on the value before it is filled, you can use the deserializeUsing
method:
Str::make('name')->deserializeUsing(
static fn($value) => strtoupper($value)
);
When filling the attribute on the model, we use the fill()
method to ensure your mass-assignment rules are respected. If you would like to ignore mass-assignment for a specific attribute, use the unguarded
method:
Str::make('name')->unguarded()
If you need complete control over how a value is hydrated into the model attribute, use the fillUsing
method:
Hash::make('coordinates')->fillUsing(
static function ($model, $column, $value, array $validatedData) {
$model->fill([
"{$column}_longitude" = $value['long'],
"{$column}_latitude" = $value['lat'],
]);
}
);
As shown in the example, the callback you pass to the fillUsing()
method receives four arguments:
- The model that is being filled.
- The model's column name, as set on the attribute field.
- The deserialized value (i.e. the value after any
deserializeUsing()
callback has been applied.) - All the validated data that is being filled. This allows you to calculate values based on multiple fields provided by the client, if needed.
Read-Only Fields
There are times when you may want to allow the client to only create or update certain attributes on the resource. Or you may want a field to always be read-only.
To ensure an attribute never gets filled, use the readOnly
method, which will prevent the field from being filled:
Str::make('name')->readOnly()
To make a field read only in certain circumstances, pass a closure to the readOnly
method. It will receive the current request as the first argument:
Str::make('name')->readOnly(
static fn($request) => !$request->user()->isAdmin()
)
If you only want to set the attribute to read only when creating or updating resources, you may use the readOnlyOnCreate
or readOnlyOnUpdate
methods:
Str::make('name')->readOnlyOnCreate()
Str::make('name')->readOnlyOnUpdate()
Nullable Fields
It is not necessary to mark fields as nullable. This is because we only fill validated data into the model. If a field does not support a null
value, you should ensure that the value is rejected when it is validated.
Attribute Serialization
Schemas are used to convert models to JSON:API resource objects. Each attribute field you define will use the value returned by the model as the value that appears in the serialized JSON.
If you want to perform any conversion on the value before it appears in the JSON, you can use the serializeUsing
method:
Str::make('name')->serializeUsing(
static fn($value) => strtolower($value)
);
If you need complete control over how a value is extracted from a model, use the extractUsing()
method:
Str::make('name')->extractUsing(
static fn($model, $column, $value) => $model->getSomeCustomImplementation()
);
As shown in the example, the callback you pass to the extractUsing()
method receives three arguments:
- The model from which the value is to be extracted.
- The column name, as set on the attribute field.
- The serialized value (i.e. the value after any
serializeUsing()
callback has been applied).
TIP
While the extractUsing()
method gives you complete control over the extraction of a specific attribute, it is inefficient to use for lots of attributes. If you need complete control over the serialization of a model to a JSON:API resource object, we recommend using a Resource
class.
When a model has a resource class, the schema attributes will NOT be used when serializing the model. Instead, you will have complete control over serialization within the resource class itself.
Hiding Fields
When serializing attributes to JSON, you may want to omit a field from the JSON. To do this, use the hidden
method:
Str::make('password')->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:
Str::make('secret')->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 attributes should appear in the resource's JSON, you should use our resource classes which give you complete control over the serialization. This includes supporting conditional attributes.
Sparse Fields
By default, all attribute field types are allowed as sparse fields. If you do not want to allow an attribute to be a sparse field, you should use the notSparseField
method:
Str::make('name')->notSparseField()
Attribute Types
Laravel JSON:API ships with a variety of attribute types that match the values that can be received in a decoded JSON document:
We also support the following additional attribute types:
- DateTime: parses strings containing ISO-8601 date times.
- Map: maps an associative array to multiple database columns.
Array Fields
The ArrayList
and ArrayHash
fields may be used to represent an attribute that is a PHP array. Typically your database column will be a JSON column in which you store the array.
You must use ArrayList
when the value in JSON is a zero-indexed array. For example, ["a", "b", "c"]
or an empty value of []
.
You must use ArrayHash
when the value is a JSON object - i.e. a PHP associative array. For example {"foo": "bar"}
or null
for an empty value.
Array Lists
Assume your database has a JSON column called permissions
, in which you store a list of permission values. You may attach an ArrayList
field to your schema like so:
use LaravelJsonApi\Eloquent\Fields\ArrayList;
ArrayList::make('permissions')
If you want the array values to always be in a sorted order, use the sorted()
method:
ArrayList::make('permissions')->sorted()
Associative Arrays
Assume your database has a JSON column called options
, in which you store an associative array of option values. You may attach an ArrayHash
field to your schema like so:
use LaravelJsonApi\Eloquent\Fields\ArrayHash;
ArrayHash::make('options')
If you want the array to always be sorted by its keys, use the sortKeys()
method:
ArrayHash::make('options')->sortKeys()
Alternatively, if you want the array to always be sorted by its values, use the sorted()
method:
ArrayHash::make('options')->sorted()
When working with associative arrays, you may find you need to convert the case of the keys. For example, if your JSON:API resource object uses camel-case keys, but you prefer to store associative arrays using the Eloquent convention of snake-case keys. In this scenario we would use the camelizeFields()
and snakeKeys()
methods to indicate the JSON:API field case and the database key case respectively:
ArrayHash::make('options')
->camelizeFields()
->snakeKeys()
We support the following conversions, with *Fields
methods indicating the JSON:API field case, and *Keys
indicating the model case:
camelizeFields()
andcamelizeKeys()
: convert to camel-case.dasherizeFields()
anddasherizeKeys()
: convert to dash-case.snakeFields()
orsnakeKeys()
: convert to snake-case (underscored).underscoreFields()
andunderscoreKeys()
: convert to underscored (snake).
TIP
If the above key conversions do not fit your use-case, you can use the deserializeUsing
method to fully customise how the input value is deserialized.
Boolean Field
The Boolean
field may be used to represent a field that is a boolean in the JSON - and typically a "tiny integer" column in your database. For example, assuming your database has a boolean column named active
, you may attach a Boolean
field to your schema like so:
use LaravelJsonApi\Eloquent\Fields\Boolean;
Boolean::make('active')
TIP
When validating a boolean field, we recommend using our boolean
rule instead of Laravel's boolean
rule. This is because the Laravel rule is loosely-typed, i.e. it will accept string values as booleans. However, our boolean
rule ensures the JSON value is a real boolean. See the Validating Booleans section for more information.
DateTime Field
The DateTime
field may be used to represent an attribute that is a string in JSON but represents a date time value. You would normally validate these as ISO-8601 strings. For example, assuming your database has a timestamp column named published_at
, you may attach a DateTime
field to your schema like so:
use LaravelJsonApi\Eloquent\Fields\DateTime;
DateTime::make('publishedAt');
As ISO-8601 strings can denote a time zone, our DateTime
field allows you to specify whether the client-specified time zone should be retained, or whether you need to convert the date time value to a particular time zone for storage in your database.
By default the date time will be converted to the server-side time zone configured in your app.timezone
setting. If you need to store the value as a different time zone, use the useTimezone
method:
DateTime::make('publishedAt')->useTimezone('Europe/London');
If you want to retain the time zone as provided by the client, use the retainTimezone
method:
DateTime::make('publishedAt')->retainTimezone();
Map Field
The Map
field may be used to map the values of an associative array to different columns in your database.
For example, assuming you have a resource that has an options
attribute that can contain the keys foo
and bar
. If the database stores these into the columns option_foo
and option_bar
, we can use a Map
attribute as follows:
use LaravelJsonApi\Eloquent\Fields\Map;
use LaravelJsonApi\Eloquent\Fields\Number;
use LaravelJsonApi\Eloquent\Fields\Str;
Map::make('options', [
Str::make('foo', 'option_foo'),
Number::make('bar', 'option_bar'),
])
As you can see from the example, the Map
attribute takes an array of attributes, constructed using the make
methods of the relevant classes. Each of these sub-attributes defines the name of the expected key in the associative array, and the column the value should be mapped to. In our example, the options.foo
value will be mapped to the option_foo
column in our database.
When mapping values, if any keys are missing the column will retain its existing values. For example, if the options
array only had a foo
key, then the option_foo
column will be updated but the option_bar
column will retain its existing value.
WARNING
The Map
attribute does not support the deserializeUsing
or fillUsing
methods, because it delegates filling values to its sub-attributes.
Null Values
By default, if the value being mapped is null
instead of an array, all columns will be set to null
. So in our example, if options
is null
, then both the option_foo
and option_bar
columns will be set to null
.
If you do not want this default null
behaviour, you have two options. Firstly, because models are only filled with validated values, you can use validation rules to reject options
if it is null
. Otherwise, you can using the ignoreNull
method on the Map
attribute:
Map::make('options', [
Str::make('foo', 'option_foo'),
Number::make('bar', 'option_bar'),
])->ignoreNull()
Number Field
The Number
field may be used to represent an attribute that is an integer or float in JSON. For example, assuming your database has an integer column named failure_count
, you may attach a Number
field to your schema like so:
use LaravelJsonApi\Eloquent\Fields\Number;
Number::make('failures', 'failure_count')
By default our Number
field only accepts integers or floats. This is because JSON has a number type, which decodes to a PHP integer or float. However, there is a gotcha with this. Laravel's integer
and numeric
validation rules are loosely-typed: i.e. they will accept a numeric string.
We therefore recommend using our number
or integer
validation rules, as described in the Validating Numbers section.
If however you do want to accept numeric strings, call the acceptStrings()
method on the Number
field:
use LaravelJsonApi\Eloquent\Fields\Number;
Number::make('failures')->acceptStrings();
String Field
The Str
(string) field may be used to represent an attribute that is a string in JSON. For example, assuming your database has a string column named display_name
, you may attach a Str
field to your schema like so:
use LaravelJsonApi\Eloquent\Fields\Str;
Str::make('displayName');
Attributes from Related Models
Sometimes you may want to add attributes to your resource that are derived from a related model, instead of using a resource relationship.
For example, imagine a scenario where our User
model has-one UserProfile
model. We could choose either to add a profile
relationship to our users
resource, or we may want to add column values from the UserProfile
to our users
resource.
If we opt to add UserProfile
column values to our users
resource, we will need to tell our attribute field that the value is derived from a related model. To do this, we use the on()
method. For example, our users
resource fields could look like this:
public function fields(): array
{
return [
ID::make(),
DateTime::make('createdAt')->readOnly(),
Str::make('description')->on('profile'),
Str::make('email'),
Str::make('image')->on('profile'),
Str::make('name'),
DateTime::make('updatedAt')->readOnly(),
];
}
In this example, the createdAt
, email
, name
and updatedAt
attributes are from the User
model. However, the description
and image
attributes are derived from the UserProfile
model returned by the User
model's profile
relationship.
TIP
When using attributes from related models, we automatically take care of eager loading the related models to prevent N+1 loading problems.
Related Column Names
When using the on()
method, the field's column name must match the column name on the related model. So in this example:
Str::make('profile', 'description')->on('profile')
The JSON:API field profile
will be derived from the UserProfile
model's description
column.
Related Attribute Hydration
If the attribute from a related model is fillable, you must ensure that the withDefault()
method is called on the Eloquent model's relationship. For example, our User
model's profile
relationship must look like this:
class User extends Model
{
public function profile()
{
return $this
->hasOne(UserProfile::class)
->withDefault();
}
}
This is required so that when attribute values are being filled, the related model (UserProfile
in this example) can be created if it does not already exist.
TIP
Refer to the Laravel documentation for more details on the withDefault()
method.
Related Map Fields
You can use the Map
field to nest values from a related model within your resource's attributes. For example, if we wanted to nest the columns derived from our UserProfile
model our fields would look like this:
public function fields(): array
{
return [
ID::make(),
DateTime::make('createdAt')->readOnly(),
Str::make('email'),
Str::make('name'),
Map::make('profile', [
Str::make('description'),
Str::make('image'),
])->on('profile'),
DateTime::make('updatedAt')->readOnly(),
];
}
This would result in the following attributes in our JSON:API resource:
{
"createdAt": "2021-04-16T12:00:00.000000Z",
"email": "john.doe@example.com",
"name": "John Doe",
"profile": {
"description": "...",
"image": "http://localhost/public/images/john-doe.jpg"
},
"updatedAt": "2021-04-16T12:00:00.000000Z",
}
By calling the on()
method on the Map
field, we tell the field that all its child fields exist on the UserProfile
model. This means that we do not need to call the on()
method on each child field.
If you wanted to use a Map
field that contains a mixture of attributes from the User
model and UserProfile
model, do not call the on()
method on the Map
field. Instead, call the on()
method on the child fields that are derived from a related model. For example:
Map::make('profile', [
Str::make('description')->on('profile'),
Str::make('synopsis'),
Str::make('image')->on('profile'),
])
In this example, the description
and image
are derived from the profile
relationship, while the synopsis
column exists on the primary model (the User
in our example).