Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make Jackson deserialize into private fields without setter or constructor

Tags:

java

json

jackson

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.

like image 598
GreenTurtle Avatar asked Feb 06 '18 11:02

GreenTurtle


People also ask

Does Jackson use constructor or setters?

Deserialization Without No-arg Constructors. By default, Jackson recreates data objects by using no-arg constructors.

Does Jackson require setters?

Jackson can handle serialization or deserialization without getter or setter methods for a public field.

Does Jackson require default constructor?

Need of Default ConstructorBy 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.

Does Jackson serialize private fields?

Jackson can't serialize private fields - without accessor methods - with its default settings. PrivatePerson has two private fields with no public accessors.


1 Answers

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.

like image 108
Junior Dussouillez Avatar answered Oct 21 '22 11:10

Junior Dussouillez