Documentation — Quick Start · Annotations · Examples · Changelog · Benchmarks
A lightweight, non-intrusive Java library for reading and writing fixed-width flat-file records using annotations. Originally published in 2008 and actively maintained ever since. Dramatically faster since 1.7.0 — field metadata caching and MethodHandle dispatch deliver 1.6× to 13.8× throughput gains over earlier releases, depending on record size and field types, verified by JMH microbenchmarks.
Fixed-width flat files are the lingua franca of banking, payroll, EDI, and government data exchange. Every character has a meaning — no delimiters, no headers, just positional fields that have to land exactly right. Consider a typical payroll settlement line:
EMP004232SMITH 0000185000CR20260418003
That single line encodes: employee ID (00423), department code (2), name (SMITH ), net pay in implied cents (0000185000 = $1,850.00), direction (CR), pay date (20260418), region code (003).
Without FixedFormat4J you write the parsing by hand for every field — and a symmetric block of String.format calls for export:
String name = line.substring(9, 29).trim();
BigDecimal netPay = new BigDecimal(line.substring(29, 39))
.movePointLeft(2);
LocalDate payDate = LocalDate.parse(line.substring(40, 48),
DateTimeFormatter.ofPattern("yyyyMMdd"));
// Repeat for every field. Off-by-one errors guaranteed.With FixedFormat4J you declare the layout once — as annotations on a plain Java class — and never write parsing or formatting code again:
@Getter @Setter @NoArgsConstructor
@Record
public class PayrollRecord {
@Field(offset = 4, length = 5, align = Align.RIGHT, paddingChar = '0')
private Integer employeeId;
@Field(offset = 10, length = 20)
private String name;
@Field(offset = 30, length = 10, align = Align.RIGHT, paddingChar = '0')
@FixedFormatDecimal(decimals = 2, useDecimalDelimiter = false)
private BigDecimal netPay;
@Field(offset = 41, length = 8)
@FixedFormatPattern("yyyyMMdd")
private LocalDate payDate;
}Works with Lombok — place @Field directly on fields and let Lombok generate the getters and setters. @Getter @Setter @NoArgsConstructor and you're done. No boilerplate, no hand-written accessors.
Spring-ready — FixedFormatManagerImpl is a plain Java object with no Spring dependency. Register it once as a @Bean and inject it wherever you need it:
@Bean
public FixedFormatManager fixedFormatManager() {
return new FixedFormatManagerImpl();
}For every annotation attribute, type, and advanced option see the Annotations reference.
| FixedFormat4J | BeanIO | FlatWorm | Manual substring |
|
|---|---|---|---|---|
| Configuration | Java annotations | XML schema file | XML schema file | none |
| External schema file | no | yes | yes | no |
| Spring integration | drop-in @Bean |
separate module | manual | manual |
| Enum support | built-in (@FixedFormatEnum) |
built-in | limited | manual |
| Custom formatters | yes (FixedFormatter<T>) |
yes | limited | n/a |
| Lombok support | yes (field annotations) | no | no | no |
| Active maintenance | yes | limited | no | n/a |
FixedFormat4J's annotation-only approach means no external schema files to version, no XML to keep in sync with your Java classes, and no framework-specific integration layer to pull in.
See the Changelog for what's new.
FixedFormat4J is published to Maven Central. No repository configuration or authentication is needed.
Maven
<dependency>
<groupId>com.ancientprogramming.fixedformat4j</groupId>
<artifactId>fixedformat4j</artifactId>
<version>1.9.1</version>
</dependency>Gradle (Groovy DSL)
implementation 'com.ancientprogramming.fixedformat4j:fixedformat4j:1.9.1'Gradle (Kotlin DSL)
implementation("com.ancientprogramming.fixedformat4j:fixedformat4j:1.9.1")Ivy
<dependency org="com.ancientprogramming.fixedformat4j"
name="fixedformat4j"
rev="1.9.1"/>Requires Java 11 or later. If you want log output, add an SLF4J binding such as logback-classic; without one the library still works, just silently.
See Get It for full setup instructions.
import com.ancientprogramming.fixedformat4j.annotation.*;
import com.ancientprogramming.fixedformat4j.format.FixedFormatManager;
import com.ancientprogramming.fixedformat4j.format.impl.FixedFormatManagerImpl;
import java.time.LocalDate;
@Record
public class EmployeeRecord {
private String name;
private Integer employeeId;
private LocalDate hireDate;
@Field(offset = 1, length = 20)
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@Field(offset = 21, length = 6, align = Align.RIGHT, paddingChar = '0')
public Integer getEmployeeId() { return employeeId; }
public void setEmployeeId(Integer employeeId) { this.employeeId = employeeId; }
@Field(offset = 27, length = 8)
@FixedFormatPattern("yyyyMMdd")
public LocalDate getHireDate() { return hireDate; }
public void setHireDate(LocalDate hireDate) { this.hireDate = hireDate; }
}@Recordmarks the class as a fixed-format record.@Fieldgoes on the getter or directly on the field (since 1.5.0);offsetis 1-based.- Each mapped field needs a getter and a setter.
@Getter @Setter @NoArgsConstructor
@Record
public class EmployeeRecord {
@Field(offset = 1, length = 20)
private String name;
@Field(offset = 21, length = 6, align = Align.RIGHT, paddingChar = '0')
private Integer employeeId;
@Field(offset = 27, length = 8)
@FixedFormatPattern("yyyyMMdd")
private LocalDate hireDate;
}Place @Field on the fields, let Lombok generate the getters and setters, and the result is identical.
On JDK 16+ the same mapping works as a Java record (since 1.9.0) — annotate the record components; no setters and no no-arg constructor are needed:
@Record
public record EmployeeRecord(
@Field(offset = 1, length = 20)
String name,
@Field(offset = 21, length = 6, align = Align.RIGHT, paddingChar = '0')
Integer employeeId,
@Field(offset = 27, length = 8)
@FixedFormatPattern("yyyyMMdd")
LocalDate hireDate) {}Every annotation — @Field, @Fields, @FixedFormatPattern, @FixedFormatDecimal, @FixedFormatNumber, @FixedFormatBoolean, @FixedFormatEnum — applies to record components exactly as to getter methods, including nested @Record components and repeating fields. load() binds all parsed values through the canonical constructor in one call, and export() reads the component accessors (record.name()). The library itself still runs on Java 11 — record binding activates only when a record class is encountered.
FixedFormatManager manager = new FixedFormatManagerImpl();
String line = "Jane Smith 00042320260405";
EmployeeRecord record = manager.load(EmployeeRecord.class, line);
System.out.println(record.getName()); // "Jane Smith"
System.out.println(record.getEmployeeId()); // 423
System.out.println(record.getHireDate()); // 2026-04-05record.setEmployeeId(999);
String exported = manager.export(record);
System.out.println(exported);
// "Jane Smith 00099920260405"Every field is re-padded to its declared length using the configured alignment and padding character.
FixedFormatReader (since 1.8.1) reads files and streams directly, routing each line to the right record class:
// Homogeneous file — every line is the same record type
FixedFormatReader reader = FixedFormatReader.builder()
.addMapping(EmployeeRecord.class, LinePattern.matchAll())
.build();
List<EmployeeRecord> records = reader.read(Path.of("employees.txt"))
.get(EmployeeRecord.class);// Heterogeneous file — route lines by prefix
FixedFormatReader reader = FixedFormatReader.builder()
.addMapping(HeaderRecord.class, LinePattern.prefix("HDR"))
.addMapping(DetailRecord.class, LinePattern.prefix("DTL"))
.build();
ReadResult result = reader.read(Path.of("payroll.txt"));
List<HeaderRecord> headers = result.get(HeaderRecord.class);
List<DetailRecord> details = result.get(DetailRecord.class);See File Processing for the full API including streaming process(), charset overloads, and per-line error strategies.
@Fields groups multiple @Field annotations on a single getter, writing the same value at two or more positions — useful for legacy formats that duplicate a field:
@Fields({
@Field(offset = 11, length = 8),
@Field(offset = 19, length = 8)
})
public Date getDateData() { return dateData; }ByTypeFormatter maps Java types to the right formatter automatically. All of the following work out of the box — no formatter = attribute required:
| Java type | Supplementary annotation | Notes |
|---|---|---|
String |
— | padded or trimmed to declared length |
Integer / int |
@FixedFormatNumber |
sign handling, zero-padding |
Long / long |
@FixedFormatNumber |
|
Short / short |
@FixedFormatNumber |
|
Double / double |
@FixedFormatDecimal |
|
Float / float |
@FixedFormatDecimal |
|
BigDecimal |
@FixedFormatDecimal |
implied decimals, optional delimiter |
Boolean / boolean |
@FixedFormatBoolean |
configurable trueValue / falseValue literals |
Character / char |
— | single character |
LocalDate |
@FixedFormatPattern |
e.g. "yyyyMMdd" |
LocalDateTime |
@FixedFormatPattern |
e.g. "yyyyMMddHHmmss" |
java.util.Date |
@FixedFormatPattern |
legacy date support |
Any Enum |
@FixedFormatEnum |
LITERAL (name) or NUMERIC (ordinal) |
Nested @Record class |
— | recursive load / export |
For any type not in this table, implement FixedFormatter<T> and reference it via formatter = on @Field.
Full documentation is available at https://jeyben.github.io/fixedformat4j/:
- Quick Start — step-by-step walkthrough
- Annotations reference — every annotation attribute explained
- Examples — financial records, booleans, file processing, custom formatters
- File Processing — reading files and streams with
FixedFormatReader - Nested Records — embedding one record inside another
- Compile-time Validation — optional
fixedformat4j-processorartifact that turns@Field/@Recordmisconfigurations intojavacerrors - Schema Introspection — query the field layout of a
@Recordclass at runtime viaFixedFormatIntrospector - Metrics — optional
fixedformat4j-micrometerartifact with load/export timers and parse-error counters for any Micrometer registry - FAQ
- Changelog
Reports:
- Mutation Report — latest PIT mutation testing results (updated on release)
JMH microbenchmarks compare load() and export() performance across releases. Charts are published at jeyben.github.io/fixedformat4j/benchmarks.
To run benchmarks locally (requires Java 11 and 1.6.1 on Maven Central):
./benchmarks/run.sh # 1.6.1 vs master (default)
./benchmarks/run.sh 1.6.1 1.6.0 master # explicit version listResults are written to docs/assets/benchmarks/ as JMH JSON.
- Fork the repository and create a feature branch.
- Write a failing test first — TDD is required; see CLAUDE.md for the full workflow.
- Run
export JAVA_HOME=$(/usr/libexec/java_home -v 11) && mvn testto verify everything passes. - Open a pull request against
master.
Bug reports and feature requests are welcome as GitHub issues.
Apache License 2.0 — see LICENSE for details.