Resource Validation
Introduction
Our resource request classes allow you to validate the JSON:API document sent by the client against your application-specific validation rules. You can also define validation rules to determine whether a resource can be deleted.
For any resource type that you allow to be created and/or updated (including updating relationships), you will need to create a resource request class. To generate a resource request, use the jsonapi:request
Artisan command:
php artisan jsonapi:request posts --server=v1
This will generate the following request class: App\JsonApi\V1\Posts\PostRequest
TIP
The --server
option is not required if you only have one server.
Validation Approach
Resource objects are validated using Laravel form request validators. If any field fails the validation rules, a 422 Unprocessable Entity
response will be sent. The validator's error messages will be converted to JSON:API errors, with the rule failure message in the detail
member of the error object. Each error will also have a JSON source pointer set identifying the location in the request content of the validation failure.
TIP
Why are validation rules not defined on schema fields, like Nova resources?
This is a good question. Although at first it would seem like a good idea for our schema fields to implement Nova-style validation rules, we do not believe this is a good fit for validating JSON. Whereas Nova is tying its validation to HTML input fields, your JSON:API resource objects can contain complex structures, such as arrays and nested JSON objects.
This makes it far more complex to attach validation rules to specific fields, so we therefore opted for the simplicity of defining rules using Laravel's form request approach.
Validation Data
Creating Resources
Validators are provided with the resource fields that were submitted by the client. Collectively these are the type
, id
, attributes
and relationships
of the resource. To make it easier to write validation rules, we set the value of relationship fields to the data
member of the relationship.
This is best illustrated with an example. Given this request:
POST /api/v1/posts HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "posts",
"attributes": {
"content": "...",
"slug": "hello-world",
"title": "Hello World"
},
"relationships": {
"author": {
"data": {
"type": "users",
"id": "123"
}
},
"tags": {
"data": [
{
"type": "tags",
"id": "1"
},
{
"type": "tags",
"id": "3"
}
]
}
}
}
}
Your validator will be provided with the following array of data:
[
"author" => [
"type" => "users",
"id" => "123",
],
"content" => "...",
"id" => null,
"slug" => "hello-world",
"tags" => [
["type" => "tags", "id" => "1"],
["type" => "tags", "id" => "3"],
],
"title" => "Hello World",
"type" => "posts",
];
Updating Resources
When updating resources, the JSON:API specification says:
If a request does not include all of the attributes for a resource, the server MUST interpret the missing attributes as if they were included with their current values. The server MUST NOT interpret missing attributes as null values.
The same applies to relationships.
As Laravel provides validation rules that allow you to compare values that are being validated (e.g. a date that must be before
another value), we take the existing field values of your resource and merge the values provided by the client over the top.
For example, in the following PATCH
request the client has not provided values for the content
, slug
and author
fields:
PATCH /api/v1/posts/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "posts",
"id": "1",
"attributes": {
"title": "Hello World"
},
"relationships": {
"tags": {
"data": [
{
"type": "tags",
"id": "1"
}
]
}
}
}
}
To comply with the JSON:API specification we must assume that the missing fields are the current values stored on the resource. We therefore get the current resource values and then merge the client values on-top. In the above example, this means that the validator receives the following data:
[
"author" => [
"type" => "users",
"id" => "345",
],
"content" => "...",
"id" => "1",
"slug" => "hello-world",
"tags" => [
["type" => "tags", "id" => "1"],
],
"title" => "Hello World",
"type" => "posts",
];
When working out the current values, we only take the values of BelongsTo
and MorphTo
relations. This is because it would be extremely inefficient for us to read the value of every relation. For example, our posts
resource could have hundreds of comments
, which are not required for validation.
If you need the values of a relationship that is not included by default, use the mustValidate()
method on the field in your schema. For example, if we wanted the current value of tags
to be used:
class PostSchema extends Schema
{
// ...
public function fields(): array
{
return [
ID::make(),
// ...attribute fields
BelongsTo::make('author'),
HasMany::make('comments'),
HasMany::make('tags')->mustValidate(),
];
}
}
If you want to exclude a BelongsTo
or MorphTo
relation that we automatically include, use the notValidated()
method on the field in your schema. For example, if we wanted to exclude the author
relationship:
BelongsTo::make('author')->notValidated()
Customising Existing Values
If you want to adjust any current values before the client-provided values are merged, implement the withExisting()
method on your request class. This method receives the model and the JSON:API resource representation as its arguments.
class PostRequest extends ResourceRequest
{
// ...
/**
* Modify the existing resource before it is merged with client values.
*
* @param \App\Models\Post $model
* @return array|null
*/
protected function withExisting(Post $model, array $resource): ?array
{
unset($resource['attributes']['foobar']);
return $resource;
}
}
You must return the array that you want the validator to use. If you return null
, then the validator will assume you did not modify the resource and will use it as-is.
Modifying Relationships
The JSON:API specification provides relationship endpoints for modifying resource relations. To-one and to-many relationships can be replaced using a PATCH
request. For to-many relationships, resources can be attached via a POST
request or detached using a DELETE
request.
Given this request:
PATCH /api/v1/posts/123/relationships/tags HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": [
{
"type": "tags",
"id": "1"
},
{
"type": "tags",
"id": "6"
}
]
}
Your validator will be provided with the following array of data:
[
"type" => "posts",
"id" => "1",
"tags" => [
["type" => "tags", "id" => "1"],
["type" => "tags", "id" => "3"],
],
];
TIP
In this scenario, we filter the resource rules returned from your rules
method to only include rules that have a key starting with tags
.
Validation Rules
Defining Resource Rules
To validate this data, you define the validation rules in the rules
method of your resource request class. We use the validated data to fill models, so you must validate every attribute and relationship that you expect to be filled into your model.
For example, our PostRequest
rules might look like this:
namespace App\JsonApi\V1\Posts;
use App\Models\Post;
use Illuminate\Validation\Rule;
use LaravelJsonApi\Laravel\Http\Requests\ResourceRequest;
use LaravelJsonApi\Validation\Rule as JsonApiRule;
class PostRequest extends ResourceRequest
{
/**
* @return array
*/
public function rules(): array
{
return [
'author' => JsonApiRule::toOne(),
'content' => ['required', 'string'],
'slug' => ['required', 'string'],
'tags' => JsonApiRule::toMany(),
'title' => ['required', 'string'],
];
}
}
TIP
You may type-hint any dependencies you need within the rules method's signature. They will automatically be resolved via the Laravel service container.
Relationship Rules
In the example above, you'll notice that the exists
rule is not used in the validation for the author
or tags
relationships. This is because the package complies with the JSON:API spec and validates all resource identifiers in relationships to check that they exist. If any do not exist, the request will be rejected when the document is parsed for compliance with the JSON:API specification.
Instead for relationships all we need to do is provide the LaravelJsonApi\Validation\Rule::toOne()
and toMany()
rules. These use the schema for the request resource type to ensure that the relationships contain the correct type of resource.
Remember you must validate every relationship that you expect to be filled into your model.
Client-Generated IDs
If your resource accepts client-generated IDs you must add validation rules for the id
field, for example:
use LaravelJsonApi\Validation\Rule as JsonApiRule;
public function rules(): array
{
return [
'id' => ['required', JsonApiRule::clientId()],
// ... other rules
];
}
The client ID rule ensures that the ID provided matches the identifier pattern set in your schema. There is therefore no need to use Laravel's regex
validation rule.
If you always expect a client to provide an id
, use Laravel's required
rule as shown in the example above. If your server handles the client not providing an ID (for example, if you automatically generate a random UUID if one is not provided), then use the nullable
rule instead of required
.
TIP
As shown in the example, there is no need to use the exists
rule to check whether the client-generated ID already exists. This is because the JSON:API specification defines that servers should send a 409 Conflict
response when creating a resource with a client-generated ID that already exists. We therefore reject this scenario when checking if the document complies with the specification.
Accessing the Model
If you need to access the model when determining your validation rules, use the model
method. As your rules are used for both create and update requests, this method will return null
for a create request, and the model for an update request.
For example:
use Illuminate\Validation\Rule;
/**
* @return array
*/
public function rules(): array
{
$unique = Rule::unique('posts');
/** @var \App\Models\Post|null $post */
if ($post = $this->model()) {
$unique->ignore($post);
}
return [
'slug' => ['required', 'string', $unique],
// other rules...
];
}
Accessing Validation Data
If you need to access the validation data in your rules
method, call the validationData
method:
/**
* @return array
*/
public function rules(): array
{
$data = $this->validationData();
return [
// ...rules
];
}
Adding After Hooks
If you would like to add an "after" hook to a form request, you may use the withValidator
method. This method receives the fully constructed validator, allowing you to call any of its methods before the validation rules are actually evaluated:
/**
* Configure the validator instance.
*
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator($validator)
{
if ($this->isCreatingOrUpdating()) {
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add(
'field',
'Something is wrong with this field!'
);
}
});
}
}
WARNING
When adding after hooks, you will need to use the request's helper methods to determine what type of request it is. The example above checks whether the request is creating or updating a resource.
You need to do this because the withValidator
method will be called for all the different request types described in this chapter, i.e. creating/updating a resource, modifying a relationship and deleting a resource.
Complex Conditional Validation
The withValidator
method can also be used to add complex conditional validation, using the sometimes
method on the validator. For example:
/**
* Configure the validator instance.
*
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator($validator)
{
if ($this->isCreatingOrUpdating()) {
$validator->sometimes(['reason', 'cost'], 'required', function ($input) {
return $input->games >= 100;
});
}
}
WARNING
When adding conditional validation, you will need to use the request's helper methods to determine what type of request it is. The example above checks whether the request is creating or updating a resource before applying the conditional validation rules.
You need to do this because the withValidator
method will be called for all the different request types described in this chapter, i.e. creating/updating a resource, modifying a relationship and deleting a resource.
Validating Booleans
In JSON, a boolean is always true
or false
. However Laravel's boolean
validation rule is loosely typed - i.e. it will accept true
, false
, 0
, 1
, "0"
, and "1"
as valid values.
For JSON it is better to by strictly typed for boolean values. Use our boolean
rule to enforce only accepting true
and false
as valid values:
use LaravelJsonApi\Validation\Rule as JsonApiRule;
return [
'published' => JsonApiRule::boolean(),
];
Validating Dates
JSON:API recommends using the ISO 8601 format for date and time strings in JSON. This is not possible to validate using Laravel's date_format
validation rule, because W3C state that a number of date and time formats are valid. For example, all of the following are valid:
2018-01-01T12:00Z
2018-01-01T12:00:00Z
2018-01-01T12:00:00.123Z
2018-01-01T12:00:00.123456Z
2018-01-01T12:00+01:00
2018-01-01T12:00:00+01:00
2018-01-01T12:00:00.123+01:00
2018-01-01T12:00:00.123456+01:00
To accept any of the valid formats for a date field, this package provides a rule object. This can be used as follows:
use LaravelJsonApi\Validation\Rule as JsonApiRule;
return [
'publishedAt' => ['nullable', JsonApiRule::dateTime()]
];
Validating Numbers
Laravel's integer
and numeric
rules are loosely typed - that is, they will accept integers, floats and string representations of numbers. JSON however can be strictly typed - i.e. when JSON decoded we can expect a value to be a PHP integer or float.
If you have a field that can accept an integer or float, use our number
validation rule to ensure it is strictly typed:
use LaravelJsonApi\Validation\Rule as JsonApiRule;
return [
'published' => JsonApiRule::number(),
];
To only accept integers, use the integer
rule:
use LaravelJsonApi\Validation\Rule as JsonApiRule;
return [
'published' => JsonApiRule::integer(),
];
Required Rule
Using the required
rule can result in a JSON:API error object with a JSON pointer to either /data
or the actual field that is required, e.g. /data/attributes/content
. This will vary based on whether the client omits the field or sends an empty value for the field.
If you always want the pointer to relate to the actual field, e.g. /data/attributes/content
, ensure your client always sends a value for the field, even if that value is empty (e.g. null
).
To illustrate this, here are two requests that fail the required
rule and the resulting error response:
Field Omitted
POST /api/v1/posts HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "posts",
"attributes": {
"title": "Hello World"
}
}
}
In this scenario, a JSON pointer of /data/attributes/content
cannot be used as it would point at a field that does not exist in the request JSON. Instead, the /data
pointer indicates the error is caused by the resource object held in the top-level data
member:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"detail": "The content field is required.",
"source": {
"pointer": "/data"
},
"status": "422",
"title": "Unprocessable Entity"
}
],
"jsonapi": {
"version": "1.0"
}
}
Field Empty
POST /api/v1/posts HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "posts",
"attributes": {
"content": null,
"title": "Hello World"
}
}
}
In this scenario, the pointer can be /data/attributes/content
as the field actually exists in the request JSON:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "422",
"title": "Unprocessable Entity",
"detail": "The content field is required.",
"source": {
"pointer": "/data/attributes/content"
}
}
],
"jsonapi": {
"version": "1.0"
}
}
Confirmed Rule
Laravel's confirmed
rule expects there to be a field with the same name and _confirmation
on the end. For example, if using the confirmed
rule on the password
field, it expects there to be a password_confirmation
field.
If you are not using underscores in your field names, this means the confirmed
rule will not work. For example if using camel-case your extra field will be called passwordConfirmation
. Unfortunately Laravel does not provide a way of customising the expected confirmation field name.
In this scenario you will need to use the following rules to get passwordConfirmation
working:
public function rules(): array
{
return [
'name' => 'required|string',
'password' => "required|string",
'passwordConfirmation' => "required_with:password|same:password",
];
}
Remember to note the guidance above about update requests, where the server must assume that missing values have the current value. For password scenarios, your validator will not have access to the current value.
You would therefore need to adjust your use of the required
and required_with
rules to only add them if the client has sent a password. For example:
public function rules(): array
{
$model = $this->model();
$rules = [
'name' => 'required|string',
'password' => [
$model ? 'filled' : 'required',
'string',
],
];
// when creating, we do expect the password confirmation to always exist
if (!$model) {
$rules['passwordConfirmation'] = 'required_with:password|same:password';
}
return $rules;
}
public function withValidator($validator)
{
if ($this->isUpdating()) {
$validator->sometimes(
'passwordConfirmation',
'required_with:password|same:password',
fn($input) => isset($input['password']),
);
}
}
Customising Error Messages
You may customize the error messages used by the form request by overriding the messages
method. This method should return an array of attribute / rule pairs and their corresponding error messages:
/**
* Get the error messages for the defined validation rules.
*
* @return array
*/
public function messages()
{
return [
'title.required' => 'A title is required',
'body.required' => 'A message is required',
];
}
Customising The Validation Attributes
If you would like the :attribute
portion of your validation message to be replaced with a custom attribute name, you may specify the custom names by overriding the attributes
method. This method should return an array of attribute / name pairs:
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes()
{
return [
'email' => 'email address',
];
}
Validated Data
Laravel form requests allow you to retrieve the data that was validated using the validated
method. On our request class, this returns the data that was validated for a resource create or update request.
For relationship requests, you should use the validatedForRelation
method to retrieve the validated value.
Accessing Related Models
For requests that modify a relationship, it is possible to access the related model (for a to-one relationship) or models (for a to-many relationship). These are the models that are referenced in the data
member of the request JSON.
WARNING
As the request JSON is untrusted input, you should only access the related model or models after validation has successfully completed.
For to-one relationships, use the toOne()
method. This method returns either the related model or null
, depending on what is in the request JSON. For example:
/** @var \App\Models\User|null $author */
$author = $request->toOne();
For to-many relationships, use the toMany()
method. This method returns a collection of the related models referenced in the request JSON. For example:
/** @var \Illuminate\Support\Collection $tags */
$tags = $request->toMany();
Deleting Resources
It is possible to add validation rules for deleting resources. This is useful if you want to prevent the deletion of a resource in certain circumstances. This validation is optional. If your validators class does not define any delete rules, the delete request will be allowed.
For example, if you did not want to allow API clients to delete posts
that have comments
:
class PostRequest extends ResourceRequest
{
// ...
/**
* @return array
*/
public function deleteRules(): array
{
return [
'meta.no_comments' => 'accepted',
];
}
/**
* @return array
*/
public function deleteMessages(): array
{
return [
'meta.no_comments.accepted' =>
'You cannot delete a post with comments.',
];
}
/**
* @param \App\Models\Post $post
* @return array
*/
public function metaForDelete(Post $post): array
{
return [
'no_comments' => $post->comments()->doesntExist(),
];
}
}
Deletion Validation Data
By default we pass the resource's current field values to the delete validator. This is the same process as described above for updating resources, although we allow you to add specific data for the delete request.
To add data for the delete validation, use the metaForDelete
method to return an array of values. This is then available to validate using rules for the meta
value.
In the above example, the metaForDelete
method is used to add a boolean to indicate that posts
resource has no comments. This is then validated using the meta.no_comments
path.
Deletion Validation Rules
Define delete validation rules in your validators deleteRules
method, as shown in the above example.
As with the rules
method, you may type-hint any dependencies you need within the rules method's signature. They will automatically be resolved via the Laravel service container.
You can also access the model being deleted via the model
method.
Deletion Error Messages
To add any custom error messages for your delete resource rules, implement the deleteMessages
method. This should return an array of custom messages, which will be merged with your resources custom error messages.
Deletion Attribute Names
To add any custom attribute names for your delete resource rules, implement the deleteAttributes
method. This should return an array of custom attributes, which will be merged with your resource's custom attributes.
Helper Methods
Our resource request class has a number of helper methods, to enable you to determine what type of request the class is handling. The available methods are:
- isCreating
- isUpdating
- isCreatingOrUpdating
- isDeleting
- getFieldName
- isUpdatingRelationship
- isAttachingRelationship
- isDetachingRelationship
- isModifyingRelationship
TIP
We recommend using these methods instead of using the isMethod
method, because the HTTP verb can be used for both resource and relationship requests. E.g. POST
is used both for creating a resource and replacing the contents of a relationship.
isCreating
Returns true
if the request will create a new resource. For example:
POST /api/v1/posts HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
...
}
isUpdating
Returns true
if the request will update an existing resource. For example:
PATCH /api/v1/posts/123 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
...
}
isCreatingOrUpdating
Returns true
if the request will create or update a resource, i.e.:
$request->isCreatingOrUpdating() ===
($request->isCreating() || $request->isUpdating());
isDeleting
Returns true
if the request will delete an existing resource. For example:
DELETE /api/v1/posts/123 HTTP/1.1
Accept: application/vnd.api+json
getFieldName
For a relationship request, returns the field name of the relationship that is being modified. Otherwise returns null
.
isUpdatingRelationship
Returns true
if the request will replace the contents of relationship on an existing resource. For example:
PATCH /api/v1/posts/123/relationships/tags HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
...
}
isAttachingRelationship
Returns true
if the request will attach resources to a to-many relation. For example:
POST /api/v1/posts/123/relationships/tags HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
...
}
isDetachingRelationship
Returns true
if the request will detach resources from a to-many relation. For example:
DELETE /api/v1/posts/123/relationships/tags HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
...
}
isModifyingRelationship
Returns true
if the request is for any of the following:
- updating a relationship;
- attaching resources to a relationship;
- detaching resources from a relationship.