How to exclude property from Lombok builder?

108.9k views Asked by At

I have a class called as "XYZClientWrapper" , which have following structure:

@Builder
XYZClientWrapper{
    String name;
    String domain;
    XYZClient client;
}

What I want no build function generated for property XYZClient client

Does Lombok supports such use case?

13

There are 13 answers

16
Roel Spilker On BEST ANSWER

Yes, you can place @Builder on a constructor or static (factory) method, containing just the fields you want.

Disclosure: I am a Lombok developer.

0
Sōōng On

Not sure if this thread is still valid, I just found a solution from a feature request to Lombok, in case somebody needs it, as Lombok team is most likely not going to add a @Builder.Ignore to its code base.

@Builder
public class User{
  @ToString.Exclude
  private String passwordHash;
  // ... rest of the properties

  public static class UserBuilder {
    private UserBuilder passwordHash(final String passwordHash){
      return this;
    }
  }
}

It works like a charm

1
Dogan Ersoz On

Adding a so called 'partial builder' to the class with Lombok @Builder can help. The trick is to add a inner partial builder class like this:

@Getter
@Builder
class Human {
    private final String name;
    private final String surname;
    private final Gender gender;
    private final String prefix; // Should be hidden, depends on gender

    // Partial builder to manage dependent fields, and hidden fields
    public static class HumanBuilder {

        public HumanBuilder gender(final Gender gender) {
            this.gender = gender;
            if (Gender.MALE == gender) {
                this.prefix = "Mr.";
            } else if (Gender.FEMALE == gender) {
                this.prefix = "Ms.";
            } else {
                this.prefix = "";
            }
            return this;
        }

        // This method hides the field from external set 
        private HumanBuilder prefix(final String prefix) {
            return this;
        }

    }

}

PS: @Builder allows the generated builder class name to be changed. The example above assumed the default builder class name is used.

1
Gogogo On

To exclude field from builder, try using @Builder.Default

2
SexyNerd On

Here is my preferred solution. With that, you can create your field client at the end and have it depending on other fields that previously set by the builder.

XYZClientWrapper{
    private final String name;
    private final String domain;
    private final XYZClient client;
    
    @Builder
    public XYZClientWrapper(String name, String domain) {
        this.name = name;
        this.domain = domain;
        this.client = calculateClient();
    }
}
0
kattoha On

I found one more solution You can wrap your field into initiated final wrapper or proxy. The easiest way to wrap it into AtomicReference.

@Builder
public class Example {
    private String field1;
    private String field2;
    private final AtomicReference<String> excluded = new AtomicReference<>(null);
}

You can interact with it inside by get and set methods but it won't be appeared in builder.

excluded.set("Some value");
excluded.get();
0
Youans On

For factory static method example

class Car {
   private String name;
   private String model;


   private Engine engine; // we want to ignore setting this
   
   @Builder
   private static Car of(String name, String model){
      Car car=new Car();
      car.name = name;
      car.model = model;
      constructEngine(car); // some static private method to construct engine internally
      return car;  
   }

   private static void constructEngine(Car car) {
       // car.engine = blabla...
       // construct engine internally
   }
}

then you can use as follows:

Car toyotaCorollaCar=Car.builder().name("Toyota").model("Corolla").build();
// You can see now that Car.builder().engine() is not available

Notice the static method of will be called whenever build() is called, so doing something like Car.builder().name("Toyota") won't actually set the value "Toyota" into name unless build() is called and then assigning logic within the constructor static method of is executed.

Also, Notice that the of method is privately accessed so that build method is the only method visible to the callers

0
Northnroro On

I have another approach using @Delegate and Inner Class, which supports "computed values" for the excluded fields.

First, we move the fields to be excluded into an Inner Class to avoid Lombok from including them in the Builder.

Then, we use @Delegate to expose Getters/Setters of the builder-excluded fields.

Example:

@Builder
@Getter @Setter @ToString
class Person {

    private String name;
    private int value;
    /* ... More builder-included fields here */

    @Getter @Setter @ToString
    private class BuilderIgnored {

        private String position; // Not included in the Builder, and remain `null` until p.setPosition(...)
        private String nickname; // Lazy initialized as `name+value`, but we can use setter to set a new value
        /* ... More ignored fields here! ... */

        public String getNickname(){ // Computed value for `nickname`
            if(nickname == null){
                nickname = name+value;
            }
            return nickname;
        }
        /* ... More computed fields' getters here! ... */

    }
    @Delegate @Getter(AccessLevel.NONE) // Delegate Lombok Getters/Setters and custom Getters
    private final BuilderIgnored ignored = new BuilderIgnored();

}

