jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Setting ValueDeserializer on Property using BeanDeserializerBuilder.addOrReplaceProperty does not work when POJO has constructor

Open vuquocbao opened this issue 4 years ago • 2 comments

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.

vuquocbao avatar Feb 16 '22 00:02 vuquocbao

That sounds likely. Would be great to have an actual full unit test (with Java 8), if anyone has time to implement it.

cowtowncoder avatar Feb 16 '22 01:02 cowtowncoder

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

}

ivermolaev avatar Sep 19 '22 10:09 ivermolaev