Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom JSON Deserialization with Jackson

I'm using the Flickr API. When calling the flickr.test.login method, the default JSON result is:

{
    "user": {
        "id": "21207597@N07",
        "username": {
            "_content": "jamalfanaian"
        }
    },
    "stat": "ok"
}

I'd like to parse this response into a Java object:

public class FlickrAccount {
    private String id;
    private String username;
    // ... getter & setter ...
}

The JSON properties should be mapped like this:

"user" -> "id" ==> FlickrAccount.id
"user" -> "username" -> "_content" ==> FlickrAccount.username

Unfortunately, I'm not able to find a nice, elegant way to do this using Annotations. My approach so far is, to read the JSON String into a Map<String, Object> and get the values from there.

Map<String, Object> value = new ObjectMapper().readValue(response.getStream(),
        new TypeReference<HashMap<String, Object>>() {
        });
@SuppressWarnings( "unchecked" )
Map<String, Object> user = (Map<String, Object>) value.get("user");
String id = (String) user.get("id");
@SuppressWarnings( "unchecked" )
String username = (String) ((Map<String, Object>) user.get("username")).get("_content");
FlickrAccount account = new FlickrAccount();
account.setId(id);
account.setUsername(username);

But I think, this is the most non-elegant way, ever. Is there any simple way, either using Annotations or a custom Deserializer?

This would be very obvious for me, but of course it doesn't work:

public class FlickrAccount {
    @JsonProperty( "user.id" ) private String id;
    @JsonProperty( "user.username._content" ) private String username;
    // ... getter and setter ...
}
like image 298
Moritz Petersen Avatar asked Oct 03 '13 11:10

Moritz Petersen


People also ask

How does Jackson deserialize dates from JSON?

How to deserialize Date from JSON using Jackson. In order to correct deserialize a Date field, you need to do two things: 1) Create a custom deserializer by extending StdDeserializer<T> class and override its deserialize(JsonParser jsonparser, DeserializationContext context) method.

What is Jackson deserialization?

Jackson is a powerful and efficient Java library that handles the serialization and deserialization of Java objects and their JSON representations. It's one of the most widely used libraries for this task, and runs under the hood of many other frameworks.


2 Answers

You can write custom deserializer for this class. It could look like this:

class FlickrAccountJsonDeserializer extends JsonDeserializer<FlickrAccount> {

    @Override
    public FlickrAccount deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        Root root = jp.readValueAs(Root.class);

        FlickrAccount account = new FlickrAccount();
        if (root != null && root.user != null) {
            account.setId(root.user.id);
            if (root.user.username != null) {
                account.setUsername(root.user.username.content);
            }
        }

        return account;
    }

    private static class Root {

        public User user;
        public String stat;
    }

    private static class User {

        public String id;
        public UserName username;
    }

    private static class UserName {

        @JsonProperty("_content")
        public String content;
    }
}

After that, you have to define a deserializer for your class. You can do this as follows:

@JsonDeserialize(using = FlickrAccountJsonDeserializer.class)
class FlickrAccount {
    ...
}
like image 155
Michał Ziober Avatar answered Oct 10 '22 03:10

Michał Ziober


Since I don't want to implement a custom class (Username) just to map the username, I went with a little bit more elegant, but still quite ugly approach:

ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(in);
JsonNode user = node.get("user");
FlickrAccount account = new FlickrAccount();
account.setId(user.get("id").asText());
account.setUsername(user.get("username").get("_content").asText());

It's still not as elegant as I hoped, but at least I got rid of all the ugly casting. Another advantage of this solution is, that my domain class (FlickrAccount) is not polluted with any Jackson annotations.

Based on @Michał Ziober's answer, I decided to use the - in my opinion - most straight forward solution. Using a @JsonDeserialize annotation with a custom deserializer:

@JsonDeserialize( using = FlickrAccountDeserializer.class )
public class FlickrAccount {
    ...
}

But the deserializer does not use any internal classes, just the JsonNode as above:

class FlickrAccountDeserializer extends JsonDeserializer<FlickrAccount> {
    @Override
    public FlickrAccount deserialize(JsonParser jp, DeserializationContext ctxt) throws 
            IOException, JsonProcessingException {
        FlickrAccount account = new FlickrAccount();
        JsonNode node = jp.readValueAsTree();
        JsonNode user = node.get("user");
        account.setId(user.get("id").asText());
        account.setUsername(user.get("username").get("_content").asText());
        return account;
    }
}
like image 16
Moritz Petersen Avatar answered Oct 10 '22 04:10

Moritz Petersen