Appendix A: JSON:API Syntax

JSON:API

The Drupal core JSON:API module implements the JSON:API spec for Drupal entities. It provides a zero-configuration required, opinionated, way to allow RESTful CRUD for a Drupal site’s content.

It is closely coupled to Drupal’s Entity and Field APIs, response caching, and authentication and authorization systems. Because it follows the shared JSON:API conventions it can help increase productivity and allow you to take advantage of non-Drupal specific tooling.

Refer to the Drupal documentation for more information on filtering API requests.

Filtering

Collections are listings of resources. When you make an unfiltered request to a collection endpoint like /jsonapi/node/article, you’ll just get every article that you’re allowed to see. Without filters, you can’t get only the articles that you want.

The simplest, most common filter is a key-value filter:

/data?
   filter[field_1]=value_1&
   filter[field_2]=value_2

This matches all resources with field_1 equal to "value_1" and field_2 equal to "value_2".

Conditions

The fundamental building blocks of JSON:API filters are Conditions and Groups.

  • Conditions assert that something is TRUE
  • Groups compose sets of conditions

Those sets can be nested to make super fine queries. You can think of a nested set as a tree:

Conventional representation:

is_A <- \(x) any(x == "A")
is_B <- \(x) any(x == "B")
is_C <- \(x) any(x == "C")
is_D <- \(x) any(x == "D")
is_E <- \(x) any(x == "E")

x <- c("A", "B", "C", "D", "E")

is_A(x[is_B(x) & is_C(x[is_D(x) | is_E(x)])])
[1] TRUE
x <- c("A", "B", "C", "F", "G")

is_A(x[is_B(x) & is_C(x[is_D(x) | is_E(x)])])
[1] FALSE

Tree representation:

   A
  / \
 B & C
    / \
   D | E

In both representations:

  • D & E are members of C in an OR group
  • B & C are members of A in an AND group

A condition has 3 primary parts:

  1. [path]: Dataset Field
  2. [operator]: Logical comparison
  3. [value]: Dataset Row

In JSON:API syntax, these are formatted as a key=value pair to work inside a URL query string:

/data?
   filter[fID-1][condition][path]=FIRST_NAME&
   filter[fID-1][condition][operator]=%3D&
   filter[fID-1][condition][value]=Janis

Note: %3D is the URL-encoded = symbol. All operators must be URL-encoded.

Notice the ID inside the first set of square brackets. Every condition or group should have a unique identifier.

Let’s add another filter so we only get Janises with a last name that starts with "J":

/data?
   filter[fID-1][condition][path]=FIRST_NAME&
   filter[fID-1][condition][operator]=%3D&
   filter[fID-1][condition][value]=Janis&
   filter[fID-2][condition][path]=LAST_NAME&
   filter[fID-2][condition][operator]=STARTS_WITH&
   filter[fID-2][condition][value]=J

Groups

A group is a set of conditions joined by a conjunction, either AND or OR. Let’s say we want to find all users with a last name that starts with “J” and have either the first name “Janis” or “Joan”. To do that, we add a group:

/data?
   filter[gID-1][group][conjunction]=OR

Then, we need assign filters to that new group. To do that, we add a memberOf key. Every condition and group can have a memberOf key.

/data?
   filter[gID-1][group][conjunction]=OR&
   filter[fID-1][condition][path]=first_name&
   filter[fID-1][condition][operator]=%3D&
   filter[fID-1][condition][value]=Janis&
   filter[fID-1][condition][memberOf]=gID-1&
   filter[fID-2][condition][path]=first_name&
   filter[fID-2][condition][operator]=%3D&
   filter[fID-2][condition][value]=Joan&
   filter[fID-2][condition][memberOf]=gID-1&
   filter[fID-3][condition][path]=last_name&
   filter[fID-3][condition][operator]=STARTS_WITH&
   filter[fID-3][condition][value]=J

Note: Groups can have a memberOf key just like conditions, which means you can have groups of groups. Every filter without a memberOf key is assumed to be part of a root group with a conjunction of AND.

