Word Documents with XWPF

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-09. This page was written against Apache POI 5.5.1 (the latest release on that date; the 6.0 line is in development and has not been released), Java 21 and later, Spring Boot 4.x on Spring Framework 7 and Jakarta EE 11. It is the first hands-on page about Word. It explains the object model that every other Word page builds on, how a .docx file is turned into XWPFDocument, XWPFParagraph and XWPFRun objects, how to create paragraphs and runs, how to format them (fonts, colours, alignment, spacing, styles, breaks, hyperlinks), how to read a document back in order, and where the high-level API ends and the raw XML beans begin. The examples use the Bookshelf scenario described in the section landing page: a loan-letter.docx written to a reader and a monthly-report.docx for the library staff.

The big picture (components, jars, API levels) is in Introduction & Architecture, the Maven and Gradle coordinates in Setup & Dependencies, and the physical structure of a .docx (the zip package, the main document part, the relationships) in File Formats: OOXML & OLE2. The following Word pages continue from here: Word Tables, Lists & Images, Word Headers, Footers, Sections & Fields, Word Templates & Placeholder Replacement, Word Charts & Embedded Objects and, for the old binary format, Legacy .doc with HWPF.

What XWPF is

XWPF ("XML Word Processor Format") is the Apache POI component that reads and writes Word 2007 and later documents (.docx, and the macro-enabled .docm). It lives in the poi-ooxml jar, in the package org.apache.poi.xwpf.usermodel. A .docx file is a zip package (OOXML, ECMA-376) whose main content is the part /word/document.xml, written in the WordprocessingML vocabulary. XWPF wraps that XML in a Java object model.

Two facts shape everything on this page:

  • XWPF is a thin layer over XML beans. Each high-level class (XWPFParagraph, XWPFRun, XWPFTable …​) holds an Apache XMLBeans object (CTP, CTR, CTTbl …​) generated from the OOXML schema and exposes the common properties as Java methods. Anything XWPF does not wrap is still reachable through the bean, at the price of working with the raw schema. The POI project describes XWPF as fairly stable but incomplete.

  • There is no layout engine. Word decides where lines and pages break when it opens the file. XWPF stores instructions ("this text is bold, this paragraph is centred") and never measures anything. It cannot tell you how many pages the document has, and it cannot render or convert the document to PDF.

XWPF and HWPF (the .doc binary format) are separate APIs with no common interface. A program that must handle both needs two code paths, or the text-extraction classes that hide the difference when only the text matters.

The model: document, body elements and runs

A Word document is an ordered list of body elements (paragraphs, tables, and content controls) inside a body. A paragraph contains runs, and a run contains the text and the character formatting that applies to it. A run is the smallest unit that carries formatting: the paragraph holds block properties (alignment, spacing, indentation, style), the run holds character properties (bold, italic, font, size, colour).

One paragraph of loan-letter.docx split into five runs, each with its own formatting and text, and the simplified document.xml that stores them
Class Represents Main operations

XWPFDocument

The whole file: the body, the styles, numbering, headers, footers, footnotes, the core properties and the package.

createParagraph, createTable, getBodyElements, getParagraphs, getTables, getStyles, createStyles, getProperties, write, close

IBodyElement

Common interface of everything that can sit in the body or in a table cell. Reports its BodyElementType (PARAGRAPH, TABLE, CONTENTCONTROL).

getElementType, getBody, getPart

XWPFParagraph

A block of text that Word wraps and breaks as a unit (w:p). Owns the paragraph properties.

createRun, getRuns, getIRuns, setAlignment, setSpacingAfter, setIndentationLeft, setStyle, getText

XWPFRun

A stretch of text with uniform character formatting (w:r).

setText, setBold, setItalic, setFontFamily, setFontSize, setColor, setUnderline, addBreak, addTab

XWPFHyperlinkRun

A run inside a hyperlink (w:hyperlink). Extends XWPFRun.

getHyperlink, getAnchor, plus everything a run has

XWPFTable

A table: rows, cells, and in each cell again paragraphs and tables. Detailed in Word Tables, Lists & Images.

createRow, getRows, getRow

XWPFSDT

A block-level content control (structured document tag), often used for form fields.

getTag, getTitle, getContent

XWPFStyles / XWPFStyle

The style definitions of the document (/word/styles.xml) and one definition.

getStyle, getStyleWithName, addStyle, styleExist

XWPFHeader / XWPFFooter

Header and footer parts. Covered in Word Headers, Footers, Sections & Fields.

createParagraph, getParagraphs

The diagram shows how the main classes relate. Note that a table cell is itself a body (IBody): it contains paragraphs and nested tables, which is why the same XWPFParagraph class appears at the document level and inside a cell.

