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 
  • 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() 

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 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() 

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.