I have the following class:
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import java.io.Serializable;
import java.util.HashMap;
#JsonIgnoreProperties(ignoreUnknown = true)
public class Theme implements Serializable {
#JsonProperty
private String themeName;
#JsonProperty
private boolean customized;
#JsonProperty
private HashMap<String, String> descriptor;
//...getters and setters for the above properties
}
When I execute the following code:
HashMap<String, Theme> test = new HashMap<String, Theme>();
Theme t1 = new Theme();
t1.setCustomized(false);
t1.setThemeName("theme1");
test.put("theme1", t1);
Theme t2 = new Theme();
t2.setCustomized(true);
t2.setThemeName("theme2");
t2.setDescriptor(new HashMap<String, String>());
t2.getDescriptor().put("foo", "one");
t2.getDescriptor().put("bar", "two");
test.put("theme2", t2);
String json = "";
ObjectMapper mapper = objectMapperFactory.createObjectMapper();
try {
json = mapper.writeValueAsString(test);
} catch (IOException e) {
e.printStackTrace();
}
The json string produced looks like this:
{
"theme2": {
"themeName": "theme2",
"customized": true,
"descriptor": {
"foo": "one",
"bar": "two"
}
},
"theme1": {
"themeName": "theme1",
"customized": false,
"descriptor": null
}
}
My problem is getting the above json string to de-serizlize back into a
HashMap<String, Theme>
object.
My de-serialization code looks like this:
HashMap<String, Themes> themes =
objectMapperFactory.createObjectMapper().readValue(json, HashMap.class);
Which de-serializes into a HashMap with the correct keys, but does not create Theme objects for the values. I don't know what to specify instead of "HashMap.class" in the readValue() method.
Any help would be appreciated.
You should create specific Map type and provide it into deserialization process:
TypeFactory typeFactory = mapper.getTypeFactory();
MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Theme.class);
HashMap<String, Theme> map = mapper.readValue(json, mapType);
You can use TypeReference class which does the type casting for map with user defined types. More documentation at https://github.com/FasterXML/jackson-databind/
ObjectMapper mapper = new ObjectMapper();
Map<String,Theme> result =
mapper.readValue(src, new TypeReference<Map<String,Theme>>() {});
You can make a POJO that extends a Map.
This is important for dealing with nested maps of objects.
{
key1: { nestedKey1: { value: 'You did it!' } }
}
This can be deserialized via:
class Parent extends HashMap<String, Child> {}
class Child extends HashMap<String, MyCoolPojo> {}
class MyCoolPojo { public String value; }
Parent parent = new ObjectMapper().readValue(json, Parent.class);
parent.get("key1").get("nestedKey1").value; // "You did it!"
Related
I'm trying to use the Java Jackson ObjectMapper to parse a three level JSON object stucture with dynamic keys. I tried the following:
public class AssetsPushManifest {
private Map<String, List<Asset>> manifest = new HashMap<>();
public void addPushManifest(Resource manifestResource) throws Exception {
ObjectMapper mapper = new ObjectMapper();
Map<String, Map<String, Asset>> manifestData = mapper.readValue(manifestResource.getInputStream(), new TypeReference<Map<String, Map<String, Asset>>>() {});
for (String requestedPathName : manifestData.keySet()) {
if (!this.manifest.containsKey(requestedPathName)) {
this.manifest.put(requestedPathName, new LinkedList());
}
List<Asset> requestedPath = this.manifest.get(requestedPathName);
for (String servePath : manifestData.get(requestedPathName).keySet()) {
Asset asset = manifestData.get(requestedPathName).get(servePath);
asset.path = servePath;
requestedPath.add(asset);
}
}
...
}
public class Asset {
public String path;
public String type;
public Integer weight;
}
}
To parse this:
{
"theme/test-theme/index.html": {
"theme/test-theme/somestyling.css": {
"type": "document",
"weight": 1
}
}
}
But it won't work, why oh why? Is it too many levels? (still Java beginner here)
The end goal is to parse the several JSON structures like above into a structure like Map> so any other ways of doing this would also be fine.
I would solve this in a different way: parse the json into a map: if you give Jackson a map as type reference, it will deserialize the JSON into multi-level map:
`Map<String, Object> manifestData = mapper.readValue(manifestResource.getInputStream(), Map.class);`
Now that the json parsing hurdle is behind us, it is easier to construct an instance of an Asset by querying the map. I would do it by adding a constructor to the Asset class:
public Asset(Map<String, Object> manifestData) {
Map<String, Object> assetData = (Map<String, Object>)manifestData.values().iterator().next(); // get the single value of the map
this.path = assetData.keySet().iterator().next();
this.type = assetData.get("type");
this.weight = assetData.get("weight");
}
I've a JSON with duplicate keys as shown below.
{
"name": "Test",
"attributes": [{
"attributeName": "One",
"attributeName": "Two",
"attributeName": "Three"
}]
}
When I transform it to a Map<String, Object> using Jackson, it's transformed as shown below.
{name=Test, attributes=[{attributeName=Three}]}
The value of last occurrence of attribute name is considered. Is there a way to tell Jackson to represent it as a Multimap instead? I am ok to use any implementation of Multimap. My current code is as shown below:
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestJSON {
public static void main(String[] args) throws Exception{
ObjectMapper mapper = new ObjectMapper();
String json = "{\"name\": \"Test\",\"attributes\": [{\"attributeName\": \"One\",\"attributeName\": \"Two\",\"attributeName\": \"Three\"}]}";
Map<String, Object> map = new HashMap<>();
map = mapper.readValue(json, new TypeReference<Map<String, Object>>(){});
System.out.println(map);
}
}
Don't think that Jackson will handle it in his native way, but you can wrap this JSON to simple POJO and get Multimap out of it. Example:
public class Attribute {
private String attributeName;
// getter and setter here
}
public class AttributeContainer {
private String name;
private List<Attribute> attributes;
public Multimap<String, String> getAttributeMultiMap() {
ImmutableMultimap.Builder<String, String> builder = ImmutableMultimap.builder();
for (Attribute attribute : attributes) {
builder.put("attributeName", attribute.getAttributeName())
}
return builder.build();
}
// getters and setters here
}
public void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
String json = "{\"name\": \"Test\",\"attributes\": [{\"attributeName\": \"One\",\"attributeName\": \"Two\",\"attributeName\": \"Three\"}]}";
AttributeContainer attributeContainer;
attributeContainer = mapper.readValue(json, new TypeReference<AttributeContainer>(){});
System.out.println(attributeContainer.getAttributeMultiMap());
}
I load a ressource file json
with the text format
{
"sources": [{
"prop1": "1",
"prop2": "2"
},
{
"prop1": "1",
"prop2": "2"
},
],
"redirection": [{
"prop1": "1",
"prop2": "2"
}
]
}
I have a class with this properties prop1 and prop2
I want to recover with ObjectMapper a list class. What the method ?
This code doesn't work ....
Map<String, Object> mp = mapper.readValue(jsonResource.getInputStream(),new TypeReference<Map<String, Object>>() {});
String sourceText= new ObjectMapper().readTree(jsonResource.getInputStream()).get("sources").asText();
mapper.readValue(sourceText, new TypeReference<List<MyClass>>(){});
Thanks for your help
In your case, I would write a custom JsonDeserializer. Haven't really tested the code, but I think the idea is clear:
final MyClassDeserializer myClassDeserializer = new MyClassDeserializer();
final SimpleModule deserializerModule = new SimpleModule();
deserializerModule.addDeserializer(MyClass.class, myClassDeserializer);
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(deserializerModule);
And the code for JsonDeserializer:
public class MyClassDeserializer extends JsonDeserializer<MyClass> {
#Override
public MyClass deserialize(final JsonParser jsonParser, final DeserializationContext context)
throws IOException {
final JsonNode node = jsonParser.getCodec().readTree(jsonParser);
final JsonNode sourcesNode = node.get("sources");
if(node.isArray()) {
final ArrayNode arrayNode = (ArrayNode) node;
final Iterable<JsonNode> nodes = arrayNode::elements;
final Set<Source> set = StreamSupport.stream(nodes.spliterator(), false)
.map(mapper)
.collect(Collectors.toSet());
...
}
...
}
First thing: Your JSON is invalid. There is a comma after the second object in the sources array. This has to be deleted.
Second: I think you didn't choose the right type for your result. What your JSON represents is a map which maps from string to an array of objects. So the type should be something like Map<String, Props[]> (Since you didn't provide the name of your class, I called it Props.
With these considerations you can construct a MapType by using ObjectMappers getTypeFactory() method and deserialize the value using the constructed type like shown below.
ObjectMapper mapper = new ObjectMapper();
TypeFactory typeFactory = mapper.getTypeFactory();
MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, Props[].class);
Map<String, Props[]> map = mapper.readValue(s, mapType);
I actually voted for the other answer, but this is my idea, to create the classes and let jackson do the work :
public class ResourceTest {
#Test
public void test1() throws IOException {
assertTrue(true);
Resource resource = new Resource();
resource.getRedirectrions().add(makeRedirectrion("rprop11", "rprop12"));
resource.getRedirectrions().add(makeRedirectrion("rprop21", "rprop22"));
resource.getSources().add(makeSource("sprop11","sprop12"));
resource.getSources().add(makeSource("sprop21","sprop22"));
String json = new ObjectMapper().writeValueAsString(resource);
System.out.println(json);
Resource resource1 = new ObjectMapper().readValue(json, Resource.class);
System.out.println(resource1);
}
private Source makeSource(String prop1, String prop2) {
Source source = new Source();
source.setProp1(prop1);
source.setProp2(prop2);
return source;
}
private Redirectrion makeRedirectrion(String prop1, String prop2) {
Redirectrion redirectrion = new Redirectrion();
redirectrion.setProp1(prop1);
redirectrion.setProp2(prop2);
return redirectrion;
}
}
Output is:
{"sources":[{"prop1":"sprop11","prop2":"sprop12"},{"prop1":"sprop21","prop2":"sprop22"}],"redirectrions":[{"prop1":"rprop11","prop2":"rprop12"},{"prop1":"rprop21","prop2":"rprop22"}]}
Resource{sources=[Source{prop1='sprop11', prop2='sprop12'}, Source{prop1='sprop21', prop2='sprop22'}], redirectrions=[Source{prop1='rprop11', prop2='rprop12'}, Source{prop1='rprop21', prop2='rprop22'}]}
Using Flexjson, I am trying to serialize an object ("Payload") that contains a List. Each MyBean has a field "items", which is a Map>. When I serialize this Payload object, the map field ("items") is empty.
public class Payload {
private List<MyBean> myBeans = new ArrayList<MyBean>();
//the JSON returned has blank values for myBeans.values.items
public String toJson() {
return new JSONSerializer()
.exclude("*.class")
.include("myBeans")
.serialize(this);
}
}
However, when I serialize the MyBean object directly, it works fine.
public class MyBean {
private Map<String, List<SomeBean>> items = new HashMap<String, List<SomeBean>>();
//this works
public String toJson() {
return new JSONSerializer()
.exclude("*.class")
.deepSerialize(this);
}
}
Any suggestions?
After trying a bunch of things, I found this solution.
I created a custom transformer for maps. Just copied the Flexjson MapTransformer and commented out a IF condition. New code below
public class Payload {
private List<MyBean> myBeans = new ArrayList<MyBean>();
//the JSON returned has blank values for myBeans.values.items
public String toJson() {
return new JSONSerializer()
.exclude("*.class")
.include("myBeans")
.transform(new SOMapTransformer(), Map.class)
.serialize(this);
}
}
public class MyBean {
private Map<String, List<SomeBean>> items = new HashMap<String, List<SomeBean>>();
//this works
public String toJson() {
return new JSONSerializer()
.exclude("*.class")
.transform(new SOMapTransformer(), "items")
.deepSerialize(this);
}
}
Here is the custom SOMapTransformer:
import com.mycompany.mypackage.SomeBean;
import flexjson.JSONContext;
import flexjson.Path;
import flexjson.TypeContext;
import flexjson.transformer.AbstractTransformer;
import flexjson.transformer.TransformerWrapper;
public class SOMapTransformer extends AbstractTransformer {
public void transform(Object object) {
JSONContext context = getContext();
Path path = context.getPath();
Map<String, List<SomeBean>> value = (Map<String, List<SomeBean>>) object;
TypeContext typeContext = getContext().writeOpenObject();
for (Object key : value.keySet()) {
path.enqueue((String) key);
//DPD 2013-11-04: This bloody line of code cost me 12 hours. Comment it out!
// if (context.isIncluded((String) key, value.get(key))) {
TransformerWrapper transformer = (TransformerWrapper)context.getTransformer(value.get(key));
if(!transformer.isInline()) {
if (!typeContext.isFirst()) getContext().writeComma();
typeContext.setFirst(false);
getContext().writeName(key.toString());
}
typeContext.setPropertyName(key.toString());
transformer.transform(value.get(key));
// }
path.pop();
}
getContext().writeCloseObject();
}
I am doing the following:
IronRunId Id = new IronRunId("RunObject", "Runid1", 4);
ObjectMapper MAPPER = new ObjectMapper();
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("RunId", Id);
String json = MAPPER.writeValueAsString(map);
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map1 = mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
IronRunId runId = (IronRunId) (map1.get("RunId"));
But this gives me an error: Cannot cast java.util.LinkedHashMap to IronRunId
Why is the object returned by map.get() of type linkedhashmap?
On the contrary, if I do:
List<Object> myList = new ArrayList<Object>();
myList.add("Jonh");
myList.add("Jack");
map.put("list", myList);
Then the object returned by map.get() after doing mapper.readValue is of type ArrayList.
Why the difference? Inserting default types into the map returns the correct object. But inserting custom made object in the map does not.
Does anyone know how to do this?
Map<String, Object> map1 = mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
basically translated to, return me a Map with keys of type String and values of type Object. So, Jackson gave you keys of type String and values of type Object. Jackson doesn't know about your custom object, thats why it gave you its own native bound for Object which is a Map, specifically, a LinkedHashMap, and thus the reason why your are getting a LinkedHashMap when doing a get to the returned Map
So change it to :
Map<String, IronRunId> map1 = mapper.readValue(json, new TypeReference<Map<String, IronRunId>>() {});
Also, it is a good practice to declare an Object of its interface type than its concrete type. So instead of
HashMap<String, Object> map = new HashMap<String, Object>();
make it
Map<String, Object> map = new HashMap<String, Object>();
Edit
As a response to your added questions, you can create a wrapper object that will handle all your objects. Something like this.
class Wrapper{
private IronRunId ironRunId;
private long time;
private Map<String, String> aspects;
private String anotherString;
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public Map<String, String> getAspects() {
return aspects;
}
public void setAspects(Map<String, String> aspects) {
this.aspects = aspects;
}
public String getAnotherString() {
return anotherString;
}
public void setAnotherString(String anotherString) {
this.anotherString = anotherString;
}
public IronRunId getIronRunId() {
return ironRunId;
}
public void setIronRunId(IronRunId ironRunId) {
this.ironRunId = ironRunId;
}
}
You can then store different objects in this class.
Revised version
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException{
IronRunId Id = new IronRunId("RunObject", "Runid1", 4);
Map<String, String> aspects = new HashMap<String, String>();
aspects.put("aspectskey1", "aspectsValue1");
aspects.put("aspectskey2", "aspectsValue2");
aspects.put("aspectskey3", "aspectsValue3");
String anotherString = "anotherString";
long time = 1L;
Wrapper objectWrapper = new Wrapper();
ObjectMapper objectMapper = new ObjectMapper();
objectWrapper.setIronRunId(Id);
objectWrapper.setTime(time);
objectWrapper.setAnotherString(anotherString);
objectWrapper.setAspects(aspects);
Map<String, Wrapper> map = new HashMap<String, Wrapper>();
map.put("theWrapper", objectWrapper);
String json = objectMapper.writeValueAsString(map);
Map<String, Wrapper> map1 = objectMapper.readValue(json, new TypeReference<Map<String, Wrapper>>() {});
Wrapper wrapper = map1.get("theWrapper");
System.out.println("run id : " + wrapper.getIronRunId().toString());
System.out.println("time : " + wrapper.getTime());
System.out.println("aspects : " + wrapper.getAspects().toString());
System.out.println("anotherString : " + wrapper.getAnotherString());
}
new TypeReference<Map<String, Object>> is too generic. It is equivalent Map or "untyped" Map mentioned in Data Binding With Generics. The only way for you to deserialize different datatypes in a map or collection is to use TypeFactory.parametricType