Programming for the Web

Displaying JSON Records with Templates


Learning Objectives

  • You can follow records from a JSON file through a context dictionary into HTML.
  • You can use interpolation, a loop, and a condition in a template.
  • You can distinguish displayed data from HTML markup.

Writing every task inside a Python HTML string repeats the page structure whenever the records change. A template describes the structure once and marks where values belong. We use Jinja templates, which run on the server before the browser receives the response.

Continue in the same web-app folder and environment from Chapter 2. This chapter replaces the inline-HTML application with a version that reads records from JSON and renders a template.

Add JSON Records and Templates

Keep main.py, .venv, and requirements.txt in web-app. Add support.py, tasks.json, and a templates folder containing tasks.html. The listings below provide the contents of each application file:

web-app/
.venv/
requirements.txt
main.py
support.py
tasks.json
templates/
tasks.html

Save the JSON listing below as tasks.json, beside main.py.

From JSON Records to HTML

The tasks.json file contains JSON text:

[
{
"id": "T01",
"title": "Read the article",
"category": "reading"
},
{
"id": "T02",
"title": "Practise loops",
"category": "practice"
}
]

The short fragments below trace one record; the complete replacement files follow after the syntax practice. First, the JSON text above stores the title under "title". Second, read_records("tasks.json") constructs Python dictionaries from that text. The route can place the first dictionary in a context, which gives names to values available in a template:

records = read_records("tasks.json")
context = {"task": records[0]}

Here records[0] is a dictionary containing all three fields. The context key "task" names that whole dictionary; it does not rename the dictionary’s "title" field.

Third, a template looks up the named dictionary and one of its fields:

<p>{{ task["title"] }}</p>

Double braces insert a value. Bracket lookup chooses the dictionary field, just as it does in Python. Fourth, the server renders this fragment into returned HTML:

<p>Read the article</p>

The diagram below follows the title from JSON text to returned HTML:

One title passes from stored text through a named Python record into HTML.

A template looking up heading needs that name in its context. A context key named title does not also create a key named heading.

context = {"heading": "Study tasks"}

With this context, <h1>{{ heading }}</h1> produces <h1>Study tasks</h1>. Changing the key without changing the template breaks that connection.

One Template Change

0 / 5 points

The route supplies this context to the template:

context = {"heading": "Weekly study plan", "note": "Bring your notes"}

The template contains:

<h1>{{ note }}</h1>
<p>{{ note }}</p>

The heading should display Weekly study plan, while the paragraph keeps displaying Bring your notes. Which single change produces that result?

The next exercise supplies the application setup. FastAPI supplies request when it calls the route. The call to templates.TemplateResponse(...) combines the template selected by name with the values in context. Connect the record to that context and complete the template’s lookups.

A Featured Task Record

0 / 20 points

Display the supplied featured_record in a paragraph, using its identifier and title. For the example record, the paragraph displays T01: Read the article.

  1. In main.py, use the record as the value of the task context key.
  2. In templates/page.html, complete the two marked lookups.

You have connected one record to a template. A list page supplies {"tasks": records} so the template can repeat a fragment for every record. Its loop uses different delimiters:

<ul>
{% for task in tasks %}
<li>{{ task["title"] }}</li>
{% endfor %}
</ul>

The for directive repeats the enclosed fragment. endfor closes that region; neither directive appears in the returned HTML. A condition chooses which fragment to render:

{% if tasks %}
<p>Tasks are available.</p>
{% else %}
<p>No tasks.</p>
{% endif %}

As in Python, an empty list is false. Jinja renders the first branch for a nonempty tasks list and the else branch for []. endif closes the choice.

Practise the interpolation, loop, and condition notation with small edits in templates/page.html.

Supplied JSON Reading Code

Now connect the local project to tasks.json. Save this reading function in support.py, beside main.py. It uses the JSON operations from the earlier parts:

import json
def read_records(path):
with open(path, encoding="utf-8") as source:
return json.load(source)
The Example Data File

These examples assume the JSON file exists and contains records in the shown format. To try an empty collection, use [] as the complete file contents.

Supplied Application Setup