It will be transparent to outside of this Person class that position and nickname are actually inner class' fields.

Person p = Person.builder().name("Test").value(123).build();
System.out.println(p); // Person(name=Test, value=123, ignored=Person.BuilderIgnored(position=null, nickname=Test123))
p.setNickname("Hello World");
p.setPosition("Manager");
System.out.println(p); // Person(name=Test, value=123, ignored=Person.BuilderIgnored(position=Manager, nickname=Hello World))

Pros:

  • Do not force the excluded fields to be final
  • Support computed values for the excluded fields
  • Allow computed fields to refer to any fields set by the builder (In other words, allow the inner class to be non-static class)
  • Do not need to repeat the list of all fields (Eg. listing all fields except the excluded ones in a constructor)
  • Do not override Lombok library's @Builder (Eg. creating MyBuilder extends FooBuilder)

Cons:

  • The excluded fields are actually fields of Inner Class; however, using private identifier with proper Getters/Setters you can mimic as if they were real fields
  • Therefore, this approach limits you to access the excluded fields using Getters/Setters
  • The computed values are lazy initialized when Getters are invoked, not when .build().
0
Richard Collette On

I found that I was able to implement a "shell" of the static Builder class, add the method I want to hide with a private access modifier, and it is no longer accessible in the builder. Likewise I can add custom methods to the builder as well.

package com.something;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import java.time.ZonedDateTime;

@Data
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MyClass{

    //The builder will generate a method for this property for us.
    private String anotherProperty;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "localDateTime", column = @Column(name = "some_date_local_date_time")),
            @AttributeOverride(name = "zoneId", column = @Column(name = "some__date_zone_id"))
    })
    @Getter(AccessLevel.PRIVATE)
    @Setter(AccessLevel.PRIVATE)
    private ZonedDateTimeEmbeddable someDateInternal;

    public ZonedDateTime getSomeDate() {
        return someDateInternal.toZonedDateTime();
    }

    public void setSomeDate(ZonedDateTime someDate) {
        someDateInternal = new ZonedDateTimeEmbeddable(someDate);
    }

    public static class MyClassBuilder {
        //Prevent direct access to the internal private field by pre-creating builder method with private access.
        private MyClassBuilder shipmentDateInternal(ZonedDateTimeEmbeddable zonedDateTimeEmbeddable) {
            return this;
        }

        //Add a builder method because we don't have a field for this Type
        public MyClassBuilder someDate(ZonedDateTime someDate) {
            someDateInternal = new ZonedDateTimeEmbeddable(someDate);
            return this;
        }
    }

}
0
Vuk Djapic On

One method I like and use is this. Keep required parameters in constructor, and set optional through builder. Works if number of required is not very big.

class A {
    private int required1;
    private int required2;
    private int optional1;
    private int optional2;

    public A(int required1, int required2) {
        this.required1 = required1;
        this.required2 = required2;
    }

    @Builder(toBuilder = true)
    public A setOptionals(int optional1, int optional2) {
        this.optional1 = optional1;
        this.optional2 = optional2;
        return this;
    }
}

And then construct it with

A a = new A(1, 2).builder().optional1(3).optional2(4).build();

Nice thing with this approach is that optionals can also have default value.

0
MEE On

One approach I have used before was to group instance fields into Configuration fields and Session fields. Configuration fields go as class instances and are visible to the Builder, while Session fields go into a nested private static class and are accessed via a concrete final instance field (which the Builder will ignore by default).

Something like this:

@Builder
class XYZClientWrapper{
    private String name;
    private String domain;
 
    private static class Session {
        XYZClient client;
    }

    private final Session session = new Session();

    private void initSession() {
        session.client = ...;
    }
 
    public void foo() {
        System.out.println("name: " + name);
        System.out.println("domain: " + domain;
        System.out.println("client: " + session.client);
    }
}

9
Stephan On

Alternatively, I found out that marking a field as final, static or static final instructs @Builder to ignore this field.

@Builder
public class MyClass {
   private String myField;

   private final String excludeThisField = "bar";
}

Lombok 1.16.10

5
Bill H On

Create the builder in code and add a private setter for your property.

@Builder
XYZClientWrapper{
    String name;
    String domain;
    XYZClient client;

    public static class XYZClientWrapperBuilder {
        private XYZClientWrapperBuilder client(XYZClient client) { return this; }
    }
}