Skip to content

Repository files navigation

jvm-typesafe-codegen

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.

Annotation Summary

@GenerateFieldVisitor

Generates a visitor class for type-safe processing of each field/component in the annotated type.

Options

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.

Usage

@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();

Real world usage

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.

Alternatives

  • (Partial) IntelliJ IDEA checks that copy constructor handles all fields

@GenerateFieldEnum

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.

Options

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.

Usage

@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.

Real world usage

  • UI table column configuration (show/hide/sort) with exhaustive switch over 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.

@GenerateFieldNames

Generates an interface containing all field/component names of the annotated type as constant String fields.

Options

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.

Usage

@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");

Real world usage

  • 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.

Alternatives

  1. Java: No viable alternative without other annotation processors
  2. Lombok @FieldNameConstants can generate field name constants.

@GenerateTransformMapper

Generates a mapper for type-safe field-by-field transformation.

Options

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.

Behavior

  • 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.

Usage

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();

Real world usage

  • 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.

Alternatives

  1. MapStruct provides compile-time generated mappers with explicit field mapping support.
  2. 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.

Setup

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:

  1. Clone this repository to your local machine
  2. Run ./gradlew clean publishToMavenLocal

Usage in a Gradle project

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.

Usage in a Maven project

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.

Possible Future Improvements

  • FieldVisitor annotation could have options to generate fieldName and fieldType parameters in visitor methods
  • Investigate GraalVM native build support

Limitations

  • 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

Links

About

Code generation tools for JVM languages (Java etc.)

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages