wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

Yes No Maybe Boolean deserialization with Jackson


The Robustness principle demands: be lenient in what you accept and strict in what you emit. I was facing this challenge when deserializing boolean values.

What is true

Glancing at data, we can spot, mostly easily what looks trueish:

  • true
  • "True"
  • "Yes"
  • 1
  • "Si"
  • "Ja"
  • "Active"
  • "isActive"
  • "enabled"
  • "on"

The last three options aren't as clear cut, they depend on your use case. Using a simple class, lets try to deserialize from JSON to an instance of a Java class instance using Jackson.

Java doesn't have native support for JSON, so we need to rely on libraries like Jackson, Google GSON (or any other listed on the JSON page). I choose Jackson, since it is the library underpinning the JsonObject of the Eclipse Vert.x Framework I'm fond of. Over at Baeldung you will find more generic Jackson tutorials.

Let's look at a simple Java class (Yes, Java14 will make it less verbose), that sports fromJson() and toJson() as well as convenient overwrite of equals() and toString()

package com.notessensei.blogsamples;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.vertx.core.json.JsonObject;

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class Component {

  public static Component fromJson(final JsonObject source) {
    return source.mapTo(Component.class);
  }

  private String name;
  private boolean active = false;

  public Component() {
    // Default empty constructor required
  }

  public String getName() {
    return name;
  }

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

  public boolean getActive() {
    return active;
  }

  public void setActive(boolean isActive) {
    this.active = isActive;
  }

  public JsonObject toJson() {
    return JsonObject.mapFrom(this);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Component) {
      return this.toString().equals(obj.toString());
    }
    return super.equals(obj);
  }

  @Override
  public String toString() {
    return this.toJson().encode();
  }

}

Trying to instantiate a class instance with the following JSON will work:

{
  "name": "Heater",
  "active": false
}
{
  "name": "Aircon"
}
{
  "name": "Fridge",
  "active": true,
  "PowerConsumption": {
    "unit": "kw",
    "measure": 7
  }
}

However it will fail with those:

{
  "name": "System1",
  "active": "on"
}
{
  "name": "System2",
  "active": "yes"
}

You get the charming error Cannot deserialize value of type boolean from String "yes": only "true"/"True"/"TRUE" or "false"/"False"/"FALSE" recognized`. Interestingly numbers work.

On a side note: Jackson uses the presence of getters/setters to decide (de)serialization and needs getActive and setActive or isActive. When you name your variable isActive Eclipse would generate setActive and isActive instead of getIsActive / isIsActive and setIsActive. So simply avoid the is... prefix for internal variables.

To bend the Jackson deserializer to our will, we need to overwrite JsonDeserializer, an abstract class with one method we need to override: deserialize(JsonParser p, DeserializationContext ctxt).

For our case it was sufficient to check the first character of the property value. Using getTextCharacters we get access to the char array and getTextOffset tells us where to start. Put all this together gives us the custom deserializer.

package package com.notessensei.blogsamples;

import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

public class RelaxedBooleanDeserializer extends JsonDeserializer<Boolean> {

  @Override
  public Boolean deserialize(JsonParser p, DeserializationContext ctxt)
      throws IOException {

    char[] chars = p.getTextCharacters();
    int start = p.getTextOffset();
    char isbool = chars[start];

    /* What is true
     * - boolean true
     * - Letters: T,t (True), Y,y (Yes sir), A,a (active), I,i (isActive), E,e (enabled), J,j (Jawohl), S,s (si senior)
     * - Numbers: anything not starting with 0 (0.3 would be false)
     */
    return "TtYyeAaEIiJjSs123456789".contains(String.valueOf(isbool))
        ? Boolean.TRUE
        : Boolean.FALSE;
  }
}

Depending on the use case, a different logic might be needed, e.g. when true/false comes in as started/stopped or isActive/isDisabled. So attention to detail is required. Using the Jackson annotation @JsonDeserialize we activate the use of our deserializer on our variable

@JsonDeserialize(using = RelaxedBooleanDeserializer.class)
private boolean active;

Looks good, until we test it. The deserialization barfs on value "" and on submission of null. The first one can be remedied by checking the lenght of chars, while the null value requires overriding the getNullValue method. Our result:

public class RelaxedBooleanDeserializer extends JsonDeserializer<Boolean> {

  @Override
  public Boolean deserialize(JsonParser p, DeserializationContext ctxt)
      throws IOException {

    char[] chars = p.getTextCharacters();

    if (chars.length < 1) {
      return Boolean.FALSE;
    }

    int start = p.getTextOffset();
    char isbool = chars[start];

    return "TtYyeEIiJjSs123456789".contains(String.valueOf(isbool))
        ? Boolean.TRUE
        : Boolean.FALSE;
  }

  @Override
  public Boolean getNullValue(DeserializationContext ctxt) throws JsonMappingException {
    return Boolean.FALSE;
  }
}

As usual YMMV


Posted by on 07 May 2022 | Comments (0) | categories: Java

Comments

  1. No comments yet, be the first to comment