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);
}
}
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.
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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With