We have a view that needs to export a taxonomy term as JSON along with the content that's associated with the term. So, the structure is like this:

{
  events: [{
    "title": "Term Title",
    "relatedItems": [
      {
        "title": "Post #1 title"
      },
      {
        "title": "Post #2 title"
      }
    ]
  }]
}

There is more information returned about the term and the items than in this simple example, but you can see the structure.

I found that using Views Field View allows me to embed another display of the same view to get the nested structure, as long as the other display also is set to output JSON in "Views API mode" (otherwise you get no output since the other display halts request processing). The only problem is that the top-level escapes the value. Technically, that's the expected behavior -- normally you wouldn't want a value that looks like JSON to throw off the actual JSON wrapper -- but in this case, it would be great if I could just tell the outer display not to escape that one field.

To support this, I assume we'd need to do the following:

  1. Add a new table to the "JSON" display style settings that presents each of the fields along with a checkbox for "Render raw, without escaping".
  2. Alter _views_json_json_encode() to take an additional parameter called $unescaped_field_titles that specifies which fields (if any) should not be escaped. The function would need to basically go row-by-row and pull those fields out to avoid them being escaped. Then, after escaping the rest of the content, we'd need to basically go in and inject the raw JSON data in the right place in the data stream.
  3. Alter calls to _views_json_json_encode() to obtain and pass the list of fields, based on the settings specified in #1.

I know that #2 sounds like an ugly kludge, but I don't see an obvious, convenient way to avoid escaping certain parts of the data passed to json_encode(). Perhaps we could just walk the rows recursively and encode fields more explicitly, piecewise? I suppose that would be less error prone.

Comments

guypaddock’s picture

Title: Allow certain fields to be designated for "raw passthrough" » For nesting JSON structures, allow certain fields to be designated for "raw passthrough"

Updated issue title so others find this topic when trying to do nested data.

guypaddock’s picture

Here's a utility function we're using in the interim. We call this from a custom Views JSON style output template, in place of what are normally calls to _views_json_encode_formatted():

/**
 * Encodes the provided nested array into JSON format, preserving the contents
 * of the specified field as-is as a raw, nested JSON value.
 *
 * @param $rows
 *   The nested array to encode into JSON.
 *
 * @param $jsonOptions
 *   The JSON encoder settings bitmask, if the format that would be passed for
 *   $options to PHP's json_encode() function.
 *
 * @param $previewAsHtml
 *   Whether or not the JSON is being previewed in the Views UI interface.
 *
 * @param $rootObjectName
 *   The name of the root value in the array.
 *
 * @param $nestedFieldName
 *   The name of the field in each row that is to be protected from escaping
 *   because it already contains JSON that should be passed through raw.
 *
 * @return string
 *   Either the JSON encoded string on success; or FALSE on
 *   failure.
 */
function render_unfiltered_nested_json($rows, $jsonOptions, $previewAsHtml, $rootObjectName, $nestedFieldName) {
  if (isset($rows[$rootObjectName])) {
    /* Pick a random number to include in the pattern, to further reduce the
     * chances of coming across content that just happens to match our
     * placeholder pattern.
     */
    $rawFieldPlaceholderPattern =
      sprintf(
        "%s_PLACEHOLDER_%d_",
        $nestedFieldName,
        rand(1, 2000));

    $unescapedItems = array();

    $events =& $rows[$rootObjectName];

    foreach ($events as $index => &$event) {
      if (isset($event[$nestedFieldName])) {
        $placeholderContent = $rawFieldPlaceholderPattern . $index;

        /* JSON wraps the content in quotes, making it a string.
         * For us to nest the JSON as JSON, we need to strip the quotes, so
         * we include them in the key so that strtr() handle the replacement.
         */
        $placeholderKey     = '"' . $placeholderContent . '"';

        $unescapedItems[$placeholderKey]  = trim($event[$nestedFieldName]);
        $event[$nestedFieldName]          = $placeholderContent;
      }
    }

    if ($previewAsHtml) {
      $jsonContent = _views_json_encode_formatted($rows, $jsonOptions);
    }
    else {
      $jsonContent = _views_json_json_encode($rows, $jsonOptions);
    }

    /* Replace the placeholders with the corresponding raw JSON values we saved
     * before we passed the object through json_encode().
     */
    $result = strtr($jsonContent, $unescapedItems);
  }
  else {
    if ($previewAsHtml) {
      $result = _views_json_encode_formatted($rows, $jsonOptions);
    }
    else {
      $result = _views_json_json_encode($rows, $jsonOptions);
    }
  }

  return $result;
}

A sample template:

/**
 * @file views-views-json-style-simple--json-events.tpl.php
 *  Template for rendering events and their products using
 *  the Views JSON style plugin and the "simple" format.
 *
 *  Variables:
 *  - $view: The View object.
 *  - $rows: Hierarchical array of key=>value pairs to convert to JSON.
 *  - $options: Array of options for this style.
 *
 * @ingroup views_templates
 */

$jsonp_prefix = $options['jsonp_prefix'];

