# Query Parameters

# Introduction

Our query request classes allow you to validate the request query parameters sent by the client. We provide rules so that you can validate parameters according to JSON:API semantics, as well as validating them against your application-specific rules.

Our query request classes are optional. If our implementation cannot find a query request class, it will validate the request query parameters using information from your schema.

WARNING

Making the query classes optional helps to rapidly prototype API endpoints. However, for production applications we recommend generating the classes so that the query parameters are further validated against your specific application rules.

# Concept

The query parameters sent by a client affect the response it receives. Query classes therefore relate to the resource type contained in the response document. For relationship responses, this is often a different resource type to the resource that is subject of the request.

Take for example a comments resource. A request to the comments index - GET /api/v1/comments - will have comments resources in the response. Likewise, a request to fetch a posts resource's comments relationship - GET /api/v1/posts/123/comments - will contain comments responses. The subject of the request is the posts resource with an id of 123, but the response resource type is comments.

In both instances, the query parameters that are sent by the client refer to the comments resource type, because that is the resource type that will be in the response document.

TIP

This explains why resource types have separate resource request and query request classes. It allows us to use the query class for relationships.

# Query Classes

There are two query classes. For example, a posts resource can have a PostQuery and PostCollectionQuery.

The PostQuery is used for any request that will result in zero-to-one posts resources in the response. For example:

Verb URI Action
POST /posts store
GET /posts/{post} show
PATCH /posts/{post} update
GET /likes/{like}/post showRelated
GET /likes/{like}/relationships/post showRelationship
PATCH /likes/{like}/relationships/post updateRelationship

The PostCollectionQuery is used for any request that will result in zero-to-many resources in the response. For example:

Verb URI Action
GET /posts index
GET /users/{user}/posts showRelated
GET /users/{user}/relationships/posts showRelationship
PATCH /users/{user}/relationships/posts updateRelationship
POST /users/{user}/relationships/posts attachRelationship
DELETE /users/{user}/relationships/posts detachRelationship

TIP

The reason these are separate is because the query parameters accepted are likely to be different for a singular posts resource than a collection. For example, it makes sense to allow pagination for a collection, but pagination would not make sense for a singular resource.

# Generating Query Classes

To generate a resource query, use the jsonapi:query Artisan command:

php artisan jsonapi:query posts --server=v1

This will generate the following request class: App\JsonApi\V1\Posts\PostQuery

TIP

The --server option is not required if you only have one server.

To generate a resource collection query, use the jsonapi:query Artisan command with the --collection flag:

php artisan jsonapi:query posts --collection --server=v1

This will generate the following request class: App\JsonApi\V1\Posts\PostCollectionQuery

To generate both at once, use the --both flag:

php artisan jsonapi:query posts --both --server=v1

# Validation Approach

Query parameters are validated using Laravel form request validators (opens new window). If any field fails the validation rules, a 400 Bad Request 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 source parameter, identifying which query parameter has caused the request to fail.

# Validation Rules

# Defining Rules

As our query classes extend Laravel's form requests, you should use the rules method to define your validation rules.

For example, our PostQuery rules might look like this:

namespace App\JsonApi\V1\Posts;

use LaravelJsonApi\Validation\Rule as JsonApiRule;
use LaravelJsonApi\Laravel\Http\Requests\ResourceQuery;

class PostQuery extends ResourceQuery
{

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'fields' => [
                'nullable',
                'array',
                JsonApiRule::fieldSets(),
            ],
            'filter' => [
                'nullable',
                'array',
                JsonApiRule::filter(),
            ],
            'include' => [
                'nullable',
                'string',
                JsonApiRule::includePaths(),
            ],
            'page' => JsonApiRule::notSupported(),
            'sort' => JsonApiRule::notSupported(),
        ];
    }
}

And our PostCollectionQuery rules might look like this:

namespace App\JsonApi\V1\Posts;

use LaravelJsonApi\Validation\Rule as JsonApiRule;
use LaravelJsonApi\Laravel\Http\Requests\ResourceQuery;

class PostCollectionQuery extends ResourceQuery
{

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules(): array
    {
        return [
            'fields' => [
                'nullable',
                'array',
                JsonApiRule::fieldSets(),
            ],
            'filter' => [
                'nullable',
                'array',
                JsonApiRule::filter(),
            ],
            'include' => [
                'nullable',
                'string',
                JsonApiRule::includePaths(),
            ],
            'page' => [
                'nullable',
                'array',
                JsonApiRule::page(),
            ],
            'sort' => [
                'nullable',
                'string',
                JsonApiRule::sort(),
            ],
        ];
    }
}

