Skip to content

How-to guideFor front-end developers, module developersDrupal 11

JSON:API relationships and writes

References between entities are first-class in JSON:API. This page shows how to pull related resources into one response with the include parameter, then how to enable writes, what headers a write request must carry, and which access checks govern them.

Traverse relationships without extra round trips, and turn the JSON:API endpoint from read-only into one that accepts POST, PATCH, and DELETE. By the end you can fetch an article together with its author and image in a single request, and create, update, and delete content over HTTP.

Enabling writes widens the site’s attack surface. Do it only when a trusted client needs it, and read the access section below before you flip the switch in production.

A reference field appears under relationships, not attributes. Each relationship holds a data member of resource identifiers (a type and an id) plus its own links. An article’s author and image look like this:

{
"relationships": {
"uid": { "data": { "type": "user--user", "id": "a1b...c2" } },
"field_image": { "data": { "type": "file--file", "id": "f3d...9e",
"meta": { "alt": "A harbor at dawn" } } }
}
}

The identifier tells you the related resource exists and where to fetch it, but not its attributes. Relationship metadata, such as an image’s alt text, rides on the meta member of the identifier.

To get the related resources in the same response, list their relationship field names in the include parameter:

Terminal window
curl -H 'Accept: application/vnd.api+json' \
'https://my-site.ddev.site/jsonapi/node/article/9b5...e1?include=uid,field_image'

The response is now a compound document. The article stays in data; the user and file resources land in a top-level included array, each a full resource object with its own attributes. The client matches a relationship identifier to its resource by type and id. The Next.js quickstart does exactly this to render an article’s image without a second request.

Includes nest with dots and apply to collections too. include=field_image,uid.user_picture on a collection pulls in every article’s image and every author’s avatar, deduplicated, in one response.

include builds a compound document

With the include parameter, the primary resource stays in the data member and keeps only resource identifiers under relationships. The full related resources land deduplicated in the top-level included array. The client matches an identifier to its resource by type and id; include does not nest the related objects inside data.

Two routes expose a relationship directly. The relationships route returns the bare identifiers; the related route returns the target resources as a normal collection:

Terminal window
curl -H 'Accept: application/vnd.api+json' \
https://my-site.ddev.site/jsonapi/node/article/9b5...e1/relationships/uid
curl -H 'Accept: application/vnd.api+json' \
https://my-site.ddev.site/jsonapi/node/article/9b5...e1/uid

Use these when you want to read or modify a single reference without loading the whole host entity.

JSON:API ships read-only. The read_only flag in jsonapi.settings defaults to true (core/modules/jsonapi/config/install/jsonapi.settings.yml), and while it is on, the module rejects every POST, PATCH, and DELETE with 405 Method Not Allowed before any access check runs. Turn it off at Administer → Configuration → Web services → JSON:API/admin/config/services/jsonapi, or with config:

Terminal window
ddev drush config:set -y --input-format=yaml jsonapi.settings read_only false

Pass --input-format=yaml. Without it, Drush stores the string "false", which the boolean config schema casts to true, and the site stays read-only. This is a site-wide switch. It opens write routes for every resource type at once; you scope what actually happens through permissions and access, not through this setting.

Send a JSON:API document whose data describes the new resource to the collection URL. Three headers are mandatory:

  • Accept: application/vnd.api+json and Content-Type: application/vnd.api+json. The write routes require the application/vnd.api+json media type on the request body; a different Content-Type returns 415 Unsupported Media Type.
  • an X-CSRF-Token header (cross-site request forgery protection) when authenticating with a session cookie. Every write route carries the _csrf_request_header_token requirement. Fetch the token from /session/token and send it back in the header. Token-based clients that do not use the session cookie do not need it.

Writes also need credentials. The examples below authenticate with HTTP Basic as the api-writer account that the Verify section creates.

Terminal window
curl -X POST https://my-site.ddev.site/jsonapi/node/article \
--user api-writer:api-writer-pass \
-H 'Content-Type: application/vnd.api+json' \
-H 'Accept: application/vnd.api+json' \
-d '{"data":{"type":"node--article","attributes":{"title":"Posted over HTTP"}}}'

A successful create returns 201 Created with the new resource (including its server-assigned UUID) in data. Omit id and let Drupal assign the UUID, or supply your own UUID in data.id to create with a known identifier.

PATCH the individual resource URL with only the fields you are changing. Include data.id matching the URL; fields you omit keep their current values:

Terminal window
curl -X PATCH https://my-site.ddev.site/jsonapi/node/article/9b5...e1 \
--user api-writer:api-writer-pass \
-H 'Content-Type: application/vnd.api+json' \
-H 'Accept: application/vnd.api+json' \
-d '{"data":{"type":"node--article","id":"9b5...e1","attributes":{"title":"Edited"}}}'

DELETE the individual resource URL with no body. A successful delete returns 204 No Content:

Terminal window
curl -X DELETE https://my-site.ddev.site/jsonapi/node/article/9b5...e1 \
--user api-writer:api-writer-pass \
-H 'Accept: application/vnd.api+json'

Turning off read-only does not grant anyone the right to write. Every write route enforces the entity type’s standard access:

  • Create runs the entity create-access check (_entity_create_access), which for nodes means the create <bundle> content permission.
  • Update and delete run _entity_access for the update and delete operations on the specific entity, so node grants and the edit any/delete any permissions apply exactly as they do in the UI.
  • Per-field access is checked on top: a POST or PATCH that touches a field the user may not edit returns 403, naming the offending field (the response points at /data/attributes/<field>). A request whose UUID does not resolve to an entity returns 404; a denied update or delete on an entity that does exist returns 403.

JSON:API adds no permissions of its own beyond read_only. It reuses the same access system the rendered site uses, so a client can only ever write what its authenticated account could write through the admin UI. Grant the writing account a narrowly scoped role rather than broad permissions.

Install core’s Basic authentication module, create an account whose role may create articles, and turn read-only off:

Terminal window
ddev drush pm:install basic_auth
ddev drush role:perm:add content_editor 'create article content,edit own article content,delete own article content'
ddev drush user:create api-writer --password=api-writer-pass
ddev drush user:role:add content_editor api-writer
ddev drush config:set -y --input-format=yaml jsonapi.settings read_only false
ddev drush config:get jsonapi.settings read_only

The last command prints 'jsonapi.settings:read_only': false. A POST as that account now returns 201 and the article appears in the collection:

Terminal window
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
https://my-site.ddev.site/jsonapi/node/article \
-H 'Content-Type: application/vnd.api+json' \
-H 'Accept: application/vnd.api+json' \
--user api-writer:api-writer-pass \
-d '{"data":{"type":"node--article","attributes":{"title":"Verify write"}}}'

The command prints 201. Re-enable read-only with ddev drush config:set -y --input-format=yaml jsonapi.settings read_only true once you are done testing.

  • Set a relationship on create. Add a relationships member alongside attributes in the POST body, with resource identifiers pointing at existing entities (for example an author under uid).
  • Modify one relationship in isolation. POST, PATCH, or DELETE against the /relationships/<field> route changes a single reference without sending the whole entity.
  • Upload files. POST the raw file bytes to the file field’s own route, such as /jsonapi/node/article/field_image, with Content-Type: application/octet-stream and Content-Disposition: file; filename="photo.jpg". The response is the new file--file resource, which you then reference from the entity. See the JSON:API architecture topic.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.