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.
Prerequisites
Section titled “Prerequisites”- The reading workflow from JSON:API basics: collections, single resources, and query parameters.
- Authentication configured for the writing client, per Authentication and CORS. Writes are never anonymous on a sane site.
- An understanding of access: users, roles, and permissions, because every write runs the entity’s own access checks.
Relationships in a resource object
Section titled “Relationships in a resource object”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.
Resolve references with include
Section titled “Resolve references with include”To get the related resources in the same response, list their relationship field names in the include parameter:
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.
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.
Fetch a relationship on its own
Section titled “Fetch a relationship on its own”Two routes expose a relationship directly. The relationships route returns the bare identifiers; the related route returns the target resources as a normal collection:
curl -H 'Accept: application/vnd.api+json' \ https://my-site.ddev.site/jsonapi/node/article/9b5...e1/relationships/uidcurl -H 'Accept: application/vnd.api+json' \ https://my-site.ddev.site/jsonapi/node/article/9b5...e1/uidUse these when you want to read or modify a single reference without loading the whole host entity.
Enable writes
Section titled “Enable writes”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:
ddev drush config:set -y --input-format=yaml jsonapi.settings read_only falsePass --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.
Create a resource with POST
Section titled “Create a resource with POST”Send a JSON:API document whose data describes the new resource to the collection URL. Three headers are mandatory:
Accept: application/vnd.api+jsonandContent-Type: application/vnd.api+json. The write routes require theapplication/vnd.api+jsonmedia type on the request body; a differentContent-Typereturns415 Unsupported Media Type.- an
X-CSRF-Tokenheader (cross-site request forgery protection) when authenticating with a session cookie. Every write route carries the_csrf_request_header_tokenrequirement. Fetch the token from/session/tokenand 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.
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.
Update with PATCH and delete with DELETE
Section titled “Update with PATCH and delete with DELETE”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:
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:
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'How access governs writes
Section titled “How access governs writes”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 thecreate <bundle> contentpermission. - Update and delete run
_entity_accessfor theupdateanddeleteoperations on the specific entity, so node grants and theedit any/delete anypermissions apply exactly as they do in the UI. - Per-field access is checked on top: a
POSTorPATCHthat touches a field the user may not edit returns403, naming the offending field (the response points at/data/attributes/<field>). A request whose UUID does not resolve to an entity returns404; a deniedupdateordeleteon an entity that does exist returns403.
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.
Verify
Section titled “Verify”Install core’s Basic authentication module, create an account whose role may create articles, and turn read-only off:
ddev drush pm:install basic_authddev 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-passddev drush user:role:add content_editor api-writerddev drush config:set -y --input-format=yaml jsonapi.settings read_only falseddev drush config:get jsonapi.settings read_onlyThe last command prints 'jsonapi.settings:read_only': false. A POST as that account now returns 201 and the article appears in the collection:
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.
Variations
Section titled “Variations”- Set a relationship on create. Add a
relationshipsmember alongsideattributesin thePOSTbody, with resource identifiers pointing at existing entities (for example an author underuid). - Modify one relationship in isolation.
POST,PATCH, orDELETEagainst the/relationships/<field>route changes a single reference without sending the whole entity. - Upload files.
POSTthe raw file bytes to the file field’s own route, such as/jsonapi/node/article/field_image, withContent-Type: application/octet-streamandContent-Disposition: file; filename="photo.jpg". The response is the newfile--fileresource, which you then reference from the entity. See theJSON:API architecture topic.
Related
Section titled “Related”- JSON:API basics covers reading, filtering, and pagination.
- Authentication and CORS sets up the credentials and the CSRF token that writes need.
- Access: users, roles, and permissions explains the access system every write runs through.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.