91
votes

I am trying to map some JSON objects to Java objects with Jackson. Some of the fields in the JSON object are mandatory(which I can mark with @NotNull) and some are optional.

After the mapping with Jackson, all the fields that are not set in the JSON object will have a null value in Java. Is there a similar annotation to @NotNull that can tell Jackson to set a default value to a Java class member, in case it is null?

Edit: To make the question more clear here is some code example.

The Java object:

class JavaObject {
    @NotNull
    public String notNullMember;

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

The JSON object can be either:

{
    "notNullMember" : "notNull"
}

or:

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

The @DefaultValue annotations is just to show what I am asking. It's not a real annotation. If the JSON object is like in the first example I want the value of the optionalMember to be "Value" and not null. Is there an annotation that does such a thing?

9
I am looking similar question but for final field.Daneel Yaitskov

9 Answers

95
votes

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";
}
36
votes

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; 
    }
}
5
votes

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;
 }
4
votes

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.

2
votes

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;
}
2
votes

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

1
votes

use the [JsonSetter][1] annotation with the value [Nulls.SKIP][2]

usually, if u dont use @JsonSetter(nulls = Nulls.SKIP) then the default value will be initialised if there is no value coming in JSON, but if someone explicitly put a null then it can lead to 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;
    ...
}


0
votes

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.

0
votes

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;
        }
    }
}