JSON Strings and Java Objects
April 8, 2010
Introduction
In the previous posting I used the Jackson JSON Processor libraries to parse and format JSON strings. The Java representation of the JSON String being a HashMap whose contents are named attributes some of which are themselves HashMaps, or indeed arrays of HashMaps. This is a very flexible representation, but writing client code that works with the data is (to my eyes) quite cumbersome:
HashMap<String,Object> payload
= mapper.readValue(from, typeRef);
Boolean success = (Boolean) payload.get("success");
System.out.println("success " + success);
ArrayList<HashMap<String,Object>> artists
= (ArrayList<HashMap<String,Object>>)
payload.get("artists");
System.out.println("artist count " + artists.size());
for (HashMap<String,Object> oneArtist: artists){
System.out.println("artist name " + oneArtist.get("name"));
}
We see access to attributes such as “success” and “artists” and the requirement for the coder to cast the values to the appropriate type. Note that any errors in either the names of the attributes or their types will not be discovered until runtime.
Coming from an Object Oriented background my personal preference would be to have Object representations of attributes. I would like to have objects such as Artist and Albumn – I feel uncomfortable having
map.get(“interestingField”)
code scattered across my business logic. (Your tastes may differ!) This article shows how to use Jackson to populate such objects, and in particular how to customize the mapping to Java objects, which is useful in more complex cases.
Sparse Object
Let’s begin with a simple object representing the payload.
package djna.jackson.eg;
public class CatalogueResponse {
public boolean success;
}
I’m creating this object on the basis of my understanding of the JSON string to be parsed. [For simplicity I’ve made the attribute public, in real life it would be private with a suitable accessor method.]
{
"success": true,
"artists": [
{
"name": "Ashley Hutchings",
and to keep things simple I just consider one attribute: “success”.
I adjust my application to specify that CatalogueResponse is the class the parser should produce.
CatalogueResponse catalogue
= mapper.readValue(from, CatalogueResponse.class);
System.out.println("Catalogue " + catalogue);
This code runs but produces the exception:
Exception org.codehaus.jackson.map.JsonMappingException:
Unrecognized field "artists"
At development time this is potentially a helpful error message, telling us that our payload object does not match all the fields in the JSON String. When deployed in a production environment we would probably prefer to decouple the client from upgrades to the server that produce new attributes. We’d request that such unmapped fields are silently ignored. This is specified using another configuration feature:
mapper.getDeserializationConfig().set(
DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES,
false
);
Running the application now produces the expected (if minimal) output:
Catalogue djna.jackson.eg.CatalogueResponse@3d0a3d0a
{
"success" : true
}
Complete.
Objects, Names and Values
We can extend our object to allow access to the artist array. A simple possibility is to add that as an array of Objects:
public class CatalogueResponse {
public boolean success;
public Object[] artists;
}
This works, and can be convenient if we have no specific interest in artists, we just get an array of HashMaps as before. More likely we want to create suitable classes for artist and albumnre
public class Artist {
public String name;
public Albumn albumns[];
}
public class Albumn {
public String title;
public Object[] properties;
}
and our response class can now refer to Artists
public class CatalogueResponse {
public boolean success;
public Artist[] artists;
}
The creation of these payload classes is a little tedious and error-prone but is up-front work producing classes that can be re-used many times.
Custom Mapping
Looking at the Albumn class we see that the “properties” attribute is representing a set of descriptive fields that may be attached to any albumn. This approach gives a flexible data structure that can be extended in the future.
"properties": [
{ "name": "artist",
"value" : "Michael Chapman"
},
{ "name": "genre",
"value" : "instrumental"
},
{ "name": "id",
"value" : "BP315CD"
}
The default mapping produced by Jackson would be an array of HashMaps, each HashMap containing a “name” entry and a “value” entry. It would be much more convenient from the client developer’s point of view to represent the properties by a single HashMap with keys such as “artist”, “genre” etc. In effect transforming the above structure to this:
"properties": [
{ "artist" : "Michael Chapman" },
{ "genre: "instrumental" },
{ "id : "BP315CD" }
This is achieved by using custom serialize/deserialize code.
Serialization annotation
The first step is to annotate the payload class to specify that we are using a custom mapping.
public class Albumn {
public String title;
@JsonSerialize(using=PropertyMapSerializer.class)
@JsonDeserialize(using=PropertyMapDeserializer.class)
public Map<String, String> properties;
}
Here I’m annotating the attribute directly, specifying the JsonSerialize and JsonDeserialize information. I would usually have a private attribute with getter and setter methods and in which case I would annotate the getter with the JsonSerialize and the setter with JsonDeserialize.
We now need to write the custom Serialize classes as referenced in the annotations.
The Deserializer Class
We create a class
package djna.jackson.eg;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
public class PropertyMapDeserializer
extends JsonDeserializer<
Map<String, String>
>
{
The key point here is that this class extends the Jackson class JsonDeserialize, which is specialised with the type corresponding to our property field. So
extends JsonDeserializer<
Map<String, String>
>
matches
public Map<String, String> properties;
The class then also must implement the deserializer method
@Override
public Map<String, String> deserialize(
JsonParser parser, DeserializationContext context)
throws IOException,
JsonProcessingException {
The purpose of this method is to read data from the parser and construct and return the value that will be placed into our property.
Implementing the Deserializer
The implementation is in three steps: initialisation of some variables, reading the raw data and constructing the property hash map.
Initialisation
We initialise two variables
Map<String, String> retMap =
new HashMap<String, String>();
TypeReference<HashMap<String,String>[]> typeRef
= new TypeReference<
HashMap<String,String>[]
>() {};
The retMap is the return value we are populating. The typeRef is going to be used when reading the JSON string to specify the type of the raw data to be produced. [This is a common Jackson idiom that I used in the previous article.]
Reading Raw Data
We read the raw data into a natural representation of the JSON string using a Jackson ObjectMapper.
ObjectMapper mapper = new ObjectMapper();
HashMap<String, String>[] maps
= mapper.readValue(parser, typeRef);
In this case we’re reading an array of HashMaps. Each HashMap in the array contains the “name” and “value” corresponding to a property:
"properties": [
{ "name": "artist",
"value" : "Michael Chapman"
},
We then transfer this data into the single hash map we are creating
Populate Returned Hash Map
We simply iterate the array and extract the name and value. (Yes, there should be some error handling.)
for (HashMap<String, String> oneMap : maps){
String name = oneMap.get("name");
String value = oneMap.get("value");
retMap.put(name, value);
}
return retMap;
Using the HashMap
The client code can now access the Albumn properties in quite a natural way:
albumn.properties.get(“genre”);
Next we need to provide the inverse functionality, to serialize to a JSON string.
Implementing the Serializer
The serialization code is also quite simple. First we create the class, which must extend an appropriate specialisation of the Jackson JsonSerializer
package djna.jackson.eg;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializerProvider;
public class PropertyMapSerializer extends JsonSerializer<
Map<String, String>
>
{
Note that the specialisation matches the type of the field we are serialising.
@JsonSerialize(using=PropertyMapSerializer.class)
@JsonDeserialize(using=PropertyMapDeserializer.class)
public Map<String, String> properties;
We then need to implement the serialisation method:
@Override
public void serialize(Map<String, String> data,
JsonGenerator generator,
SerializerProvider provider) throws IOException,
JsonProcessingException {
The requirement is to write to the JsonGenerator an appropriate representation of the Map<String, String> data received. We could do this by constructing an array of HashMaps, but instead I decided to create an array of a simple Property class. I think that this make for more readable code.
The Property Class
The class is a simple representation of the property data with a convenient constructor.
public class Property {
public Property(String newName, String newValue){
name = newName;
value = newValue;
}
public String name;
public String value;
}
The payload we are writing is a representation of an array of these objects.
Preparing the Payload
The payload is obtained from the data paramter passed by Jackson to our serialisation method
Property values[] = new Property[data.size()];
int i = 0;
for ( String key: data.keySet() ){
values[i++] = new Property(key, data.get(key));
}
we now have a payload to write:
Writing the payload
Once again we construct a mapper object and use it to write our property array to the generator, which Jackson supplied to our serlialisation method.
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(generator, values);
And that completes the custom mapping.
Summary
Although it has taken a while to explain each step in preparing a mapping between Java Classes and JSON representations I wrote the code with very little effort, the Jackson programming model seems very effective.
One other note: You might feel that the custom mapping example I chose is somewhat contrived. In fact it is a simplified form of a real-life system I have been working with. The capability to provide a suitable custom mapping of the JSON payload is extremely useful in my real-world example.
This is the best article I’ve seen regarding Ser/Deser, with clean explaination. Great!!!
Thanks Vikas.
Regards, Dave.
You don’t happen to have a zip file of the complete source code for this sample do you?
Karl, thanks for the interest. It’s annoying, but I seem to have fouled up my archives, can’t put my hands on a clean copy.
Hi djna, you referred this link to me. My question, however, may be somewhat different.
I have a class defined as follow:
public class JST {
class Data{
public HashMap Data;
}
public HashMap desc;
public ArrayList Datalist;
public JST(){}
//Getter and Setter functions omitted for convenient look
}
And the main function is as follow:
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException{
//Omit initialization of the jst variable and its children variables
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(new File(“c:/yyt/test/user-modified.json”), jst);
}
My question is that the output file looks like this:
{“desc”:{},”reviewlist”:[{},{},{},{},{},{},{},{},{},{}]}
How haven’t the values initialized been printed out? Which part of your article will you point out as a solution to my problem?
Thanks
Hi Jeff,
I read your question as :
“It seems that I need to add a serializor, but how?”
And the referenced article I think talks about creating new serialializers. I’m afraid I don’t have time to look in detail at your problem, my guess is that your serializer isn’t quite doing the right thing. I’d be inclined to focus on serializing just one item in your dataList (or is it review list) and once you have confidence in that, then look at the more complex object. Good luck …
Thanks.
Great post, saved me a lot of time thank you!
Thanks, glad it was helpful.
Hi djna. Your article is greate, and it ‘s very helpful for me .
Now, I face a new proplem.
My POJO :
class Person{
public String name;
public int age;
}
And the JSON is {“name”:”Jackson”,”age”:”"}.
If I write the code like this:
Person person = mapper.readValue(“{\”name\”:\”wanxf\”,\”age\”:\”\”}”, Person.class);
A Exception is thrown, it complains that
Can not construct instance of int from String value ”: not a valid int value.
If the JSON is “{\”name\”:\”Jackson\”,\”age\”:null}”, it’s OK.
But now , I don’t want to modify the JSON. And how can I do ?
Thanks for your interest. For this kind of question I think it’s best to post to a wider forum. I suggest that you ask the question on http://www.stackoverflow.com, and I’ll have a try to answer it there – I’m djna over there too. The reason I like StackOverflow is that there’s a wider range of expertise there (some really very clever people) and the speed of answering tends to be very quick. Here, I might be out of teh office for a few days and not see questions like this immediately.
I will have a try. Thank for your suggest@@
Thanks Dave, this was exactly what I needed to get Jackson behaving as I’d hoped it would. I did have to reassemble your code in Eclipse so a source attachment would be handy for others I’m sure. Thanks though!