Skip to content

Migrating from JXLS to XLFill

If you’re coming from JXLS, you’re going to feel right at home. XLFill is a Go port of the JXLS 3.0 template engine — same jx: command syntax, same cell comment approach, same template philosophy. Your existing templates will work with minimal changes.

The difference? XLFill adds 12 commands JXLS doesn’t have, fixes long-standing JXLS pain points, and runs as a native Go binary — no JVM, no Maven, no Spring Boot.

FeatureJXLS 3.0XLFill
Area definitionjx:area(lastCell="C2")jx:area(lastCell="C2")
Loopjx:each(items="employees" var="e" lastCell="C2")jx:each(items="employees" var="e" lastCell="C2")
Conditionaljx:if(condition="e.salary > 50000" lastCell="C2")jx:if(condition="e.Salary > 50000" lastCell="C2")
Expressions${e.name}${e.Name}
Grid layoutjx:grid(headers="headers" data="data" lastCell="A2")jx:grid(headers="headers" data="data" lastCell="A2")
Imagejx:image(src="logoBytes" lastCell="C5")jx:image(src="logoBytes" lastCell="C5")
Cell mergingjx:mergeCells(lastCell="C2")jx:mergeCells(lastCell="C2")

The commands are literally the same. The only consistent difference is Go’s convention of capitalized exported field names (e.Name vs e.name).

Pro tip: If your JXLS templates use lowercase field names like ${e.name}, you can keep them — just use map[string]any in Go with lowercase keys. No template changes needed.

CommandWhat it doesJXLS equivalent
jx:tableAuto-filter tables with bandingNone — requires POI post-processing
jx:chartBar, line, pie, and 5 more chart typesNone — requires POI post-processing
jx:sparklineInline trend charts in cellsNone
jx:conditionalFormatData bars, color scales, icon setsNone — JXLS issue #152
jx:dataValidationDropdowns, number/date constraintsNone — JXLS issue #46
jx:definedNameNamed ranges for downstream formulasNone
jx:groupCollapsible row/column groupsNone
jx:autoRowHeightAuto-fit row heightsNone
jx:autoColWidthAuto-fit column widthsNone
jx:freezePanesFreeze header rows/columnsNone
jx:pageBreakPage breaks for printingNone
jx:protectSheet protection with passwordNone

Every one of these is a zero-code template command. In JXLS, you’d need to manipulate the output file with Apache POI after JXLS runs — writing Java code, managing POI dependencies, and hoping the post-processing doesn’t break JXLS’s output.

Your JXLS template works as-is with XLFill. Copy it into your Go project.

Terminal window
cp /java-project/src/main/resources/templates/report.xlsx /go-project/templates/

If your templates use lowercase field names (${e.name}), they’ll work fine with map[string]any data. If you want to use Go structs, update the expressions to match struct field names (${e.Name}).

JXLS (Java):

Context context = new Context();
context.putVar("employees", employeeList);
context.putVar("title", "Monthly Report");
JxlsHelper.getInstance().processTemplate(
new FileInputStream("template.xlsx"),
new FileOutputStream("output.xlsx"),
context
);

XLFill (Go):

data := map[string]any{
"employees": employees,
"title": "Monthly Report",
}
xlfill.Fill("template.xlsx", "output.xlsx", data)

Same concept, fewer lines, no context object ceremony.

Step 3: Convert Java collections to Go data

Section titled “Step 3: Convert Java collections to Go data”

XLFill accepts map[string]any with slices of maps or structs. Here’s how common JXLS data patterns translate:

Java List becomes Go slice of maps:

data := map[string]any{
"employees": []map[string]any{
{"name": "Alice", "department": "Engineering", "salary": 95000},
{"name": "Bob", "department": "Sales", "salary": 72000},
},
}

Java List becomes Go slice of structs:

type Employee struct {
Name string
Department string
Salary float64
}
data := xlfill.StructSliceToData("employees", []Employee{
{Name: "Alice", Department: "Engineering", Salary: 95000},
{Name: "Bob", Department: "Sales", Salary: 72000},
})

Pro tip: StructSliceToData converts struct fields to map entries automatically. It handles nested structs, so employee.Address.City works in templates just like JXLS.

JSON from an API — if your Java app was calling a REST API and feeding the JSON into JXLS, XLFill has a one-liner:

data, err := xlfill.JSONToData(jsonBytes)
xlfill.Fill("template.xlsx", "output.xlsx", data)

Database queries — if your JXLS data came from JDBC:

