Insight

Using JSONa to Help Manage Unruly JSON:API Responses

Female with long brown hair is standing smiling in front of a metal wall with a hand on one hip
Rosemary Reilman Technology Lead

Overview

In the last two years, all of our Drupal 8 sites so far have been decoupled and our preferred front end framework is React. This means that we’re relying pretty heavily on tools like JSON:API module (which as of 8.7.x is now part of Drupal Core)

Modules required: Media, Consumer Image Styles (Requires Consumers, JSON:API Extras), JSON:API

With our first iteration of decoupled Drupal 8 we found JSON:API to be great for filtering different node types as needed and at the same time including different entities and fields that we wanted in a single request. This is perfect because many of our clients rely heavily on media and imagery in their projects.

For example a schema for a node with a media entity reference field that we want to include the actual file information would look like this:

https://drupal.localhost/jsonapi/node/page?fields[node--page]=title,body,status,path,field_media&include=field_media,field_media.field_media_image_image

In our request above, not only are we telling JSON:API to return the field_media relationship but also the field named image on the media entity field_media that holds the actual file. The returned response would look something like this:

Object {jsonapi: Object, data: Array[1], included: Array[2], links:Object}

 

/**
  * Match up included data from a JSON API response with relationships.
  *
  * @param {object} [relationships={}] object containing relationships data
  * @param {array} [included=[]] data included with a JSON API response
  * @param {boolean} [recurse=false] to recursively match included data
  * @returns matched relationships
  * @memberof IK.Drupal
  */
 matchIncludes (relationships = {}, included = [], recurse = false, i = 0) {
   if (relationships.constructor !== Object || !Array.isArray(included)) {
     return new Error(
       'Provided relationships or included parameter was of wrong type.'
     )
   }

   if (!Object.keys(relationships).length) {
     return new Error('Provided relationships parameter was empty.')
   }

   if (!included.length) {
     return new Error('Provided included parameter was empty.')
   }

   return Object.keys(relationships).reduce((result, element) => {
     const rel = relationships[element]

     if (rel.data) {
       // Comes through as a single object if only one exists, this evens it out.
       const data = Array.isArray(rel.data) ? rel.data : [rel.data]
       const matches = included.filter(
         x => data.filter(y => y.id === x.id).length
       )

       if (matches.length && recurse) {
         matches.forEach((x, index) => {
           if (x.relationships) {
             matches[index].included = this.matchIncludes(
               x.relationships,
               included,
               i < 2 ? recurse : false,
               i + 1
             )
           }
         })
       }

       rel.included = matches
       result[element] = rel
     }

     return result
   }, {})
 }

 

While this was helpful, our matchIncludes method it also became problematic on deeply nested fields.

Enter JSONa.

JSONa is a data formatter that simplifies a JSON:API response to a little more manageable format. It took the response output above, and turned it into this:

[Object] 0:Object type: "node--page"

JSONa was able to reformat the response to place the field_media out of the relationship array and into the main object array. We can then utilize this information in our react app response.field_media.field_media_image.uri.url which will return the relative path to the image.

Overall, we have really found JSONa super helpful and flexible in that you can write your own formatters and deserializers.