Does that look familiar? It should, we saw it above as a tree:

   A         A = root & g1
  / \
 /   \       B = f3
B  &  C      C = g1
     / \
    /   \    D = f1
   D  |  E   E = f2

Paths

Paths provide a way to filter based on relationship values. Up to this point, we’ve just been filtering by the hypothetical first_name and last_name. Suppose we want to filter by the name of a user’s career, where career types are stored as a separate resource. We could add a filter like this:

/data?
   filter[career][condition][path]=field_career.name&
   filter[career][condition][operator]=%3D&
   filter[career][condition][value]=DOCTOR

Paths use a “dot notation” to traverse relationships. If a resource has a relationship, you can add a filter against it by concatenating the relationship field name and the relationship’s field name with a . (dot). You can even filter by relationships of relationships (and so on) just by adding more field names and dots.

You can filter on a specific index of a relationship by putting a non-negative integer in the path. So the path some_relationship.1.some_attribute would only filter by the 2nd related resource.

Tip: You can filter by sub-properties of a field. For example, a path like field_phone.country_code will work even though field_phone isn’t a relationship.

When filtering against configuration properties, you can use an asterisk (*) to stand-in for any portion of a path.

For example,

/api
/field_config
/field_config?
   filter[dependencies.config.*]=comment.type.comment

would match all field configs in which [attributes][dependencies][config] (an indexed array) contains the value "comment.type.comment".

Shortcuts

When the operator is =, you don’t have to include it:

/data?
   filter[fID-1][condition][path]=FIRST_NAME&
   filter[fID-1][condition][operator]=%3D&
   filter[fID-1][condition][value]=Janis

becomes

/data?
   filter[fID-1][condition][path]=FIRST_NAME&
   filter[fID-1][condition][value]=Janis

When the operator is = and you don’t need to filter by the same field twice, the path can be the identifier:

/data?
   filter[FIRST_NAME][value]=Janis

Reduce the simplest equality checks down to a key-value form:

/data?
   filter[FIRST_NAME]=Janis

Filters and Access Control

First, a warning: don’t make the mistake of confusing filters for access control. Just because you’ve written a filter to remove something that a user shouldn’t be able to see, doesn’t mean it’s not accessible. Always perform access checks on the backend.

With that big caveat, let’s talk about using filters to complement access control. To improve performance, you should filter out what your users will not be able to see. The most frequent support request in the JSON:API issue queues can be solved by this one simple trick! If you know your users cannot see unpublished content, add the following filter:

/data?
   filter[status][value]=1

Using this method, you’ll lower the number of unnecessary requests that you need to make. That’s because JSON:API doesn’t return data for resources to which a user doesn’t have access. You can see which resources may have been affected by inspecting the meta.errors section of JSON:API document. So, do your best to filter out inaccessible resources ahead of time.

Examples

Exact Match on Column

/data?
   filter[PROVIDER_TYPE_DESC]=PRACTITIONER - GENERAL PRACTICE

CONTAINS

Search on One Column

/data?
   filter[fID-1][condition][path]=PROVIDER_TYPE_DESC&
   filter[fID-1][condition][operator]=CONTAINS&
   filter[fID-1][condition][value]=SUPPLIER

CONTAINS & EQUALS

Combination Search on Two Columns

/data?
   filter[fID-1][condition][path]=PROVIDER_TYPE_DESC&
   filter[fID-1][condition][operator]=CONTAINS&
   filter[fID-1][condition][value]=PRACTITIONER&
   filter[fID-2][condition][path]=STATE_CD&
   filter[fID-2][condition][operator]=%3D&
   filter[fID-2][condition][value]=MD

EQUALS Simplified

This example is an equals filter searching the Accountable Care Organizations 2021 dataset for a single ID.

/data?
   filter[fID-1][condition][path]=aco_id&
   filter[fID-1][condition][operator]=%3D&
   filter[fID-1][condition][value]=A4807

An equals filter can be simplified like this.

/data?
   filter[aco_id]=A4807

Keyword