classDiagram class IBody { <<interface>> getBodyElements() List getParagraphs() List getTables() List createParagraph() XWPFParagraph createTable() XWPFTable } class IBodyElement { <<interface>> getElementType() BodyElementType } class XWPFDocument { createParagraph() XWPFParagraph createTable(rows, cols) XWPFTable getStyles() XWPFStyles createStyles() XWPFStyles getProperties() POIXMLProperties write(OutputStream) close() } class XWPFParagraph { createRun() XWPFRun getRuns() List setAlignment(ParagraphAlignment) setStyle(styleId) getText() String } class XWPFRun { setText(value) setBold(boolean) setFontFamily(name) setFontSize(points) addBreak(BreakType) } class XWPFHyperlinkRun class XWPFTable { createRow() XWPFTableRow getRows() List } class XWPFTableCell { getParagraphs() List } class XWPFSDT { getTag() String getContent() ISDTContent } class XWPFStyles { getStyle(styleId) XWPFStyle getStyleWithName(name) XWPFStyle addStyle(XWPFStyle) } class XWPFStyle class XWPFHeader class XWPFFooter IBody <|.. XWPFDocument IBody <|.. XWPFTableCell IBody <|.. XWPFHeader IBody <|.. XWPFFooter IBodyElement <|.. XWPFParagraph IBodyElement <|.. XWPFTable IBodyElement <|.. XWPFSDT XWPFRun <|-- XWPFHyperlinkRun XWPFDocument "1" --> "*" IBodyElement : body XWPFDocument "1" --> "0..1" XWPFStyles : styles XWPFStyles "1" --> "*" XWPFStyle : defines XWPFParagraph "1" --> "*" XWPFRun : runs XWPFTable "1" --> "*" XWPFTableCell : cells XWPFTableCell "1" --> "*" IBodyElement : content

How the classes map to the package:

XWPF object OOXML part and element

XWPFDocument

The package, with /word/document.xml as its main part, /word/styles.xml, /word/numbering.xml, /word/settings.xml, headers, footers and media parts reached through relationships

XWPFParagraph

<w:p> in the body, with block properties in <w:pPr>

XWPFRun

<w:r> in the paragraph, with character properties in <w:rPr> and text in one or more <w:t> elements

XWPFTable

<w:tbl> with <w:tr> rows and <w:tc> cells

XWPFStyles, XWPFStyle

/word/styles.xml and a <w:style w:styleId="..."> element inside it

XWPFHyperlinkRun

<w:hyperlink r:id="..."> containing a <w:r>; the target URI lives in the relationships of the main part

Choosing how to open and write documents

Creating an empty document is one constructor call. Opening an existing file has three forms:

Form Behaviour

new XWPFDocument()

An empty document with one empty body and no styles part. Add content and call write.

new XWPFDocument(InputStream)

Reads the whole package from a stream. Closing the document does not write anything back. Use it for uploads.

new XWPFDocument(OPCPackage)

Uses a package you opened yourself. With OPCPackage.open(file, PackageAccess.READ) the file is opened read-only.

Opening a File through OPCPackage.open(File) uses read-write access by default, and closing that package writes the changes back into the same file. For code that only reads a document, always pass PackageAccess.READ. Closing the XWPFDocument closes the underlying package, so a single try-with-resources on the document is enough.

Writing is document.write(OutputStream). It serialises the whole in-memory model into a new zip stream, so it can be called several times and the document stays usable afterwards.

Creating loan-letter.docx

The first example writes the letter that the Bookshelf library sends when a loan is about to expire. It uses plain paragraphs and runs with direct formatting; everything is explained in the following sections.

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;

public final class LoanLetterWriter {

    private LoanLetterWriter() {
    }

    public static void write(Path target, String reader, String title, String dueDate)
            throws IOException {
        try (XWPFDocument document = new XWPFDocument();
             OutputStream out = Files.newOutputStream(target)) {

            XWPFParagraph header = document.createParagraph();
            header.setAlignment(ParagraphAlignment.RIGHT);
            XWPFRun library = header.createRun();
            library.setText("Bookshelf Public Library");
            library.setBold(true);
            library.setFontFamily("Calibri");
            library.setFontSize(14);
            library.addBreak();
            XWPFRun address = header.createRun();
            address.setText("12 Reading Lane, 28001 Madrid");
            address.setFontSize(10);
            address.setColor("555555");

            XWPFParagraph greeting = document.createParagraph();
            greeting.setSpacingBefore(600);   // twips: 30 pt
            greeting.setSpacingAfter(200);    // twips: 10 pt
            greeting.createRun().setText("Dear " + reader + ",");

            XWPFParagraph body = document.createParagraph();
            body.setAlignment(ParagraphAlignment.BOTH);
            body.setSpacingAfter(200);
            body.createRun().setText("Your loan of ");
            XWPFRun book = body.createRun();
            book.setText(title);
            book.setBold(true);
            book.setItalic(true);
            body.createRun().setText(" is due on ");
            XWPFRun due = body.createRun();
            due.setText(dueDate);
            due.setBold(true);
            due.setColor("C0392B");
            body.createRun().setText(". You can renew it once at the desk or online.");

            XWPFParagraph closing = document.createParagraph();
            closing.createRun().setText("Kind regards,");
            closing.createRun().addBreak();
            closing.createRun().setText("The Bookshelf team");

            document.write(out);
        }
    }
}

Source: POI XWPF Quick Guide and the official SimpleDocument example.

Two behaviours of this example deserve attention. closing.createRun().setText(...) followed by closing.createRun().addBreak() produces three runs in one paragraph; the break is a run that contains a <w:br/> element and no text. And the resulting file contains exactly the formatting that was asked for, no more: there is no "Normal" style definition, so Word falls back to its own defaults (typically Calibri 11 pt, or whatever the reader’s Word installation uses for documents without a styles part).

