3
votes

I need to deserialize JsonArray to a boolean value. If an array exists and is not empty the value should be set to "true". The problem is, my custom deserializer, while functional, breaks deserialization of the rest of the fields - they are being set to null.

Object:

private static class TestObject {
    private String name;

    @JsonProperty("arr")
    @JsonDeserialize(using = Deserializer.class)
    private Boolean exists = null;

    private Integer logins;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Boolean getExists() {
        return exists;
    }

    public void setExists(boolean exists) {
        this.exists = exists;
    }

    public Integer getLogins() {
        return logins;
    }

    public void setLogins(Integer logins) {
        this.logins = logins;
    }

    @Override
    public String toString() {
        return "TestObject{" +
                "name='" + name + '\'' +
                ", exists=" + exists +
                ", logins=" + logins +
                '}';
    }
}

Deserializer:

public class Deserializer extends JsonDeserializer<Boolean> {
    @Override
    public Boolean deserialize(JsonParser jp, DeserializationContext ctxt)
        throws IOException {
        if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
            return true;
        }
        return false;
    }
}

Test

@Test
public void test() throws JsonParseException, IOException {
    Boolean result = deserialize();
}

private Boolean deserialize() throws IOException, JsonParseException,
        JsonProcessingException {
    TestObject testObject = mapper.readValue("{\n" +
                    "  \"arr\": [\n" +
                    "     {\"value\": \"New\"}\n" +
                    "  ],\n" +
                    "  \"name\": \"name\",\n" +
                    "  \"logins\": 36" +
                    "}",
                    TestObject.class);

    System.out.println(testObject.toString());

    return testObject.getExists();
}

If i remove the "arr" array or move it to the bottom of the Json, everything's fine. If i leave it at the top - TestObject{name='null', exists=true, logins=null}.

There was a similar question (Jackson Custom Deserializer breaks default ones), but unfortunately it has zero answers. Since the code works properly when i rearrange Json, it doesn't look like custom deserializer is used for all fields, rather Jackson stops deserialization when it executes custom deserializer.

3
My suggestion is to not deserialize an array to a boolean. Deserialize the array to an array/list/set and have getExists return arr != null or some such.Paul
@Paul This actually works, thanks for the idea, somehow i did not think of that. The problem is that it looks a bit ugly and obscure, but if there will be no other way i'll do it like that.Dmitry Korchemkin
Sometimes you have to choose the prettier of two ugly solutions :) I'm glad I could help...I'll make my comment an answer.Paul

3 Answers

2
votes

Your deserializer may not be interested in the contents of the array but must still advance the read mark on the parser.

You could read the value of arr at once and decide according to the size of the collection:

@Override
public Boolean deserialize(JsonParser jp, DeserializationContext ctxt)
        throws IOException {
    JsonNode node = jp.getCodec().readTree(jp);
    return node.size() != 0;
}

Deciding on the size of the collection and not on the existence of a collection at all is necessary because either your stringified object contains an arr or the Deserializer.deserialize() is never executed. The property exist will be null in this case. So the only possible semantics to express does not exist is an empty collection.

Just for curiousity I tried a second more explicit way to keep the parser on track. For real world use the above version is definitely preferable.

@Override
public Boolean deserialize(JsonParser jp, DeserializationContext ctxt)
        throws IOException {
    if (jp.currentToken() == JsonToken.START_ARRAY) {
        jp.nextToken();
        int recursionLevel = 1;
        while(recursionLevel > 0) {
            switch (jp.currentToken()) {
                case START_ARRAY:
                    // just in case of nested arrays
                    recursionLevel++;
                    break;
                case END_ARRAY:
                    recursionLevel--;
                    break;
            }
            jp.nextToken();
        }
        return true;
    }
    return false;
}
1
votes

My suggestion is to not deserialize an array to a boolean. Deserialize the array to an array/list/set and have getExists return arr != null or some such.

For example, if the JSON you're deserializing looks like this:

{
  "arr": [{"value": "New"}],
  "name": "name",
  "logins": 36
}

Write your entity like this:

private static class TestObject
{
  private List<Map<String, String>> arr;

  // other getters/setters removed for brevity

  public boolean getExists()
  {
    return arr !=null && !arr.isEmpty();
  }
}
0
votes

There's actually a third way to do it, that fits my case - JsonArray will always contain at least one element when it's not missing, so all i need is to advance the read marker properly and then return true:

if (jp.getCurrentToken().equals(JsonToken.START_ARRAY)) {
    while (!jp.getCurrentToken().equals(JsonToken.END_ARRAY)){
        jp.nextToken();
    }
}
return true;

Unless you don't need the contents of the array for some additional work, just moving the marker is a couple milliseconds faster than readTree.