Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serializing Maps with Properties Bug #1540

Closed
mlmitch opened this issue Mar 3, 2017 · 5 comments
Closed

Serializing Maps with Properties Bug #1540

mlmitch opened this issue Mar 3, 2017 · 5 comments

Comments

@mlmitch
Copy link

mlmitch commented Mar 3, 2017

In my application, I am attempting to serialize and deserialize a Map implementation with additional properties. According to @cowtowncoder adding the @JsonFormat(shape = JsonFormat.Shape.OBJECT) annotation will make Jackson treat the Map implementation as a plain old object. The result being JSON that includes additional properties as well as successfully deserializing those properties. This is stated in the blog post On Jackson: Serializing Lists, Maps with properties. The example given there deals with a List implementation and the same functionality is promised for Map implementations. I have used this technique with success for Lists, but it is not working for Maps.

Here is a reproduction of the issue. First, the map implementation:

import com.fasterxml.jackson.annotation.JsonFormat;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public class MapImplementation implements Map<Integer, Integer> {

	private Map<Integer, Integer> map;

	private int property;

	public MapImplementation() {
		map = new HashMap<>();
		property = 0;
	}

	public Map<Integer, Integer> getMap() {
		return map;
	}

	public void setMap(Map<Integer, Integer> map) {
		this.map = map;
	}

	public int getProperty() {
		return property;
	}

	public void setProperty(int property) {
		this.property = property;
	}

	@Override
	public int size() {
		return map.size();
	}

	@Override
	public boolean isEmpty() {
		return map.isEmpty();
	}

	@Override
	public boolean containsKey(Object key) {
		return map.containsKey(key);
	}

	@Override
	public boolean containsValue(Object value) {
		return map.containsValue(value);
	}

	@Override
	public Integer get(Object key) {
		return map.get(key);
	}

	@Override
	public Integer put(Integer key, Integer value) {
		return map.put(key, value);
	}

	@Override
	public Integer remove(Object key) {
		return map.remove(key);
	}

	@Override
	public void putAll(Map<? extends Integer, ? extends Integer> m) {
		map.putAll(m);
	}

	@Override
	public void clear() {
		map.clear();
	}

	@Override
	public Set<Integer> keySet() {
		return map.keySet();
	}

	@Override
	public Collection<Integer> values() {
		return map.values();
	}

	@Override
	public Set<Entry<Integer, Integer>> entrySet() {
		return map.entrySet();
	}
}

And here are two unit tests I expect to pass, but currently fail:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;

import java.io.IOException;

public class SerializationTest {

	@Test
	public void serializationTest1() throws IOException {
		ObjectMapper objectMapper = new ObjectMapper();

		MapImplementation map = new MapImplementation();
		map.setProperty(55);
		map.put(12, 45);
		map.put(6, 88);

		String jsonMap = objectMapper.writeValueAsString(map);

		String expectedJsonMap = "{\"map\" : {\"6\" : 88, \"12\" : 45}, \"property\" : 55}";

		//compare the json strings ignoring whitespace
		Assert.assertEquals(expectedJsonMap.replace("\\s+", ""), jsonMap.replace("\\s+", ""));
	}

	@Test
	public void serializationTest2() throws IOException {

		ObjectMapper objectMapper = new ObjectMapper();

		MapImplementation expectedMap = new MapImplementation();
		expectedMap.setProperty(55);
		expectedMap.put(12, 45);
		expectedMap.put(6, 88);

		String jsonMap = "{\"map\" : {\"6\" : 88, \"12\" : 45}, \"property\" : 55}";

		//This line throws:
		//com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not deserialize Map key of type java.lang.Integer from String "map"
		MapImplementation map = objectMapper.readValue(jsonMap, MapImplementation.class);

		Assert.assertEquals(expectedMap.getProperty(), map.getProperty());
		Assert.assertEquals(expectedMap.getMap(), map.getMap());
	}

}

The first test checks the serialized map against the expected JSON output. We recieve {"12":45,"6":88} when I expect {"map" : {"6" : 88, "12" : 45}, "property" : 55}. That is, the property is not included in the serialization.

The second test checks that a MapImplementation can be reproduced from the JSON string {"map" : {"6" : 88, "12" : 45}, "property" : 55}. This operation fails saying an Integer cannot be created from "map", implying that the top level object is expected to be a Map<Integer, Integer>.

I am using Jackson 2.8.6. To deal with this issue, I currently have a workaround in place using a custom serializer and deserializer. Thanks for reading.

@cowtowncoder
Copy link
Member

Thank you for reporting this. Serialization is definitely problematic. I don't recall 100% if deserialization side is (yet) supported (ideally it should be, of course), but I can check that at same time.
I hope to look into this issue in near future, but it may take a while since there's bit of increase on bug report volume (which is great in that we get to fix them :) ).

@mlmitch
Copy link
Author

mlmitch commented Mar 6, 2017

Thanks! If it matters, deserialization is the painful part because of the custom deserializer. I can get the desired serialization pretty easily with a @JsonSerialize(as = MyInterface.class) annotation, where MyInterface.java has getter methods for the fields I want in the serialized object.

@cowtowncoder
Copy link
Member

Right. Ironically enough, actual deserialization "as POJO" would be rather trivially easy... the problem is more in figuring out when to use BeanDeserializer, and this is done by preventing discovery of MapDeserializer. But since there needs to be multiple levels of overrides (as it is legal to add @JsonFormat on property too, not just class), there's need to change this contextually too.
For Jackson 3.x it'd be great to figure out an improved mechanism for locating handlers but for now there are limitations.

Having said that, I think that per-class annotation is easier one to handle, so challenges should not be as big for this particular issue.

@cowtowncoder
Copy link
Member

Looking at code, I do not think Shape.OBJECT is supported at all for deserialization at this point.
But use of @JsonCreator should be, which would seem like most likely way to go anyway.

I will go ahead and see if I can figure out what's with serialization; but I think a separate issue is needed for support deserialization side. Code to do that should probably be quite easy, added in DeserializerCache; there is already code to block default deserializer for Collection instances.
So I will create a separate issue for deser side.

Note, too, that there's another related issue for per-property annotation, #1419.
"Shape-shifting" is rather difficult to make work at this point.

@cowtowncoder
Copy link
Member

Ok: so, as per #476 support for "Maps as POJOs" is only added for 2.9, where it does actually work.
Implementation will not be backported in 2.8 since it has some potential for regression.

But before #1554 deserialization did not; that has been added.
So... This should now work, with the caveat that per-property shape definition will not work, only class-annotation (and "config overrides" via ObjectMapper which similarly affects types, not properties).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants