This package automatically checks both request query parameters and content for compliance with the JSON
API specification. Any non-compliant requests will receive a 4xx
HTTP response containing JSON API
error objects describing how the request is not compliant.
In addition, each resource can have a validators class that defines your application-specific validation rules for requests.
JSON API requests to the controller actions provided by this package are automatically checked for compliance with the JSON API specification.
As an example, this request:
PATCH /api/posts/123 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "posts",
"id": 123,
"attributes": {
"title": "Hello World"
}
}
}
Will be rejected because the specification states that resource ids must be strings. This package will result in the following response:
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors": [
{
"title": "Non-Compliant JSON API Document",
"status": "400",
"detail": "The member id must be a string.",
"source": {
"pointer": "/data/id"
}
}
]
}
The response may contain multiple error objects if there are a number of non-compliance problems with the request.
This package allows you to define application-specific validation rules for each resource type. This
is implemented on a Validators
class. The validators allow you to define both rules for the resource
object for create and update requests, plus rules for the query parameters.
Application-specific validation rules are run after validation against the JSON API specification, and will not run unless the request passes the specification checks.
Validators are optional. If you do not define one for your resource type, only the JSON API compliance validation will be run.
To generate validators for a resource type, use the following command:
$ php artisan make:json-api:validators <resource-type> [<api>]
The same validators class is used for both Eloquent and generic resources.
This will generate the following:
<?php
namespace App\JsonApi\Posts;
use CloudCreativity\LaravelJsonApi\Validation\AbstractValidators;
class Validators extends AbstractValidators
{
/**
* The include paths a client is allowed to request.
*
* @var string[]|null
* the allowed paths, an empty array for none allowed, or null to allow all paths.
*/
protected $allowedIncludePaths = [];
/**
* The sort field names a client is allowed send.
*
* @var string[]|null
* the allowed fields, an empty array for none allowed, or null to allow all fields.
*/
protected $allowedSortParameters = [];
/**
* Get resource validation rules.
*
* @param mixed|null $record
* the record being updated, or null if creating a resource.
* @param array $data
* the data being validated.
* @return mixed
*/
protected function rules($record, array $data): array
{
return [
//
];
}
/**
* Get query parameter validation rules.
*
* @return array
*/
protected function queryRules(): array
{
return [
//
];
}
}
Resource objects are validated using Laravel validations. If any field
fails the validation rules, a 422 Unprocessable Entity
response will be sent. JSON API errors will be included
containing the Laravel validation messages in the detail
member of the error object. Each error will also
have a JSON source point set identifying the location in the request content of the validation failure.
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/posts HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "posts",
"attributes": {
"title": "Hello World",
"content": "...",
"another-field": "..."
},
"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:
[
"type" => "posts",
"id" => null,
"title" => "Hello World",
"content" => "...",
"another-field" => "...",
"author" => ["type" => "users", "id" => "123"],
"tags" => [
["type" => "tags", "id" => "1"],
["type" => "tags", "id" => "3"],
],
];
The following is an example query rule for the above-mentioned data:
use CloudCreativity\LaravelJsonApi\Rules\HasMany;
use CloudCreativity\LaravelJsonApi\Rules\HasOne;
use CloudCreativity\LaravelJsonApi\Validation\AbstractValidators;
class Validators extends AbstractValidators
{
// ...
protected function rules($record, array $data): array
{
return [
'title' => 'required|string|min:1|max:255',
'content' => 'required|string|min:1',
"another-field" => 'string|unique:posts,another_field',
'author' => [
'required',
new HasOne('users'),
],
'tags' => new HasMany('tags'),
];
}
}
You'll notice that the exists
rule is not used in the validation for the author
and tags
relationships.
This is because the package complies with the JSON API spec and validates all relationship identifiers to
check that they exist. Therefore the following does not need to be used:
protected function rules($record, array $data): array
{
return [
'author.id' => 'exists:users,id',
'tags.*.id' => 'exists:tags,id'
];
}
The HasOne
and HasMany
rules accept a list of resource types for polymorphic relationships. If no
type is provided to the constructor, then the plural form of the attribute name will be used. For
example:
protected function rules($record, array $data): array
{
return [
'author' => [
'required',
new HasOne(), // expects 'authors' resources
],
'tags' => new HasMany(), // expects 'tags' 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.
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 attributes of your resource and merge
the attributes provided by the client over the top.
For example, given this request:
PATCH /api/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"
}
]
}
}
}
}
If your posts
resource had a content
and published
attributes that were not provided by
the client, your validator will be provided with the following array of data:
[
"type" => "posts",
"id" => "1",
"title" => "Hello World",
"content" => "...",
"published" => true,
"tags" => [
["type" => "tags", "id" => "1"],
],
];
We use your resource schema's
getAttributes
method to obtain the existing attribute values.
There is no reliable way for us to work out the existing values of any relationships that were missing in the request document. If you need to add any existing relationship values, you can do this by returning the relationships in their JSON API form. For example:
class Validators extends AbstractValidators
{
// ...
/**
* @param \App\Post $record
* @return iterable
*/
protected function existingRelationships($record): iterable
{
return [
'author' => $record->user,
];
}
}
If you need to disable the merging of the existing values, set the $validateExisting
property
of your validators class to false
. If you need to programmatically work out whether to merge the existing
values, overload the mustValidateExisting()
method.
If you want to use the merging of existing values, but need to adjust the extraction of current
attributes, you can overload the existingAttributes
method. For example, if you are using the not_present
rule for an attribute, you would not want the existing value to be merged in. In this case you could
forget the existing value as follows:
class Validators extends AbstractValidators
{
// ...
/**
* @param \App\Post $record
* @return iterable
*/
protected function existingAttributes($record): iterable
{
return collect(parent::existingAttributes($record))->forget('foobar');
}
}
Define resource object validation rules in your validators rules
method.
This method receives either the record being updated, or null
for a create request. For example:
use CloudCreativity\LaravelJsonApi\Rules\HasOne;
use CloudCreativity\LaravelJsonApi\Validation\AbstractValidators;
class Validators extends AbstractValidators
{
// ...
protected function rules($record, array $data): array
{
return [
'title' => "required|string|min:3",
'content' => "required|string",
'author' => [
'required',
new HasOne('users'),
],
];
}
}
When validating the request document for compliance with the JSON API spec, we check that all relationships contain identifiers for resources that exist in your API. This means that when writing rules for fields that are relationships, you do not need to check if the
id
provided exists. However you must validate thetype
of a related resource to ensure it is the expected resource type for the relationship. As shown in the above example, the relatedauthor
is checked to ensure that it is ausers
resource.
To add any custom error messages for your resource object rules, define them on the $messages
property:
class Validators extends AbstractValidators
{
// ...
protected $messages = [
'title.required' => 'Your post must have a title.',
];
}
Alternatively you can overload the messages
method. Like the rules
method, this receives the
record being updated or null
for a create request.
To define any custom attribute names, add them to the $attributes
property on your validators:
class Validators extends AbstractValidators
{
// ...
protected $attributes = [
'email' => 'email address',
];
}
Alternatively you can overload the attributes
method. Like the rules
method, this receives the
record being updated or null
for a create request.
If you need to conditionally add rules, you can
do this by overloading either the create
or update
methods. For example:
class Validators extends AbstractValidators
{
// ...
/**
* @param array $document
* @return \CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorInterface
*/
public function create(array $document): ValidatorInterface
{
$validator = parent::create($document);
$validator->sometimes('reason', "required|max:500", function ($input) {
return $input->games >= 100;
});
return $validator;
}
}
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 added
to the relationship via a POST
request or removed using a DELETE
request.
Given this request:
PATCH /api/posts/1/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"],
],
];
This data will be passed to a validator that only receives any rules that are for the tags
field. I.e.
we filter the resource rules returned from your rules()
method to only include rules that have a key
starting with tags
.
If you need to customise the rules used to validate relationships, overload the
relationshipRules()
method on your validators class.
Custom validation messages (on the $messages
property) and attribute names (on the $attributes
property)
are also passed to your validator.
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 - for example, if you did not want to allow API clients to delete posts that have comments.
This validation is optional. If your validators class does not define any delete rules, the delete request will be allowed.
By default we pass the resource's current field values to the delete validator, using the
existingRelationships
method to work out the values of any relationships.
(The existingRelationships
method is discussed above in the update resource validation section.)
If a posts
resource had a title
and content
attributes, given the following validators class:
class Validators extends AbstractValidators
{
// ...
/**
* @param \App\Post $record
* @return iterable
*/
protected function existingRelationships($record): iterable
{
return [
'author' => $record->user,
];
}
}
The validator would receive this data:
[
"title": "posts",
"id": "1",
"title": "Hello World!",
"content": "...",
"author": [
"type": "users",
"id": "123"
]
]
If you wanted to use different data for validating a delete request, overload the dataForDelete
method.
This is shown in the next example.
Define delete validation rules in your validators deleteRules
method. For example, if we wanted to
stop a posts
resource from being deleted if it has any comments:
class Validators extends AbstractValidators
{
// ...
/**
* @var array
*/
protected $deleteMessages = [
'no_comments.accepted' => 'Cannot delete a post with comments.',
];
/**
* @param \App\Post $record
* @return array|null
*/
protected function deleteRules($record): ?array
{
return [
'no_comments' => 'accepted',
];
}
/**
* @param \App\Post $record
* @return array
*/
protected function dataForDelete($record): array
{
return [
'no_comments' => $record->comments()->doesntExist(),
];
}
}
Returning an empty array or
null
from thedeleteRules
validator indicates that a delete request does not need to be validated.
To add any custom error messages for your delete resource rules, define them on the $deleteMessages
property.
This is shown in the example above.
Alternatively you can overload the deleteMessages
method. Like the deleteRules
method, this receives the
record being deleted.
To define any custom attribute names for delete resource validation, add them to the $deleteAttributes
property on your validators.
Alternatively you can overload the deleteAttributes
method. Like the deleteRules
method, this receives the
record being deleted.
If you need to conditionally add rules, you can
do this by overloading the delete
method. For example:
class Validators extends AbstractValidators
{
// ...
/**
* @param \App\Post $record
* @return array
*/
protected function dataForDelete($record): array
{
return [
'is_author' => \Auth::user()->is($record->author),
'no_comments' => $record->comments()->doesntExist(),
];
}
/**
* @param \App\Post $record
* @return \CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorInterface
*/
public function delete($record): ValidatorInterface
{
$validator = parent::create($document);
$validator->sometimes('no_comments', 'accepted', function ($input) use ($record) {
return !$input->is_author;
});
return $validator;
}
}
In addition to defining rules for validating the JSON API document sent by a client, your validators class also holds rules for validating query parameters. This allows you to:
- Whitelist expected
filter
,include
,page
,sort
andfields
parameters. - Validate
filter
,page
and non-JSON API query parameters using Laravel validation rules.
One important thing to note with query parameter validation is that when fetching relationships, the validators class for the related resource will be used. As an example, when a client submits this request:
GET /api/posts/1/relationships/tags HTTP/1.1
Accept: application/vnd.api+json
The query parameters are validated using the tags
validators class, because the response will contain
tags
resources, not posts
resources. Filters, pagination, etc all therefore need to be valid for tags
.
For this to work, we need to know the inverse resource type when you specify relationship routes. By default
we assume that the inverse resource type is the pluralised form of the relationship name. E.g. for an
author
relationship we assume the inverse resource type is authors
. If it was actually users
, you
need to specify this when defining the relationship route. For example:
JsonApi::register('default', ['namespace' => 'Api'], function ($api, $router) {
$api->resource('posts', [
'has-one' => [
'author' => ['inverse' => 'users'],
],
]);
});
Expected parameters can be defined using any of the following properties on your validators class:
$allowedFilteringParameters
$allowedIncludePaths
$allowedPagingParameters
$allowedSortParameters
$allowedFieldSets
The default values for each of these and how to customise them is discussed in the Filtering, Inclusion, Pagination, Sorting and Sparse Fieldsets chapters.
In addition to whitelisting parameters you can also use Laravel validation rules for the query
parameters. To define these, add them to your queryRules()
method. For example:
class Validators extends AbstractValidators
{
// ...
protected function queryRules(): array
{
return [
'filter.author' => 'exists:users',
'page.number' => 'integer|min:1',
'page.size' => 'integer|between:1,100',
'foobar' => 'in:baz,bat',
];
}
}
To add any custom error messages for your query parameter rules, define them on the $queryMessages
property:
class Validators extends AbstractValidators
{
// ...
protected $queryMessages = [
'fitler.author.exists' => 'The author does not exist.',
];
}
Alternatively you can overload the queryMessages
method.
To define any custom attribute names, add them to the $queryAttributes
property on your validators:
class Validators extends AbstractValidators
{
// ...
protected $queryAttributes = [
'filter.author' => "the post's author",
];
}
Alternatively you can overload the queryAttributes
method.
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: DateTimeIso8601
.
This can be used as follows:
use CloudCreativity\LaravelJsonApi\Rules\DateTimeIso8601;
return [
'publishedAt' => ['nullable', new DateTimeIso8601()]
];
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:
POST /api/posts HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "posts",
"attributes": {
"title": "Hello World"
}
}
}
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"
}
}
]
}
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.
POST /api/posts HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "posts",
"attributes": {
"title": "Hello World",
"content": null
}
}
}
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"
}
}
]
}
In this scenario, the pointer can be /data/attributes/content
as the field actually exists in the request
JSON.
Laravel's confirmed
rule expects there to be a field with the same name and _confirmation
on the end: i.e.
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
rules will not work. For example
if using dash-case your extra field will be called password-confirmation
. 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 password-confirmation
working:
namespace App\JsonApi\Users;
use CloudCreativity\LaravelJsonApi\Validation\AbstractValidators;
class Validators extends AbstractValidators
{
// ...
protected function rules($record, array $data): array
{
return [
'name' => 'required|string',
'password' => "required|string",
'password-confirmation' => "required_with:password|same:password",
];
}
}
Remember to note the guidance above about PATCH
requests, where the server must assume that missing values
being the current values. 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:
namespace App\JsonApi\Users;
use CloudCreativity\LaravelJsonApi\Contracts\Validation\ValidatorInterface;
use CloudCreativity\LaravelJsonApi\Validation\AbstractValidators;
class Validators extends AbstractValidators
{
// ...
public function update($record, array $document): ValidatorInterface
{
$validator = parent::update($record, $document);
$validator->sometimes('password-confirmation', 'required_with:password|same:password', function ($input) {
return isset($input['password']);
});
return $validator;
}
protected function rules($record, array $data): array
{
$rules = [
'name' => 'required|string',
'password' => [
$record ? 'filled' : 'required',
'string',
],
];
if (!$record) {
$rules['password-confirmation'] = 'required_with:password|same:password';
}
return $rules;
}
}
This package makes it possible to include a machine-readable reason why a value failed validation within
the JSON API error object's meta
member. This is an opt-in feature because it is not standard practice
for Laravel to JSON encode validation failure information with validation error messages.
For example, if a value fails to pass the between
rule, then by default this package will return the
following response content:
{
"errors": [
{
"status": "422",
"title": "Unprocessable Entity",
"detail": "The value must be between 1 and 10.",
"source": {
"pointer": "/data/attributes/value"
}
}
]
}
If you opt-in to showing failed meta, the response content will be:
{
"errors": [
{
"status": "422",
"title": "Unprocessable Entity",
"detail": "The value must be between 1 and 10.",
"source": {
"pointer": "/data/attributes/value"
},
"meta": {
"failed": {
"rule": "between",
"options": [
"1",
"10"
]
}
}
}
]
}
The rule name will be the dash-case version of the Laravel rule. For example, before_or_equal
will
be before-or-equal
. If the rule is a rule object, we use the dash-case of the class basename.
For example, CloudCreativity\LaravelJsonApi\Rules\DateTimeIso8601
will be date-time-iso8601
.
The options
member will only exist if the rule has options. We intentionally omit rule options
for the exists
and unique
rules as the options for these database rules reveal information
about your database setup.
To opt-in to this feature, add the following to the register
method of your AppServiceProvider
:
<?php
namespace App\Providers;
use CloudCreativity\LaravelJsonApi\LaravelJsonApi;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
LaravelJsonApi::showValidatorFailures();
}
// ...
}