if ($view->override_path) {
  // We're inside a live preview where the JSON is pretty-printed.
  $json = render_unfiltered_nested_json($rows, $options, TRUE, 'events', 'relatedItems');

  if ($jsonp_prefix) {
    $json = "$jsonp_prefix($json)";
  }

  print "$json";
}
else {
  $json = render_unfiltered_nested_json($rows, $bitmask, FALSE, 'events', 'relatedItems');

  if ($options['remove_newlines']) {
    $json = preg_replace(array('/\\\\n/'), '', $json);
  }

  if (isset($_GET[$jsonp_prefix]) && $jsonp_prefix) {
    $json = check_plain($_GET[$jsonp_prefix]) . '(' . $json . ')';
  }

  if ($options['using_views_api_mode']) {
    // We're in Views API mode.
    print $json;
  }
  else {
    // We want to send the JSON as a server response so switch the content
    // type and stop further processing of the page.
    $content_type = ($options['content_type'] == 'default') ? 'application/json' : $options['content_type'];

    drupal_add_http_header("Content-Type", "$content_type; charset=utf-8");

    print $json;

    drupal_page_footer();

    exit;
  }
}
guypaddock’s picture

Issue summary: View changes
phponwebsites’s picture

I have created new views in which page, pie chart and attach,ment_1 is avilable. When i try to access that page, it display only first page elements. I want to get all three in views. how to do this?

guypaddock’s picture

@phponwebsites: Please open a different issue. Your request has nothing to do with JSON or this ticket, as far as I can tell.

mauriziopinotti’s picture

You may want to have a look at the REST/JSON module as an alternative to Views Datasource:

https://www.drupal.org/project/rest_json

It features out-of-the-box rest APIs and it's primarily meant for devalopers (no UI, configuration in code, a few hooks to alter the default behavior).

guypaddock’s picture

We looked at Rest JSON but the output was too verbose for the use case we had. Views Datasource was a lot closer to ideal because we could select just the data we wanted.

studio-days’s picture

Has there been any developments with this? I'm not a php developer but it just seems like if you could have a format option in the field settings to not include the inverted commas on output.

Not being able to customise nested arrays is a huge drawback with this module as is.

lolandese’s picture

StatusFileSize
new1.8 KB

If a module uses a field format that provides valid json or an array, it makes sense to pass the data unprocessed and without wrapping quotes.

Provided patch is work in progress (please ignore).

lolandese’s picture

lolandese’s picture

Status: Active » Needs work
StatusFileSize
new1.21 KB

Still work in progress.

lolandese’s picture

StatusFileSize
new1.22 KB

Still troublesome to get a well formatted Views preview. The output itself is as expected and can be reviewed or used if needed.

marceldeb’s picture

This is a very interesting thread. The reason why is that i am implementing the same solution as OP.

However, my nested array does not contain values and only shows '[]'.

The funny thing is that it works in admin preview mode but not on the actual website / front end

Anyone has had the same problem?

katekuratsu’s picture

Same issue here. My nested array only shows '[]'. It looks fine on admin preview mode.

imclean’s picture

I've created a small module to do something similar, REST Export Nested. Does that help?

Nesting multiple levels is possible via Views Field View fields.

katekuratsu’s picture

The nested array using Views Field View fields works before, but after the new update to 8.5, it returns an empty array on Postman. The view preview mode looks fine.

wlofgren’s picture

The function _views_json_json_nested only allows to pass in a single nested field name. We should allow multiple field names to be passed into the function. Below is the modified function that allows multiple field names.

/**
 * Encodes the provided nested array into JSON format, preserving the contents
 * of the specified field as-is as a raw, nested JSON value.
 *
 * @param $rows
 *   The nested array to encode into JSON.
 *
 * @param $jsonOptions
 *   The JSON encoder settings bitmask, if the format that would be passed for
 *   $options to PHP's json_encode() function.
 *
 * @param $previewAsHtml
 *   Whether or not the JSON is being previewed in the Views UI interface.
 *
 * @param $rootObjectName
 *   The name of the root value in the array.
 *
 * @param $nestedFieldNames
 *   An array of field names in each row that is to be protected from escaping
 *   because it already contains JSON that should be passed through raw.
 *
 * @return string
 *   Either the JSON encoded string on success; or FALSE on
 *   failure.
 */
function _views_json_json_nested($rows, $jsonOptions, $previewAsHtml, $rootObjectName, $nestedFieldNames) {
  if (isset($rows[$rootObjectName])) {
    /* Pick a random number to include in the pattern, to further reduce the
     * chances of coming across content that just happens to match our
     * placeholder pattern.
     */

    $unescapedItems = array();

    $events =& $rows[$rootObjectName];

    foreach ($nestedFieldNames as $nestedFieldName) {

      $rawFieldPlaceholderPattern =
        sprintf(
          "%s_PLACEHOLDER_%d_",
          $nestedFieldName,
          rand(1, 2000));


      foreach ($events as $index => &$event) {
        if (isset($event[$nestedFieldName])) {
          $placeholderContent = $rawFieldPlaceholderPattern . $index;

          /* JSON wraps the content in quotes, making it a string.
          * For us to nest the JSON as JSON, we need to strip the quotes, so
          * we include them in the key so that strtr() handle the replacement.
          */
          $placeholderKey     = '"' . $placeholderContent . '"';

          $unescapedItems[$placeholderKey]  = trim($event[$nestedFieldName]);
          $event[$nestedFieldName]          = $placeholderContent;
        }
      }
    }

    if ($previewAsHtml) {
      $jsonContent = _views_json_encode_formatted($rows, $jsonOptions);
    }
    else {
      $jsonContent = _views_json_json_encode($rows, $jsonOptions);
    }

    /* Replace the placeholders with the corresponding raw JSON values we saved
     * before we passed the object through json_encode().
     */
    $result = strtr($jsonContent, $unescapedItems);
  }
  else {
    if ($previewAsHtml) {
      $result = _views_json_encode_formatted($rows, $jsonOptions);
    }
    else {
      $result = _views_json_json_encode($rows, $jsonOptions);
    }
  }

  return $result;
}