8

I have a webservice which prints "null" as a string for any property instead of null literal. It does that for almost all data types(String or Date). For example, in ideal case it returns

{
    "item" : {
        "title": "Some title",
        "expires": "2014-11-02 00:00:00"
    }
}

But sometimes it returns:

{
    "item" : {
        "title": "null",
        "expires": "2014-11-02 00:00:00"
    }
}

Which makes the title property value as "null" instead of setting it to null. Or sometime this:

{
    "item" : {
        "title": "Some title",
        "expires": "null"
    }
}

Which makes the deserialization fail because the dateformat does not match. How can I configure objectmapper or annotate my model classes to resolve these problems during deserialization?

My model class looks like:

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Item {
    public String title;
    @JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
    public Date expires;
}

It's an android app so I have no control over the webservice. Thanks in advance

Max
  • 3,371
  • 2
  • 29
  • 30
  • The JSON says the value is a string, and no parser is going to regard it otherwise. About all you can do is add code to, after the fact, compare the string to "null" and act accordingly. – Hot Licks Sep 17 '14 at 01:55

3 Answers3

4

Since someone asked this question again a few minutes ago, I did some research and think I found a good solution to this problem (when dealing with Strings).

You have to create a custom JsonDeserializer class as follows:

class NullStringJsonDeserializer extends JsonDeserializer<String> {
    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String result = StringDeserializer.instance.deserialize(p, ctxt);
        return result!=null && result.toLowerCase().equals(null+"") ? null : result;
    }
}

Last but not least, all you have to do is tell your ObjectMapper that it should use your custom json string deserializer. This depends on how and where you use your ObjectMapper but could look like that:

ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new NullStringJsonDeserializer());
objectMapper.registerModule(module);
FlorianDe
  • 1,202
  • 7
  • 20
1

If you use a custom Deserializer, you can do this, but I don't think it's available using standard annotations.

Borrowing and modifying some code from http://www.baeldung.com/jackson-deserialization :

public class ItemDeserializer extends JsonDeserializer<Item> {

    @Override
    public Item deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        String title = null;
        TextNode titleNode = (TextNode)node.get("title");
        if ( ! titleNode.toString().equals("null")) {
            title = titleNode.toString();
        }

        Date expires = null;
        // similar logic for expires

        return new Item(title, expires);
    }
}
Dathan
  • 7,266
  • 3
  • 27
  • 46
  • It's probably simpler to just write a constructor for Item that accepts a JsonNode or whatever. – Hot Licks Sep 17 '14 at 02:15
  • Perhaps - but I already flinch a little bit at annotating the application domain objects with serialization annotations -- adding deserialization logic directly into the POJO constructor makes me really cringe -- IMO that's really poor separation of concerns. – Dathan Sep 17 '14 at 02:17
  • I have about 30 properties in the Item class and have similar few other classes in my project. This not an acceptable solution for me. I was looking for a more generic solution. May be BeanDeserializerModifier? or custom null detector? – Max Sep 17 '14 at 02:17
  • I cringe when I see Jackson-style stuff. Really distorts both JSON and the class, whereas a constructor that accepts a Map is very straight-forward and sensible. It doesn't tie you to JSON ("mix concerns") at all if you use a JSON kit that gives you Maps instead of custom JSON objects. – Hot Licks Sep 17 '14 at 02:20
  • 1
    @HotLicks, in what way does Jackson databinding distort JSON? Also, it's barely more effort to write a custom deserializer that pulls nodes out of a JsonParser and passes them to a constructor compared to pulling items out of a map inside the constructor, and doesn't open you up to the unpleasantness of having to thoroughly document and sanitize the keys of the map. Whenever you need to convert between loosely typed (e.g., JSON) and statically typed data, keep that in its own layer -- it makes life better, if somewhat more verbose, as a result. – Dathan Sep 17 '14 at 02:28
  • @mamnun I've never used `BeanDeserializerModifer`, so I'm not sure what its capabilities are, though the name certainly sounds like it's on the right track. The best I could really offer from my experience would be a custom `Deserializer` that applies this logic, and you can then annotate your constructor arguments with `@JsonDeserialize(using=MyCustomDeserializer.class, as=MyProeprtyType.class, contentAs=myPropertyType.class, keyAs=MyPropertyType.class)`. Unfortunately, that's more verbose than the original approach I proposed. – Dathan Sep 17 '14 at 02:32
1

Not sure if it is desired, but if you are using ObjectMapper you can do the following:

1) Remove @JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss"). I also added @JsonRootName for the way you showed your input data.

@JsonRootName("item")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Item {
    public String title;
    public Date expires;
    @Override
    public String toString() {
        return "Item{" +
           "title='" + title + '\'' +
           ", expires=" + expires +
          '}';
    }
}

2) And configure the ObjectMapper with the DateFormat you want to use:

mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:s"));

Here is the full example I tested with:

public class Test {
    private static final String T1 = "{\n"+
            "    \"item\" : {\n"+
            "        \"title\": \"Some title\",\n"+
            "        \"expires\": \"2014-11-02 00:00:00\"\n"+
            "    }\n"+
            "}";
    private static final String T2 = "{\n" +
            "    \"item\" : {\n" +
            "        \"title\": \"null\",\n" +
            "        \"expires\": \"2014-11-02 00:00:00\"\n" +
            "    }\n" +
            "}";
    private static final String T3 = "{\n" +
            "    \"item\" : {\n" +
            "        \"title\": \"Some title\",\n" +
            "        \"expires\": \"null\"\n" +
            "    }\n" +
            "}";

    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:s"));
        Item t1 = mapper.readValue(T1, Item.class);
        Item t2 = mapper.readValue(T2, Item.class);
        Item t3 = mapper.readValue(T3, Item.class);
        System.out.println(t1);
        System.out.println(t2);
        System.out.println(t3);
        System.out.println(mapper.writeValueAsString(t1));
        System.out.println(mapper.writeValueAsString(t2));
        System.out.println(mapper.writeValueAsString(t3));
    }
}

Results. Last printout shows how the @JsonInclude affects the output, and how the serialization of the input String 'null' is done with this configuration:

Item{title='Some title', expires=Sun Nov 02 00:00:00 PDT 2014}
Item{title='null', expires=Sun Nov 02 00:00:00 PDT 2014}
Item{title='Some title', expires=null}
{"title":"Some title","expires":"2014-11-02 00:00:0"}
{"title":"null","expires":"2014-11-02 00:00:0"}
{"title":"Some title"}
mkobit
  • 43,979
  • 12
  • 156
  • 150