Frequently Asked Questions
Can fixedformat4j help me parse large text files?
Yes. Since 1.8.0, FixedFormatReader provides built-in file processing. Supply a HandlerRegistry to process — each record is parsed and dispatched immediately without loading the whole file into memory:
import com.ancientprogramming.fixedformat4j.io.read.LinePattern;
FixedFormatReader reader = FixedFormatReader.builder()
.addMapping(MyRecord.class, LinePattern.matchAll())
.build();
reader.process(Path.of("large.txt"),
new HandlerRegistry().on(MyRecord.class, processor::process));
See File Processing for the full API including output shapes, strategies, and heterogeneous-file support.
Can I use fixedformat4j with Spring?
Yes — both FixedFormatManager and FixedFormatReader are plain Java objects with no Spring dependency; wire them as @Beans in a @Configuration class and inject them wherever you need them.
Registering FixedFormatManager:
@Configuration
public class FixedFormatConfig {
@Bean
public FixedFormatManager fixedFormatManager() {
return new FixedFormatManagerImpl();
}
}
Because the bean is registered against the FixedFormatManager interface, it is easy to mock in tests and swap implementations without touching call sites.
Registering FixedFormatReader:
import com.ancientprogramming.fixedformat4j.io.read.LinePattern;
@Bean
public FixedFormatReader payrollReader() {
return FixedFormatReader.builder()
.addMapping(HeaderRecord.class, LinePattern.prefix("HDR"))
.addMapping(DetailRecord.class, LinePattern.prefix("DTL"))
.build();
}
Each FixedFormatReader instance is immutable and thread-safe after construction, so a single singleton bean shared across the application is fine.
Injecting into a service:
@Service
public class PayrollService {
private final FixedFormatManager manager;
private final FixedFormatReader reader;
public PayrollService(FixedFormatManager manager, FixedFormatReader reader) {
this.manager = manager;
this.reader = reader;
}
public List<DetailRecord> load(Path file) {
return reader.read(file).get(DetailRecord.class);
}
}
No Spring Boot auto-configuration or starter is required — add the dependency, write the @Configuration class, and you’re done.
Can I use Lombok with fixedformat4j?
Yes. Since 1.5.0, @Field can be placed directly on Java fields rather than getter methods. Add @Getter @Setter @NoArgsConstructor to the class; the manager derives getter/setter names by convention and no boilerplate is needed:
@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;
}
Key points:
- Supplementary annotations (
@FixedFormatPattern,@FixedFormatBoolean,@FixedFormatDecimal,@FixedFormatNumber) can also be placed on fields. @NoArgsConstructoris required — the manager instantiates the record via its no-arg constructor before calling setters.- If
@Fieldappears on both a field and its getter, the field annotation takes precedence (an error is logged). Annotate only one location.
See Example 6 for a full worked example including the equivalent plain-POJO style.
Can I apply my own custom formatter?
Yes.
Extend AbstractFixedFormatter<T> (recommended) or implement FixedFormatter<T> directly. The two methods you must provide are asObject (string → your type) and asString (your type → string):
public class CustomFormatter extends AbstractFixedFormatter<MyType> {
@Override
public MyType asObject(String value, FormatInstructions instructions) {
// 'value' has already had padding stripped by the base class
return MyType.parse(value);
}
@Override
public String asString(MyType value, FormatInstructions instructions) {
return value.toFixedString();
}
}
To access supplementary annotation data (e.g., a pattern supplied via @FixedFormatPattern), use the FormatInstructions argument:
String pattern = instructions.getFixedFormatPatternData().getPattern();
To instruct the manager to use your custom formatter, set the formatter attribute on the @Field annotation:
@Field(offset = 1, length = 10, formatter = CustomFormatter.class)
See Example 5 — Custom formatter for a complete working implementation.
Can I change how padding is stripped on read?
Yes. The default behaviour — strip paddingChar from the alignment side — is defined in AbstractFixedFormatter.stripPadding. Override it in a formatter subclass when you need different semantics, then wire it in with @Field(formatter = ...).
Preserve the raw value (no stripping):
public class RawStringFormatter extends StringFormatter {
@Override
protected String stripPadding(String value, FormatInstructions instructions) {
return value;
}
}
Use when leading or trailing characters are meaningful — e.g. a formatted identifier where the padding is part of the value. Round-trip export is preserved because no information is dropped.
Strip padding from both ends:
public class BothSidesStringFormatter extends StringFormatter {
@Override
protected String stripPadding(String value, FormatInstructions instructions) {
char pad = instructions.getPaddingChar();
return Align.LEFT.remove(Align.RIGHT.remove(value, pad), pad);
}
}
Use when a producer pads inconsistently. Note: export re-pads on the alignment side only, so round-trip is not preserved — opposite-side padding is permanently dropped on read.
Strip all whitespace (including embedded):
public class AllWhitespaceStringFormatter extends StringFormatter {
@Override
protected String stripPadding(String value, FormatInstructions instructions) {
return value.replaceAll("\\s+", "");
}
}
Use when the source field contains internal whitespace that should be discarded. Round-trip is lossy.
Scope note:
stripPaddingis the extension point forString,Character,Boolean, and enum fields. Numeric formatters (AbstractNumberFormatter) and date/time formatters (AbstractPatternFormatter) overrideparse/stripPaddingwith their own sign-aware and pattern-aware logic, so a custom padding strategy for those types must extend the corresponding concrete formatter and override the relevant method directly.
How do I parse blank fields as a default value instead of throwing?
Numeric formatters call Integer.parseInt / Long.parseLong / BigDecimal’s constructor on the stripped slice, so a blank numeric slice throws NumberFormatException by default. Two approaches cover the “blank = default” convention without a custom annotation attribute.
For reference types (Integer, Long, BigDecimal, Date, etc.) — use a POJO field default plus nullChar:
When nullChar activates on a blank slice, the manager sets the computed value to null and skips the setter entirely, so whatever the POJO initialised the field with is preserved.
@Record(length = 20)
public class Invoice {
private Integer repairHours = 0; // default when the slice is all nullChar
@Field(offset = 1, length = 4, paddingChar = '0', nullChar = ' ', align = Align.RIGHT)
public Integer getRepairHours() { return repairHours; }
public void setRepairHours(Integer h) { this.repairHours = h; }
}
- Blank slice
" "→ setter skipped →repairHoursstays at0. - Non-blank slice
"0042"→ setter invoked →repairHours = 42.
The key activation rule: nullChar must be explicitly set (anything other than the default '\0' sentinel). Since 1.7.2, setting nullChar == paddingChar also works and enables the “blank-is-null” convention — useful when you do not have a distinct sentinel character available (e.g. all-spaces dates, all-zeros numerics).
For primitive types (int, long, double, etc.) — use a lenient formatter subclass:
Primitives cannot be set to null, so @Field(nullChar = ...) is rejected at validation time. Override asObject on the built-in formatter and substitute your default when the input is empty:
public class LenientIntegerFormatter extends IntegerFormatter {
@Override
public Integer asObject(String string, FormatInstructions fi) {
return string.isEmpty() ? 0 : super.asObject(string, fi);
}
}
@Field(offset = 1, length = 4, paddingChar = '0', formatter = LenientIntegerFormatter.class)
public int getRepairHours() { return repairHours; }
The same pattern applies to LongFormatter, ShortFormatter, DoubleFormatter, FloatFormatter, and BigDecimalFormatter.
How do I handle records with different layouts in the same file?
Define a separate @Record-annotated class for each layout. When reading the file line by line, inspect a discriminator field (such as a record-type code in a known column) and call manager.load(...) with the matching class:
while ((line = reader.readLine()) != null) {
String type = line.substring(0, 1); // type code in column 1
if ("H".equals(type)) {
headers.add(manager.load(HeaderRecord.class, line));
} else if ("D".equals(type)) {
details.add(manager.load(DetailRecord.class, line));
}
}
Since 1.8.0, FixedFormatReader handles this pattern directly — register each record class with a LinePattern and let the reader route lines automatically:
import com.ancientprogramming.fixedformat4j.io.read.LinePattern;
FixedFormatReader reader = FixedFormatReader.builder()
.addMapping(HeaderRecord.class, LinePattern.prefix("H"))
.addMapping(DetailRecord.class, LinePattern.prefix("D"))
.build();
ReadResult result = reader.read(Path.of("data.txt"));
List<HeaderRecord> headers = result.get(HeaderRecord.class);
List<DetailRecord> details = result.get(DetailRecord.class);
See File Processing for the full API, or Example 4 for the manual loop approach.