TIP

As these are Laravel Form Requests, you may type-hint any dependencies you need within the rules method's signature. They will automatically be resolved via the Laravel service container.

# 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)
{
    $validator->after(function ($validator) {
        if ($this->somethingElseIsInvalid()) {
            $validator->errors()->add(
              'field',
              'Something is wrong with this field!'
            );
        }
    });
}

TIP

You should use the Helper methods described later in this chapter if you want to add after hooks for specific types of requests.

# Complex Conditional Validation

The withValidator method shown in the previous example can also be used to add complex conditional validation (opens new window), using the sometimes method on the validator.

# 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 [
        'page.number.required' => 'Pagination is compulsory for this resource.',
    ];
}

# 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 [
        'page.size' => 'quantity per-page',
    ];
}

# Validated Parameters

To get the validated data, use the validated method.

Our query classes also have helper methods for retrieving the JSON:API query parameters defined in the specification. All of these methods use the validated data when returning values. The methods are:

  • includePaths
  • sparseFieldSets
  • sortFields
  • page
  • filter

TIP

Our query request classes implement our LaravelJsonApi\Contracts\Query\QueryParameters interface. If you are writing your own controller actions, this means you can pass the class directly to repository methods that query the database.

# Available Rules

Laravel JSON:API ships with a number of rule objects to help you validate query parameters according to JSON:API semantics. You can fluently construct these from the LaravelJsonApi\Validation\Rule class.

TIP

Typically we import this Rule class using an alias of JsonApiRule. This is to avoid collisions with the Illuminate\Validation\Rule class.

The available rules are:

# Field Sets

Our field sets rule validates the fields parameter according to JSON:API semantics. This will ensure that the client is only allowed to request sparse field sets for:

  • valid JSON:API resource types; and
  • fields that are defined as sparse fields on the resource type's schema.

It can be added to your rules as follows:

use LaravelJsonApi\Validation\Rule as JsonApiRule;

public function rules(): array
{
  return [
      'fields' => [
        'nullable',
        'array',
        JsonApiRule::fieldSets(),
      ],

      // ...
  ];
}

# Filter

Our filter rule validates the filter parameter to ensure it only contains keys for filters that are defined on your resource schema. It can be added to your rules as follows:

use LaravelJsonApi\Validation\Rule as JsonApiRule;

public function rules(): array
{
  return [
      'filter' => [
        'nullable',
        'array',
        JsonApiRule::filter(),
      ],

      // ...
  ];
}

If you want to deny the use of any of your defined filters, use the forget method. For example, if we had id and slug filters that only made sense to use when fetching zero-to-many resources. On our PostQuery we would remove them as valid filters like so:

JsonApiRule::filter()->forget('id', 'slug')

TIP

There are also forgetIf and forgetUnless helper methods. These take a boolean as the first argument, then a string or array of strings as the second argument.

The forgetIf method forgets the specified filters if the first argument is true. The forgetUnless method forgets them if the first argument is false.

It is important to note that this only checks that the keys in the filter are valid - it does not validate filter values. Therefore, we recommend adding additional rules for each value. For example:

use LaravelJsonApi\Validation\Rule as JsonApiRule;

public function rules(): array
{
  return [
      'filter' => [
        'nullable',
        'array',
        JsonApiRule::filter(),
      ],

      'filter.foo' => [
        'filled',
        'string',
      ],

      // ...
  ];
}

# Include Paths

Our include path rule validates that the include paths provided by the client only match those defined by your resource's schema. It can be added to your rules as follows:

use LaravelJsonApi\Validation\Rule as JsonApiRule;

public function rules(): array
{
  return [
      'filter' => [
        'nullable',
        'string',
        JsonApiRule::includePaths(),
      ],

      // ...
  ];
}

If we wanted to deny the use of any of the include paths defined on the schema, we would use the forget, forgetIf and/or forgetUnless methods to remove specific paths.

For example:

JsonApiRule::includePaths()
  ->forget('foo.bar', 'baz.bat')
  ->forgetIf($someCondition, ['foobar', 'bazbat']);

# Not Supported

Our not supported rule allows you to deny the use of JSON:API query parameter. For example, it does not make sense to support pagination and sorting on a request that will only return zero-to-one resource.

We would therefore prevent the page and sort parameters from being used as follows:

