"Your template is the mold. Data is poured in, documents take shape."
DocXCast turns a Word document's content controls into a typed schema, and casts your data back into a formatted document.
The Idea:
A .docx template is a natural schema editor. In Word's Developer tab, anyone can insert content controls and describe them:
| Word control property | Role in DocXCast |
|---|---|
| Title | field name (e.g. name) |
| Tag | extraction requirement for the LLM (e.g. the candidate's full name) |
| Repeating section | array of records |
| Building block gallery | Section with its own extraction rule |
Group control with a name? Title |
conditional block (see below) |
Locked control (sdtLocked) |
required field |
The full control inventory β every construct's Word-visible pseudo-structure and the OOXML it is made of β lives in SYNTAX.md.
Two independent capabilities:
derive_schema(template)β read the controls and produce aSchema, serializable to JSON Schema for LLM structured extraction.render(template, data)β pour data back into the template: clone repeating sections, fill values, unwrap controls, keeping every run's formatting.
- Your template IS the schema β field names, extraction requirements and repeats all live in the docx, editable by non-programmers.
- Typed control mapping β text β
string, dropdown βenum, date / date-time βformat, checkbox βboolean, picture β derived and removed. - LLM-friendly contract validation β
schema.validate(data)returns structured, machine-readable issues (path/code/message/expected/got) that can be fed back into an LLM correction loop. - Lenient by default, strict on demand β missing fields keep their template values;
strict=Trueraises on the first error. - Clean output β controls are unwrapped by default;
keep_controls=Truekeeps them for round-trip editing. - Zero magic β a single runtime dependency (
python-docx).
from docxcast import derive_schema, render
schema = derive_schema('resume-template.docx')
# hand this to your LLM as the extraction contract
json_schema = schema.to_json_schema()
# validate what the LLM extracted
report = schema.validate(data)
if not report.ok:
# feed report.issues back into the LLM for correction
...
# cast data into the template; controls are unwrapped
result = render('resume-template.docx', data)
result.save('resume-output.docx')Schema.validate() never raises; it returns a ValidationResult:
result.ok # False if any error-level issue exists
result.errors # missing_required / type_mismatch / enum_invalid / format_invalid
result.warnings # unknown_keyrender(..., strict=True) raises RenderError on the first error-level issue.
Wrap a block in a group control and mark the guarded control's Title with a trailing ? (TS-style): a falsy value removes the whole group β label text included β while a truthy value fills normally.
Nationality: [nationality?] truthy β "Nationality: Chinese" falsy β the line is gone
Work Experiences both the heading and the repeat
[repeat: work_experiences?] disappear when the array is empty
The ? is a render directive only β the derived schema (and the LLM contract) see a plain nationality field. Marker placement, inline vs block-level drops, positional has_prev? / has_next? separators and every misuse error live in SYNTAX.md.
picturecontrols are recognized in the schema but removed on render (no image insertion yet).- Date values are written verbatim; the template's display mask is not enforced.
- Unnamed controls are skipped in the schema β every field needs a Title to be addressable.
- No whitespace control: text outside a dropped inline conditional span survives β put separators inside the span.