在与 Jackson 映射时将默认值设置为 null 字段

我试图用 Jackson 将一些 JSON 对象映射到 Java 对象。JSON 对象中的一些字段是强制性的(我可以用 @NotNull标记) ,一些是可选的。

在与 Jackson 进行映射之后,JSON 对象中未设置的所有字段在 Java 中都将有一个空值。是否有类似于 @NotNull的注释可以告诉 Jackson 设置 Java 类成员的默认值,以防它为 null?

编辑: 为了使问题更清楚,这里有一些代码示例。

Java 对象:

class JavaObject {
@NotNull
public String notNullMember;


@DefaultValue("Value")
public String optionalMember;
}

JSON 对象可以是:

{
"notNullMember" : "notNull"
}

或:

{
"notNullMember" : "notNull",
"optionalMember" : "optional"
}

@DefaultValue注释只是为了显示我所要求的内容。这不是一个真正的注释。如果 JSON 对象类似于第一个示例,我希望 optionalMember的值是 "Value"而不是 null。有这样的注释吗?

228802 次浏览

Looks like the solution is to set the value of the properties inside the default constructor. So in this case the java class is:

class JavaObject {


public JavaObject() {


optionalMember = "Value";
}


@NotNull
public String notNullMember;


public String optionalMember;
}

After the mapping with Jackson, if the optionalMember is missing from the JSON its value in the Java class is "Value".

However, I am still interested to know if there is a solution with annotations and without the default constructor.

There is no annotation to set default value.
You can set default value only on java class level:

public class JavaObject
{
public String notNullMember;


public String optionalMember = "Value";
}

Make the member private and add a setter/getter pair. In your setter, if null, then set default value instead. Additionally, I have shown the snippet with the getter also returning a default when internal value is null.

class JavaObject {
private static final String DEFAULT="Default Value";


public JavaObject() {
}


@NotNull
private String notNullMember;
public void setNotNullMember(String value){
if (value==null) { notNullMember=DEFAULT; return; }
notNullMember=value;
return;
}


public String getNotNullMember(){
if (notNullMember==null) { return DEFAULT;}
return notNullMember;
}


public String optionalMember;
}

Only one proposed solution keeps the default-value when some-value:null was set explicitly (POJO readability is lost there and it's clumsy)

Here's how one can keep the default-value and never set it to null

@JsonProperty("some-value")
public String someValue = "default-value";


@JsonSetter("some-value")
public void setSomeValue(String s) {
if (s != null) {
someValue = s;
}
}

I had a similar problem, but in my case the default value was in database. Below is the solution for that:

 @Configuration
public class AppConfiguration {
@Autowired
private AppConfigDao appConfigDao;


@Bean
public Jackson2ObjectMapperBuilder builder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.deserializerByType(SomeDto.class,
new SomeDtoJsonDeserializer(appConfigDao.findDefaultValue()));
return builder;
}

Then in SomeDtoJsonDeserializer use ObjectMapper to deserialize the json and set default value if your field/object is null.

You can create your own JsonDeserializer and annotate that property with @JsonDeserialize(as = DefaultZero.class)

For example: To configure BigDecimal to default to ZERO:

public static class DefaultZero extends JsonDeserializer<BigDecimal> {
private final JsonDeserializer<BigDecimal> delegate;


public DefaultZero(JsonDeserializer<BigDecimal> delegate) {
this.delegate = delegate;
}


@Override
public BigDecimal deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return jsonParser.getDecimalValue();
}


@Override
public BigDecimal getNullValue(DeserializationContext ctxt) throws JsonMappingException {
return BigDecimal.ZERO;
}
}

And usage:

class Sth {


@JsonDeserialize(as = DefaultZero.class)
BigDecimal property;
}

There are already a lot of good suggestions, but here's one more. You can use @JsonDeserialize to perform an arbitrary "sanitizer" which Jackson will invoke post-deserialization:

@JsonDeserialize(converter=Message1._Sanitizer.class)
public class Message1 extends MessageBase
{
public String string1 = "";
public int integer1;


public static class _Sanitizer extends StdConverter<Message1,Message1> {
@Override
public Message1 convert(Message1 message) {
if (message.string1 == null) message.string1 = "";
return message;
}
}
}

Another option is to use InjectableValues and @JacksonInject. It is very useful if you need to use not always the same value but one get from DB or somewhere else for the specific case. Here is an example of using JacksonInject:

protected static class Some {
private final String field1;
private final String field2;


public Some(@JsonProperty("field1") final String field1,
@JsonProperty("field2") @JacksonInject(value = "defaultValueForField2",
useInput = OptBoolean.TRUE) final String field2) {
this.field1 = requireNonNull(field1);
this.field2 = requireNonNull(field2);
}


public String getField1() {
return field1;
}


public String getField2() {
return field2;
}
}


