Search code examples
javajacksonoption-typelombokjackson-databind

How to deserialize Java Optional field with lombok builder?


I want to deserialize Java Optional field in lombok builder. Below is my code

@JsonDeserialize(builder = AvailabilityResponse.Builder.class)
@Getter
@ToString
@EqualsAndHashCode
@Builder(setterPrefix = "with", builderClassName = "Builder", toBuilder = true)
public class AvailabilityResponse {

    private final List<Train> trainDetails;
    private final String name;
    private final Optional<String> detail;

    public static class Builder {

        @JacksonXmlProperty(localName = "TRAIN")
        private List<Train> trainDetails;

        @JacksonXmlProperty(localName = "NAME")
        private String name;

        @JacksonXmlProperty(localName = "DETAIL_TRAIN", isAttribute = true)
        private String detail;

        public AvailabilityResponse build() {
            return new AvailabilityResponse(
                trainDetails, // I have field validation here. if null or empty throwing Exception
                name, // I have field validation here. if null or empty throwing Exception
                Optional.ofNullable(detail)); // This is Optional field
        }
    }   

}    

If I override the builder method like below, able to deserialize

    private Optional<String> name; // this is static builder class field

        @JacksonXmlProperty(localName = "DETAIL_TRAIN", isAttribute = true)
        public Builder detail(final String theDetail) {
            this.detail = Optional.ofNullable(theDetail);
            return this;
        }   

I have used setterPrefix ="with" in @Builder. But if I override the above method with "with" prefix, its not working.

Please someone help me to acheive this


Solution

  • Jackson supports Optional using its jackson-datatype-jdk8 module. You can simply add it to the <dependencies> section in your pom.xml:

    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jdk8</artifactId>
        <version>2.10.3</version>
    </dependency>
    

    Then, initialize your mapper as follows:

    ObjectMapper mapper = new XmlMapper().registerModule(new Jdk8Module());
    

    That mapper will automatically detect Optionals: If the XML field has a value, it will be wrapped in an Optional; if the XML field is empty (i.e., <DETAIL_TRAIN/>), then the result will be an Optional.empty(). (In your case you have an attribute; those cannot be empty.)

    Jackson maps fields or attributes that do not exist in the XML to null (or, more precisely, it simply does not call the setter, so the field value will still be the default). If you also want to have an empty Optional in that case, you need to set the field default to Optional.empty() and add a @Builder.Default to that field.

    Finally, lombok can automatically copy your annotations from the fields to the generated builder. You need to tell lombok which annotations to copy using a lombok.config file in your project root containing:

    lombok.copyableAnnotations += com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
    config.stopBubbling = true
    

    This means you do not need to customize your builder class at all:

    @JsonDeserialize(builder = AvailabilityResponse.Builder.class)
    @Getter
    @ToString
    @EqualsAndHashCode
    @Builder(setterPrefix = "with", builderClassName = "Builder", toBuilder = true)
    public class AvailabilityResponse {
    
        @JacksonXmlProperty(localName = "TRAIN")
        @JsonProperty("TRAIN")
        private final List<Train> trainDetails;
    
        @JacksonXmlProperty(localName = "NAME")
        @JsonProperty("NAME")
        private final String name;
    
        @JacksonXmlProperty(localName = "DETAIL_TRAIN", isAttribute = true)
        @JsonProperty("DETAIL_TRAIN")
        @Builder.Default
        private final Optional<String> detail = Optional.empty();
    }
    

    Note that you also need @JsonProperty annotations on your fields due to a Jackson bug.

    If you want to validate field values, you should do that in the constructor, not in the builder. In this way you'll also catch those instantiations where the builder is not used. To do so, simply implement the all-args constructor yourself; the build() method will use it automatically:

    private AvailabilityResponse(List<Train> trainDetails, String name, Optional<String> detail) {
        this.trainDetails = Objects.requireNonNull(trainDetails);
        this.name = Objects.requireNonNull(name);
        this.detail = Objects.requireNonNull(detail);
    }
    

    I suggest making this constructor private, because

    • it enforces users of your class to use the builder, and
    • it is bad style to have Optionals as parameters in your public API.