Fill DOCX placeholders with JSON data to generate personalized Word documents.
dotnet add package RecursiveAI.OneDocument.DocumentsRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
The sample template.docx in the zip contains the placeholder line Hello {{ customer }}! and a second paragraph, Order: {{ order }}.
using RecursiveAI.OneDocument; using RecursiveAI.OneDocument.Documents; using System.Text.Json; using var data = JsonDocument.Parse( """{"customer":"Ada","order":42}"""); Template.Render("template.docx", data, "filled.docx");
Writes filled.docx with “Hello Ada!” and “Order: 42”; the snippet prints nothing. The input file is in the zip.
Unzip the example, then dotnet run.
Full program
template.docx is included in the example. It has these two paragraphs:
Hello {{ customer }}! Order: {{ order }}
The JSON data is shown in the snippet.
using RecursiveAI.OneDocument; using RecursiveAI.OneDocument.Documents; using System.Text.Json; using var data = JsonDocument.Parse( """{"customer":"Ada","order":42}"""); var warnings = Template.Render( "template.docx", data, "filled.docx"); Console.WriteLine($"Warnings: {warnings.Count}"); using var doc = Document.Open("filled.docx"); Console.WriteLine(doc.ExtractText().Trim());
dotnet run prints:
Warnings: 0 Hello Ada! Order: 42
Missing data renders empty and returns a template.missing-data warning by default; strict mode throws TemplateException.
Variations
Fail on missing data
Replace the full program with this example, which deliberately omits customer. The template still contains Hello {{ customer }}!, so strict mode throws TemplateException; the catch prints the exception.
using RecursiveAI.OneDocument; using RecursiveAI.OneDocument.Documents; using RecursiveAI.OneDocument.Templating; using System.Text.Json; // Missing customer: template.docx still asks for it. using var data = JsonDocument.Parse("""{"order":42}"""); try { Template.Render("template.docx", data, "strict.docx", new TemplateOptions { Strict = true }); } catch (TemplateException error) { Console.WriteLine(error.GetType().Name); Console.WriteLine(error.Message); }
Output:
TemplateException Fatal template.missing-data at /word/document.xml#body/p2@9: No data named 'customer'.
Related recipes
- Find and replace text in a Word document in C#
- Convert PowerPoint to PDF in C#
- Mail merge DOCX files in C#
- Extract text from DOCX, XLSX and PPTX in C#
The short answer
Put placeholder tags in a DOCX file, supply JSON data and call Template.Render(templatePath, data, outputPath). OneDocument reads the template, fills the tags and saves a separate document. The example replaces a customer name and order number while leaving the template available for another customer.
This is a tag-based template, not Word mail merge. Use it when you control the document’s placeholder syntax and want expressions or repeating content. If your file already contains Word MERGEFIELD fields, use the mail-merge recipe instead. Both approaches produce DOCX files, but their markers and data-processing rules are different.
How do I create a Word document from a template?
Start with an ordinary DOCX containing the two paragraphs shown above. The data object has customer and order properties. Keeping the data as a JsonDocument makes the input explicit and avoids needing a generated model class in this small example. Keep that JSON document alive until rendering finishes.
The template engine reads each paragraph as one unit. A Word run is a piece of text with shared formatting, such as bold or italic; the engine joins these pieces when it reads a tag. A tag may be split across runs because of typing, spell-checking or formatting and still be recognized. It may not span two paragraphs. That difference is important when a placeholder appears visually wrapped: a line wrap is not necessarily a paragraph boundary, but pressing Enter creates a different unit.
Text inserted by a tag takes the run properties where the tag began. Text after the tag keeps its own run properties. Style the start of a placeholder in the template to control the replacement’s formatting. Don’t assume that the last character of a split tag decides the result’s font or emphasis.
The template engine handles the body, headers, footers, footnotes, endnotes, comments, text boxes and content-control content. Document property parts are not templated. Tags in alternative-content fallback branches, shape names and chart text are not read. These boundaries are documented in Word templating; they matter if a visible placeholder remains unchanged after the main paragraphs have rendered correctly.
How do I create a new document based on a template?
Give each output its own path and render again with the next data object. The file-based call opens the source template for each render and saves to the requested output extension. A .dotx template can also render to .docx; saving with the DOCX extension writes the document content type. You don’t need to rename archive members by hand.
For a production template, decide how missing values should behave. The default is lenient: missing data prints empty and returns a located warning. An expression that evaluates to an error prints its error value and returns a warning. The full program reports the number of warnings. For a required customer name or invoice number, strict mode is usually the clearer choice: it throws TemplateException on missing data or an expression error rather than silently producing an incomplete letter.
The template engine also supports conditions, loops and formula expressions. Those are evaluated by the template engine, not by compiling C# embedded in the document. Use those features when the structure genuinely varies with the data; use simple substitutions for a fixed two-line letter. Cached field results are copied, not recalculated, so a filled template containing page or other fields is not a request to update every Word field. Review the resulting document in its intended viewer before sending it.