Paragraphs

document.createParagraph() appends a new empty paragraph at the end of the body and returns it. A paragraph with no run is still valid and shows as an empty line. The paragraph owns the block-level formatting:

Method Unit or type Effect

setAlignment(ParagraphAlignment)

LEFT, CENTER, RIGHT, BOTH (justified), DISTRIBUTE …​

Horizontal alignment. BOTH justifies the text.

setSpacingBefore(int), setSpacingAfter(int)

Twips (one twentieth of a point)

Space above and below the paragraph. Both are ignored between two paragraphs of the same style when the style says "do not add space between paragraphs of the same style".

setSpacingBetween(double), setSpacingBetween(double, LineSpacingRule)

Multiple of the line, or points, depending on the rule

Line spacing. LineSpacingRule.AUTO interprets the number as a multiple (1.5 is a line and a half).

setIndentationLeft(int), setIndentationRight(int)

Twips

Left and right indent of the whole paragraph.

setFirstLineIndent(int), setIndentFromLeft, setIndentFromRight

Twips

First-line indent. A negative first line with a positive left indent produces a hanging indent.

setPageBreak(boolean)

boolean

Starts the paragraph on a new page (the "page break before" property). Not the same as a break run; see below.

setKeepNext(boolean), setKeepLines(boolean)

boolean

Keep with the next paragraph or keep all lines on one page.

setStyle(String)

Style ID

Applies a paragraph style. See the styles section.

setBorderBottom(Borders) and friends

Borders enum

Paragraph borders.

setWordWrapped(boolean)

boolean

Allow wrapping of Latin text in the middle of a word.

Because Word measures lengths in twips in the file, so does XWPF: 20 twips are one point and 1,440 twips are one inch. There is no unit conversion in the paragraph methods, so a small helper makes code readable:

final class Twips {

    private Twips() {
    }

    static int fromPoints(double points) {
        return (int) Math.round(points * 20);
    }

    static int fromCentimetres(double cm) {
        return (int) Math.round(cm / 2.54 * 1440);
    }
}

A paragraph also reports what it contains. paragraph.getText() concatenates the text of its runs (including hyperlink runs and content controls), paragraph.getRuns() returns the runs as a List<XWPFRun>, and paragraph.getIRuns() returns every run-level object as IRunElement, including some that getRuns does not model as XWPFRun. To insert a run at a position use insertNewRun(int); to delete one use removeRun(int). Both take the position in the list returned by getRuns(), not in the getIRuns() list; POI updates the getIRuns() list as a side effect.

Runs and text

A run is created with paragraph.createRun(). It starts empty and unformatted. The most important method is also the easiest to misuse:

Call Behaviour

run.setText(String)

Appends a new text element to the run. Calling it twice leaves both strings in the run.

run.setText(String, int pos)

Replaces the text element at index pos (0 is the first). With pos equal to the current number of text elements it appends; a larger value throws an exception.

run.getText(int pos)

The text element at pos, or null.

run.text()

All the text of the run joined, including the characters produced by tabs and breaks.

run.toString()

The same joined text; handy for debugging.

run.addBreak(), run.addBreak(BreakType)

A line break (BreakType.TEXT_WRAPPING is the default), a page break, or a column break, as a child of the run.

run.addTab()

A tab character (<w:tab/>).

run.addCarriageReturn()

A carriage return element (<w:cr/>).

The consequence: to replace the text of a run, call run.setText(newValue, 0). Calling run.setText(newValue) on a run that already has text produces a run holding the old and the new text one after the other. This is the most common mistake in code that edits existing documents, and it is why the template page spends so long on replacing text.

Control characters inside the string are not instructions. A "\n" inside setText does not start a new line in Word, and a "\t" is not a reliable tab. Use addBreak() and addTab() to insert real break and tab elements, one run per line when the text comes from a multi-line string:

static void addMultilineText(XWPFParagraph paragraph, String text) {
    XWPFRun run = paragraph.createRun();
    String[] lines = text.split("\\R", -1);
    for (int i = 0; i < lines.length; i++) {
        if (i > 0) {
            run.addBreak();
        }
        run.setText(lines[i], run.getCTR().sizeOfTArray());
    }
}

Leading and trailing spaces are preserved: POI adds xml:space="preserve" to the text element when the string starts or ends with white space (see the first run in the figure). Characters that are not valid in XML 1.0 (most control characters below U+0020) cannot be stored; strip them from user-provided text before writing, otherwise the document is rejected by Word as corrupt or the write fails.

Character formatting

All character formatting lives on the run. The setters create the property element in the run’s <w:rPr> as needed:

Method Argument Effect

setBold(boolean), setItalic(boolean)

boolean

Bold and italic.

setUnderline(UnderlinePatterns)

SINGLE, DOUBLE, DOTTED, WAVE, NONE …​

Underline style.

setStrikeThrough(boolean), setDoubleStrikethrough(boolean)

boolean

Strike-through.

setFontFamily(String)

Font name, for example "Calibri"

Sets the Latin font. setFontFamily(String, FontCharRange) sets the font for another script (East Asian, complex script).

