英文:
How to deserialize Java Optional field with lombok builder?
问题
我想要在 lombok 的 builder 中反序列化 Java Optional 字段。以下是我的代码示例:
```Java
@JacksonXmlRootElement(localName = "Availability")
@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, // 这里有字段验证。如果为 null 或空,则抛出异常
name, // 这里有字段验证。如果为 null 或空,则抛出异常
Optional.ofNullable(detail)); // 这是一个 Optional 字段
}
}
}
如果我像下面这样覆盖 builder 方法,就能够反序列化:
private Optional<String> name; // 这是静态 builder 类的字段
@JacksonXmlProperty(localName = "DETAIL_TRAIN", isAttribute = true)
public Builder detail(final String theDetail) {
this.detail = Optional.ofNullable(theDetail);
return this;
}
我在 @Builder
中使用了 setterPrefix = "with"
。但如果我使用 "with" 前缀覆盖上述方法,它就无法正常工作。
请有人帮助我实现这个目标。
<details>
<summary>英文:</summary>
I want to deserialize Java Optional field in lombok builder. Below is my code
``` JacksonXmlRootElement(localName = "Availability")
@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
答案1
得分: 4
Jackson支持使用其jackson-datatype-jdk8
模块来处理Optional
。您只需将其添加到您的pom.xml
文件的<dependencies>
部分中:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.10.3</version>
</dependency>
然后,按以下方式初始化您的映射器:
ObjectMapper mapper = new XmlMapper().registerModule(new Jdk8Module());
该映射器将自动检测Optional
:如果XML字段具有值,它将被包装在Optional
中;如果XML字段为空(即<DETAIL_TRAIN/>
),则结果将是Optional.empty()
。(在您的情况下,您有一个属性;这些属性不能是空的。)
Jackson将不存在于XML中的字段或属性映射为null
(或者更准确地说,它根本不调用setter方法,因此字段值仍将是默认值)。如果您还想在这种情况下拥有一个空的Optional
,您需要将字段默认值设置为Optional.empty()
,并在该字段上添加@Builder.Default
注解。
最后,Lombok可以自动将字段上的注解复制到生成的构建器中。您需要使用项目根目录中的lombok.config
文件告诉Lombok要复制哪些注解:
lombok.copyableAnnotations += com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
config.stopBubbling = true
这意味着您不需要定制您的构建器类:
@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();
}
请注意,由于Jackson的一个错误,您还需要在字段上使用@JsonProperty
注解。
如果您想要验证字段值,您应该在构造函数中进行,而不是在构建器中。通过这种方式,您还将捕获那些未使用构建器的实例化。要实现这一点,只需自己实现全参构造函数;build()
方法将自动使用它:
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);
}
我建议将此构造函数设置为private
,因为:
-
它强制要求使用您的类的用户使用构建器,以及
-
在您的公共API中将
Optional
作为参数是不好的风格。
英文:
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 Optional
s: 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
Optional
s as parameters in your public API.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论