@Test
public void testReadValueInjectables() throws JsonParseException, JsonMappingException, IOException {
final ObjectMapper mapper = new ObjectMapper();
final InjectableValues injectableValues =
new InjectableValues.Std().addValue("defaultValueForField2", "somedefaultValue");
mapper.setInjectableValues(injectableValues);


final Some actualValueMissing = mapper.readValue("{\"field1\": \"field1value\"}", Some.class);
assertEquals(actualValueMissing.getField1(), "field1value");
assertEquals(actualValueMissing.getField2(), "somedefaultValue");


final Some actualValuePresent =
mapper.readValue("{\"field1\": \"field1value\", \"field2\": \"field2value\"}", Some.class);
assertEquals(actualValuePresent.getField1(), "field1value");
assertEquals(actualValuePresent.getField2(), "field2value");
}

Keep in mind that if you are using constructor to create the entity (this usually happens when you use @Value or @AllArgsConstructor in lombok ) and you put @JacksonInject not to the constructor but to the property it will not work as expected - value of the injected field will always override value in json, no matter whether you put useInput = OptBoolean.TRUE in @JacksonInject. This is because jackson injects those properties after constructor is called (even if the property is final) - field is set to the correct value in constructor but then it is overrided (check: https://github.com/FasterXML/jackson-databind/issues/2678 and https://github.com/rzwitserloot/lombok/issues/1528#issuecomment-607725333 for more information), this test is unfortunately passing:

protected static class Some {
private final String field1;


@JacksonInject(value = "defaultValueForField2", useInput = OptBoolean.TRUE)
private final String field2;


public Some(@JsonProperty("field1") final String field1,
@JsonProperty("field2") @JacksonInject(value = "defaultValueForField2",
useInput = OptBoolean.TRUE) final String field2) {
this.field1 = requireNonNull(field1);
this.field2 = requireNonNull(field2);
}


public String getField1() {
return field1;
}


public String getField2() {
return field2;
}
}


@Test
public void testReadValueInjectablesIncorrectBehavior() throws JsonParseException, JsonMappingException, IOException {
final ObjectMapper mapper = new ObjectMapper();
final InjectableValues injectableValues =
new InjectableValues.Std().addValue("defaultValueForField2", "somedefaultValue");
mapper.setInjectableValues(injectableValues);


final Some actualValueMissing = mapper.readValue("{\"field1\": \"field1value\"}", Some.class);
assertEquals(actualValueMissing.getField1(), "field1value");
assertEquals(actualValueMissing.getField2(), "somedefaultValue");


final Some actualValuePresent =
mapper.readValue("{\"field1\": \"field1value\", \"field2\": \"field2value\"}", Some.class);
assertEquals(actualValuePresent.getField1(), "field1value");
// unfortunately "field2value" is overrided because of putting "@JacksonInject" to the field
assertEquals(actualValuePresent.getField2(), "somedefaultValue");
}


Another approach is to use JsonDeserializer, e.g.:

    public class DefaultValueDeserializer extends JsonDeserializer<String> {


@Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException {
return jsonParser.getText();
}


@Override
public String getNullValue(DeserializationContext ctxt) {
return "some random value that can be different each time: " + UUID.randomUUID().toString();
}
}


and then annotate a field like that:

    public class Content {
@JsonDeserialize(using = DefaultValueDeserializer.class)
private String someField;
...
}

keep in mind that you can use attributes in getNullValue(DeserializationContext ctxt) passed using

mapper.reader().forType(SomeType.class).withAttributes(singletonMap("dbConnection", dbConnection)).readValue(jsonString);

like that:

        @Override
public String getNullValue(DeserializationContext ctxt) {
return ((DbConnection)ctxt.getAttribute("dbConnection")).getDefaultValue(...);
}

Hope this helps to someone with a similar problem.

P.S. I'm using jackson v. 2.9.6

Use the JsonSetter annotation with the value Nulls.SKIP

If you want to assign a default value to any param which is not set in json request then you can simply assign that in the POJO itself.

If you don't use @JsonSetter(nulls = Nulls.SKIP) then the default value will be initialised only if there is no value coming in JSON, but if someone explicitly put a null then it can lead to a problem. Using @JsonSetter(nulls = Nulls.SKIP) will tell the Json de-searilizer to avoid null initialisation.

Value that indicates that an input null value should be skipped and the default assignment is to be made; this usually means that the property will have its default value.

as follow:

public class User {
@JsonSetter(nulls = Nulls.SKIP)
private Integer Score = 1000;
...
}

There is a solution, if you use Lombok's Builder annotation, you can combine Lombok with Jackson via the @Jacksonized annotation.

Without this, the combination of Lombok and Jackson is not working for this.

Via adding the @Builder.Default on the property you are then able to set default values.

@Value
@Builder
@Jacksonized
public class SomeClass {


String field1;
     

@Builder.Default
String field2 = "default-value";


}

So, in the incoming json request, if the field2 is not specified, then the Builder.default annotation will allow the Builder interface to set the specified default-value into the property, if not, the original value from the request is set into that.