Releases: jeyben/fixedformat4j
Release list
1.9.1
Maintenance release on the 1.9.x line.
Bug fix: custom formatter= on enum fields no longer triggers the enum length check (#161) — fixed in lock-step in both the runtime (FieldValidator) and the compile-time annotation processor (FieldChecker). Forward-ported from the 1.7.4 / 1.8.2 maintenance releases.
See changelog for details.
1.8.2
1.7.4
Maintenance release on the 1.7.x line.
Corrected republish of 1.7.3 — the 1.7.3 artifact on Maven Central was built from the wrong sources (release-tooling bug, fixed in #164); 1.7.4 ships the intended 1.7.x build carrying the #161 fix: custom formatter= on enum fields no longer triggers the built-in enum length validation.
See changelog for details.
1.9.0
A feature release introducing two new optional artifacts (Micrometer instrumentation and a compile-time annotation processor), Java record support, a schema introspection API, and substantial performance work — while staying fully compatible with Java 11 and existing 1.8.x record classes.
New features
- Micrometer instrumentation — new optional
fixedformat4j-micrometerartifact (#120) — decorator-based metrics for any Micrometer registry (Spring Boot Actuator, Quarkus, Micronaut, plain Java).FixedFormatMetrics.of(registry).instrument(manager)publishesfixedformat.load/fixedformat.exporttimers, a parse-error counter, and a metadata-cache gauge; wrapper factories add reader line counters. The core artifact is unchanged and gains no dependencies; steady-state overhead is tens of nanoseconds per operation, only on instrumented managers (#140). See Metrics. - Compile-time annotation validation — new optional
fixedformat4j-processorartifact (#118) — validates@Field/@Recordconfiguration duringjavac, turning misconfigurations into compile errors instead of runtimeFixedFormatExceptions. Checks patterns, enum widths, null sentinels, rest-of-line rules, record-length overflow, and overlapping offsets. Strictly opt-in; zero runtime cost. Note: the two layout checks are stricter than the runtime and can surface latent bugs in existing code. See Compile-time validation. - Java
recordsupport (#119) —@Recordclasses can now be Javarecordtypes (JDK 16+). Annotate the components directly;load()binds through the canonical constructor,export()reads the accessors. All annotations work exactly as on getters, including nested records, repeating fields, and null sentinels. The artifact still runs on Java 11 — record binding activates only when a record class is encountered. - Schema introspection API —
FixedFormatIntrospector(#117) —introspect(Class<?>)returns one immutableFieldInfoper@Field(ordered by offset): name, offset, length, type, alignment, padding, null sentinels, formatter, repeat count, nested-record flag. Delivered as a separate interface so third-partyFixedFormatManagerimplementations stay compatible. See Schema introspection. @Field.nullValue— literal null sentinel string (#130) — complement tonullCharfor feeds where the null marker is a mixed-character string (e.g."9998"). Strictly opt-in; mutually exclusive withnullChar.- Pluggable type registry (#116) —
FixedFormatManagerImpl.builder().registerType(UUID.class, UUIDFormatter.class).build()registers a custom type-to-formatter mapping once at startup instead of repeatingformatter=on every field. FixedFormatWriter— write-side IO symmetry (#114) — fluent builder API for writing fixed-format files toWriter,OutputStream, orPath, fromIterableor lazyStreamsources, with charset and line-separator control. Heterogeneous record lists supported; instances are thread-safe.FixedFormatReader.openStream()— lazy stream processing (#115) — returns a lazyStream(untyped or filtered to a record type) so arbitrarily large files can be processed with bounded memory. Caller owns the stream lifecycle via try-with-resources.
Performance improvements
Repeating fields (@Field(count > 1)) are significantly faster: per-class metadata caching makes loading ~1.9× and exporting ~1.25× faster in the bundled JMH benchmark. ByTypeFormatter resolves its delegate once instead of reflectively per call, enum constants are cached, and decimal export skips DecimalFormat entirely (~15% faster small-record export, and output is now guaranteed locale-independent — previously a JVM defaulting to a non-Latin-digit locale could export digits the parser couldn't round-trip).
Bug fixes
- LEFT-aligned signed numbers no longer lose their leading digit —
Sign.PREPEND/APPENDfreed the sign slot from the wrong side for LEFT alignment, corrupting the value (e.g.5exported as"+ "and round-tripped as0). RIGHT-aligned output is byte-for-byte unchanged. - NUMERIC enum ordinal 0 survives a round trip with
paddingChar='0'— an all-zeros field now loads as the ordinal-0 constant instead ofnull. - Parse failures keep their root cause — date/time formatters now chain the underlying
ParseException/DateTimeParseExceptioninstead of discarding it.
Dependency changes
- commons-lang3 removed — replaced with Java 11 natives.
fixedformat4jnow has a single compile-scope dependency:slf4j-api. Action required only if your project relied on the transitive commons-lang3 — declare it directly in that case.
Build
- All modules compile with
--release 11, validating builds on newer JDKs against the Java 11 API signature.
Full details (including documented known limitations) in the changelog.
Full diff: 1_8_1...1_9_0
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) { … }