setFontSize(int)

Points

Size in points. XWPF converts to the half-points that the file stores. getFontSize returns -1 when the run has no explicit size (it inherits).

setColor(String)

Hexadecimal "RRGGBB" without #

Text colour.

setTextHighlightColor(String)

A Word highlight name such as "yellow"

Marker-style highlight (a fixed palette, unlike shading).

setSubscript(VerticalAlign)

SUPERSCRIPT, SUBSCRIPT, BASELINE

Raised or lowered text.

setCapitalized(boolean), setSmallCaps(boolean)

boolean

All capitals and small capitals.

setCharacterSpacing(int)

Twips

Expanded or condensed spacing.

setStyle(String)

Character style ID

Applies a character style defined in the styles part.

Getters mirror the setters but behave differently from what one expects. run.isBold() returns true only when the run itself says so. A run that is bold because its paragraph style or character style makes it bold reports false, and getFontFamily() returns null unless the run sets a font. XWPF does not resolve style inheritance for you; a program that wants the effective formatting has to walk run, character style, paragraph style and document defaults itself.

The following helper creates runs for the monthly report. It shows the habit that keeps formatting code readable: a small, named method per kind of text rather than repeated setters.

import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;

final class Runs {

    private Runs() {
    }

    static XWPFRun plain(XWPFParagraph paragraph, String text) {
        XWPFRun run = paragraph.createRun();
        run.setText(text);
        return run;
    }

    static XWPFRun bold(XWPFParagraph paragraph, String text) {
        XWPFRun run = plain(paragraph, text);
        run.setBold(true);
        return run;
    }

    static XWPFRun emphasis(XWPFParagraph paragraph, String text) {
        XWPFRun run = plain(paragraph, text);
        run.setItalic(true);
        run.setUnderline(UnderlinePatterns.SINGLE);
        return run;
    }

    static XWPFRun status(XWPFParagraph paragraph, String text, boolean overdue) {
        XWPFRun run = plain(paragraph, text);
        run.setBold(true);
        run.setColor(overdue ? "C0392B" : "1E8449");
        return run;
    }
}

Source: XWPFRun Javadoc.

Styles

Direct formatting (setting bold and size on every run) is easy but does not scale: changing the look of all headings means editing all headings. Word’s answer is styles: named bundles of paragraph and character properties defined once in /word/styles.xml and applied by name. XWPF models them with XWPFStyles (the collection) and XWPFStyle (one definition).

The central rule: setStyle takes the style ID, not the name shown in Word. The ID is the w:styleId attribute and is usually the display name without spaces: the style shown as "Heading 1" has the ID Heading1, "List Paragraph" has ListParagraph. Styles can also have a different, user-defined ID entirely. XWPFStyles can look up both:

XWPFStyles styles = document.getStyles();                   // null when the document has no styles part
XWPFStyle byId = styles.getStyle("Heading1");               // by ID
XWPFStyle byName = styles.getStyleWithName("heading 1");    // by name; Word stores built-in names in lower case
boolean defined = styles.styleExist("Heading1");

The second rule is the surprise for newcomers: a new XWPFDocument() has no style definitions at all. Calling paragraph.setStyle("Heading1") on such a document writes the reference but there is nothing for it to refer to, so Word shows the paragraph in its normal formatting. There are three ways to get real styles:

Approach How Trade-off

Start from a template

Open a .docx designed in Word that already defines the styles (headings, body text, table styles) and add content to it. See Word Templates & Placeholder Replacement.

The best option for corporate documents: designers own the look and code only picks style IDs.

Add style definitions in code

Build a CTStyle bean and add it with XWPFStyles.addStyle. See the example below.

Self-contained, but you write schema-level code and have to define each style fully.

Direct formatting

Set properties on every run and paragraph.

Fine for a few elements; nothing to maintain, but nothing for the user to restyle either. The loan letter above does this.

The example defines a heading style programmatically and uses it for the monthly report. The outline level makes the paragraphs appear in Word’s navigation pane and lets a table of contents (covered on the headers, footers and fields page) pick them up.

import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.YearMonth;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFStyle;
import org.apache.poi.xwpf.usermodel.XWPFStyles;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STStyleType;

public final class MonthlyReportWriter {

    private static final String HEADING_1 = "BookshelfHeading1";
    private static final String HEADING_2 = "BookshelfHeading2";

    private MonthlyReportWriter() {
    }

    public static void write(Path target, YearMonth month, int loans, int returns, int overdue)
            throws IOException {
        try (XWPFDocument document = new XWPFDocument();
             OutputStream out = Files.newOutputStream(target)) {

            XWPFStyles styles = document.createStyles();
            addHeadingStyle(styles, HEADING_1, "Bookshelf Heading 1", 36, 0);
            addHeadingStyle(styles, HEADING_2, "Bookshelf Heading 2", 28, 1);

            XWPFParagraph title = document.createParagraph();
            title.setStyle(HEADING_1);
            title.createRun().setText("Monthly report " + month);

            XWPFParagraph intro = document.createParagraph();
            intro.setAlignment(ParagraphAlignment.BOTH);
            intro.createRun().setText("This report summarises the activity of the Bookshelf library "
                    + "during the month. Figures come from the loan register.");

            XWPFParagraph section = document.createParagraph();
            section.setStyle(HEADING_2);
            section.createRun().setText("Loans");

            XWPFParagraph figures = document.createParagraph();
            figures.createRun().setText("Loans started: " + loans + ". Returns: " + returns + ". ");
            var warning = figures.createRun();
            warning.setText("Overdue: " + overdue);
            warning.setBold(true);
            warning.setColor(overdue > 0 ? "C0392B" : "1E8449");

            document.write(out);
        }
    }

