Java

Usage

First, add the annotation and processor dependencies to your favorite build-tool. See below for Setup.

Mark your domain-primitive with the @LazyValue annotation and the processor will figure out the rest.

Quantity Record
@LazyValue()
public record Quantity(int value) {

    public Quantity {
        if(value <= 0){
            throw new IllegalArgumentException("Quantity must be greater than 0");
        }
    }
}
ISBN Object with factory method
@LazyValue
public final class Isbn {

    private final String value;

    private Isbn(String value){
        Objects.requireNonNull(value);
        this.value = value;
    }

    public String value(){
        return value;
    }

    // will be used by the annotation processor (factory methods have higher precedence)
    public static Isbn parse(String value) throws IllegalArgumentException {
        Objects.requireNonNull(value, "ISBN cannot be null");

        String cleanValue = value.replaceAll("[-\\s]", "");

        if (cleanValue.length() == 10) {
            validateIsbn10(cleanValue);
        } else if (cleanValue.length() == 13) {
            validateIsbn13(cleanValue);
        } else {
            throw new IllegalArgumentException("Invalid ISBN length. Must be 10 or 13 digits (excluding hyphens)");
        }

        return new Isbn(value);
    }

    // ... validation, equals, hashCode, toString
Birthdate with derived state
@LazyValue
public final class Birthdate {

    // ISO-8601 string, the canonical storage form
    private final String value;
    // derived state computed from `value` — excluded from validation by `transient`
    private final transient LocalDate parsed;

    private Birthdate(String value) {
        this.value = value;
        this.parsed = LocalDate.parse(value);
    }

    public String value() {
        return value;
    }

    public LocalDate asLocalDate() {
        return parsed;
    }

    public static Birthdate of(String isoDate) {
        Objects.requireNonNull(isoDate, "Birthdate cannot be null");
        return new Birthdate(isoDate);
    }
}

A value type may carry derived fields that are computed from the stored payload (here parsed from value). Lazyval validates that exactly one non-transient instance field is the stored payload, so any derived field must be marked transient to keep it out of the validation. Additional constructors, accessors, or convenience factories with other signatures are allowed and ignored by the processor.

Setup

The following snippets only contain the relevant parts to set up and configure Lazyval. The jakarta-ee dependency can be replaced by any that ships jakarta-persistence. Keep in mind, Lazyval will only activate generators whose required dependencies are available.

These samples show the full setup, using the smallest dependencies. In case a feature is not needed, just remove the dependency.
  • Maven

  • Maven 4

  • Gradle

  • Mill

pom.xml
    <properties>
        <version.lazyval>0.3.2</version.lazyval>
        <version.mapstruct>1.6.3</version.mapstruct>
        <version.jakarta-ee>11.0.0</version.jakarta-ee>
        <version.jackson>3.1.0</version.jackson>
        <version.cassandra-driver>4.19.2</version.cassandra-driver>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.qualityminds.lazyval</groupId>
            <artifactId>lazyval</artifactId>
            <version>${version.lazyval}</version>
            <optional>true</optional> (1)
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${version.mapstruct}</version>
        </dependency>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>${version.jakarta-ee}</version>
            <scope>provided</scope>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>tools.jackson.core</groupId>-->
<!--            <artifactId>jackson-databind</artifactId>-->
<!--            <version>${version.jackson}</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.apache.cassandra</groupId>
            <artifactId>java-driver-query-builder</artifactId>
            <version>${version.cassandra-driver}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <release>17</release>
                    <proc>full</proc>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>com.qualityminds.lazyval</groupId>
                            <artifactId>lazyval-processor</artifactId>
                            <version>${version.lazyval}</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${version.mapstruct}</version>
                        </path>
                    </annotationProcessorPaths>
                    <compilerArgs>
                        <arg>-Amapstruct.unmappedTargetPolicy=ERROR</arg>
                        <arg>-Alazyval.generators.basePackage=test</arg>
                        <arg>-Alazyval.mapstruct.package=test.custom</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
1 mark the dependency as optional to avoid runtime-dependency

Maven 4 changes the way the annotation-processor-classpath is set.

pom.xml
    <properties>
        <version.lazyval>0.3.2</version.lazyval>
        <version.mapstruct>1.6.3</version.mapstruct>
        <version.jakarta-ee>11.0.0</version.jakarta-ee>
        <version.jackson>3.1.0</version.jackson>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.qualityminds.lazyval</groupId>
            <artifactId>lazyval</artifactId>
            <version>${version.lazyval}</version>
            <optional>true</optional> (1)
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${version.mapstruct}</version>
        </dependency>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>${version.jakarta-ee}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>tools.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${version.jackson}</version>
        </dependency>
        <!-- new way to define processors in Maven 4 (parameters remain in maven-compiler-plugin) -->
        <dependency>
            <groupId>com.qualityminds.lazyval</groupId>
            <artifactId>lazyval-processor</artifactId>
            <version>${version.lazyval}</version>
            <type>classpath-processor</type> (2)
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${version.mapstruct}</version>
            <type>classpath-processor</type>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>4.0.0-beta-3</version>
                <configuration>
                    <release>17</release>
                    <proc>full</proc>
                    <compilerArgs>
                        <arg>-Amapstruct.unmappedTargetPolicy=ERROR</arg>
                        <arg>-Alazyval.generators.basePackage=test</arg>
                        <arg>-Alazyval.mapstruct.package=test.custom</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
1 mark the dependency as optional to avoid runtime-dependency
2 annotation-processors have their own scope now
build.gradle.kts
dependencies {
    compileOnly("com.qualityminds.lazyval:lazyval:$versionLazyval")
    implementation("org.mapstruct:mapstruct:1.6.3")
    compileOnly("jakarta.platform:jakarta.jakartaee-api:11.0.0")
    implementation("tools.jackson.core:jackson-databind:3.1.0")

    annotationProcessor("com.qualityminds.lazyval:lazyval-processor:$versionLazyval")
    annotationProcessor("org.mapstruct:mapstruct-processor:1.6.3")
}

tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.apply{
        add("-Amapstruct.unmappedTargetPolicy=ERROR")
        add("-Alazyval.generators.basePackage=test")
        add("-Alazyval.mapstruct.package=test.custom")
    }
}
build.mill
  override def mvnDeps: T[Seq[Dep]] = Seq(
    mvn"org.mapstruct:mapstruct:${Version.mapstruct}",
  )

  override def compileMvnDeps: T[Seq[Dep]] = Seq(
    mvn"com.qualityminds.lazyval:lazyval:${Version.lazyval}",
    mvn"jakarta.persistence:jakarta.persistence-api:${Version.jpa}",
    mvn"tools.jackson.core:jackson-databind:${Version.jackson}"
  )

  override def annotationProcessorsMvnDeps = Seq(
    mvn"com.qualityminds.lazyval:lazyval-processor:${Version.lazyval}",
    mvn"org.mapstruct:mapstruct-processor:${Version.mapstruct}"
  )

  def javacOptions = super.javacOptions() ++ Seq(
    "-Amapstruct.unmappedTargetPolicy=ERROR",
    "-Alazyval.generators.basePackage=test",
    "-Alazyval.mapstruct.package=test.custom",
  )