8

I am parsing input JSON. For a field, there are 3 possibilities:

  • the field is absent;
  • the value is set to null;
  • the value is set to something valid.

Different behavior is implemented: for an absent value in the JSON, the default value is inserted into the database; for a null value in the JSON, a null value is inserted into the database.

I thought about Optional to model this:

public class Data {
    private Optional<String> field;
}

Which of the following two options make most sense?

  1. If field is null, the field was absent in the JSON. If field is Optional.empty, the field is null in the JSON.
  2. If field is null, the field was null in the JSON. If field is Optional.empty, the field is absent in the JSON.

FWIW, I am using Jackson with module jackson-datatype-jdk8 to parse the input JSON.

  • 1
    What is the meaning of the field being omitted? What is the meaning of the field being null? Is it the same? – assylias Jul 16 '15 at 14:02
  • Should the behavior be different in case of missing fields than in case of null? – Simon Jul 16 '15 at 14:02
  • Different behavior is implemented. For an absent value, the default value is inserted into the database. For a null value, a null value is inserted into the database. –  Jul 16 '15 at 14:03
  • With the same reasoning you could have `Optional>`. It seems semantically easier to have a tri-state `Property("default")` or such special class. – Joop Eggen Jul 16 '15 at 14:19
  • @JoopEggen Could you elaborate in an answer? –  Jul 16 '15 at 14:33
  • http://stackoverflow.com/a/9010622/125344 might be relevant. I think maintaining an "isSet" state for each property would be a useful alternative. Doing it with a tri-state class like what @JoopEggen suggests seems ideal. – Kevin Condon Jul 16 '15 at 14:35
  • @KevinCondon Very useful comment! Thanks. –  Jul 16 '15 at 14:41
  • @DaveTeezo I think my comment might not have sufficient quality. Ideally one would signal errors on required missing fields, not pass default values, and so on, maybe having one single static final _default_ object. Much depends on the usage, before overdesigning a class `Property`. – Joop Eggen Jul 16 '15 at 14:46

5 Answers5

4

I think you shouldn't use Optional for this scenario. As @dkatzel has mentioned in his answer, it's meant to be used as an API return value more than as a field.

Despite this academic discussion, you can accomplish what you want simply by initializing fields in your Data class to their default values:

public class Data {
    private String field = DEFAULT_VALUE;
}

And then let Jackson do the rest.


EDIT as per OP's comment:

When your JSON comes with a null value for the field, Jackson will set it to null, and that's what will be stored in the database.

When your JSON does not contain the field, the DEFAULT_VALUE will be automatically loaded in your Data instance.

And when your JSON does actually contain a value for the field, Jackson will set it, and that value will reach the database.


EDIT 2, considering OP's requirement to find out if the field was either filled in, set to null or was absent in the input JSON, after parsing the JSON input:

If, after parsing the input JSON, you need to know whether the field was either filled in, set to null or was absent, then consider this example, which shows the approach I'd take:

public class Data {

    private String field1 = "hello";

    private Integer field2 = 10;

    private Double field3 = 3.75;

    private static final Data DEFAULTS = new Data(); // defaults will be kept here

    public String getField1() {
        return this.field1;
    }

    public void setField1(String field1) {
        this.field1 = field1;
    }

    public Integer getField2() {
        return this.field2;
    }

    public void setField2(Integer field2) {
        this.field2 = field2;
    }

    public Double getField3() {
        return this.field3;
    }

    public void setField3(Double field3) {
        this.field3 = field3;
    }

    @Override
    public String toString() {
        return "Data [field1=" + this.field1 + 
                   ", field2=" + this.field2 + 
                   ", field3=" + this.field3 + "]";
    }

    public boolean isDefault(Function<Data, Object> getter) {
        Object defaultProperty = getter.apply(DEFAULTS);
        Object actualProperty = getter.apply(this);
        return defaultProperty != null // needed to support fields with no default value
            && defaultProperty.equals(actualProperty);
    }

    public boolean isNull(Function<Data, Object> getter) {
        return getter.apply(this) == null;
    }

    public boolean isSet(Function<Data, Object> getter) {
        return !this.isNull(getter) && !this.isDefault(getter);
    }
}

Here I've used a private static attribute to hold your Data's default values and 3 methods to query any field state (default, null or set). In order to determine which field to query, these methods receive a Function<Data, Object>, which are given a Data instance and return an Object that is supposed to be the desired field. (If you stop to think it, getters can be seen as functions that take the instance as input and return a specific field of the instance).

So later, when you need to know how a certain field arrived in your JSON input, just use those 3 query methods to find out:

ObjectMapper m = new ObjectMapper();

String json = "{\"field1\":null,\"field2\":20}";
Data data = m.readValue(json, Data.class);

System.out.println(data); // Data [field1=null, field2=20, field3=3.75]

System.out.println("field1 default ? " + data.isDefault(Data::getField1)); // false
System.out.println("field1 null ? " + data.isNull(Data::getField1)); // true
System.out.println("field1 set ? " + data.isSet(Data::getField1)); // false

System.out.println("field2 default ? " + data.isDefault(Data::getField2)); // false
System.out.println("field2 null ? " + data.isNull(Data::getField2)); // false
System.out.println("field2 set ? " + data.isSet(Data::getField2)); // true

