This repository provides Java tools to generate code for type-safe "for each object's field" kind of loops. If e.g. a new field is added, or a field is removed/renamed, the generated code will signal with compilation error and require manual action to handle the changed field. This is similar to IDE errors from missing cases in switch-on-enum, but more generic.
The additional code is generated at compile time using Java annotation processing. After initial configuration, no manual compilation or generated-source directory setup is necessary.
@GenerateFieldVisitor: generates an abstract visitor for type-safe processing of all fields/components.@GenerateFieldEnum: generates an enum containing all field/component names withgetFieldName().@GenerateFieldNames: generates an interface with string constants for all field/component names.@GenerateTransformMapper: generates an abstract mapper for field-by-field transformation.
Generates a visitor class for type-safe processing of each field/component in the annotated type.
| Option | Type | Default | Description |
|---|---|---|---|
generatedName |
String |
"" |
Custom name of the generated type. Empty value uses default <TypeName>FieldVisitor. |
visibility |
GeneratedVisibility |
PUBLIC |
Visibility of the generated type; it does not change constructor access. |
@GenerateFieldVisitor
public class Order {
private long id;
private List<OrderItem> items;
// ...
}Generates an OrderFieldVisitor abstract base class that can be extended to customize processing.
Its constructor is package-private by design, so named or anonymous implementations must be declared in the same
package as the annotated type. Visitors are intended as short-lived helpers, although visitAll() is not restricted to
a single call.
public class OrderFieldProcessor extends OrderFieldVisitor {
OrderFieldProcessor(Order instance) {
super(instance);
}
@Override
protected void visitId(long value) {
// process id field
}
@Override
protected void visitItems(List<OrderItem> value) {
// process order items field
}
}It can then be used to process all fields of an instance:
new OrderFieldProcessor(order).visitAll();The main benefit is that the compiler checks that all fields/components are handled. Compilation breaks when the model changes and a new field/component is not handled yet.
- Ensure that a copy constructor handles all fields
- Ensure that soft-delete also marks child entities as soft-deleted
- Separate serialization logic to another class
- Separate validation logic to another class
- PII redaction before logging, where each sensitive field must be explicitly handled.
- Audit trail generation where every field/component must be classified.
- (Partial) IntelliJ IDEA checks that copy constructor handles all fields
Generates an enum containing all fields/components of the annotated type as enum values. The enum values have getFieldName() getter returning the original field/component name.
| Option | Type | Default | Description |
|---|---|---|---|
generatedName |
String |
"" |
Custom name of the generated type. Empty value uses default <TypeName>Fields. |
visibility |
GeneratedVisibility |
PUBLIC |
Visibility of the generated type. |
When @GenerateFieldNames and @GenerateFieldEnum are applied to the same type, their effective generated names must
differ, including by more than letter case. Since their defaults are identical, set generatedName on at least one.
@GenerateFieldEnum
public class OrderLine {
String productName;
int quantity;
}Generates enum values and a field-name getter:
assertEquals("productName", OrderLineFields.PRODUCT_NAME.getFieldName());Field/component names that normalize to the same enum constant are rejected. For example, myField and my_field
would both generate MY_FIELD.
- UI table column configuration (show/hide/sort) with exhaustive
switchover generated enum constants. - API sort/filter allowlists where new fields/components must be explicitly approved.
- Export mapping (CSV/JSON) where each field/component gets a required mapping rule.
Generates an interface containing all field/component names of the annotated type as constant String fields.
| Option | Type | Default | Description |
|---|---|---|---|
generatedName |
String |
"" |
Custom name of the generated type. Empty value uses default <TypeName>Fields. |
visibility |
GeneratedVisibility |
PUBLIC |
Visibility of the generated type. |
@GenerateFieldNames
public class Order {
String id;
}Generated constants can be used for reflection safely:
Field idField = Order.class.getDeclaredField(OrderFields.id);
idField.setAccessible(true);
idField.set(order, "ORDER-1");- Reflection-based patch/update handling without string literals.
- Dynamic query or filter builders with compiler-safe field/component names.
- Field-level authorization maps keyed by generated constants.
- Java: No viable alternative without other annotation processors
- Lombok
@FieldNameConstantscan generate field name constants.
Generates a mapper for type-safe field-by-field transformation.
| Option | Type | Default | Description |
|---|---|---|---|
generatedName |
String |
"" |
Custom name of the generated type. Empty value uses default <TypeName>FieldMapper. |
visibility |
GeneratedVisibility |
PUBLIC |
Visibility of the generated type; it does not change constructor access. |
- For classes, generated
mapAllTo(target)maps values into the provided target instance. - For records, generated
mapAll()creates and returns a new record instance. - Generated mapper constructors are package-private by design. Named or anonymous implementations must be declared in the same package as the annotated type and are intended as short-lived helpers.
For classes:
@GenerateTransformMapper
public class Shipment {
String id;
String sender;
String receiver;
ShipmentState status;
}
public class ReturnShipmentMapper extends ShipmentFieldMapper {
public ReturnShipmentMapper(Shipment source) {
super(source);
}
@Override
protected void setId(Shipment source, String sourceFieldValue, Consumer<String> setter) {
setter.accept(sourceFieldValue);
}
@Override
protected void setSender(Shipment source, String sourceFieldValue, Consumer<String> setter) {
setter.accept(source.receiver);
}
@Override
protected void setReceiver(Shipment source, String sourceFieldValue, Consumer<String> setter) {
setter.accept(source.sender);
}
@Override
protected void setStatus(Shipment source, ShipmentState sourceFieldValue, Consumer<ShipmentState> setter) {
setter.accept(ShipmentState.AT_ORIGIN);
}
}
// Usage
Shipment target = new Shipment();
new ReturnShipmentMapper(source).mapAllTo(target);For records:
@GenerateTransformMapper
public record ShipmentRecord(String id, int quantity) {
}
public class ShipmentRecordMapper extends ShipmentRecordFieldMapper {
public ShipmentRecordMapper(ShipmentRecord source) {
super(source);
}
@Override
protected String mapId(ShipmentRecord source, String sourceFieldValue) {
return sourceFieldValue + "-mapped";
}
@Override
protected int mapQuantity(ShipmentRecord source, int sourceFieldValue) {
return sourceFieldValue + 1;
}
}
ShipmentRecord mapped = new ShipmentRecordMapper(source).mapAll();- Return shipment creation, where sender/receiver are swapped and status is reset.
- DTO-to-entity update flows with per-field normalization/conversion rules.
- Model version migration (
v1 -> v2) where added/renamed fields/components break compilation until mapped.
- MapStruct provides compile-time generated mappers with explicit field mapping support.
- A reflection-based unit test can be made for this case in such a way that the test fails on unknown fields and has known fields categorized to "stays same", "is nulled", etc. categories.
Java 21 is required. The library is distributed as two artifacts:
| Artifact | Responsibility |
|---|---|
com.github.emick.codegen:foreach-field-gen-core:0.1.0-SNAPSHOT |
Annotations, GeneratedVisibility, and runtime reflection support. |
com.github.emick.codegen:foreach-field-gen-processor:0.1.0-SNAPSHOT |
Java annotation processors and their compile-time dependencies. |
The processor is needed only at compile time. Generated visitors and class mappers use FieldGenReflectionUtil at runtime;
when using named Java modules, the packages containing reflected fields must be opened to
com.github.emick.codegen.core.
To publish both artifacts to Maven local:
- Clone this repository to your local machine
- Run
./gradlew clean publishToMavenLocal
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'com.github.emick.codegen:foreach-field-gen-core:0.1.0-SNAPSHOT'
annotationProcessor 'com.github.emick.codegen:foreach-field-gen-processor:0.1.0-SNAPSHOT'
}Gradle automatically adds annotation-processor output to the main compilation. The generated types are available to the same source set and are not runtime dependencies of the processor artifact.
After publishing to Maven local, add the core artifact as a normal dependency and the processor artifact to the compiler's annotation-processor path:
<dependencies>
<dependency>
<groupId>com.github.emick.codegen</groupId>
<artifactId>foreach-field-gen-core</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
</dependencies>Configure the compiler plugin with both artifacts on its processor path:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
<annotationProcessorPaths>
<path>
<groupId>com.github.emick.codegen</groupId>
<artifactId>foreach-field-gen-core</artifactId>
<version>0.1.0-SNAPSHOT</version>
</path>
<path>
<groupId>com.github.emick.codegen</groupId>
<artifactId>foreach-field-gen-processor</artifactId>
<version>0.1.0-SNAPSHOT</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>The four annotations generate types in the annotated type's package. Class fields are read and written reflectively at runtime; record components use their accessor methods. Only non-static fields declared directly by a class are generated, and generated visitor/mapper constructors are package-private, so implementations belong in the annotated type's package.
- FieldVisitor annotation could have options to generate
fieldNameandfieldTypeparameters in visitor methods - Investigate GraalVM native build support
- Java 21+ is required
- Only fields/components declared directly by the annotated type are generated. Runtime subclasses are supported, but fields declared or hidden by those subclasses are not processed.
- GraalVM native build is not tested and most likely not supported for all annotations due to the usage of reflection
- https://github.com/ryandens/auto-delegate - generate base class for proxy/decorator pattern to avoid unnecessary super-calling methods
- https://github.com/cmelchior/realmfieldnameshelper - Realm extension to create type-safe field references