rows, _ := db.Query("SELECT name, department, salary FROM employees")
data, err := xlfill.SQLRowsToData("employees", rows)
xlfill.Fill("template.xlsx", "output.xlsx", data)
Terminal window
go run main.go

Open the output. Compare it with JXLS output. It should be identical for the shared command set.

FeatureJXLS 3.0XLFill
Template commands820
Built-in expression functions018
Template validation (pre-fill)NoYes
Data contract validationNoYes
Streaming modeNo3x speed, 60% less memory
Parallel processingNoBuilt-in
Auto-mode optimizationNoBuilt-in
Compiled/cached templatesNoBuilt-in
Batch generationNoFillBatch
HTTP handlerNoBuilt-in
Progress reportingNoBuilt-in
Context cancellationNoBuilt-in
Structured error messagesNoYes (kind + cell + message)
“Did you mean?” suggestionsNoYes
Custom expression delimitersNoYes
Debug trace outputNoYes
Loop filter/sort/groupPartial (custom Java)Built-in attributes
Multisheet from single loopPartialBuilt-in

JXLS can’t preserve data validation (dropdowns) when rows expand. If you had a dropdown in your template row, it disappeared in the output. XLFill’s jx:dataValidation command explicitly handles this:

Cell A2 comment:
jx:each(items="employees" var="e" lastCell="C2")
Cell C2 comment:
jx:dataValidation(type="list" formula="Active,Inactive,On Leave" lastCell="C2")
Cell C2: ${e.Status}

Every output row gets the dropdown. No post-processing needed.

Conditional formatting performance (JXLS issue #152)

Section titled “Conditional formatting performance (JXLS issue #152)”

JXLS had no built-in support for conditional formatting. Users resorted to Apache POI post-processing, which was slow and fragile. XLFill handles it natively:

Cell B2 comment:
jx:conditionalFormat(lastCell="B2" type="colorScale" minColor="FF0000" maxColor="00FF00")

Every salary cell gets a color scale — red for low, green for high. Zero Java code, zero POI manipulation.

Collapsible row groups were a frequent JXLS feature request with no official support. XLFill’s jx:group command handles it:

Cell A2 comment:
jx:each(items="departments" var="dept" lastCell="C10")
jx:group(lastCell="C10" collapsed="false")

Department sections become collapsible outline groups in the output.

JXLS uses JEXL (Java Expression Language). XLFill uses expr-lang, a fast Go expression evaluator. Most expressions work identically, but there are a few differences:

ExpressionJXLS (JEXL)XLFill (expr-lang)
Null-safe accesse.?namee.Name (nil-safe by default)
String concatenatione.first + ' ' + e.laste.First + " " + e.Last
Ternarye.active ? 'Yes' : 'No'e.Active ? "Yes" : "No"
Method callse.getFullName()Not supported — use functions
Built-in functionsNone18 built-in: upper(), formatNumber(), sumBy(), etc.

Gotcha: JXLS allows calling Java methods in expressions (e.getFullName()). XLFill doesn’t — Go structs don’t have the same reflection model. Instead, either compute the value before filling, or register a custom function with WithFunction.

JXLS custom transformers → XLFill equivalents

Section titled “JXLS custom transformers → XLFill equivalents”
JXLS patternXLFill equivalent
Custom TransformerWithCommand(name, factory) for new commands
AreaListener (Java)WithAreaListener(listener) — same concept, Go interface
Custom function in JEXLWithFunction(name, fn)
Post-processing with POIWithPreWrite(fn) callback, or post-process with excelize
  • Start with one template. Pick your simplest JXLS template, migrate it, and verify the output. Then move to complex ones.
  • Use Validate() liberally. XLFill can check your template for syntax errors without data — JXLS couldn’t do this. Run it after copying every template.
  • Leverage new features gradually. Once the basic migration works, add jx:chart, jx:table, jx:conditionalFormat to templates that needed POI post-processing. Delete that Java code.
  • Test with WithStrictMode(true). This turns unknown command warnings into errors — catches typos and leftover JXLS-only syntax.
  • Batch convert templates. If you have dozens of templates, write a small Go test that calls Validate() on each one — instant migration verification.
func TestAllTemplates(t *testing.T) {
templates, _ := filepath.Glob("templates/*.xlsx")
for _, tmpl := range templates {
issues, err := xlfill.Validate(tmpl)
if err != nil {
t.Errorf("%s: %v", tmpl, err)
}
for _, issue := range issues {
t.Errorf("%s: %s", tmpl, issue)
}
}
}