    private static void addHeadingStyle(XWPFStyles styles, String id, String name, int halfPoints,
            int outlineLevel) {
        CTStyle ctStyle = CTStyle.Factory.newInstance();
        ctStyle.setStyleId(id);
        ctStyle.setType(STStyleType.PARAGRAPH);
        ctStyle.addNewName().setVal(name);

        CTPPrGeneral paragraphProperties = ctStyle.addNewPPr();
        paragraphProperties.addNewKeepNext();
        paragraphProperties.addNewOutlineLvl().setVal(BigInteger.valueOf(outlineLevel));
        paragraphProperties.addNewSpacing().setBefore(BigInteger.valueOf(240));
        paragraphProperties.getSpacing().setAfter(BigInteger.valueOf(120));

        CTRPr runProperties = ctStyle.addNewRPr();
        runProperties.addNewB();
        runProperties.addNewColor().setVal("1F3864");
        runProperties.addNewSz().setVal(BigInteger.valueOf(halfPoints));

        styles.addStyle(new XWPFStyle(ctStyle, styles));
    }
}

Source: XWPFStyles Javadoc, XWPFStyle Javadoc and the official SimpleDocument example, which adds a style the same way.

In addHeadingStyle the size is in half-points (36 is an 18 pt heading), because the CTStyle bean is the raw schema and the unit conversion that XWPFRun.setFontSize does for you is not available. This is the first appearance of the low-level layer; the section "Dropping down to the CT beans" below explains when it is worth it.

A few more points on styles:

  • A run can also have a style: run.setStyle("Strong") applies a character style. Paragraph styles and character styles are different kinds (STStyleType.PARAGRAPH and STStyleType.CHARACTER) and are not interchangeable.

  • Styles can inherit from another one through basedOn. Word’s built-in styles are all based on "Normal", which a document without a styles part does not define. The example above leaves out basedOn for this reason.

  • A template can define a style that has the same name but another ID than you expect, because the ID is derived when the style is first created. Look up by name with getStyleWithName and read getStyleId() from the result instead of hard-coding IDs whenever the template is not under your control.

  • XWPFStyles.getDefaultRunStyle() and getDefaultParagraphStyle() expose the document defaults for diagnostics.

Breaks, tabs and page breaks

Line breaks, page breaks and column breaks are all <w:br> elements inside a run, selected by BreakType:

BreakType Effect

TEXT_WRAPPING

A line break inside the paragraph (the default of addBreak()). Equivalent to Shift+Enter in Word. With a floating image it also clears the text wrapping.

PAGE

The following content starts on a new page. The break is inside the current paragraph; Word starts the next line on a new page, which leaves the remainder of the paragraph on the new page.

COLUMN

The following content starts in the next column in a multi-column section.

There are two ways to start a page, and the difference matters for the report. run.addBreak(BreakType.PAGE) puts the break in the middle of a paragraph, so an empty paragraph that only holds the break leaves a trailing empty line at the top of the next page in some Word versions. paragraph.setPageBreak(true) sets the "page break before" property on the paragraph itself, which starts that paragraph on a fresh page without an extra empty line. For a chapter title prefer setPageBreak(true) on the title paragraph; reserve the break run for a break inside running text.

XWPFParagraph chapter = document.createParagraph();
chapter.setPageBreak(true);          // "page break before"
chapter.setStyle(HEADING_1);
chapter.createRun().setText("Overdue loans");

XWPFParagraph tabs = document.createParagraph();
XWPFRun cell = tabs.createRun();
cell.setText("Title");
cell.addTab();
cell.setText("Author", cell.getCTR().sizeOfTArray());
cell.addTab();
cell.setText("Due", cell.getCTR().sizeOfTArray());

Page layout, section breaks, orientation and margins are not paragraph properties; they belong to the section and are covered in Word Headers, Footers, Sections & Fields.

XWPFParagraph.createHyperlinkRun(String uri) creates a hyperlink to an external address. POI registers the target as an external relationship of the main document part and returns an XWPFHyperlinkRun, which is a normal run for formatting purposes. Word does not style the link by itself: a link without formatting looks like plain text, so apply the usual blue and underline explicitly or through a character style such as "Hyperlink".

XWPFParagraph catalogue = document.createParagraph();
catalogue.createRun().setText("Browse the catalogue at ");

XWPFHyperlinkRun link = catalogue.createHyperlinkRun("https://library.example.org/catalogue");
link.setText("library.example.org");
link.setColor("0563C1");
link.setUnderline(UnderlinePatterns.SINGLE);

catalogue.createRun().setText(".");

Reading the target goes through the document, because the URI is stored in the relationships:

