45

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 ...
}
Eliran Malka
  • 15,305
  • 5
  • 75
  • 98
Moritz Petersen
  • 12,297
  • 3
  • 36
  • 43

5 Answers5

54

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 {
    ...
}
Brian
  • 5,039
  • 7
  • 35
  • 45
Michał Ziober
  • 34,365
  • 17
  • 89
  • 132
  • 1
    Thank you. The part that I was missing in this was the annotation. I had to supply the @JsonDeserialize annotation despite the object being a subclass of a type registered in a module. – th3morg Apr 15 '15 at 15:28
  • 1
    [Baeldung has a shorter example](http://www.baeldung.com/jackson-deserialization), without the need to create inner static classes to model the json schema. – ruhong Jun 23 '18 at 21:19
14

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;
    }
}
Community
  • 1
  • 1
Moritz Petersen
  • 12,297
  • 3
  • 36
  • 43
3

You can also use SimpleModule.

    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier() {
    @Override public JsonDeserializer<?> modifyDeserializer(
        DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        if (beanDesc.getBeanClass() == YourClass.class) {
            return new YourClassDeserializer(deserializer);
        }

        return deserializer;
    }});

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(module);
    objectMapper.readValue(json, classType);
sendon1982
  • 8,572
  • 53
  • 40
3

I made it this way:

public class FlickrAccount {
  private String id;
  @JsonDeserialize(converter = ContentConverter.class)
  private String username;
  
  private static class ContentConverter extends StdConverter<Map<String, String>, String> {
    @Override
    public String convert(Map<String, String> content) {
      return content.get("_content"));
    }
  }
}
  • I am actually working with Kotlin and wanted to custom deserialize just one attribute of my class and none of the answers helped me ( I was able to write a custom one for the whole class but not just for one attribute ). But this answer worked like a charm for me. Thanks! – Kancha Oct 01 '21 at 20:10
-2

You have to make Username a class within FlickrAccount and give it a _content field

tom
  • 2,646
  • 14
  • 28
  • The JSON represents the username as an object, so when you map it, it maps as an object. It's not terrible though, you can put a getUsername method in the FlickrAccount class that returns the Username._content value so in use it would be transparent. – tom Oct 03 '13 at 11:58
  • True, but not what I want or need. There is a [feature request](http://jira.codehaus.org/browse/JACKSON-781) describing a feature, that would do what I want, but it's still open. – Moritz Petersen Oct 03 '13 at 12:06