Gson serialize/deserialize Map to/from list of KeyValuePairs - java

On the server side I got this API (example) (I can't modify this.)
namespace MyNameSpace
{
[Serializable][DataContract]
public class GetMyObject
{
[DataMember]
public Dictionary<int, int> MyDictionary { get; set; }
}
}
And the server sends this JSON:
{
"MyDictionary" :
[{
"Key" : 1,
"Value" : 1
},
{
"Key" : 2,
"Value" : 2
},
{
"Key" : 3,
"Value" : 3
},
{
"Key" : 4,
"Value" : 4
}]
}
And on the client side, I have to create these classes for correct deserialization:
class GetMyObject {
#SerializedName("MyDictionary")
private List<MyDictionaryItem> myDictionary;
}
class MyDictionaryItem {
#SerializedName("Key")
private int key;
#SerializedName("Value")
private int value;
}
How can I configure GSON to simply use this: (to serialize and deserialize)
class GetMyObject {
#SerializedName("MyDictionary")
private Map<Integer, Integer> myDictionary;
}
It even more intresting with complex key object like:
class ComplexKey {
#SerializedName("Key1")
private int key1;
#SerializedName("Key2")
private String key2;
}
class GetMyObject {
#SerializedName("MyDictionary")
private Map<ComplexKey, Integer> myDictionary;
}

Create a custom JsonDeserializer for Map<?, ?>:
public class MyDictionaryConverter implements JsonDeserializer<Map<?, ?>> {
public Map<Object, Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext ctx) {
Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(typeOfT, $Gson$Types.getRawType(typeOfT));
Map<Object, Object> vals = new HashMap<Object, Object>();
for (JsonElement item : json.getAsJsonArray()) {
Object key = ctx.deserialize(item.getAsJsonObject().get("Key"), keyAndValueTypes[0]);
Object value = ctx.deserialize(item.getAsJsonObject().get("Value"), keyAndValueTypes[1]);
vals.put(key, value);
}
return vals;
}
}
And register it:
gsonBuilder.registerTypeAdapter(new TypeToken<Map>(){}.getType(),
new MyDictionaryConverter());

an alternative, Jackson JSON Processor
#JsonDeserialize(contentAs=Integer.class)
private Map<ComplexKey, Integer> myDictionary;

Related

Java Jackson deserialize an object containing a list of object with/without custom Deserializer?

