How to Turn JSON Data into Word Reports with Python
JSON is a convenient format for moving data between applications. An API can return a list of support requests, a scheduling system can export workshop registrations, and an operations dashboard can store its settings as a JSON object.
But the people who need that information may prefer a Word document they can review, annotate, and share.
Turning JSON into a useful report involves a few decisions: which fields belong in the document, how they should be labeled, and whether paragraphs or tables will make them easier to read.
In this tutorial, we’ll use Free Spire.Doc for Python to build three progressively richer documents:
- A summary created from a JSON object.
- A formatted table created from a JSON array.
- A grouped table created from nested JSON data.
Each example uses a different dataset and produces an editable DOCX file.
1. Set Up the Python Environment
Free Spire.Doc for Python is a Word document processing library that can create and modify documents without requiring Microsoft Word to be installed. The free edition supports documents containing up to 500 paragraphs and 25 tables, so keep those limits in mind when expanding the examples.
Install the package:
pip install spire.doc.freeWe’ll also use Python’s built-in json module, which requires no separate installation.
The division of work is straightforward: json reads the source data, while Spire.Doc creates the paragraphs, tables, and formatting in the output document.
2. Load JSON Data from a File
Before creating a Word document, let’s look at how JSON enters a Python script.
Suppose a scheduling application exports the following data to a file named studio_booking.json:
{
"booking_id": "BK-260418",
"studio": "Harbor Recording Room",
"session_date": "2026-04-18",
"duration_hours": 3,
"confirmed": true
}Read the file using UTF-8 encoding:
import json
with open("studio_booking.json", "r", encoding="utf-8") as file:
booking = json.load(file)
print(booking["studio"])
print(booking["duration_hours"])A JSON object becomes a Python dictionary, while a JSON array becomes a list. JSON values also become their Python equivalents: for example, true becomes True, and null becomes None.
That conversion matters when building reports. A boolean might be clearer as “Confirmed” or “Pending,” and a missing value may need to appear as “Not provided.”
For convenience, the remaining examples embed JSON strings directly in their scripts and parse them with json.loads(). This lets you run each example independently. When working with exported files, replace that step with json.load().
3. Turn Key–Value Pairs into a Word Summary
A small JSON object often works well as a sequence of labeled paragraphs.
Consider a community workshop registration. It contains a participant’s name, an event title, and a few logistical details. A table would work, but a short summary gives each field enough room without adding much visual structure.
The following example creates a centered heading, a brief introduction, and one paragraph per field:
import json
from spire.doc import *
from spire.doc.common import *
# Parse the source JSON.
registration = json.loads("""
{
"participant_name": "Maya Collins",
"workshop_title": "Urban Sketching Essentials",
"event_date": "2026-05-23",
"venue": "Riverside Arts Center",
"seats_reserved": 2,
"materials_included": true,
"accessibility_notes": null
}
""")
# Map source fields to reader-friendly labels.
fields = [
("participant_name", "Participant"),
("workshop_title", "Workshop"),
("event_date", "Date"),
("venue", "Venue"),
("seats_reserved", "Seats reserved"),
("materials_included", "Materials included"),
("accessibility_notes", "Accessibility notes")
]
document = Document()
section = document.AddSection()
# Add the report title.
title = section.AddParagraph()
title.Format.HorizontalAlignment = HorizontalAlignment.Center
title.Format.AfterSpacing = 14
title_text = title.AppendText("Workshop Registration Summary")
title_text.CharacterFormat.FontName = "Calibri"
title_text.CharacterFormat.FontSize = 18
title_text.CharacterFormat.Bold = True
intro = section.AddParagraph()
intro.Format.AfterSpacing = 12
intro_text = intro.AppendText(
"Please review the registration details before attending."
)
intro_text.CharacterFormat.FontName = "Calibri"
intro_text.CharacterFormat.FontSize = 11
# Write each field as a labeled paragraph.
for key, label in fields:
value = registration.get(key)
if value is None:
display_value = "Not provided"
elif isinstance(value, bool):
display_value = "Yes" if value else "No"
else:
display_value = str(value)
paragraph = section.AddParagraph()
paragraph.Format.AfterSpacing = 8
label_text = paragraph.AppendText(f"{label}: ")
label_text.CharacterFormat.FontName = "Calibri"
label_text.CharacterFormat.FontSize = 11
label_text.CharacterFormat.Bold = True
value_text = paragraph.AppendText(display_value)
value_text.CharacterFormat.FontName = "Calibri"
value_text.CharacterFormat.FontSize = 11
document.SaveToFile(
"WorkshopRegistration.docx",
FileFormat.Docx
)
document.Close()The fields list controls both the order and the labels in the document. This gives the report a stable layout even if the source JSON changes its field order or includes additional properties.
Separating the label and value into two text ranges also lets us make only the label bold. Small formatting choices like this make a short document easier to scan.
4. Build a Word Table from a JSON Array
Paragraphs become repetitive when the source contains several records with the same fields. A table makes those records easier to compare.
Get Alexander Stock’s stories in your inbox
Join Medium for free to get updates from this writer.
For this example, we’ll use a list of equipment maintenance requests. Each request becomes a row, and an explicit field mapping determines the columns.
We’ll also add a shaded header, alternating row colors, and a consistent font.
import json
from spire.doc import *
from spire.doc.common import *
# Parse an array of maintenance requests.
requests = json.loads("""
[
{
"request_id": "MT-310",
"equipment": "Label Printer",
"location": "Dispatch Office",
"priority": "High",
"status": "Awaiting parts"
},
{
"request_id": "MT-311",
"equipment": "Conference Display",
"location": "Meeting Room 4",
"priority": "Normal",
"status": "Scheduled"
},
{
"request_id": "MT-312",
"equipment": "Document Scanner",
"location": "Records Room",
"priority": "Normal",
"status": "In progress"
},
{
"request_id": "MT-313",
"equipment": "Access Card Reader",
"location": "West Entrance",
"priority": "High",
"status": "Completed"
}
]
""")
columns = [
("request_id", "Request"),
("equipment", "Equipment"),
("location", "Location"),
("priority", "Priority"),
("status", "Status")
]
document = Document()
section = document.AddSection()
title = section.AddParagraph()
title.Format.AfterSpacing = 12
title_text = title.AppendText("Equipment Maintenance Requests")
title_text.CharacterFormat.FontName = "Calibri"
title_text.CharacterFormat.FontSize = 18
title_text.CharacterFormat.Bold = True
if not requests:
section.AddParagraph().AppendText(
"No maintenance requests are available."
)
else:
table = section.AddTable(True)
table.ResetCells(len(requests) + 1, len(columns))
table.PreferredWidth = PreferredWidth(
WidthType.Percentage, 100
)
table.TableFormat.Borders.BorderType = BorderStyle.Single
table.TableFormat.Borders.LineWidth = 0.5
table.TableFormat.Borders.Color = Color.get_LightGray()
# Mark the first row as the table header.
table.Rows[0].IsHeader = True
for column_index, (_, heading) in enumerate(columns):
cell = table.Rows[0].Cells[column_index]
cell.CellFormat.BackColor = Color.get_DarkBlue()
cell.CellFormat.VerticalAlignment = VerticalAlignment.Middle
paragraph = cell.AddParagraph()
paragraph.Format.AfterSpacing = 4
text = paragraph.AppendText(heading)
text.CharacterFormat.FontName = "Calibri"
text.CharacterFormat.FontSize = 10
text.CharacterFormat.Bold = True
text.CharacterFormat.TextColor = Color.get_White()
# Fill the data rows.
for row_index, record in enumerate(requests, start=1):
for column_index, (key, _) in enumerate(columns):
cell = table.Rows[row_index].Cells[column_index]
cell.CellFormat.VerticalAlignment = VerticalAlignment.Middle
if row_index % 2 == 0:
cell.CellFormat.BackColor = Color.get_AliceBlue()
value = record.get(key)
display_value = "—" if value is None else str(value)
paragraph = cell.AddParagraph()
paragraph.Format.AfterSpacing = 4
text = paragraph.AppendText(display_value)
text.CharacterFormat.FontName = "Calibri"
text.CharacterFormat.FontSize = 10
document.SaveToFile(
"MaintenanceRequests.docx",
FileFormat.Docx
)
document.Close()ResetCells() creates the required rows and columns, including an extra row for the headings. The library also provides cell background colors and table width settings for controlling the presentation.
Notice that the script checks for an empty list before creating the table. An empty export still produces a meaningful document instead of an unexplained blank table.
The code also leaves row heights flexible. Equipment names, locations, and status descriptions may grow over time, so allowing rows to expand helps keep the text visible.
5. Display Nested JSON with Grouped Table Rows
So far, each record has contained only simple values. Nested JSON adds another layer.
Imagine an internal training schedule organized by learning track. Each track contains several sessions. We could repeat the track name on every row, but merging its cells makes the grouping easier to see.
The next example first counts all sessions to determine the table size. It then writes the session rows and merges the first column across each track’s range.
import json
from spire.doc import *
from spire.doc.common import *
# Parse a nested training schedule.
schedule = json.loads("""
{
"Data Literacy": [
{
"session": "Reading Business Dashboards",
"facilitator": "Elena Brooks",
"minutes": 75
},
{
"session": "Spotting Misleading Charts",
"facilitator": "Owen Patel",
"minutes": 60
}
],
"Workplace Communication": [
{
"session": "Writing Clear Project Updates",
"facilitator": "Nora Kim",
"minutes": 90
},
{
"session": "Running Focused Meetings",
"facilitator": "Caleb Morris",
"minutes": 45
}
],
"Digital Organization": [
{
"session": "Building a Shared Filing System",
"facilitator": "Leah Turner",
"minutes": 60
}
]
}
""")
document = Document()
section = document.AddSection()
title = section.AddParagraph()
title.Format.AfterSpacing = 12
title_text = title.AppendText("Team Learning Schedule")
title_text.CharacterFormat.FontName = "Calibri"
title_text.CharacterFormat.FontSize = 18
title_text.CharacterFormat.Bold = True
total_sessions = sum(len(sessions) for sessions in schedule.values())
if total_sessions == 0:
section.AddParagraph().AppendText(
"No training sessions are scheduled."
)
else:
table = section.AddTable(True)
table.ResetCells(total_sessions + 1, 4)
table.PreferredWidth = PreferredWidth(
WidthType.Percentage, 100
)
table.TableFormat.Borders.BorderType = BorderStyle.Single
table.TableFormat.Borders.LineWidth = 0.5
table.TableFormat.Borders.Color = Color.get_LightGray()
headers = ["Learning Track", "Session", "Facilitator", "Duration"]
table.Rows[0].IsHeader = True
for column_index, heading in enumerate(headers):
cell = table.Rows[0].Cells[column_index]
cell.CellFormat.BackColor = Color.get_DarkBlue()
text = cell.AddParagraph().AppendText(heading)
text.CharacterFormat.FontName = "Calibri"
text.CharacterFormat.FontSize = 10
text.CharacterFormat.Bold = True
text.CharacterFormat.TextColor = Color.get_White()
current_row = 1
for track, sessions in schedule.items():
if not sessions:
continue
first_row = current_row
for session_index, session in enumerate(sessions):
values = [
track if session_index == 0 else "",
session["session"],
session["facilitator"],
f'{session["minutes"]} min'
]
for column_index, value in enumerate(values):
cell = table.Rows[current_row].Cells[column_index]
cell.CellFormat.VerticalAlignment = VerticalAlignment.Middle
if column_index == 0:
cell.CellFormat.BackColor = Color.get_AliceBlue()
paragraph = cell.AddParagraph()
paragraph.Format.AfterSpacing = 4
text = paragraph.AppendText(value)
text.CharacterFormat.FontName = "Calibri"
text.CharacterFormat.FontSize = 10
text.CharacterFormat.Bold = column_index == 0
current_row += 1
last_row = current_row - 1
# Merge only groups that span multiple rows.
if last_row > first_row:
table.ApplyVerticalMerge(0, first_row, last_row)
document.SaveToFile(
"TeamLearningSchedule.docx",
FileFormat.Docx
)
document.Close()ApplyVerticalMerge() takes the column index followed by the starting and ending row indexes. Here, column 0 holds the learning track, and the tracked row boundaries identify each group.
Only the first row of each group receives the track name. This avoids placing repeated labels inside the merged area. Tracks with one session need no merge, while empty tracks are skipped.
This layout assumes a specific structure: track names mapped to lists of session objects. If your JSON contains deeper nesting, decide which levels deserve headings, separate tables, or their own sections before writing the document.
6. Make the Output Easier to Read and Maintain
Once the basic export works, the most useful improvements usually concern consistency.
- Keep field mappings explicit.
A report should show the information its readers need in a predictable order. Using separate source keys and display labels also lets you change wording without changing the input format.
- Choose how to display missing values.
The paragraph example uses “Not provided,” while the maintenance table uses a dash. Either can work, but use one convention consistently within a report. Avoid treating 0 or False as missing values.
- Format values for their meaning.
Dates, currency amounts, and durations deserve deliberate formatting. The training example adds “min” to a numeric duration so readers can understand it without inspecting the source data.
- Allow room for longer content.
Test the document with realistic names and descriptions. A layout that looks balanced with short sample values may wrap awkwardly when the source contains longer text.
- Separate data preparation from document layout.
For recurring reports, load and validate the data first, then pass the prepared values to the document creation code. This makes it easier to diagnose whether a problem comes from the input or its presentation.
Final Thoughts
A useful Word report starts with choosing a layout that matches the data.
Labeled paragraphs suit a single record. Tables make repeated records easier to compare. Grouped rows can preserve a simple hierarchy without repeating the same category on every line.
With those patterns in place, the same approach can support registration summaries, maintenance reports, training schedules, and other recurring documents. The JSON supplies the changing information; your Python code gives it a consistent form that readers can use.









