Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson prefers private constructor over @JsonCreator when deserializing a class with @JsonValue

Tags:

java

json

jackson

I have a simple class with a private constructor and a static factory. I want the class to serialize as a number, so I've annotated the getter for the field with @JsonValue. However, Jackson appears to prefer the private constructor over the static factory, even when I annotate the static factory with @JsonCreator. It works if I annotate the private constructor with @JsonIgnore, but that feels a bit off.

I've seen some posts claiming that @JsonCreator only works if the parameters are annotated with @JsonProperty; however, that seems to be the case for objects serialized as JSON objects. This object is being serialized as a number, and thus there is no property to supply to the annotation.

Is there something I'm missing?

example class:

package com.example;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.common.base.Preconditions;

public class NonNegative {
  private final double n;

  private NonNegative(double n) {
    this.n = n;
  }

  @JsonCreator
  public static NonNegative checked(double n) {
    Preconditions.checkArgument(n >= 0.0);
    return new NonNegative(n);
  }

  @JsonValue
  public double getValue() {
    return n;
  }

  @Override
  public int hashCode() {
    return Objects.hash(n);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj instanceof NonNegative) {
      NonNegative that = (NonNegative) obj;
      return Objects.equals(n, that.n);
    }
    return false;
  }
}

example tests:

package com.example;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class NonNegativeTest {
  private static final ObjectMapper MAPPER = new ObjectMapper();

  @Test
  public void itSerializesAndDeserializes() throws Exception {
    NonNegative nonNegative = NonNegative.checked(0.5);

    assertThat(MAPPER.readValue(MAPPER.writeValueAsString(nonNegative), NonNegative.class)).isEqualTo(nonNegative);
  }

  /* This test fails. */
  @Test(expected = JsonMappingException.class)
  public void itDoesNotDeserializeANegativeNumber() throws Exception {
    MAPPER.readValue(MAPPER.writeValueAsString(-0.5), NonNegative.class);
  }
}
like image 691
NickAldwin Avatar asked Dec 19 '14 17:12

NickAldwin


People also ask

Which constructor does Jackson use?

Jackson Deserialization Using Lombok Builders This class is also immutable and it has a private constructor. Hence, we can create instances only through its builder. This is enough to use this class for deserialization with Jackson. Also notice that Lombok uses build as the default name of the build method.

Does Jackson use the default constructor?

By default, Java provides a default constructor(if there's no parameterized constructor) which is used by Jackson to parse the response into POJO or bean classes. and the exception log.

Does Jackson need no arg constructor?

Jackson won't use a constructor with arguments by default, you'd need to tell it to do so with the @JsonCreator annotation. By default it tries to use the no-args constructor which isn't present in your class.

Why is JsonCreator used?

We can use the @JsonCreator annotation to tune the constructor/factory used in deserialization. It's very useful when we need to deserialize some JSON that doesn't exactly match the target entity we need to get.


1 Answers

Indeed Jackson will override JsonCreator method with constructor method in case if parameter is Java standard type. I would say this is a bug in BasicDeserializerFactory#_handleSingleArgumentConstructor method.

So, the problem is that constructor has higher priority then static factory method in case if that constructor and static factory method has regular Java type. There is few ways how to workaround it.

Set creator visibility level to NON_PRIVATE:

@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.NON_PRIVATE)
class NonNegative {

Second way is to delete static factory method and use constructor. I moved Preconditions.checkArgument to the constructor (it doesn't do much... Just throw an IllegalArgumentException if condition is not satisfied):

public class NonNegative {
  private final double n;

  private NonNegative(double n) {
    Preconditions.checkArgument(n >= 0.0);
    this.n = n;
  }

  @JsonValue
  public double getValue() {
    return n;
  }
}

Another way is to use @JsonIgnore annotation but you mention that you don't like this approach :)

Update I've logged a bug: https://github.com/FasterXML/jackson-databind/issues/660

Update Jackson bug that prefers constructor over static factory method was resolved: https://github.com/FasterXML/jackson-databind/commit/257ae1c7a88c5ccec2882433a39c0df1de2b73aa

like image 192
Ilya Ovesnov Avatar answered Nov 16 '22 00:11

Ilya Ovesnov