The keyword search will look for matching words in every column. This example will check for “Alex” in the Order and Referring dataset. Notice that it finds matches on both the first and last name fields.

/data?
   keyword=Alex

Multiple Conditions at Once

This search returns results from the Medicare Fee-For-Service Public Provider Enrollment dataset where the provider specialty is “PRACTITIONER - OPTOMETRY” and the location is Virginia.

/data?&
   filter[ROOT][group][conjunction]=AND&
   filter[GID-1][group][conjunction]=AND&
   filter[GID-1][group][memberOf]=ROOT&
   filter[FID-1][condition][path]=PROVIDER_TYPE_DESC&
   filter[FID-1][condition][operator]=%3D&
   filter[FID-1][condition][value]=PRACTITIONER - OPTOMETRY&
   filter[FID-1][condition][memberOf]=GID-1&
   filter[FID-2][condition][path]=STATE_CD&
   filter[FID-2][condition][operator]=%3D&
   filter[FID-2][condition][value]=VA&
   filter[FID-2][condition][memberOf]=GID-1

IN

This search returns results from the Opioid Treatment Program Providers dataset where the provider is located from MD, MI, or VA with the results sorted by NPI.

/data?
   filter[condition][path]=STATE&
   filter[condition][operator]=IN&
   filter[condition][value][]=MI&
   filter[condition][value][]=VA&
   filter[condition][value][]=MD&
   sort=NPI

Note About Empty Brackets:

When utilizing square brackets for multiple values filters, do not just use empty square brackets for a new value. While these work when typed into the URL, Guzzle and other HTTP clients will only create one value, as the array key will be seen to be the same and override the previous value. It is better to use an index to create unique array elements.

Note the two square brackets added behind the value to make it into an array:

/data?
   filter[fID-1][condition][path]=STATE&
   filter[fID-1][condition][operator]=IN&
   filter[fID-1][condition][value][1]=MI&
   filter[fID-1][condition][value][2]=VA&
   filter[fID-1][condition][value][3]=MA

Sort Results

Use the sort query parameter to specify which column the results should be sorted by:

Lowest first:

/data?
   sort=NPI
   
Highest first:

/data?
   sort=-NPI

Subset Columns

Add a comma-separated string of column names to the column query parameter to limit the columns returned:

/data?
   column=NPI,FIRST_NAME,LAST_NAME

Only Published Nodes

A very common scenario is to only load the nodes that are published. This is a very easy filter to add.

SHORT
   filter[status][value]=1

NORMAL
   filter[fID-1][condition][path]=status
   filter[fID-1][condition][value]=1

Nodes by Value of Entity Reference

A common strategy is to filter content by a entity reference.

SHORT
   filter[uid.id][value]=BB09E2CD-9487-44BC-B219-3DC03D6820CD

NORMAL
   filter[fID-1][condition][path]=uid.id
   filter[fID-1][condition][value]=BB09E2CD-9487-44BC-B219-3DC03D6820CD

To fully comply with the JSON API specification, while Drupal internally uses the uuid property, JSON:API uses id instead.

Since Drupal 9.3 it is possible to filter on target_id also instead of only filtering by uuid property.

SHORT
   filter[field_tags.meta.drupal_internal__target_id]=1

NORMAL
   filter[fID-1][condition][path]=field_tags.meta.drupal_internal__target_id
   filter[fID-1][condition][value]=1

Nested Filters

It’s possible to filter on fields from referenced entities like the user, taxonomy fields or any entity reference field. You can do this easily but just using the the following notation. reference_field.nested_field. In this example the reference field is uid for the user and name which is a field of the user entity.

SHORT
   filter[uid.name][value]=admin

NORMAL
   filter[fID-1][condition][path]=uid.name
   filter[fID-1][condition][value]=admin

Filtering with Arrays

You can give multiple values to a filter for it to search in. Next to the field and value keys you can add an operator to your condition.

Usually it’s "=" but you can also use "IN", "NOT IN", ">", "<", "<>", BETWEEN“.

For this example we’re going to use the "IN" operator. Note that I added two square brackets behind the value to make it into an array.

