diff --git a/modules/wsdata_extras/src/Plugin/WSDecoder/WSDecoderJSONList.php b/modules/wsdata_extras/src/Plugin/WSDecoder/WSDecoderJSONList.php
new file mode 100644
index 0000000000000000000000000000000000000000..8530cf44ca3ed9e85a6910feec35704ce55fee96
--- /dev/null
+++ b/modules/wsdata_extras/src/Plugin/WSDecoder/WSDecoderJSONList.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Drupal\wsdata_extras\Plugin\WSDecoder;
+
+use Drupal\wsdata\Plugin\WSDecoder\WSDecoderJSON;
+
+/**
+ * JSON Decoder.
+ *
+ * @WSDecoder(
+ *   id = "WSDecoderJSONList",
+ *   label = @Translation("JSON Decoder for list data", context = "WSDecoder"),
+ * )
+ */
+class WSDecoderJSONList extends WSDecoderJSON {
+
+  /**
+   * Returns help text to display with the key/token select input.
+   */
+  public function getKeyHelpText() {
+    return $this->t('Use the format markup_wrapper_element/key_1:key_2:..:key_n  where markup_wrapper_element is optional (div, p, span, ol, ul) and key elements are separated by : ');
+  }
+
+  /**
+   * Retrieve the value for the given data key.
+   *
+   * This function retrieves data from $this->data that is,
+   *  in this case, a list or array.
+   *  If defined, $key will select that key from each list item.
+   *  All selected data will be wrapped in markup and concatenated.
+   *  $key is a string, with the format "markup_element/foo:bar"
+   *  where markup_element suggests markup to wrap the items,
+   *  and the character ':' delimiting the parts of the key.
+   *  Valid markup elements are: div, ol, ul, span, p.
+   *  I.E.  The key  div/foo:bar will retrieve $this->data[*]['foo']['bar']
+   *  and wrap each item in a <div> element.
+   *
+   * @param string $key
+   *   Optional - Data key to load.
+   * @param string $lang
+   *   Optional - Language key.
+   *
+   * @return mixed
+   *   Returns the requested data, FALSE otherwise.
+   */
+  public function getData($key = NULL, $lang = NULL) {
+    if (!is_array($this->data)) {
+      return $this->data;
+    }
+
+    $return_data = FALSE;
+    // Paths to load data from.
+    $paths = [];
+
+    // Default markup element for flattening.
+    $markup_element = 'div';
+
+    // Split out the key from the markup element.
+    $key_components = explode('/', $key);
+    if (count($key_components) > 1) {
+      // Markup element specified.
+      $markup_element = $key_components[0];
+      $key = $key_components[1];
+    }
+    else {
+      // No markup element specified.
+      $key = $key_components[0];
+    }
+
+    // First, see if we want a specific language.
+    if ($this->languages) {
+      if (!is_null($lang) and array_key_exists($lang, $this->data)) {
+        $paths[$lang] = !empty($key) ? $lang . ':' . $key : $lang;
+      }
+      else {
+        foreach ($this->languages as $lang) {
+          $paths[$lang] = !empty($key) ? $lang . ':' . $key : $lang;
+        }
+      }
+    }
+    else {
+      if (!empty($key)) {
+        $paths[$key] = $key;
+      }
+    }
+
+    // Simplest case, return all data.
+    if (empty($paths)) {
+      return '<div>' . implode('</div><div>', $this->data) . '</div>';
+    }
+
+    // Second simplest case, one specific value.
+    if (!empty($paths[$key])) {
+      $location = explode(':', $paths[$key]);
+      $return_data = [];
+      // Loop through the data array and select key from each.
+      foreach ($this->data as $item_data) {
+        foreach ($location as $l) {
+          if (isset($item_data[$l])) {
+            $item_data = $item_data[$l];
+          }
+          else {
+            $item_data = FALSE;
+          }
+        }
+        // Add selected data to output array.
+        $return_data[] = $item_data;
+      }
+      // Wrap all selected data items in markup and return.
+      return $this->flatten($return_data, $markup_element);
+    }
+
+    // Third case, one specific value in a given language.
+    if (!empty($paths[$lang]) and count($paths) == 1) {
+      $location = explode(':', $paths[$lang]);
+      foreach ($location as $l) {
+        if (isset($return_data[$l])) {
+          $return_data = $return_data[$l];
+        }
+        else {
+          $return_data = FALSE;
+        }
+      }
+      // Language specific data is always keyed by the language.
+      $return_data[$lang] = $return_data;
+      return $return_data;
+    }
+
+    // Lastly, the complicated case. Keyed value for all languages.
+    if ($this->languages and count($paths) > 1) {
+      $keyed_data = [];
+      foreach ($paths as $p => $path) {
+        // Reset return data.
+        $return_data = $this->data;
+        $location = explode(':', $path);
+        foreach ($location as $l) {
+          if (isset($return_data[$l])) {
+            $return_data = $return_data[$l];
+          }
+          else {
+            $return_data = FALSE;
+          }
+        }
+        $keyed_data[$p] = $return_data;
+      }
+
+      // Finally, put the keyed data back into the return data.
+      return $keyed_data;
+    }
+  }
+
+  /**
+   * Takes an array of items and flattens it to a single string
+   * using a suggested markup element to wrap the items.
+   *
+   * @param array $items
+   *   The array of items.
+   * @param string $markup_element
+   *   The markup element in which to wrap the items.
+   *
+   * @return string
+   *   Returns a flat string with all items.
+   */
+  protected function flatten($items, $markup_element) {
+    $flat = '';
+    switch ($markup_element) {
+      case 'ol':
+      case 'ul':
+        $flat = '<'.$markup_element.'><li>' . implode('</li><li>', $items) . '</li></'.$markup_element.'>';
+        break;
+      case 'span':
+      case 'p':
+      case 'div':
+        $flat = '<'.$markup_element.'>' . implode('</div><div>', $items) . '</'.$markup_element.'>';
+        break;
+      default:
+        $flat = '<div>' . implode('</div><div>', $items) . '</div>';
+    }
+    return $flat;
+  }
+
+}