for (XWPFRun run : paragraph.getRuns()) {
    if (run instanceof XWPFHyperlinkRun linkRun) {
        XWPFHyperlink hyperlink = linkRun.getHyperlink(document);
        String target = hyperlink != null ? hyperlink.getURL() : linkRun.getAnchor();
        System.out.println(linkRun.text() + " -> " + target);
    }
}

An internal link to a bookmark has no relationship: it carries an anchor name, returned by getAnchor(). Creating bookmarks and internal links needs the CT beans (CTBookmark, CTHyperlink); XWPF has no higher-level API for them. Never build hyperlink targets from unvalidated input that may contain a javascript: or a file: scheme; allow https (and mailto) explicitly.

Reading a document in order

A document can interleave paragraphs and tables, and any program that processes a form (the Bookshelf book-request-form.docx) needs the original order. document.getParagraphs() and document.getTables() return two separate lists and lose the relative position. The ordered list is getBodyElements():

Method Returns

getBodyElements()

Every top-level element in document order: XWPFParagraph, XWPFTable, XWPFSDT (content controls).

getParagraphs()

Only the top-level paragraphs. Paragraphs inside tables or content controls are not included.

getTables()

Only the top-level tables.

getBodyElementsIterator(), getParagraphsIterator()

Iterators over the same data.

getParagraphPos(int), getPosOfParagraph(XWPFParagraph)

Conversions between positions in the lists. They are different index spaces.

The next example reads book-request-form.docx and prints an outline of its structure. It opens the file read-only (PackageAccess.READ) so that closing it cannot modify the file, and it uses a switch on the BodyElementType enum.

import java.io.IOException;
import java.nio.file.Path;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFSDT;
import org.apache.poi.xwpf.usermodel.XWPFTable;

public final class DocumentOutline {

    private DocumentOutline() {
    }

    public static void print(Path file) throws IOException, InvalidFormatException {
        try (OPCPackage pkg = OPCPackage.open(file.toFile(), PackageAccess.READ);
             XWPFDocument document = new XWPFDocument(pkg)) {

            int index = 0;
            for (IBodyElement element : document.getBodyElements()) {
                switch (element.getElementType()) {
                    case PARAGRAPH -> {
                        XWPFParagraph paragraph = (XWPFParagraph) element;
                        System.out.printf("%2d paragraph style=%s runs=%d text=%s%n", index,
                                paragraph.getStyleID(), paragraph.getRuns().size(), paragraph.getText());
                    }
                    case TABLE -> {
                        XWPFTable table = (XWPFTable) element;
                        System.out.printf("%2d table rows=%d%n", index, table.getNumberOfRows());
                    }
                    case CONTENTCONTROL -> {
                        XWPFSDT control = (XWPFSDT) element;
                        System.out.printf("%2d content control tag=%s title=%s value=%s%n", index,
                                control.getTag(), control.getTitle(), control.getContent().getText());
                    }
                    default -> System.out.printf("%2d other %s%n", index, element.getElementType());
                }
                index++;
            }
        }
    }
}

For a document such as the request form the output looks like this (the style ID is null for a paragraph without an explicit style, which Word treats as "Normal"):

 0 paragraph style=Title runs=1 text=Book request form
 1 paragraph style=null runs=3 text=Requested by: Ana García
 2 content control tag=isbn title=ISBN value=978-84-376-0494-7
 3 table rows=4
 4 paragraph style=null runs=1 text=Please return the form to the desk.

Notice runs=3 for "Requested by: Ana García": a text that reads as one line was stored as three runs. That is normal, and it is why searching or replacing text in a paragraph run by run fails for anything that spans a boundary. The reading code here only prints, so it is unaffected, but the placeholder replacement on the templates page is built around this fact, and the import of structured data from Word forms builds on the same loop.

Several details about reading:

  • A paragraph inside a table cell is reached through table.getRows(), row.getTableCells() and cell.getParagraphs() (or cell.getBodyElements() for nested tables). The loop above reports only the table, not its content.

  • paragraph.getText() includes the text of hyperlink runs and simple fields, but a complex field (a PAGE number, a table of contents) contributes its stored result text or nothing, depending on how Word saved it. Treat extracted text as a best effort, not as a faithful rendering.

  • Revision marks (tracked changes) are in the XML as insertions and deletions. getText() returns the inserted text and, depending on the construct, may omit text inside w:ins wrappers that XWPF does not model. Documents with tracked changes should have their revisions accepted before processing, or be read through the XML beans.

  • Floating text boxes and shapes hold their paragraphs in drawing markup (w:txbxContent) that is not part of the body element list. Their text can be reached through XWPFRun.getCTR() and an XPath over the run’s XML, or with the text extractors described on the text-extraction page of this section.

Document properties

The core, extended and custom properties are available through document.getProperties(), which returns a POIXMLProperties. They are the data Word shows under File > Info and that the operating system uses for search.

import org.apache.poi.ooxml.POIXMLProperties;

POIXMLProperties.CoreProperties core = document.getProperties().getCoreProperties();
core.setTitle("Monthly report 2026-09");
core.setCreator("Bookshelf reporting service");
core.setSubjectProperty("Library activity");
core.setKeywords("loans, returns, overdue");
core.setCreated(java.util.Optional.of(java.util.Date.from(java.time.Instant.now())));