NORMAL
   filter[fID-1][condition][path]=uid.name
   filter[fID-1][condition][operator]=IN
   filter[fID-1][condition][value][1]=admin
   filter[fID-1][condition][value][2]=john

Grouping Filters

Now let’s combine some of the examples above and create the following scenario. WHERE user.name = admin AND node.status = 1:

filter[and-group][group][conjunction]=AND
filter[name-filter][condition][path]=uid.name
filter[name-filter][condition][value]=admin
filter[name-filter][condition][memberOf]=and-group
filter[status-filter][condition][path]=status
filter[status-filter][condition][value]=1
filter[status-filter][condition][memberOf]=and-group

You don’t really have to add the and-group but I find that a bit easier usually.

Grouping Grouped Filters

Like mentioned in the grouping section, you can put groups into other groups. WHERE (user.name = admin) AND (node.sticky = 1 OR node.promoted = 1)

To do this we put sticky and promoted into a group with conjunction OR. Create a group with conjunction AND and put the admin filter, and the promoted/sticky OR group into that.

# Create an AND and an OR GROUP
filter[and-group][group][conjunction]=AND
filter[or-group][group][conjunction]=OR

# Put the OR group into the AND GROUP
filter[or-group][group][memberOf]=and-group

# Create the admin filter and put it in the AND GROUP
filter[admin-filter][condition][path]=uid.name
filter[admin-filter][condition][value]=admin
filter[admin-filter][condition][memberOf]=and-group

# Create the sticky filter and put it in the OR GROUP
filter[sticky-filter][condition][path]=sticky
filter[sticky-filter][condition][value]=1
filter[sticky-filter][condition][memberOf]=or-group

# Create the promoted filter and put it in the OR GROUP
filter[promote-filter][condition][path]=promote
filter[promote-filter][condition][value]=1
filter[promote-filter][condition][memberOf]=or-group

Filter for nodes where ‘title’ CONTAINS “Foo”

SHORT
filter[title][operator]=CONTAINS&filter[title][value]=Foo

NORMAL
filter[title-filter][condition][path]=title
filter[title-filter][condition][operator]=CONTAINS
filter[title-filter][condition][value]=Foo

Filter by non-standard complex fields (e.g. addressfield)

FILTER BY LOCALITY
filter[field_address][condition][path]=field_address.locality
filter[field_address][condition][value]=Mordor

FILTER BY ADDRESS LINE
filter[address][condition][path]=field_address.address_line1
filter[address][condition][value]=Rings Street

Filtering on Taxonomy term values (e.g. tags)

For filtering you’ll need to use the machine name of the vocabulary and the field which is present on your node.

filter[taxonomy_term--tags][condition][path]=field_tags.name
filter[taxonomy_term--tags][condition][operator]=IN
filter[taxonomy_term--tags][condition][value][]=tagname

Filtering on Date (Date only, no time)

Dates are filterable. Pass a time string that adheres to the ISO-8601 format.

This example is for a Date field that is set to be date only (no time).

filter[datefilter][condition][path]=field_test_date
filter[datefilter][condition][operator]=%3D
filter[datefilter][condition][value]=2019-06-27

This example is for a Date field that supports date and time.

filter[datefilter][condition][path]=field_test_date
filter[datefilter][condition][operator]=%3D
filter[datefilter][condition][value]=2019-06-27T16%3A00%3A00

Note that timestamp fields (like created or changed) currently must use a timestamp for filtering:

filter[recent][condition][path]=created
filter[recent][condition][operator]=%3D
filter[recent][condition][value]=1591627496

Filtering on empty array fields

This example is for a Checkboxes/Radio buttons field with no value selected. Consider you have a field that is a checkbox. You would like to get all nodes that do not have that value checked. When checked, the JSON API returns an array:

"my_field":["checked"]

When unchecked, the JSON API returns an empty array:

"my_field": [] 

If you would like to get all fields that are unchecked, you must use the IS NULL on the array as follows (without a value):

filter[my-filter][condition][path]=my_field
filter[my-filter][condition][operator]=IS NULL