Releases: jeyben/fixedformat4j
1.8.1
Performance improvements
load() and export() are measurably faster, especially for workloads that process many records or hit the same record class repeatedly:
- Less reflection per call — formatter instances are resolved once at cache-warm time instead of being created fresh on every field operation.
- Date and time fields are cheaper —
DateTimeFormatterandSimpleDateFormatinstances are cached and reused, eliminating the most expensive allocation in date parsing and formatting. - Tighter memory use — internal maps are pre-sized and constant string values are pre-computed at startup rather than rebuilt on every operation.
No API or behaviour change. Existing annotated record classes, custom formatters, and serialized fixed-width data are unaffected. Upgrade by bumping the version number.
Full changelog: https://jeyben.github.io/fixedformat4j/changelog
1.8.0
1.8.0 (2026-05-01)
Breaking changes
-
Removed deprecated
protected FixedFormatManagerImpl#readDataAccordingFieldAnnotation(#109) —
The method was deprecated since #77 (1.7.0) and was never on the liveload()path, which has used
ClassMetadataCacheexclusively since that release. Only consumers thatextend FixedFormatManagerImpl
and override or call this method are affected; consumers using theFixedFormatManagerinterface are
unaffected.Migration: subclassers should drive parsing through
FixedFormatManager#load(Class, String)instead. -
@Record(align)now usesRecordAligninstead ofAlign(#81) —
A new two-value enumRecordAlign { LEFT, RIGHT }replacesAlignas the type of@Record#align().
BecauseAlignincludes theINHERITsentinel, which has no meaning at the record level, the old
type admitted a combination that was only detectable at runtime (and was rejected with a
FixedFormatExceptionsince 1.7.1).RecordAlignmakes that mistake impossible at compile time
and removes the runtime check.Migration: replace
Align.LEFT/Align.RIGHTwithRecordAlign.LEFT/RecordAlign.RIGHT
on every@Recordannotation that specifies thealignattribute:// Before (1.7.x) @Record(length = 20, align = Align.RIGHT) public class MyRecord { … } // After (1.8.0+) @Record(length = 20, align = RecordAlign.RIGHT) public class MyRecord { … }
Records that do not specify
alignare unaffected — the default (RecordAlign.LEFT)
preserves the existing behaviour. TheAlignenum itself is unchanged and continues to be
used for@Field(align = …).
New features
-
FixedFormatReader— file and stream processing (#82,
#95) —
Reads fixed-format records from files, streams, orReaders line-by-line, routing each line
to one or more@Record-annotated classes viaLinePatterndiscriminators. Three factories cover
the common cases:LinePattern.prefix("HDR"),LinePattern.positional(int[], String)for
multi-position checks (e.g. type code at offset 0..2 plus a sub-type at offset 7..8), and
LinePattern.matchAll()for catch-all routing. Patterns are bucketed into hash tables at build
time, so per-line routing is near O(1) regardless of how many record types are registered.
FixedFormatReaderis unparameterized.Two output shapes:
read()— returnsReadResult, a type-safe class-keyed container;get(Class<R>)returnsList<R>with no cast required. Also providesgetAll(),contains(Class<?>), andclasses().process(source, HandlerRegistry)— push-style; dispatches each parsed record to the typedConsumer<R>registered in a per-callHandlerRegistry. Classes absent from the registry are silently ignored. Because the registry is supplied at call time, the same reader is safe to use from multiple threads.
Every shape accepts
Reader,InputStream, orPath; stream overloads default to UTF-8.Three configurable strategies:
MultiMatchStrategy(firstMatch/throwOnAmbiguity/
allMatches),UnmatchStrategy(skip/throwException), andParseErrorStrategy
(throwException/skipAndLog). AnexcludeLines(Predicate<String>)pre-filter runs
before pattern matching and bypassesUnmatchStrategy.RecordMapping<T>is the public value type carrying the class and pattern for each registered
mapping; it is surfaced as the parameter and return type ofMultiMatchStrategy.resolve().
Consumers implementing a customMultiMatchStrategymust reference it directly.FixedFormatIOException(extendsFixedFormatException) is thrown on underlyingIOException.import com.ancientprogramming.fixedformat4j.io.read.LinePattern; FixedFormatReader reader = FixedFormatReader.builder() .addMapping(HeaderRecord.class, LinePattern.prefix("HDR")) .addMapping(DetailRecord.class, LinePattern.prefix("DTL")) .build(); ReadResult result = reader.read(Path.of("data.txt")); List<HeaderRecord> headers = result.get(HeaderRecord.class); // no cast List<DetailRecord> details = result.get(DetailRecord.class); // no cast
See File processing for a complete guide.
Bug fixes
-
Classloader leak prevention via
ClassValue(#89) —
The three JVM-level caches (ClassMetadataCache,FixedFormatManagerImpl.VALIDATED_CLASSES, and
AbstractPatternFormatter.PATTERN_LENGTH_CACHE) were backed by static
ConcurrentHashMap<Class<?>, …>instances. AConcurrentHashMapholds strong references to
its keys, so aClassused as a key can never be garbage-collected — even after all application
references to it are gone. In multi-classloader environments (OSGi, servlet containers, Spring
Boot DevTools, Jakarta EE) this causes the childClassLoaderthat defined the record class to
be retained indefinitely, leaking all classes it loaded.All three caches are now backed by
ClassValue<T>. Computed values are stored inside the
Classobject itself; when the record class's definingClassLoaderbecomes unreachable the
cached metadata is collected with it — no external map, no leak.No API or behaviour change. Existing annotated record classes, custom formatters, and
serialized fixed-width data are unaffected.
1.7.2
1.7.1
New features
-
nullCharon@Field— opt-in sentinel to distinguish a genuinely-absent field from zero/empty. Null-aware handling is active only whennullChardiffers frompaddingChar. On load, an all-nullCharslice yieldsnull; on export, anullvalue is emitted aslength × nullChar. Works per-element for repeating fields (count > 1). (#29) -
Record-level default alignment via
@Record(align = …)— sets a default alignment for all fields in the record; individual fields may still override with an explicit@Field(align = …). (#30)
Validation improvements
Align.INHERITis now rejected on@Record(align)with a clearFixedFormatException(it is a field-only sentinel).nullCharis now rejected on@Fieldwith a primitive return type (int,long, etc.) — primitives can never benull.
1.7.0
Breaking changes
-
AbstractFixedFormatter.getRemovePaddingremoved — deprecated in 1.6.1 and now deleted.
Rename any override tostripPadding; the signature is identical. The call chain is now
parse()→stripPadding()directly.// Before (1.6.x) @Override protected String getRemovePadding(String value, FormatInstructions instructions) { … } // After (1.7.0+) @Override protected String stripPadding(String value, FormatInstructions instructions) { … }
New features
-
Enum support via
@FixedFormatEnum(#67) —
Annotate any getter that returns anenumtype with@FixedFormatEnumto control how the value
is serialised in the fixed-width record. Two modes are available through theEnumFormatenum:LITERAL(default) — stores and reads the enum constant name (Enum.name()/valueOf()).NUMERIC— stores and reads the ordinal as a zero-padded integer (Enum.ordinal()/ index lookup).
public enum Status { ACTIVE, INACTIVE } // LITERAL (default): stores "ACTIVE" / "INACTIVE" @Field(offset = 1, length = 8) @FixedFormatEnum public Status getStatus() { … } // NUMERIC: stores "0" / "1" @Field(offset = 1, length = 1) @FixedFormatEnum(EnumFormat.NUMERIC) public Status getStatus() { … }
Performance improvements
-
Field metadata caching (#77) —
ClassMetadataCacheprecomputes and caches all field descriptors per annotated class on first
use, eliminating repeated annotation scanning on everyload()/export()call. The cache is
process-wide and thread-safe. -
MethodHandle dispatch (#75) —
Getter and setter invocation now usesMethodHandleinstead ofMethod.invoke(), reducing
per-call overhead after JIT warmup. -
Reduced string allocations (#76) —
Padding and sign handling rewritten to minimise intermediateStringobject creation per field.
1.6.1
Bug fixes
DateFormatter(andLocalDateFormatter/LocalDateTimeFormatter) no longer over-strips padding characters (#33) — When the configuredpaddingCharhappened to be a character that also appears in the formatted date string (e.g.paddingChar = '0'with a time value whose seconds component is00), the previousstripPaddingimplementation removed those characters from the parsed string, leaving it too short and causing aParseException. The fix introducesAbstractPatternFormatter, which overridesstripPaddingto remove only leading/trailing padding characters rather than all occurrences of the character.
Deprecations
-
AbstractFixedFormatter.getRemovePaddingdeprecated — The method has been renamed tostripPadding, which better reflects its behaviour. The old name carried a misleadinggetprefix that implied a zero-argument accessor.getRemovePaddingremains callable and fully functional in 1.6.1; it now delegates tostripPadding. It will be removed in 1.7.0.Migration: rename any override of
getRemovePaddingtostripPadding— the signature is identical:// Before (1.6.0 and earlier) @Override protected String getRemovePadding(String value, FormatInstructions instructions) { … } // After (1.6.1+) @Override protected String stripPadding(String value, FormatInstructions instructions) { … }
1.6.0
What's New
Repeating Fields
- Added
countandstrictCountattributes to@Fieldfor repeating field support — a single annotated getter can now map to multiple consecutive fixed-width slots - Clarified
@Fieldsdocumentation: prefer@Field(count=…)for repeating fields
LocalDateTime Support
- Added built-in formatter for
java.time.LocalDateTimewith type-specific default patterns - Eager pattern validation catches misconfigured
@FixedFormatPatternat load time rather than at parse time
Mutation Testing (CI/CD)
- Added PIT mutation testing with automated GitHub Pages report publishing
- Nightly PIT report published to GitHub Pages with links from README and docs navigation
Documentation & Internals
- Added fixed left-sidebar navigation to docs
- Refactored
FixedFormatManagerImplinto focused collaborators for improved maintainability - Added public method Javadoc across all production classes
- Switched to SLF4J parameterized logging and
String.formatthroughout
Maintenance
- Issue tracker updated from Google Code to GitHub Issues
- Removed
maven-changes-plugin(changelog now maintained inCHANGELOG.md)
1.5.0
1.5.0
New features
-
Field-level
@Fieldand@Fieldsannotations —@Fieldand@Fieldscan now be placed directly on Java fields in addition to getter methods. The manager discovers them at runtime and derives the getter/setter by theget/isnaming convention. This enables clean usage with Lombok (@Getter/@Setter) and reduces boilerplate in plain POJOs.If both the field and its getter carry
@Field, an error is logged (configuration mismatch) and the field annotation is used.
1.4.0
1.4.0 (2026-04-05)
New features
-
LocalDatesupport —java.time.LocalDateis now a first-class field type handled automatically byByTypeFormatter. No custom formatter needed. Configure the date pattern with@FixedFormatPattern(default:yyyyMMdd).@Field(offset = 1, length = 8) @FixedFormatPattern("yyyyMMdd") public LocalDate getEventDate() { return eventDate; }
String
"20260405"parses toLocalDate.of(2026, 4, 5); exporting writes"20260405"back.
Breaking changes
- Java 11 minimum — Java 8 is no longer supported. The minimum required runtime is Java 11.
- Logging: SLF4J replaces Commons Logging — The library no longer depends on Apache Commons Logging. Logging is now done via SLF4J. If your project relied on the transitive
commons-loggingdependency, you will need to add an SLF4J binding instead (e.g.logback-classicorslf4j-simple). See Get It for details.
Documentation
- Added Quick Start guide, Examples page, and an enriched Annotations reference.
Fixedformat4j v1.3.7
Fixed bug in how negative decimal numbers between 0 and -1 is parsed big the decimal parser