document.getProperties().getCustomProperties().addProperty("report-month", "2026-09");

Do not put personal data or internal paths into these properties unless that is intended: the creator field is the first thing that leaks when documents are published. Documents created from a template inherit the template’s properties, including its author and company, so overwrite them explicitly.

Modifying an existing document

Opening an existing .docx, changing it, and writing the result under another name is the same model:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;

public final class LoanLetterEditor {

    private LoanLetterEditor() {
    }

    /** Appends a reminder paragraph and writes the result to a different file. */
    public static void addReminder(Path source, Path target, String reminder) throws IOException {
        try (InputStream in = Files.newInputStream(source);
             XWPFDocument document = new XWPFDocument(in);
             OutputStream out = Files.newOutputStream(target)) {

            XWPFParagraph paragraph = document.createParagraph();
            XWPFRun run = paragraph.createRun();
            run.setText(reminder);
            run.setItalic(true);

            // Remove a trailing empty paragraph left behind by an earlier edit.
            int last = document.getBodyElements().size() - 1;
            if (last > 0 && document.getBodyElements().get(last - 1) instanceof XWPFParagraph previous
                    && previous.getText().isBlank() && previous.getRuns().isEmpty()) {
                document.removeBodyElement(last - 1);
            }

            document.write(out);
        }
    }
}

Removing and reordering elements has two traps. document.removeBodyElement(int) takes an index in the body element list (the combined list), not in the paragraph list, so mixing the two index spaces removes the wrong thing. And document.setParagraph(XWPFParagraph, int) replaces the XML of a paragraph at an index of the paragraph list. Since a paragraph object keeps a reference to its XML node, a removed paragraph must not be used afterwards.

Inserting in the middle of a document uses a cursor: document.insertNewParagraph(XmlCursor) and insertNewTbl(XmlCursor) create an element at the cursor position of a paragraph’s XML (paragraph.getCTP().newCursor()). This is the pattern for "insert a paragraph at the marker paragraph" shown in Word Templates & Placeholder Replacement, and the XmlCursor should be released (close() or dispose(), depending on the XMLBeans version) after use.

Dropping down to the CT beans

Every XWPF object exposes its XML bean: paragraph.getCTP() returns a CTP, run.getCTR() returns a CTR, table.getCTTbl() a CTTbl, document.getDocument() a CTDocument1. These classes live in the package org.openxmlformats.schemas.wordprocessingml.x2006.main, are generated by XMLBeans from the ECMA-376 schema, and expose the complete file format. Use them when XWPF has no method for what you need:

Need Typical beans

Style definitions (the example above)

CTStyle, CTPPrGeneral, CTRPr, CTFonts, CTHpsMeasure

Bookmarks and internal links

CTBookmark, CTHyperlink

Section properties (page size, margins, orientation)

CTSectPr, CTPageSz, CTPageMar

Table cell merges, borders, widths

CTTcPr, CTTblPr, CTTcBorders

Fields and content controls

CTSimpleField, CTFldChar, CTSdtBlock

Text direction, language, text effects

CTRPr children such as CTLanguage, CTTextEffect

Working at this level has three costs you should plan for.

  • The right jar. The beans for the whole schema exist in two forms. poi-ooxml-lite (about 6 MB) contains only the schema classes that the POI code itself uses; poi-ooxml-full (about 16 MB) contains all of them. A call to a bean class or method that the lite jar left out fails at run time with a NoClassDefFoundError, not at compile time. If your code uses CT* classes beyond what XWPF touches, depend on poi-ooxml-full instead of the lite artifact. See Setup & Dependencies for the coordinates.

  • Units and conventions of the raw schema. Sizes are in half-points or twips as integers (often BigInteger in the generated setters), booleans are small CTOnOff elements (their absence is false, addNewB() alone is true), colours are hexadecimal strings, and enumerations come from ST* types. Setters on the beans do not validate that the combination makes sense to Word.

  • Order matters. The schema defines a sequence for the children of w:pPr, w:rPr and w:sectPr. The XMLBeans addNew... methods insert in the schema order, so beans created through them are fine, but hand-written XML strings inserted through a cursor must keep the order, or Word reports the file as corrupt even though POI reads it.

The generated beans are tied to the POI and XMLBeans versions in use, so code that relies on CT* details should be covered by a test that opens the produced file again (see the testing section) whenever POI is upgraded.

What XWPF does not provide

  • No layout and no pagination. XWPF cannot compute page numbers, text widths, table row heights, or the number of pages, and it does not render anything. The same applies to the question of whether text fits in a table cell.

  • No conversion to PDF. Neither .docx nor .doc can be converted to PDF by POI. The poi-scratchpad HWPF converter package only produces HTML and XSL-FO for old .doc files. Converting a Word document to PDF needs another tool (LibreOffice in headless mode, a commercial library, or a different generation approach, see the PDF Generation section of this site).

  • No template or mail-merge API. There is no replace("${name}", value) method. Placeholder replacement is code you write, and Word’s habit of splitting text across runs is what makes it non-trivial.

  • No style-resolution API. XWPF does not compute the effective formatting of a run from the inheritance chain of styles.

  • No guaranteed round-trip of unknown content. XWPF keeps the parts it does not understand, but features that it does not model (some drawings, WordArt, advanced fields, comments threads in new Word versions) can be altered or dropped in unusual combinations. Test on real templates.

  • Not a validator. POI writes what you ask for. A document that Word rejects can be produced by invalid combinations of bean values; open the result in Word (or LibreOffice) as part of the test of any generator.

  • Thread safety. An XWPFDocument is not thread safe. Use one instance per request and never share it.

