Examples
Each example below is self-contained and demonstrates a specific feature. All examples use the same entry point:
FixedFormatManager manager = new FixedFormatManagerImpl();
Example 1 — Financial record with decimals and signed amounts
This example shows @FixedFormatDecimal for implicit-decimal storage and @FixedFormatNumber for signed values — common requirements in financial and mainframe file formats.
With useDecimalDelimiter = false, the decimal point is not stored in the string; instead the last decimals digits are treated as the fractional part. This saves a character per field compared to storing "123.50".
@Record
public class TransactionRecord {
private BigDecimal amount;
private BigDecimal balance;
// "0001250" → 12.50 (last 2 digits are decimals, no delimiter stored)
@Field(offset = 1, length = 7, align = Align.RIGHT, paddingChar = '0')
@FixedFormatDecimal(decimals = 2, useDecimalDelimiter = false)
public BigDecimal getAmount() { return amount; }
public void setAmount(BigDecimal amount) { this.amount = amount; }
// "-000500" → -5.00 (sign prepended, takes up one of the 7 characters)
@Field(offset = 8, length = 7, align = Align.RIGHT, paddingChar = '0')
@FixedFormatDecimal(decimals = 2, useDecimalDelimiter = false)
@FixedFormatNumber(sign = Sign.PREPEND)
public BigDecimal getBalance() { return balance; }
public void setBalance(BigDecimal balance) { this.balance = balance; }
}
String line = "0001250-000500";
TransactionRecord record = manager.load(TransactionRecord.class, line);
System.out.println(record.getAmount()); // 12.50
System.out.println(record.getBalance()); // -5.00
record.setAmount(new BigDecimal("99.99"));
System.out.println(manager.export(record));
// "0009999-000500"
Decimal annotation options at a glance:
useDecimalDelimiter |
String stored | Parsed value |
|---|---|---|
false (default) |
"001250" |
12.50 |
true |
"012.50" |
12.50 |
Example 2 — Boolean flags with custom true/false values
The default boolean representation is "T" / "F". Use @FixedFormatBoolean to override this — for example to match a legacy format that uses "Y" / "N" or "1" / "0".
@Record
public class StatusRecord {
private Boolean active;
private Boolean verified;
@Field(offset = 1, length = 1)
@FixedFormatBoolean(trueValue = "Y", falseValue = "N")
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
@Field(offset = 2, length = 1)
@FixedFormatBoolean(trueValue = "1", falseValue = "0")
public Boolean getVerified() { return verified; }
public void setVerified(Boolean verified) { this.verified = verified; }
}
String line = "Y1";
StatusRecord record = manager.load(StatusRecord.class, line);
System.out.println(record.getActive()); // true
System.out.println(record.getVerified()); // true
record.setActive(false);
System.out.println(manager.export(record));
// "N1"
Example 3 — Right-aligned, zero-padded integers
Fixed-width formats often store numbers right-aligned and zero-padded so that every record has the same layout. The align and paddingChar attributes on @Field control this.
@Record
public class OrderRecord {
private String productCode;
private Integer quantity;
private Integer unitPriceCents;
// Left-aligned text, padded with spaces (the default)
@Field(offset = 1, length = 10)
public String getProductCode() { return productCode; }
public void setProductCode(String productCode) { this.productCode = productCode; }
// Right-aligned number, padded with zeros on the left
@Field(offset = 11, length = 5, align = Align.RIGHT, paddingChar = '0')
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
// Right-aligned, zero-padded price in cents
@Field(offset = 16, length = 8, align = Align.RIGHT, paddingChar = '0')
public Integer getUnitPriceCents() { return unitPriceCents; }
public void setUnitPriceCents(Integer unitPriceCents) { this.unitPriceCents = unitPriceCents; }
}
String line = "WIDGET-A 000120000150";
OrderRecord record = manager.load(OrderRecord.class, line);
System.out.println(record.getProductCode()); // "WIDGET-A"
System.out.println(record.getQuantity()); // 12
System.out.println(record.getUnitPriceCents()); // 150
record.setQuantity(5);
System.out.println(manager.export(record));
// "WIDGET-A 000050000150"
Alignment at a glance:
align |
Padding side | Trim side | Typical use |
|---|---|---|---|
Align.LEFT (default) |
Right | Right | Text fields |
Align.RIGHT |
Left | Left | Numeric fields |
Example 4 — Processing a file line by line
Since 1.8.0, use FixedFormatReader to process files. Build a reader once, then call read or process.
Single record type:
import com.ancientprogramming.fixedformat4j.io.read.LinePattern;
FixedFormatReader reader = FixedFormatReader.builder()
.addMapping(EmployeeRecord.class, LinePattern.matchAll())
.excludeLines(line -> line.isBlank())
.build();
List<EmployeeRecord> employees = reader.read(Path.of("employees.txt"))
.get(EmployeeRecord.class);
for (EmployeeRecord emp : employees) {
System.out.println(emp.getName() + " — ID: " + emp.getEmployeeId());
}
Multiple record types in the same file — register each class with a discriminator pattern; read groups results by class with no casts:
FixedFormatReader reader = FixedFormatReader.builder()
.addMapping(EmployeeRecord.class, LinePattern.prefix("E"))
.addMapping(ManagerRecord.class, LinePattern.prefix("M"))
.build();
ReadResult result = reader.read(Path.of("staff.txt"));
List<EmployeeRecord> employees = result.get(EmployeeRecord.class);
List<ManagerRecord> managers = result.get(ManagerRecord.class);
See File Processing for the full API including streaming large files and error-handling strategies.
Example 5 — Custom formatter
When no built-in formatter fits your field type, implement FixedFormatter<T> directly.
This example maps a field to java.time.YearMonth (e.g. "2024-03" for March 2024):
public class YearMonthFormatter extends AbstractFixedFormatter<YearMonth> {
@Override
public YearMonth asObject(String value, FormatInstructions instructions) {
// 'value' arrives already stripped of padding by the base class
return YearMonth.parse(value, DateTimeFormatter.ofPattern("yyyy-MM"));
}
@Override
public String asString(YearMonth value, FormatInstructions instructions) {
return value.format(DateTimeFormatter.ofPattern("yyyy-MM"));
}
}
Wire it in with the formatter attribute on @Field:
@Record
public class ReportRecord {
private YearMonth reportPeriod;
@Field(offset = 1, length = 7, formatter = YearMonthFormatter.class)
public YearMonth getReportPeriod() { return reportPeriod; }
public void setReportPeriod(YearMonth reportPeriod) { this.reportPeriod = reportPeriod; }
}
String line = "2024-03";
ReportRecord record = manager.load(ReportRecord.class, line);
System.out.println(record.getReportPeriod()); // 2024-03
record.setReportPeriod(YearMonth.of(2025, 1));
System.out.println(manager.export(record));
// "2025-01"
For access to supplementary annotation data inside the formatter (e.g., @FixedFormatPattern), use the FormatInstructions argument:
String pattern = instructions.getFixedFormatPatternData().getPattern();
See the FAQ for more details on the FixedFormatter interface.
Example 6 — Field annotations and Lombok
Since 1.5.0, @Field can be placed directly on a Java field instead of its getter. The manager derives the getter/setter by convention, so the two styles below are fully equivalent.
Plain POJO — annotations on fields, getters written explicitly:
@Record
public class EmployeeRecord {
@Field(offset = 1, length = 12)
private String name;
@Field(offset = 13, length = 5, align = Align.RIGHT, paddingChar = '0')
private Integer employeeId;
@Field(offset = 18, length = 8)
@FixedFormatPattern("yyyyMMdd")
private LocalDate hireDate;
@Field(offset = 26, length = 1)
@FixedFormatBoolean(trueValue = "Y", falseValue = "N")
private Boolean active;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getEmployeeId() { return employeeId; }
public void setEmployeeId(Integer employeeId) { this.employeeId = employeeId; }
public LocalDate getHireDate() { return hireDate; }
public void setHireDate(LocalDate hireDate) { this.hireDate = hireDate; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
}
With Lombok — same annotations on fields, getters/setters generated automatically:
@Getter @Setter @NoArgsConstructor
@Record
public class EmployeeRecord {
@Field(offset = 1, length = 12)
private String name;
@Field(offset = 13, length = 5, align = Align.RIGHT, paddingChar = '0')
private Integer employeeId;
@Field(offset = 18, length = 8)
@FixedFormatPattern("yyyyMMdd")
private LocalDate hireDate;
@Field(offset = 26, length = 1)
@FixedFormatBoolean(trueValue = "Y", falseValue = "N")
private Boolean active;
}
Both classes load and export identically:
FixedFormatManager manager = new FixedFormatManagerImpl();
String line = "Jane Doe 0004220260101Y";
EmployeeRecord emp = manager.load(EmployeeRecord.class, line);
System.out.println(emp.getName()); // "Jane Doe"
System.out.println(emp.getEmployeeId()); // 42
System.out.println(emp.getHireDate()); // 2026-01-01
System.out.println(emp.getActive()); // true
System.out.println(manager.export(emp));
// "Jane Doe 0004220260101Y"
Conflict behaviour: if @Field is present on both a field and its getter, an error is logged and the field annotation is used. It is recommended to annotate only one location.
Example 7 — Repeating fields
Some fixed-width formats pack several consecutive slots of the same type into a single record — for example, a shipment record that lists up to four package weights in a row. Before 1.6.0 this required a separate @Field for each slot. With count you declare it once.
Scenario: each line in a freight file holds a shipment ID (6 chars) followed by up to four package weights in grams, each stored as a 6-character right-aligned zero-padded integer. Unused trailing slots are "000000".
// positions: 1-6 = shipment ID, 7-12 = weight 1, 13-18 = weight 2, 19-24 = weight 3, 25-30 = weight 4
SHIP01002500010000000000000000
@Record
public class ShipmentRecord {
private String shipmentId;
private List<Integer> packageWeights;
@Field(offset = 1, length = 6)
public String getShipmentId() { return shipmentId; }
public void setShipmentId(String shipmentId) { this.shipmentId = shipmentId; }
// Four consecutive 6-character integer slots starting at offset 7
@Field(offset = 7, length = 6, count = 4, align = Align.RIGHT, paddingChar = '0')
public List<Integer> getPackageWeights() { return packageWeights; }
public void setPackageWeights(List<Integer> packageWeights) { this.packageWeights = packageWeights; }
}
FixedFormatManager manager = new FixedFormatManagerImpl();
String line = "SHIP01002500010000000000000000";
ShipmentRecord record = manager.load(ShipmentRecord.class, line);
System.out.println(record.getShipmentId()); // "SHIP01"
System.out.println(record.getPackageWeights()); // [2500, 1000, 0, 0]
// Update the first two weights and export
record.setPackageWeights(List.of(3200, 1800, 500, 0));
System.out.println(manager.export(record));
// "SHIP01003200001800000500000000"
Using an array instead of a List:
@Field(offset = 7, length = 6, count = 4, align = Align.RIGHT, paddingChar = '0')
public Integer[] getPackageWeights() { return packageWeights; }
Lenient export — partial collection:
If the collection may have fewer elements than count (e.g. a shipment with only 2 packages), set strictCount = false. The remaining slots are left as the template value or untouched:
@Field(offset = 7, length = 6, count = 4, align = Align.RIGHT, paddingChar = '0',
strictCount = false)
public List<Integer> getPackageWeights() { return packageWeights; }
record.setPackageWeights(List.of(3200, 1800)); // only 2 of 4 slots provided
System.out.println(manager.export(record));
// "SHIP01003200001800[slots 3 and 4 unchanged from original record]"
// Warning is logged: collection size (2) < count (4)
With strictCount = true (the default), passing a list of the wrong size throws a FixedFormatException at export time.
Example 8 — Record-level default alignment
When most or all fields in a record share the same alignment, declare it once on @Record instead of repeating align on every @Field. Individual fields can still override it.
// Before 1.7.1 — alignment repeated on every field
@Record(length = 22)
public class InvoiceRecord {
private Integer invoiceId;
private Integer amountCents;
private String currency;
@Field(offset = 1, length = 8, align = Align.RIGHT, paddingChar = '0')
public Integer getInvoiceId() { return invoiceId; }
public void setInvoiceId(Integer invoiceId) { this.invoiceId = invoiceId; }
@Field(offset = 9, length = 10, align = Align.RIGHT, paddingChar = '0')
public Integer getAmountCents() { return amountCents; }
public void setAmountCents(Integer amountCents) { this.amountCents = amountCents; }
// This text field overrides with LEFT
@Field(offset = 19, length = 3, align = Align.LEFT)
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
}
// After 1.7.1 — alignment declared once at record level
@Record(length = 22, align = RecordAlign.RIGHT)
public class InvoiceRecord {
private Integer invoiceId;
private Integer amountCents;
private String currency;
@Field(offset = 1, length = 8, paddingChar = '0')
public Integer getInvoiceId() { return invoiceId; }
public void setInvoiceId(Integer invoiceId) { this.invoiceId = invoiceId; }
@Field(offset = 9, length = 10, paddingChar = '0')
public Integer getAmountCents() { return amountCents; }
public void setAmountCents(Integer amountCents) { this.amountCents = amountCents; }
// Field-level align overrides the record default
@Field(offset = 19, length = 3, align = Align.LEFT)
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
}
FixedFormatManager manager = new FixedFormatManagerImpl();
InvoiceRecord record = new InvoiceRecord();
record.setInvoiceId(42);
record.setAmountCents(9999);
record.setCurrency("USD");
System.out.println(manager.export(record));
// "000000420000009999USD"
InvoiceRecord loaded = manager.load(InvoiceRecord.class, "000000420000009999USD");
System.out.println(loaded.getInvoiceId()); // 42
System.out.println(loaded.getAmountCents()); // 9999
System.out.println(loaded.getCurrency()); // "USD"
Example 9 — Nullable fields with nullChar
By default a fixed-width field has no notion of null — all-spaces loads as an empty string or zero. The nullChar attribute on @Field opts a single field into null-aware handling.
Scenario: an account record stores an optional credit limit. An all-spaces field means “no limit set” (null); "00000" means a limit of zero.
@Record(length = 15)
public class AccountRecord {
private String accountId;
private Integer creditLimit; // null = no limit configured
@Field(offset = 1, length = 5)
public String getAccountId() { return accountId; }
public void setAccountId(String accountId) { this.accountId = accountId; }
// spaces → null, "00000" → 0, "01000" → 1000
@Field(offset = 6, length = 5, align = Align.RIGHT, paddingChar = '0', nullChar = ' ')
public Integer getCreditLimit() { return creditLimit; }
public void setCreditLimit(Integer creditLimit) { this.creditLimit = creditLimit; }
}
FixedFormatManager manager = new FixedFormatManagerImpl();
// All-spaces in the credit-limit slot → null
AccountRecord r1 = manager.load(AccountRecord.class, "ACC01 ");
System.out.println(r1.getCreditLimit()); // null
// Zero-padded value → 0 (not null)
AccountRecord r2 = manager.load(AccountRecord.class, "ACC0200000");
System.out.println(r2.getCreditLimit()); // 0
// Export null → five spaces
r1.setAccountId("ACC03");
r1.setCreditLimit(null);
System.out.println(manager.export(r1));
// "ACC03 "
// Export a value normally
r1.setCreditLimit(1500);
System.out.println(manager.export(r1));
// "ACC0301500"
For repeating fields (count > 1) the check is applied per element: each slot is evaluated independently, so a collection can hold a mix of null and non-null values. Primitive array element types (e.g. int[]) cannot hold null and are unaffected.
Example 10 — Reading a mixed-type file with FixedFormatReader
This example shows how to read a file that contains two distinct record types — a header line and detail lines — using FixedFormatReader.
File layout (orders.txt):
HDR20260419ACME Corp
DTL000142WIDGET-A 0000099900
DTL000143BOLT-SET 0000024999
- Lines starting with
HDR: one header per file (date + company name). - Lines starting with
DTL: one detail per order (order ID, product, amount in cents).
Record classes:
@Record(length = 19)
public class OrderHeader {
private String date;
private String company;
@Field(offset = 4, length = 8)
public String getDate() { return date; }
public void setDate(String date) { this.date = date; }
@Field(offset = 12, length = 8)
public String getCompany() { return company; }
public void setCompany(String company) { this.company = company; }
}
@Record(length = 26)
public class OrderDetail {
private Integer orderId;
private String product;
private Integer amountCents;
@Field(offset = 4, length = 6, align = Align.RIGHT, paddingChar = '0')
public Integer getOrderId() { return orderId; }
public void setOrderId(Integer id) { this.orderId = id; }
@Field(offset = 10, length = 10)
public String getProduct() { return product; }
public void setProduct(String product) { this.product = product; }
@Field(offset = 20, length = 10, align = Align.RIGHT, paddingChar = '0')
public Integer getAmountCents() { return amountCents; }
public void setAmountCents(Integer amount) { this.amountCents = amount; }
}
Build the reader (shared across all output shapes below):
import com.ancientprogramming.fixedformat4j.io.read.LinePattern;
FixedFormatReader reader = FixedFormatReader.builder()
.addMapping(OrderHeader.class, LinePattern.prefix("HDR"))
.addMapping(OrderDetail.class, LinePattern.prefix("DTL"))
.build();
Reading as ReadResult (type-safe, no casts):
ReadResult result = reader.read(Path.of("orders.txt"));
OrderHeader header = result.get(OrderHeader.class).get(0); // no cast
System.out.println(header.getDate()); // "20260419"
System.out.println(header.getCompany()); // "ACME Corp"
List<OrderDetail> details = result.get(OrderDetail.class); // no cast
System.out.println(details.size()); // 2
Typed handler dispatch — push-style, no collection step:
FixedFormatReader reader = FixedFormatReader.builder()
.addMapping(OrderHeader.class, LinePattern.prefix("HDR"))
.addMapping(OrderDetail.class, LinePattern.prefix("DTL"))
.build();
reader.process(Path.of("orders.txt"), new HandlerRegistry()
.on(OrderHeader.class, header -> System.out.println("Header: " + header.getDate()))
.on(OrderDetail.class, detail -> System.out.printf("Order %d: %s — %d cents%n",
detail.getOrderId(), detail.getProduct(), detail.getAmountCents())));
For the complete FixedFormatReader API — strategies, charset overloads, and pre-match filtering — see the File Processing guide.