use LaravelJsonApi\Validation\Rule as JsonApiRule;

public function rules(): array
{
  return [
      'page' => JsonApiRule::notSupported(),
      'sort' => JsonApiRule::notSupported(),

      // ...
  ];
}

# Page

Our page rule validates the page parameter to ensure it only has keys that are allowed by the paginator on your resource's schema. It can be added to your rules as follows:

use LaravelJsonApi\Validation\Rule as JsonApiRule;

public function rules(): array
{
  return [
      'page' => [
        'nullable',
        'array',
        JsonApiRule::page(),
      ],

      // ...
  ];
}

It is important to note that this only checks that the keys in the page parameter are valid - it does not validate their values. Therefore, we recommend adding additional rules for each value. For example:

use LaravelJsonApi\Validation\Rule as JsonApiRule;

public function rules(): array
{
  return [
      'page' => [
        'nullable',
        'array',
        JsonApiRule::page(),
      ],

      'page.number' => [
        'integer',
        'min:1',
      ],

      'page.size' => [
        'integer',
        'min:1',
      ],

      // ...
  ];
}

TIP

If you want to force a client to always paginate a resource type, add the required rule to your page keys - e.g. the page.number and page.size in the above example.

# Sort

Our sort rule validates that the sort parameters provided by the client only match those defined by your resource's schema. It can be added to your rules as follows:

use LaravelJsonApi\Validation\Rule as JsonApiRule;

public function rules(): array
{
  return [
      'sort' => [
        'nullable',
        'string',
        JsonApiRule::sort(),
      ],

      // ...
  ];
}

If we wanted to deny the use of any of the sort paths defined on the schema, we would use the forget, forgetIf and/or forgetUnless methods to remove specific paths.

For example:

JsonApiRule::sort()
  ->forget('foo', 'bar')
  ->forgetIf($someCondition, ['baz', 'bat']);

TIP

Note that with sort parameters, we do not need to provide the direction, e.g. foo and -foo. The rule allows foo in either direction.

# Default Include Paths

The query request classes can be used to specify default include paths that should be used when a client provides none. Simply add the JSON:API include paths to the $defaultIncludePaths on your query request class. For example:

namespace App\JsonApi\V1\Posts;

use LaravelJsonApi\Laravel\Http\Requests\ResourceQuery;

class PostCollectionQuery extends ResourceQuery
{

    /**
     * The default include paths to use if the client provides none.
     *
     * @var array|null
     */
    protected ?array $defaultIncludePaths = ['author', 'tags'];

    // ...
}

If you need to programmatically work out the default include paths, overload the defaultIncludePaths() method on the request class.

When you use default include paths, the client will receive a compound document containing the related resources when they do not provide an include query parameter. In this scenario, if the client wants no resources to be included, it must specify this using an empty include query parameter:

GET /api/v1/posts?include= HTTP/1.1
Accept: application/vnd.api+json

# Helper Methods

Our query request classes have some helper methods. You may need to use these when deciding to forget allowed parameters. The available methods are:

# isRelationship

Returns true if the resource is being fetched as part of a relationship. For example, these URLs:

  • GET /api/v1/posts/123/comments
  • GET /api/v1/posts/123/relationships/comments
  • PATCH /api/v1/posts/123/relationships/comments
  • POST /api/v1/posts/123/relationships/comments
  • DELETE /api/v1/posts/123/relationships/comments

An example use-case would be to remove an allowed filter when the resource type appears in a relationship:

JsonApiRule::filter()->forgetIf($this->isRelationship(), [
  'foo',
  'bar',
]);

# isNotRelationship

The opposite of the isRelationship helper.

# isViewingAny

Returns true if the request is to view any resource, i.e. the index action. For example:

GET /api/v1/posts HTTP/1.1
Accept: application/vnd.api+json

# isViewingOne

Returns true if the request is to view a specific resource, i.e. the read action. For example:

GET /api/v1/posts/123 HTTP/1.1
Accept: application/vnd.api+json

# isViewingRelated

Returns true if the request is to view a related resource or related resources in a relationship, i.e. the show-related action. For example:

GET /api/v1/posts/123/author HTTP/1.1
Accept: application/vnd.api+json

# isViewingRelationship

Returns true if the request is to view a resource identifier or resource identifiers in a relationship, i.e. the show-relationship action. For example:

GET /api/v1/posts/123/relationships/tags HTTP/1.1
Accept: application/vnd.api+json
Last Updated: 7/3/2021, 12:35:55 PM