Memory and performance

XWPF builds the complete document in memory as XML beans, with no streaming writer comparable to SXSSF for spreadsheets. As a rough rule the memory used is a multiple of the file size (typically several times for text, much more with large embedded images, which stay in memory as byte arrays until write). Practical consequences:

  • Generate reports with hundreds of pages, not millions of rows. A table of 100,000 rows in Word is a bad idea for other reasons as well (Word itself becomes slow); export such data to a spreadsheet instead.

  • An uploaded .docx is a zip: apply the size limits and zip-bomb protections described on the security page of this section before opening it. Use a bounded input stream for uploads.

  • Create one XWPFDocument per request, write it to the response (or a temp file) and close it in the same try-with-resources block. A document kept in a field of a Spring singleton is a memory leak and a thread-safety bug.

  • write serialises the full model; calling it repeatedly for progress reports is wasteful.

Pitfalls

Pitfall Remedy

setText twice shows both texts

Use setText(value, 0) to replace the first text element.

\n or \t in a string do not format

Use addBreak() and addTab().

setStyle("Heading 1") has no effect

Use the style ID (Heading1), and make sure the style is defined in the document (template or XWPFStyles.addStyle).

A new document has no headings

The default XWPFDocument() has no style definitions. Start from a template, or define styles as shown above.

isBold() is false on a bold-looking run

The boldness comes from a style. XWPF does not resolve inheritance.

Closing a package opened for read-write rewrites the file

Open read-only: OPCPackage.open(file, PackageAccess.READ).

A text split in several runs

Do not search run by run; use the techniques of the templates page.

NoClassDefFoundError for a CT* class

Depend on poi-ooxml-full instead of poi-ooxml-lite.

Empty paragraph at the top of a page

Use setPageBreak(true) on the paragraph instead of an empty paragraph with a page break run.

Document rejected by Word

Invalid XML characters in text, or children added to a w:pPr in the wrong order.

Metadata leaks

Overwrite creator and company, which are inherited from templates.

Changes across POI versions

Version What matters for XWPF

3.x

Public integer constants for some enumerations, and the first XWPF classes. Many of today’s convenience methods did not exist, so old tutorials use the CT* beans for tasks that now have methods.

4.0 and 4.1

Java 8 as the minimum. More run and paragraph convenience methods were added over the 4.1 releases, among them hyperlink runs.

5.0

The OOXML dependency was reorganised: the XML schema classes are split into poi-ooxml-lite and poi-ooxml-full.

5.1 to 5.5

Logging through log4j-api only; XMLBeans 5.x; security hardening of the zip and XML parsing. 5.5.1 is the latest release as of 2026-10-09.

6.0 (in development, not released)

The POI site announces JDK 17 as the minimum, the removal of APIs deprecated in 5.x and further hardening. Code that compiles without deprecation warnings on 5.5.1 is the best preparation. Check the change log when 6.0 ships.

Testing a generator

A generator is correct when the result opens in Word, not when write returned. Two cheap tests catch most regressions: write the file to a temporary location and read it back with XWPF, asserting the text and the style IDs of the paragraphs, and open it with another independent reader (LibreOffice in headless mode, or a different library) in an integration test. AssertJ and JUnit are enough:

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.nio.file.Path;
import java.time.YearMonth;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class MonthlyReportWriterTest {

    @TempDir
    Path directory;

    @Test
    void writesTitleWithHeadingStyle() throws Exception {
        Path file = directory.resolve("monthly-report.docx");
        MonthlyReportWriter.write(file, YearMonth.of(2026, 9), 120, 98, 4);

        try (OPCPackage pkg = OPCPackage.open(file.toFile(), PackageAccess.READ);
             XWPFDocument document = new XWPFDocument(pkg)) {
            var title = document.getParagraphs().get(0);
            assertThat(title.getStyleID()).isEqualTo("BookshelfHeading1");
            assertThat(title.getText()).isEqualTo("Monthly report 2026-09");
            assertThat(document.getStyles().styleExist("BookshelfHeading2")).isTrue();
        }
    }
}

Summary

  • A .docx is an XWPFDocument holding body elements; a paragraph holds runs; a run holds text and character formatting. Block formatting is on the paragraph, character formatting on the run.

  • createParagraph and createRun append; setText(value) appends too, setText(value, 0) replaces.

  • Lengths are twips (20 per point); font sizes in the high-level API are points, in the beans half-points.

  • Styles are applied by ID and must exist in the document: a new XWPFDocument has none.

  • getBodyElements() is the only way to read paragraphs and tables in order; open files read-only with PackageAccess.READ.

  • Drop to the CT* beans for what XWPF lacks, with poi-ooxml-full when the lite jar is missing a class.

  • POI does not lay out, paginate, convert to PDF or merge templates.