I've got a JSON input like this
{
"slices": [{
"slice": {
"boundedBy": {
"Envelope": {
"axisLabels": "Lat Long ansi",
"lowerCorner": "-44.975 111.975 \"2003-01-01T00:00:00+00:00\"",
"upperCorner": "-8.975 155.975 \"2003-01-01T00:00:00+00:00\"",
"srsDimension": 3
}
},
"fileReferenceHistory": "/home/rasdaman/rasdaman_community/rasdaman/systemtest/testcases_services/test_all_wcst_import/testdata/wcs_local_metadata_tiff_no_specify_bands/GlobLAI-20030101-20030110-H01V06-1.0_MERIS-FR-LAI-HA.tiff",
"local_metadata_key": "value_1"
}
},
{
"slice": {
"boundedBy": {
"Envelope": {
"axisLabels": "Lat Long ansi",
"lowerCorner": "-44.975 111.975 \"2003-10-01T00:00:00+00:00\"",
"upperCorner": "-8.975 155.975 \"2003-10-01T00:00:00+00:00\"",
"srsDimension": 3
}
},
"fileReferenceHistory": "/home/rasdaman/rasdaman_community/rasdaman/systemtest/testcases_services/test_all_wcst_import/testdata/wcs_local_metadata_tiff_no_specify_bands/GlobLAI-20031001-20031010-H00V10-1.0_MERIS-FR-LAI-HA.tiff",
"local_metadata_key": "value_2"
}
}
],
"Title": "Drought code",
// other keys:values
}
with "slices" is an array of "slice" objects. Out of "slices" is any "keys":"values" but it is not the problem.
Then, I have a POJO class
public class CoverageMetadata {
#JsonProperty(value = "slices")
#JacksonXmlElementWrapper(useWrapping = false)
private List<LocalMetadata> localMetadataList;
private Map<String, String> globalMetadataAttributesMap;
#JsonAnySetter
public void addKeyValue(String key, String value) {
this.globalMetadataAttributesMap.put(key, value);
}
#JsonAnyGetter
public Map<String, String> getGlobalAttributesMap() {
return globalMetadataAttributesMap;
}
// other gettters, setters without Jackson annotations
}
and a class inside the list:
public class LocalMetadata {
public static final String LOCAL_METADATA_TAG = "slice";
private Map<String, String> localMetadataAttributesMap;
private BoundedBy boundedBy;
#JsonAnySetter
// NOTE: To map an unknown list of properties, must use this annotation
public void addKeyValue(String key, String value) {
this.localMetadataAttributesMap.put(key, value);
}
public LocalMetadata() {
this.localMetadataAttributesMap = new LinkedHashMap<>();
this.boundedBy = new BoundedBy();
}
#JsonAnyGetter
// NOTE: to unwrap the "map" from { "map": { "key": "value" } }, only keep { "key": "value" }
public Map<String, String> getLocalMetadataAttributesMap() {
return localMetadataAttributesMap;
}
public BoundedBy getBoundedBy() {
return this.boundedBy;
}
public void setBoundedBy(BoundedBy boundedBy) {
this.boundedBy = boundedBy;
}
public LocalMetadata(Map<String, String> localMetadataAttributesMap, BoundedBy boundedBy) {
this.localMetadataAttributesMap = localMetadataAttributesMap;
this.boundedBy = boundedBy;
}
}
And the basic code to deserialize JSON to object
ObjectMapper objectMapper = new ObjectMapper();
CoveageMetadata coverageMetadata = objectMapper.readValue(metadata, CoverageMetadata.class);
When I try to deserialize the JSON input to CoverageMetadata object, I got the error
Cannot deserialize coverage's metadata in XML/JSON by Jackson, error: Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: {"slices":[{"slice":{"boundedBy":{"Envelope":{"axisLabels":"Lat Long ansi","srsDimension":3,"lowerCorner":"-44.975 111.975 \"2003-01-01T00:00:00+00:00\"","upperCorner":"-8.975 155.975 \"2003-01-01T00:00:00+00:00\""}},"local_metadata_key":"value_1","fileReferenceHistory":"/home/rasdaman/rasdaman_community/rasdaman/systemtest/testcases_services/test_all_wcst_import/testdata/wcs_local_metadata_tiff_no_specify_bands/GlobLAI-20030101-20030110-H01V06-1.0_MERIS-FR-LAI-HA.tiff"}}],"Title":"Drought code"}; line: 1, column: 21] (through reference chain: petascope.core.gml.metadata.model.CoverageMetadata["slices"]->java.util.ArrayList[0]->petascope.core.gml.metadata.model.LocalMetadata["slice"]).
How can I deserialize this JSON input String to CoverageMetadataObject with each "slice" element will be mapped to a LocalMetadata object?
The simple answer is I create another POJO class to hold the "slices" list, in CoverageMetadata class, it will have
public class CoverageMetadata {
private Map<String, String> globalMetadataAttributesMap;
#JsonProperty(value = "slices")
private LocalMetadata localMetadata;
...
}
New POJO class (class LocalMetadata before was renamed to LocalMetadataChild)
public class LocalMetadata {
#JsonProperty(value = "slice")
// This is the most important thing to avoid duplicate <slices><slices> when serializing in XML.
#JacksonXmlElementWrapper(useWrapping = false)
private List<LocalMetadataChild> localMetadataList;
public LocalMetadata(List<LocalMetadataChild> localMetadataList) {
this.localMetadataList = localMetadataList;
}
public LocalMetadata() {
this.localMetadataList = new ArrayList<>();
}
public List<LocalMetadataChild> getLocalMetadataList() {
return localMetadataList;
}
public void setLocalMetadataList(List<LocalMetadataChild> localMetadataList) {
this.localMetadataList = localMetadataList;
}
}

Jackson serialization key and value from one object