System.out.println("field3 default ? " + data.isDefault(Data::getField3)); // true
System.out.println("field3 null ? " + data.isNull(Data::getField3)); // false
System.out.println("field3 set ? " + data.isSet(Data::getField3)); // false
fps
  • 33,623
  • 8
  • 55
  • 110
  • 1
    This means that there is no way to discriminate between an absent value and a `null` value afterwards, which is desired. –  Jul 16 '15 at 14:18
  • Thanks, it makes sense! This avoids using a map, as suggested in the other answers. Should one use the `==` operator to discriminate between absent and filled-in value? –  Jul 16 '15 at 14:33
  • 1
    @Dave Wait, do you need to know *later* when the default has been used? I thought you were asking about parsing the JSON only. – fps Jul 16 '15 at 14:35
  • After parsing, I should be able to find out if the field was either filled in, set to null or was absent in the input JSON. –  Jul 16 '15 at 14:36
  • 1
    @Dave Updated my answer again, hope this time it will be of use. – fps Jul 16 '15 at 15:36
  • Thanks, very interesting. One question, aren't we having a wrong behavior when the value of a field in the input JSON is exactly equal to the default value of that field? That's why I suggested using `==` instead of `equals`. –  Jul 16 '15 at 16:13
  • 1
    @Dave In that case, `isDefault()` would return `true`. I thought that if the incoming value is equal to the default value, then it doesn't matter whether the value was set or not. I chose to let `isDefault()` return `true` in this case. – fps Jul 16 '15 at 16:38
  • 1
    @Dave Regarding `equals()` vs `==`, I suggest you always use `equals()` and check for `==` in the very first line. – fps Jul 16 '15 at 16:48
2

I would say that the first option makes the most semantic sense. It also potentially allows for easier computation.

Where a field in java is null, it is implied that a value is missing, which matches the first option.

I suggest that you store these fields in a hash-map where the key is the JSON field name and the value is the JSON field's value. I also suggest you don't use an optional here (as it can add an unnecessary layer of complexity), and instead use either a null or non-null object in the hashmap.

HashMap<String, Value> jsonFields = new HashMap<String, Value>();
boolean hasField1 = false;
Value field1Value = null;
if(jsonFields.contains("field1"){ // It is present in the JSON file
    field1Value = jsonFields.get("field1"); // "null" here would mean that the JSON field was set to "null"
    hasField1 = true;
}
SamTebbs33
  • 5,507
  • 3
  • 22
  • 44
  • That is exactly what jackson-datatype-jdk8 is doing. It looks counter intuitive to me, hence the question. –  Jul 16 '15 at 14:07
  • @DaveTeezo What seems counter-intuitive? – SamTebbs33 Jul 16 '15 at 14:08
  • The fact that `null` means `absent` and `Optional.empty` `null`. –  Jul 16 '15 at 14:09
  • BTW, my comment was made before your edit about the hash map. I was referring to your comment saying that the first option makes most sense. –  Jul 16 '15 at 14:09
  • @DaveTeezo Ah, well I guess it is a subjective matter. I'd say that **Optional.empty()** is also a way of directly saying "absent", but in a way that prevents NullPointerExceptions. http://huguesjohnson.com/programming/java/java8optional.html – SamTebbs33 Jul 16 '15 at 14:12
  • @DaveTeezo Make sure you accept an answer so that others know that the question has been answered. – SamTebbs33 Jul 16 '15 at 15:34
  • I will! I am still contemplating which answer is prefered. –  Jul 16 '15 at 16:14
1

The second choice makes more sense to me. null means null and empty means not present.

However, Optional shouldn't really be used as a field. It's supposed to be used as an API return value.

Could you instead store the data in a Map that allows null values? And if the key (your field) isn't present in the map, then return Optional.empty ?

Community
  • 1
  • 1
dkatzel
  • 31,188
  • 3
  • 63
  • 67
0

Neither? I would annotate my POJO fields with @DefaultValue(). Then your possibilities are a null value or a non-null value specified in JSON, or the default if the field was omitted from JSON. And you can then just persist the POJO without any special per-field analysis.

Kevin Condon
  • 1,618
  • 10
  • 16
  • The database holds the default value, which can be a complex query. Using `@DefaultValue` means that you are duplicating this into the application code, and also that you are sending the default values to the database. –  Jul 16 '15 at 14:16
  • So the DB supplies the default value if you don't include it in the query? Agreed then that annotating the default value isn't a good choice. You could annotate with a special default string value like "USE_DB_DEFAULT", as long as there's a string that wouldn't conflict with valid values. Check for that special string when persisting. – Kevin Condon Jul 16 '15 at 14:23
0

If you are dealing with Object instead of String, here's a solution I find elegant:

  • use Optional.empty(); if there is no value
  • use Optional.of(value) if there is a value
  • use Optional.of(specialValue) if the value is null

where specialValue is a static singleton you can easily test, for instance: ObjectUtils.NULL (from commons.lang).

Then you can easily test your optional:

if (optional.isPresent()) {
    if (ObjectUtils.NULL.equals(optional.get())) {
        // value is there and null
    } else {
        // value is there and not null
    }
} else {
    // value is not there 
}
Nicolas Nobelis
  • 772
  • 6
  • 10