Drupal 8 & 9 Theming: How to Render and Format JSON Data With PHP and Twig Using the JSON Field Module
There's a neat little Drupal module called JSON Field and recently, I had a chance to play around with it. Out of the box, JSON field is a just a plain field where JSON data can be input and output on a web page. On its own, the module does not do much beyond just printing the raw data formatted as JSON. However, I got to thinking it would be ideal to nicely format the data with HTML. In this article, I will show you how I accomplished this with both a preprocess function and some custom code in Twig.
Getting started
First, you'll want a Drupal 8 or 9 instance running. In the root of your project, run:
composer require drupal/json_field
Note, if you get an error, you may need to append a version number, for example:
composer require drupal/json_field:1.0-rc4
Next, enable the module and create a new field on an entity, for example on a page content type. When I created my field, I chose the option, JSON stored as raw JSON in database
Next, input some JSON data, for sample data, I like to use Mockaroo. (At a high level, I could envision using the Drupal Feeds module to import JSON data in bulk and mapping it to a JSON field but I have not tested this.)
Create a preprocess function
We are rendering this data in a node so I have a basic node preprocess function setup below with a sub-theme of Olivero called Oliver. Within this, we will leverage Xdebug to examine the data up close. We write this code in our theme's .theme
file.
<?php
/**
* @file
* Functions to support theming in the Oliver theme.
*/
/**
* Prepares variables for node templates.
*/
function oliver_preprocess_node(array &$vars) {
// Custom code will go here.
}
Define and check for an instance of a node
The first thing we want to do is, since we are working within a node context, we will set some node definitions and check to ensure that we are on a node page.
At the top of our file, we will add
use Drupal\node\NodeInterface;
Then, within the function, we will define the node.
// Define the node.
$node = \Drupal::routeMatch()->getParameter('node');
Now we check for an instance of a node:
// If instance of a node.
if ($node instanceof NodeInterface) {
Field PHP magic
I named my JSON field field_json_raw
and we first want to check to see if the field exists and that it is not empty. For this, I like to use a PHP magic method. A magic method is a short cut of sorts to dig into the field data. In Drupal terms, this looks like:
if ($node->hasField('field_json_raw') &&
!$node->get('field_json_raw')->isEmpty()) {
// Custom code here...
}
The magic methods above are hasField
and get
.
Start up Xdebug
Next up, we will use Xdebug to examine the data output to see how we might prepare variables for our Twig template. Within the node preprocess function, I set an Xdebug breakpoint and start listening. Once Xdebug is running we can evaluate the field expression using the magic method again but this time adding the value on. For example, $node->get('field_json_raw')->value
. That ends up with the plain value of the field:
[
{
"id": 1,
"country": "Cuba",
"animal_name": "Cape Barren goose",
"description": "Curabitur convallis.",
"animal_scientific": "Cereopsis novaehollandiae"
},
{
"id": 2,
"country": "Vietnam",
"animal_name": "Horned puffin",
"description": "Pellentesque ultrices mattis odio.",
"animal_scientific": "Fratercula corniculata"
}
]
etc...
Convert the value into an array
What we need to do now is convert that to a usable PHP array. We use the json_decode
function:
// Set a variable for the plain field value.
$json_raw = $node->get('field_json_raw')->value;
// Convert the data into an array using json decode.
$json_array = json_decode($json_raw);
That ends up looking like this:
Create a template variable
Now we have a nicely formatted array to loop through once inside a twig template. The final piece is to check for valid JSON and set the template variable. Note, JSON field already check for valid json but it's probably good practice to do this anyway.
// Check for valid JSON.
if ($json_array !== NULL) {
// Create a variable for our template.
$vars['json_data'] = $json_array;
}
Finished preprocess function
Putting it all together, our entire preprocess function looks like this:
<?php
/**
* @file
* Functions to support theming in the Oliver theme.
*/
use Drupal\node\NodeInterface;
/**
* Prepares variables for node templates.
*/
function oliver_preprocess_node(array &$vars) {
// Define the node.
$node = \Drupal::routeMatch()->getParameter('node');
// If instance of a node.
if ($node instanceof NodeInterface) {
// Check for the field and that it is not empty.
if ($node->hasField('field_json_raw') &&
!$node->get('field_json_raw')->isEmpty()) {
// Set a variable for the field value.
$json_raw = $node->get('field_json_raw')->value;
// Convert the data into an array.
$json_array = json_decode($json_raw);
// Check for valid JSON.
if ($json_array !== NULL) {
// Create a variable for our template.
$vars['json_data'] = $json_array;
}
}
}
}
Render the JSON variable in Twig
Now we'll go into our Twig template and render the variable with a loop. At its very most basic, it will look something like this:
{% if content.field_json_raw | render %}
{% for item in json_data %}
{{ item.animal_name }}
{{ item.animal_scientific }}
{{ item.country }}
{% endfor %}
{% endif %}
Of course we want to add HTML to this to make it look nicely styled and here is where you can do most anything you want. I opted for a data table:
{% if content.field_json_raw | render %}
<h2>{{ 'Animals from around the world'|t }}</h2>
<table>
<thead>
<tr>
<th>{{ 'Animal Name'|t }}</th>
<th>{{ 'Scientific Name'|t }}</th>
<th>{{ 'Country'|t }}</th>
</tr>
</thead>
{% for item in json_data %}
<tbody>
<tr>
<td>{{ item.animal_name }}</td>
<td>{{ item.animal_scientific }}</td>
<td>{{ item.country }}</td>
</tr>
</tbody>
{% endfor %}
</table>
{% endif %}
The code above ends up looking like this:
Summary
And there you have it, nicely styled JSON data rendered in a Twig template. Using the JSON Field module might not be a common everyday item but it definitely fulfills a specific use case as outlined here.