I am trying to create a custom serializer to generate Pair object but I want to avoid "key" and "value" fields when the object is serialized.
Object pojo:
public class TypeObjectPair implements Serializable {
private final String canonicalObjectName;
private final Object object;
public String getKey() {
return canonicalObjectName;
}
public Object getValue() {
return object;
}
}
Person class. (Theoretically could be any other object class)
class Person{
int id;
String name;
}
Final object to serialize:
TypeObjectPair obj = new TypeObjectPair("com.example.Person", new Person(1, "Peter"));
Required output:
{
"com.example.object" : {
"id" : 1,
"name" : "Peter"
}
}
Any ideas on how to achieve it?
You could use a Map<K, V> with #JsonAnyGetter:
public class TypeObjectPair {
private Map<String, Object> data = new HashMap<>();
public TypeObjectPair(String key, Object value) {
data.put(key, value);
}
#JsonAnyGetter
public Map<String, Object> getData() {
return data;
}
}
Then use as follows:
ObjectMapper mapper = new ObjectMapper();
TypeObjectPair pair = new TypeObjectPair("com.example.object", new Person(1, "Peter"));
String json = mapper.writer().withDefaultPrettyPrinter().writeValueAsString(pair);
The output will be:
{
"com.example.object" : {
"id" : 1,
"name" : "Peter"
}
}

Jackson catch unrecognized field in a map

