6

I managed to configure Jackson to serialize a class without any getters or annotations on the private fields inside the class by just using

objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

But I don't manage to make a JSON string of this non-static class to get deserialized into an object without any parameterized constructor or setters. Is this possible at all - I think it should through reflection but I don't get exactly how...

Here are 2 tests which need to pass to achieve what I need:

public class ObjectMapperTest {

    private ObjectMapper mapper;

    @Before
    public void init() {
        mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    }

    @Test
    public void serialize() throws Exception {
        Payload payloadToSerialize = new Payload();
        payloadToSerialize.data = "testData";
        String serializedPayload = mapper.writeValueAsString(payloadToSerialize);
        assertThat(serializedPayload, is("{\"data\":\"testData\"}"));
        // --> OK
    }

    @Test
    public void deserialize() throws Exception {
        Payload deserializedPayload = mapper.readValue("{\"data\":\"testData\"}", Payload.class);
        assertThat(deserializedPayload.data, is("testData"));
        // com.fasterxml.jackson.databind.JsonMappingException:
        // No suitable constructor found for type [simple type, class ...ObjectMapperTest$Payload]:
        // can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)
        // at [Source: {"data":"testData"}; line: 1, column: 2]
    }

    public class Payload {

        private String data;

        public Payload() {
            // empty constructor for Jackson
        }
    }
}

Making the Payload class static would fix the test but static classes are not an option for me as I am not working with inner payload classes in the project. Any ideas how to fix it through object mapper configuration change?

EDIT As I am using the Jackson object mapper in a Spring MVC application to serialize / deserialize under the hood I need a solution which changes or extends the object mapper configuration only.

GreenTurtle
  • 1,144
  • 2
  • 21
  • 35

1 Answers1

3

You can write your own deserializer to parse the JSON and create the instance of Payload, and then set the data value using reflection. Exemple :

@Before
public void init() {
    mapper = new ObjectMapper();
    mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    mapper.registerModule(
        new SimpleModule()
        .addDeserializer(Payload.class, new JsonDeserializer<Payload>() {
            @Override
            public Payload deserialize(JsonParser parser, DeserializationContext ctx)
                throws IOException, JsonProcessingException {
                JsonNode obj = parser.readValueAsTree(); // Read the JSON as a node

                Payload payload = new Payload();
                if (obj.isObject() && obj.has("data")) { // The node is an object and has a "data" field
                    try {
                        // Use reflection to set the value
                        Field dataField = payload.getClass().getDeclaredField("data");
                        dataField.setAccessible(true);
                        dataField.set(payload, obj.get("data").asText());
                    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
                        throw new IOException("Reflection error", ex);
                    }
                }
                return payload;
            }
        }));
}

Edit: If you want something more "generic" you can try to create the instance yourself and change the accessibility of all the fields. Then you tell Jackson to update the values using the JSON.

public <T> T deserialize(final String json, final T instance) throws Exception {
    for (Field field : instance.getClass().getDeclaredFields()) {
        field.setAccessible(true);
    }
    mapper.readerForUpdating(instance).readValue(json);
    return instance;
}


@Test
public void deserializeUpdate() throws Exception {
    Payload deserializedPayload = deserialize("{\"data\":\"testData\"}", new Payload());
    assertThat(deserializedPayload.data, is("testData"));
}

I tested this on your Payload class, maybe it doesn't work on more complex objects.

Junior Dussouillez
  • 2,327
  • 3
  • 30
  • 39
  • If you let the properties as public you can use Unreflection instead – Marcos Vasconcelos Feb 08 '18 at 13:47
  • 1
    I don't want to write a deserializer for all classes in the application. It's not just that example payload class there. If I could manage to write a generic deserializer which works without setter and constructor, this would be ok. But I can't see a solution like this with the approach above. – GreenTurtle Feb 09 '18 at 07:32
  • I edited my answer with another solution more "generic" : you create the instance yourself and then give it to Jackson) – Junior Dussouillez Feb 09 '18 at 08:24
  • 2
    I'll give you +1 for your effort and the nice approaches to solve my problem, but as I am working with Spring MVC which does the JSON to object conversion under the hood I don't want to call anything manually after evaluating which type of payload class comes in. – GreenTurtle Feb 09 '18 at 13:01