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 Stephan H Wissel on 07 May 2022 | Comments (0) | categories: Java