A Java library (Jackson extension) for reading and writing Excel spreadsheets (XLSX/XLS) as POJOs.
Map spreadsheet rows to Java objects with ObjectMapper — the same API you use for JSON, CSV, and XML. No cell-level iteration, no column index counting, no manual type casting.
SpreadsheetMapper mapper = new SpreadsheetMapper();
// Read Excel to POJOs
List<Employee> employees = mapper.readValues(file, Employee.class);
// Write POJOs to Excel
mapper.writeValue(file, employees, Employee.class);That's it. If you know Jackson, you know this library.
- You need to read XLSX/XLS files into Java objects without writing cell-by-cell parsing code
- You need to write Java objects to Excel with headers, types, and styling
- You want Jackson ecosystem integration (custom deserializers, mix-ins, modules)
- You have nested object structures that need to flatten into spreadsheet columns
- You need streaming performance for large files (100K+ rows)
Available on Maven Central:
Maven:
<dependency>
<groupId>io.github.scndry</groupId>
<artifactId>jackson-dataformat-spreadsheet</artifactId>
<version>1.6.4</version>
</dependency>Gradle:
implementation "io.github.scndry:jackson-dataformat-spreadsheet:1.6.4"- Java 8+
- Jackson 2.14.0+
- Apache POI 4.1.1+ (Strict OOXML requires 5.1.0+)
@DataGrid
public class Product {
private String name;
private int quantity;
private double price;
// constructors, getters, setters
}SpreadsheetMapper mapper = new SpreadsheetMapper();
// Single object (first row)
Product product = mapper.readValue(file, Product.class);
// All rows
List<Product> products = mapper.readValues(file, Product.class);
// Specific sheet
SheetInput<File> input = SheetInput.source(file, "Products");
List<Product> products = mapper.readValues(input, Product.class);List<Product> products = Arrays.asList(
new Product("Apple", 10, 1.50),
new Product("Banana", 20, 0.80));
// Single object
mapper.writeValue(file, products.get(0), Product.class);
// All rows
mapper.writeValue(file, products, Product.class);
// Specific sheet
SheetOutput<File> output = SheetOutput.target(file, "Products");
mapper.writeValue(output, products, Product.class);Nested objects flatten into columns. Lists of nested objects expand into multiple rows.
@DataGrid(mergeColumn = OptBoolean.TRUE)
class Order {
@DataColumn("ID") int id;
@DataColumn("Customer") String customer;
@DataColumnGroup("Items") List<LineItem> items;
@DataColumn("Total") BigDecimal total;
}
class LineItem {
@DataColumn("SKU") String sku;
@DataColumn("Qty") int qty;
@DataColumn("Amount") BigDecimal amount;
}+----+----------+----------------------+-------+
| ID | Customer | Items | Total |
| | +------+-----+---------+ |
| | | SKU | Qty | Amount | |
+----+----------+------+-----+---------+-------+
| | | A-01 | 3 | 30.00 | |
| 1 | Alice +------+-----+---------+ 95.00 |
| | | B-02 | 5 | 65.00 | |
+----+----------+------+-----+---------+-------+
Read:
| Library | Time | Memory |
|---|---|---|
| jackson-spreadsheet | 190 ms | 360 MB |
| FastExcel | 208 ms | 407 MB |
| Fesod | 266 ms | 381 MB |
| Poiji | 809 ms | 2739 MB |
| Apache POI | 1173 ms | 2227 MB |
Write:
| Library | Time | Memory |
|---|---|---|
| jackson-spreadsheet | 138 ms | 125 MB |
| FastExcel | 152 ms | 149 MB |
| Apache POI | 269 ms | 204 MB |
| Fesod | 323 ms | 458 MB |
Fastest read and write throughput, lowest write memory at 100K rows. See BENCHMARK.md for full results.
| Feature | jackson-spreadsheet | Apache POI | Fesod | FastExcel |
|---|---|---|---|---|
| POJO data binding | Yes | No | Yes | No |
| Nested object support | Yes | No | No | No |
| Jackson ecosystem | Yes | No | No | No |
| Streaming read | Yes¹ | Event API | Yes | Yes |
| Streaming write | Yes¹ | SXSSF | Yes | Yes |
| Cell styling | Yes | Yes | Yes | No |
| XLSX read/write | Yes | Yes | Yes | Yes |
| XLS read/write | Yes | Yes | No | No |
| Annotation mapping | Yes | No | Yes | No |
¹ XLSX read streams via StAX, write via StringBuilder over a POI scaffold; XLS uses in-memory POI workbook (HSSF has no streaming API).
SpreadsheetReader reader = mapper.sheetReaderFor(Product.class);
try (SheetMappingIterator<Product> iter = reader.readValues(input)) {
while (iter.hasNext()) {
Product p = iter.next();
SheetLocation loc = iter.getCurrentLocation();
// loc.getRow(), loc.getColumn() — zero-based cell position
}
}StylesBuilder.simple() for a per-type starter set; build a StylesBuilder from scratch for full control.
StylesBuilder styles = new StylesBuilder()
.cellStyle("currency")
.dataFormat("#,##0.00")
.font().bold().end()
.end();
SpreadsheetMapper mapper = SpreadsheetMapper.builder()
.stylesBuilder(styles)
.build();GridConfigurer adds conditional formatting, freeze pane, and auto filter on top of the data grid — without dropping into POI Sheet/Cell code.
SpreadsheetMapper mapper = SpreadsheetMapper.builder()
.stylesBuilder(styles)
.gridConfigurer(new GridConfigurer()
.freezePane(0, 1)
.autoFilter()
.conditionalFormatting("score",
greaterThanOrEqual(80).style("highlight")))
.build();Conditional formatting rules reference columns from the model class and styles from StylesBuilder — both name-based and resolved at write time. Static-import the factory methods from ConditionalFormats for fluent chaining; see the GUIDE for typed operators, multi-rule columns, and color scale.
SpreadsheetMapper mapper = SpreadsheetMapper.builder()
.origin("B2") // start at B2 instead of A1
.useHeader(false) // no header row
.enable(SheetParser.Feature.BREAK_ON_BLANK_ROW)
.build();Built-in conversion between Java date types and Excel serial numbers. Registered by default — no setup needed. Excel renders a date cell as a date only when it carries a date format; StylesBuilder.simple() registers per-type defaults as a starter.
Supported: Date, Calendar, LocalDate, LocalDateTime
Not a POI wrapper. Extends Jackson's streaming layer directly:
SheetParser extends ParserMinimalBase— StAX pull parserSheetGenerator extends GeneratorBase— streaming cell writerSpreadsheetFactory extends JsonFactory— format detection
The XLSX path bypasses POI's User Model by default — the read path parses OOXML XML directly via StAX, the write path streams XML via StringBuilder with a POI scaffold for package metadata.
See ARCHITECTURE.md for design decisions and data flow diagrams.
- GUIDE.md — Full usage guide
- DESIGN.md — Design philosophy and abstraction boundaries
- ARCHITECTURE.md — Internal architecture and data flow
- BENCHMARK.md — JMH benchmark results
- jackson-spreadsheet-examples — Runnable examples (read/write, styling, Spring Boot, POI integration, large files)
Q: How is this different from Apache POI?
POI gives you cells. This gives you POJOs. You define a class with @DataGrid, and mapper.readValues() returns typed objects. No row.getCell(0).getStringCellValue().
Q: How is this different from Fesod?
Fesod has its own API. This extends Jackson's ObjectMapper, so you get the full Jackson ecosystem — custom deserializers, mix-ins, modules, polymorphic types.
Q: Does it support nested objects? Yes. Nested POJOs automatically flatten to columns on write and reconstruct on read. No configuration needed.
Q: How does performance compare? Fastest read and write throughput at 100K rows, with the lowest write memory. 6x faster read than Apache POI. Default writer is 10% faster than FastExcel and uses 16% less memory. See BENCHMARK.md.
Q: What Excel formats are supported? XLSX (OOXML) and XLS (legacy). XLSX uses StAX streaming; XLS uses POI object model.
Q: Is it production-ready? Yes. Version 1.6.4 on Maven Central. Java 8+, Jackson 2.14+, POI 4.1.1+. Listed as a community data format module in the FasterXML jackson repository.
Q: Is the mapper thread-safe?
The mapper instance is reusable across threads once configured (same rule as Jackson's ObjectMapper). Concurrent calls with File / InputStream / OutputStream inputs are safe — the library opens an isolated Workbook per call. If you pass a Sheet directly, POI's Workbook/Sheet are not thread-safe, so each thread needs its own.