DEV Community

Jeremy K.
Jeremy K.

Posted on

Converting JSON Data to Word Documents with Python

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • Document is the root document object.
  • Section holds page-level settings.
  • Paragraph is the text container.
  • TextRange controls 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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Two details are critical:

  • The row count passed to ResetCells is len(records) + 1, not len(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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)