I'm using Jackson in a java Rest Api to handle request params.
My Bean class :
public class ZoneModifBeanParam extends ModifBeanParam<Zone> {
#FormParam("type")
private String type;
#FormParam("geometry")
private Geometry geometry;
#FormParam("name")
private String name;
...
My API interface :
#POST
#Consumes("application/json")
#Produces("application/json; subtype=geojson")
#ApiOperation(value = "Create a zone", notes = "To create a zone")
public Response createZone(ZoneModifBeanParam zoneParam) {
...
This Works fine but I need to receive other params that aren't specified by my Bean in a Map.
Example :
{
"geometry": {...},
"name": "A circle name",
"type": "4",
"hello": true
}
By receiving this I need to store in a Map (named unrecognizedFields and declared in my bean) the couple ("hello", true).
Is there any annotation or object allowing this?
Just use #JsonAnySetter. That's what it's made for. Here is a test case
public class JacksonTest {
public static class Bean {
private String name;
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
private Map<String, Object> unrecognizedFields = new HashMap<>();
#JsonAnyGetter
public Map<String, Object> getUnrecognizedFields() {
return this.unrecognizedFields;
}
#JsonAnySetter
public void setUnrecognizedFields(String key, Object value) {
this.unrecognizedFields.put(key, value);
}
}
private final String json
= "{\"name\":\"paul\",\"age\":600,\"nickname\":\"peeskillet\"}";
private final ObjectMapper mapper = new ObjectMapper();
#Test
public void testDeserialization() throws Exception {
final Bean bean = mapper.readValue(json, Bean.class);
final Map<String, Object> unrecognizedFields = bean.getUnrecognizedFields();
assertEquals("paul", bean.getName());
assertEquals(600, unrecognizedFields.get("age"));
assertEquals("peeskillet", unrecognizedFields.get("nickname"));
}
}
The #JsonAnyGetter is used on the serialization side. When you serialize the bean, you will not see the unrecognizedFields in the JSON. Instead all the properties in the map will be serialized as top level properties in the JSON.
You may be able to ignore the unrecognized fields safely by configuring the ObjectMapper, however to specifically put them as key-value pairs of a Map field, you'll need your own de-serializer.
Here's a (heavily simplified) example:
Given your POJO...
#JsonDeserialize(using=MyDeserializer.class)
class Foo {
// no encapsulation for simplicity
public String name;
public int value;
public Map<Object, Object> unrecognized;
}
... and your custom de-serializer...
class MyDeserializer extends JsonDeserializer<Foo> {
#Override
public Foo deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// new return object
Foo foo = new Foo();
// setting unrecognized container
Map<Object, Object> unrecognized = new HashMap<>();
foo.unrecognized = unrecognized;
// initializing parsing from root node
JsonNode node = p.getCodec().readTree(p);
// iterating node fields
Iterator<Entry<String, JsonNode>> it = node.fields();
while (it.hasNext()) {
Entry<String, JsonNode> child = it.next();
// assigning known fields
switch (child.getKey()) {
case "name": {
foo.name = child.getValue().asText();
break;
}
case "value": {
foo.value = child.getValue().asInt();
break;
}
// assigning unknown fields to map
default: {
foo.unrecognized.put(child.getKey(), child.getValue());
}
}
}
return foo;
}
}
Then, somewhere...
ObjectMapper om = new ObjectMapper();
Foo foo = om.readValue("{\"name\":\"foo\",\"value\":42,\"blah\":true}", Foo.class);
System.out.println(foo.unrecognized);
Output
{blah=true}

Deserialize generic maps with jackson from a json containing different occurences for the generic type

I'm trying to deserialize generic maps from a json containing different occurences for the generic type:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
public class MyList<T> {
Map<T, String> value;
public MyList() {
}
public MyList(Map<T, String> value) {
this.value = value;
}
public Map<T, String> getValue() {
return value;
}
public void setValue(Map<T, String> value) {
this.value = value;
}
}
Implmentation 1:
public class MyStringList extends MyList<String> {
public MyStringList() {
}
public MyStringList(Map<String, String> value) {
super(value);
}
}
Implementation 2:
public class MyIntegerList extends MyList<Integer> {
public MyIntegerList() {
}
public MyIntegerList(Map<Integer, String> value) {
super(value);
}
}
I create a List<MyList<?>> myList (containing a MyStringList and a MyIntegerList) and serialize it.
List<MyList<?>> list = new ArrayList<>();
Map<String, String> stringMap = new HashMap<>();
stringMap.put("1", "1");
list.add(new MyStringList(stringMap));
Map<Integer, String> integerMap = new HashMap<>();
integerMap.put(1, "1");
list.add(new MyIntegerList(integerMap));
Here is the obtained JSON:
[{\"#class\":\"test.MyStringList\",\"value\":{\"1\":\"1\"}},{\"#class\":\"test.MyIntegerList\",\"value\":{\"1\":\"1\"}}]
I see that boths keys are "1" and not 1 (because I'm using maps, with Lists it works great).
When deserializing, the keys of the 2 maps have become String :
List<MyList<?>> result = new JodaMapper().readValue("[{\"#class\":\"test.MyStringList\",\"value\":{\"1\":\"1\"}},{\"#class\":\"test.MyIntegerList\",\"value\":{\"1\":\"1\"}}]",
new TypeReference<List<MyList<?>>>() {});
MyStringList stringList = (MyStringList)result.get(0);
MyIntegerList integerList = (MyIntegerList)result.get(1);
String s = stringList.getValue().keySet().iterator().next(); // OK
Integer i = integerList.getValue().keySet().iterator().next(); // java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Is that a bug of jackson deserializer of did I miss something?
EDIT:
The problem was with 2.3.2, and solved in 2.7.3

How to map a Json attribute name to a Java field value

How to map a JSON attribute name to Java field value using jackson?
This:
{ "list":[ { "monday":[ "apple", "bread"] } ,
{ "sunday":[ "bacon", "beer" ] } ],
"kind":"shoplist" }
to this:
public class StuffList {
public List<Stuff> list;
public String kind;
}
public class Stuff {
public String name;
public List<String> items;
}
The fragment "monday":[ "apple", "bread"] is mapped to two variables, one with the attribute name and another with the attribute value.
Your JSON represets simple list of maps, more exactly: List<Map<String, List<String>>>. You can convert this JSON into this POJO:
class JsonEntity {
public List<Map<String, List<String>>> list;
public String kind;
public StuffList toStuffList() {
StuffList stuffList = createStuffListObject();
return stuffList;
}
private StuffList createStuffListObject() {
StuffList stuffList = new StuffList();
stuffList.kind = kind;
stuffList.list = createItemsList();
return stuffList;
}
private List<Stuff> createItemsList() {
List<Stuff> items = new ArrayList<Stuff>(list.size());
for (Map<String, List<String>> item : list) {
items.add(convertToStuff(item));
}
return items;
}
private Stuff convertToStuff(Map<String, List<String>> item) {
Stuff stuff = new Stuff();
stuff.name = item.keySet().iterator().next();
stuff.items = item.values().iterator().next();
return stuff;
}
}
And now we can deserialize JSON in this way:
public static void main(String[] args) throws Exception {
String json = "{\"list\":[{\"monday\":[\"apple\", \"bread\"]},{\"sunday\":[\"bacon\", \"beer\"]} ],\"kind\":\"shoplist\"}";
ObjectMapper objectMapper = new ObjectMapper();
JsonEntity jsonEntity = objectMapper.readValue(json, JsonEntity.class);
System.out.println(jsonEntity.toStuffList());
}
Output of the program:
list=[[name=monday, items=[apple, bread]], [name=sunday, items=[bacon, beer]]], kind=shoplist
Use annotation #JsonProperty If you want to change name use annotation with argument, e.g. #JsonProperty("stuff_name")

Categories