Replace the contents of main.py with this complete application. As in the earlier examples, app is the application and the decorator registers its route. templates reads HTML templates from the templates folder:

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from support import read_records
app = FastAPI()
templates = Jinja2Templates(directory="templates")
DATA_FILE = "tasks.json"
@app.get("/tasks")
def show_tasks(request: Request):
context = {"tasks": read_records(DATA_FILE)}
return templates.TemplateResponse(
request=request, name="tasks.html", context=context
)

Each GET request to /tasks calls show_tasks, which reads the file and builds a context. templates.TemplateResponse combines that context with tasks.html and returns the resulting HTML response.

Framework Arguments and Ordinary Calls

In request: Request, request names the parameter and Request identifies its kind of value. This notation tells FastAPI to supply the current request object when it calls the route. You do not call show_tasks yourself.

Inside the route, your code calls read_records(DATA_FILE) and templates.TemplateResponse(...), supplying their arguments. name selects the template file, and context supplies its named values. Pass the current request to TemplateResponse as shown.

A Complete List Template

Save this complete document as templates/tasks.html. The condition chooses between the repeated records and an empty-collection message:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Study tasks</title>
</head>
<body>
<h1>Study tasks</h1>
{% if tasks %}
<ul id="task-list">
{% for task in tasks %}
<li>{{ task["title"] }} ({{ task["category"] }})</li>
{% endfor %}
</ul>
{% else %}
<p>No tasks.</p>
{% endif %}
</body>
</html>

Save all the files and wait for the server’s automatic reload to finish. If you stopped the server, start it again from the same web-app terminal:

uvicorn main:app --reload

Visit http://127.0.0.1:8000/tasks; you should see both titles and their categories. Keep this project for the following chapters.

Refreshing the Local Project

For a Python edit, save, wait for Uvicorn’s automatic reload, and refresh the browser. For a template or JSON edit, save and refresh: the next request reads the updated file. Python reloads recreate the application and discard values held only in memory; saved JSON remains on disk.

For the supplied file, the loop runs twice in file order. For [], the condition selects the empty message and produces no task items. Try both documents in tasks.json and refresh after each change. Use View page source to compare the task list items with the empty-list paragraph. Restore the example records afterward.

If expected task items are missing, use Chapter 2’s print technique to inspect context inside show_tasks. Check that its "tasks" key contains the records the template needs.

Use the same loop and condition patterns to display a supplied list of records.

Study Task Lists and Empty-List Messages

0 / 30 points

Complete the marked regions in templates/page.html using the supplied tasks list.

  • For a nonempty list, display an unordered list with one item per record, in record order. Each item displays the record’s id and title as id: title.
  • For an empty list, display No tasks. instead of list items.

The routes and surrounding template are supplied. /tasks shows the records; /empty-tasks shows the empty case.

Data That Resembles Markup

A title can contain quotation marks, ampersands, or angle brackets. In the example file, change a record’s title field to "title": "Read \"Loops\" & <practice>". Save and refresh the page, then use View page source.

Escaped Source and Displayed Text

Ordinary Jinja insertion escapes markup-sensitive characters in HTML templates: < becomes &lt;, > becomes &gt;, and & becomes &amp; in the returned source. The browser displays their original characters as text. For example, &lt;practice&gt; in the source displays as <practice> without creating an HTML element.

Use ordinary insertion for record data and quote attribute values. Do not mark a title as trusted HTML: it describes a task, rather than defining the page’s markup.

Restore the original records after comparing the page and its source.

Rendering a Task Title

0 / 5 points

The route supplies this ordinary dictionary:

task = {"id": "T01", "title": "Read <chapter> & practise", "category": "reading"}
context = {"task": task}

The HTML template contains <p>{{ task["title"] }}</p>. Automatic HTML escaping is enabled.

Which pair describes the returned paragraph source and the text displayed in the browser?

The next chapter selects a single record and gives it an address.

Check Your Understanding

  1. In the complete list template, what does tasks refer to, and what does task refer to during one loop iteration? How does task["category"] select the value to display?
  2. Suppose tasks contains one record whose title is an empty string. Which branch of {% if tasks %} runs? Explain which value the condition checks.
  3. In the completed local project, which file would you edit to change a stored task title, and which file would you edit to change the page heading Study tasks? Explain how the project separates record data from page structure.
  4. When a GET request to /tasks arrives, who supplies the request argument to show_tasks? Who supplies the template name and context in the call to templates.TemplateResponse(...)? Explain the difference between these two calls.