Setting ValueDeserializer on Property using BeanDeserializerBuilder.addOrReplaceProperty does not work when POJO has constructor
Describe the bug When setting a custom deserializer for a property using BeanDeserializerBuilder.addOrReplaceProperty that customer deserializer is not applied when the POJO has a constructor method.
After some debugging, it has to do with the _propertyBasedCreator field in the BaseBeanDeserializer class. If the POJO has a constructor method that the _propertyBaseCreator field is set. But the properties stored in those fields are not in sync with the _beanProperties fields which has the value deserializer applied from the addOrReplaceProperty.
Currently I'm using version 2.13 and running into this issue when using this library
https://github.com/codesqueak/jackson-json-crypto.
That library above add an annotation and if string fields are annotated with that annotation it will serialize the string into an encrypted format.
Version information 2.13
To Reproduce
If I have two classes
class A {
private String value = null
public A(String value) ...
@JsonProperty
@Encrypt
public getValue() ...
public setValue(String: value) ...
}
class B {
private String value = null
public B() ...
@JsonProperty
@Encrypt
public getValue() ...
public setValue(String: value) ...
}
And have a BeanDeserializationModifier that looks like this from the gitbug project above
public class EncryptedDeserializerModifier extends BeanDeserializerModifier {
private final EncryptionService encryptionService;
public EncryptedDeserializerModifier(final EncryptionService encryptionService) {
this.encryptionService = encryptionService;
}
@Override
public BeanDeserializerBuilder updateBuilder(final DeserializationConfig config, final BeanDescription beanDescription, final BeanDeserializerBuilder builder) {
var it = builder.getProperties();
while (it.hasNext()) {
var property = it.next();
if (null != property.getAnnotation(Encrypt.class)) {
var current = property.getValueDeserializer();
builder.addOrReplaceProperty(property.withValueDeserializer(new EncryptedJsonDeserializer(encryptionService, current)), true);
}
}
return builder;
}
}
I can properly serialize and then deserialize class B but not class A.
// Does not work
var objectA = new A("Some String")
var jsonA = objectMapper.writeValue(objectA)
var readA = objectMapper.readValue(jsonA, A.class)
// Does work
var objectB = new B("Some String")
var jsonB = objectMapper.writeValue(objectB)
var readB = objectMapper.readValue(jsonB, B.class)
Expected behavior
Both approaches should be able to properly read in a JSON value using a customer deserializer.
Additional context Add any other context about the problem here.
That sounds likely. Would be great to have an actual full unit test (with Java 8), if anyone has time to implement it.
Hey @cowtowncoder, hope this one helps
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.util.Iterator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class JacksonIssue3399UTest {
static class WithAllArgsConstructor {
private String value;
public WithAllArgsConstructor(@JsonProperty("value") String value) { this.value = value; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
static class WithNoArgsConstructor {
private String value;
public WithNoArgsConstructor() { }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
static class AlwaysReadTest extends StdDeserializer<String> {
public AlwaysReadTest() { super(String.class); }
@Override
public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
return "test";
}
}
static class Issues3399Bdm extends BeanDeserializerModifier {
@Override
public BeanDeserializerBuilder updateBuilder(
DeserializationConfig config,
BeanDescription beanDesc,
BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> it = builder.getProperties();
while (it.hasNext()) {
SettableBeanProperty prop = it.next();
SettableBeanProperty updatedProp = prop.withValueDeserializer(new AlwaysReadTest());
builder.addOrReplaceProperty(updatedProp, true);
}
return builder;
}
}
final ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void before() {
objectMapper.registerModule(
new SimpleModule("issues/3399").setDeserializerModifier(new Issues3399Bdm()));
}
@Test
void test() throws JsonProcessingException {
String json = "{ \"value\": \"1\" }";
WithNoArgsConstructor withNoArgs = objectMapper.readValue(json, WithNoArgsConstructor.class);
assertEquals("test", withNoArgs.getValue(), "With 'no args' constructor");
WithAllArgsConstructor withAllArgs = objectMapper.readValue(json, WithAllArgsConstructor.class);
assertEquals("test", withAllArgs.getValue(), "With 'all args' constructor");
}
}