JSON is a common format for exchanging data between machines. Word documents, by contrast, are designed for people to read. Converting one to the other is therefore not simply a “format conversion” but a structural mapping problem: JSON key-value pairs, arrays, and nested objects must be translated into human-readable document elements such as paragraphs, tables, and headings.
This article shows how to implement JSON-to-Word conversion with Free Spire.Doc for Python, a free library suitable for learning and lightweight automation.
Environment Setup
Free Spire.Doc for Python is a standalone Word document processing library. It does not require Microsoft Word to create or edit DOCX files. Install the free version with pip:
pip install Spire.Doc.Free
Before using it, note the free version’s capacity limits: a single document can contain at most 500 paragraphs and 25 tables, and content beyond that is truncated. This is sufficient for routine report generation, API data snapshots, and small-to-medium ledgers. If a single output will consistently exceed this scale, consider splitting the data or using another solution.
Understanding the core object hierarchy is essential for all the code that follows:
Document → Section → Paragraph → TextRange
↘ Table → Row → Cell → Paragraph
-
Documentis the root document object. -
Sectionholds page-level settings. -
Paragraphis the text container. -
TextRangecontrols formatting for a continuous run of text within a paragraph.
Mapping Principle: JSON Structure Determines Word Form
The same JSON can produce very different Word documents. Defining the mapping rules before writing code is more reliable than adjusting the output while coding. Common correspondences are:
| JSON Structure | Word Element | Typical Use Case |
|---|---|---|
| Key-value pairs in a flat object | Paragraph (key: value) |
Configuration notes, personal cards, API snapshots |
| Array of objects | Table (keys → column headers; objects → rows) | Employee lists, orders, inventory |
| Nested object | Heading + indented paragraphs (recursive) | Structured reports, hierarchical summaries |
| Image path field | Inline image | Logos, avatars, chart snapshots |
The examples below progress from flat objects → object arrays → nested objects, with a runnable snippet for each scenario.
Scenario 1: Flat Object → Key-Value Paragraphs
This is the simplest mapping: each key-value pair becomes one paragraph. It works well for data with a fixed structure and a small number of fields.
import json
from spire.doc import Document, FileFormat, HorizontalAlignment
from spire.doc.common import *
json_str = '{"Project": "Data Pipeline", "Owner": "Zhang Wei", "Status": "In Progress", "Start Date": "2025-03-01"}'
data = json.loads(json_str)
doc = Document()
section = doc.AddSection()
title = section.AddParagraph()
title.AppendText("Project Information").CharacterFormat.Bold = True
title.Format.HorizontalAlignment = HorizontalAlignment.Center
for key, value in data.items():
para = section.AddParagraph()
para.AppendText(f"{key}: {value}").CharacterFormat.FontSize = 12
para.Format.AfterSpacing = 6
doc.SaveToFile("flat_object.docx", FileFormat.Docx)
doc.Close()
AppendText returns a TextRange object. Character-level formatting such as font, size, and bold is set through it. Paragraph-level spacing and alignment are set through paragraph.Format. The same formatting approach also applies inside table cells.
Scenario 2: Array of Objects → Word Table
An array of objects maps naturally to table rows, with column headers derived from the record keys. However, do not assume that every record has exactly the same keys. Real API responses often omit optional fields, and direct access with record[key] can cause the entire batch conversion to fail because of a single record.
A more robust approach is to scan all records first and collect the union of their keys as column headers:
import json
from spire.doc import (
Document, FileFormat, HorizontalAlignment, VerticalAlignment,
TableRowHeightType, Color
)
from spire.doc.common import *
records = [
{"Product": "Laptop", "Unit Price": 6999, "Stock": 45},
{"Product": "Wireless Mouse", "Unit Price": 199, "Stock": 200},
{"Product": "Mechanical Keyboard", "Unit Price": 549, "Stock": 120},
]
# Collect the union of keys so records with different fields are supported
all_keys = []
for record in records:
for key in record:
if key not in all_keys:
all_keys.append(key)
doc = Document()
section = doc.AddSection()
table = section.AddTable(True)
table.ResetCells(len(records) + 1, len(all_keys))
# Header row
header_row = table.Rows[0]
header_row.IsHeader = True
header_row.Height = 24
header_row.HeightType = TableRowHeightType.Exactly
for col, key in enumerate(all_keys):
cell = header_row.Cells[col]
cell.CellFormat.BackColor = Color.get_Gray()
cell.CellFormat.VerticalAlignment = VerticalAlignment.Middle
p = cell.AddParagraph()
p.Format.HorizontalAlignment = HorizontalAlignment.Center
tr = p.AppendText(key)
tr.CharacterFormat.Bold = True
tr.CharacterFormat.FontSize = 11
# Data rows
for row_idx, record in enumerate(records, start=1):
row = table.Rows[row_idx]
row.Height = 22
row.HeightType = TableRowHeightType.Exactly
for col, key in enumerate(all_keys):
cell = row.Cells[col]
cell.CellFormat.VerticalAlignment = VerticalAlignment.Middle
p = cell.AddParagraph()
p.Format.HorizontalAlignment = HorizontalAlignment.Center
p.AppendText(str(record.get(key, "—")))
doc.SaveToFile("array_table.docx", FileFormat.Docx)
doc.Close()
Two details are critical:
- The row count passed to
ResetCellsislen(records) + 1, notlen(records), because the header row also occupies a row. - Use
record.get(key, "—")instead of subscript access. The placeholder keeps the number of columns consistent and prevents a single missing field from crashing the entire script.
Scenario 3: Nested Object → Recursive Heading Structure
Nested JSON represents hierarchical relationships and should not be forced into a two-dimensional table. Use recursive traversal with a level parameter to control indentation and font size, creating a clear visual hierarchy:
from spire.doc import Document, FileFormat
from spire.doc.common import *
def add_nested(section, obj, level=0):
if isinstance(obj, dict):
for key, value in obj.items():
if isinstance(value, (dict, list)):
heading = section.AddParagraph()
tr = heading.AppendText(str(key))
tr.CharacterFormat.Bold = True
tr.CharacterFormat.FontSize = max(14 - level * 2, 10)
heading.Format.SetLeftIndent(level * 18)
add_nested(section, value, level + 1)
else:
para = section.AddParagraph()
para.Format.SetLeftIndent(level * 18)
tr = para.AppendText(f"{key}: {value}")
tr.CharacterFormat.FontSize = max(12 - level, 10)
elif isinstance(obj, list):
for item in obj:
add_nested(section, item, level)
nested = {
"Report": {
"Title": "Quarterly Operations Summary",
"Period": "2025 Q2",
"Metrics": {"Revenue": "5.8 million", "YoY": "+12%"},
"Notes": "All regions achieved positive growth"
}
}
doc = Document()
section = doc.AddSection()
add_nested(section, nested)
doc.SaveToFile("nested_report.docx", FileFormat.Docx)
doc.Close()
The recursive approach is depth-agnostic: whether the data is nested two levels or five levels, the logic remains the same. Keep the free version’s 500-paragraph limit in mind, however. Deeply nested structures or very large datasets can quickly consume the paragraph quota, so estimate the output size before using this in production.
Additional: Image Fields in JSON
If the JSON contains an image path, such as "logo": "images/logo.png", insert the image at the appropriate position with AppendPicture. Use Width and Height to control the display size in points:
from spire.doc import Paragraph
para = section.AddParagraph()
pic = para.AppendPicture("images/logo.png")
pic.Width = 80
pic.Height = 50
If the image belongs inside a table cell, attach AppendPicture to the paragraph returned by cell.AddParagraph(), just as you would for a normal text paragraph.
Conclusion
The key to converting JSON to Word is not the library itself, but deciding which layer of the data structure should map to which document element: key-value pairs become paragraphs, record collections become tables, and hierarchical relationships become recursive headings.
Once the mapping rules are defined, the Document → Section → Paragraph / Table object model provided by Free Spire.Doc is direct enough to apply those rules to a DOCX file.
Top comments (0)