6

When deserializing a variety of JSON messages, I want to provide a default value for attributes of a certain type. It is generally suggested to simply specify the value in the Class, but this is error-prone if you have to do this across many Classes. You might forget one and end up with null instead of a default value. My intention is to set every property that is an Optional<T> to Optional.absent. Since null is exactly what Optional is trying to eliminate, using them with Jackson has proven to be frustrating.

Most features of Jackson that allow you to customize the deserialization process focus on the JSON that is the input, not around the process of instantiating the Object that you are deserializing into. The closest I seem to be getting to a general solution is by building my own ValueInstantiator, but there are two remaining issues I have:

  • how do I make it only instantiate Optional as absent but not interfere with the rest of the instantiation process?
  • how do I wire the end result into my ObjectMapper?

UPDATE: I want to clarify that I am looking for a solution that does not involve modifying each Class that contains Optional's. I'm opposed to violating the DRY principle. Me or my colleagues should not have to think about having to do something extra every time we add Optional's to a new or existing Class. I want to be able to say, "make every Optional field in every Class I deserialize into, pre-filled with Absent", only once, and be done with it.

That means the following are out:

  • abstract parent class (need to declare)
  • custom Builder/Creator/JsonDeserializer (needs annotation on each applicable class)
  • MixIn's? I tried this, combined with reflection, but I don't know how to access the Class I'm being mixed into...
Community
  • 1
  • 1
László van den Hoek
  • 3,955
  • 1
  • 23
  • 28

3 Answers3

2

Specifically for java.lang.Optional, there is a module by the Jackson guys themselves: https://github.com/FasterXML/jackson-datatype-jdk8

Guava Optional is covered by https://github.com/FasterXML/jackson-datatype-guava

It will create a Optional.absent for null's, but not for absent JSON values :-(.

See https://github.com/FasterXML/jackson-databind/issues/618 and https://github.com/FasterXML/jackson-datatype-jdk8/issues/2.

So you're stuck with initializing your Optionals just as you should initialize collections. It is a good practice, so you should be able to enforce it.

private Optional<Xxx> xxx = Optional.absent();
private List<Yyy> yyys = Lists.newArrayList();
GeertPt
  • 16,398
  • 2
  • 37
  • 61
  • Nope, I am using the latter one and it leaves absent values null. I don't think it would be otherwise for the native one, but I can check. – László van den Hoek Apr 29 '15 at 04:47
  • Yup, same problem. No dice on absent values :( – László van den Hoek Apr 29 '15 at 08:30
  • Actually Guava `Optional` deserializer should indeed deserialize JSON `null` as "absent", with latest versions. Use 2.5.3 at least. Serialization is another matter, however; absent value is consider "null-equivalent" so depending on default inclusion they may be stripped. But there is different between not having any value for a property in JSON (nothing is done by Jackson) and explicit JSON null. – StaxMan Apr 29 '15 at 18:43
  • @staxman: I understand what you are saying, but I want "no value present in JSON" to result in `Absent` when deserializing. – László van den Hoek Apr 29 '15 at 20:39
  • 1
    @LászlóvandenHoek There is unfortunately no way to do that with Jackson. – StaxMan Apr 29 '15 at 21:54
  • @staxman not even with a custom `ValueInstantiator`, as suggested in my question? – László van den Hoek Apr 30 '15 at 05:10
  • 1
    Ah. Actually, was about to mention that maybe via `ValueInstantiator`... :-) -- so yeah, that'd be the most likely route to get there. If possible, might be best discussed via mailing list. But, yes, it should be possible to ONLY handle instantiation, let actual deserialization proceed. – StaxMan Apr 30 '15 at 22:55
1

You can write a custom deserializer to handle the default value. Effectively you will extend the appropriate deserializer for the type of object you are deserializing, get the deserialized value, and if it's null just return the appropriate default value.

Here's a quick way to do it with Strings:

public class DefaultStringModule extends SimpleModule {
    private static final String NAME = "DefaultStringModule";

    private static final String DEFAULT_VALUE = "[DEFAULT]";

    public DefaultStringModule() {
        super(NAME, ModuleVersion.instance.version());
        addDeserializer(String.class, new DefaultStringDeserializer());
    }

    private static class DefaultStringDeserializer extends StdScalarDeserializer<String> {
        public DefaultStringDeserializer() {
            super(String.class);
        }

        public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
            String deserialized = jsonParser.getValueAsString();

            // Use a default value instead of null
            return deserialized == null ? DEFAULT_VALUE : deserialized;
        }

        @Override
        public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException {
            return deserialize(jp, ctxt);
        }
    }
}

To use this with an ObjectMapper you can register the module on the instance:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new DefaultStringModule());

To handle default values for fields not present in the JSON, I've typically seen this done through the use of a builder class that will construct the class using the values supplied and add any default values for the missing fields. Then, on the deserialized class (e.g. MyClass), add a @JsonDeserialize(builder = MyClass.Builder.class) annotation to indicate to Jackson to deserialize MyClass by way of the builder class.

Erik Gillespie
  • 3,929
  • 2
  • 31
  • 48
  • The problem with this is that `deserialize()` is not called if the attribute is not in the JSON. I want those to be mapped to `Optional.Absent` too. – László van den Hoek Apr 28 '15 at 21:12
  • Sorry, missed that part. I add a paragraph at the end of my answer to suggest one way that I've done this. – Erik Gillespie Apr 28 '15 at 21:18
  • Better, but then I would still have to annotate every class that has Optional properties. I want it to be transparent. – László van den Hoek Apr 28 '15 at 21:22
  • See @greyfairer's answer for how to get better de/serialization support for Optional. – Erik Gillespie Apr 29 '15 at 03:49
  • Unfortunately, jackson-datatype-guava has the same problem that your first example has - it only maps explicit `null`'s to `Absent`. Absent values are left as `null`. Confused yet? :) – László van den Hoek Apr 29 '15 at 08:21
  • In the Builder class, don't map directly to `Optional` classes. Instead, map to whatever the `Optional` will hold. For any `null` passed in, explicitly construct an `Absent`. No need for any additional annotations except for the `@JsonDeserialize` on the top-level class. – Erik Gillespie Apr 29 '15 at 12:59
  • But I don't want to annotate my classes. I want any `Optional`-containing Class that I deserialize into to automatically have those fields set to `Absent`. – László van den Hoek Apr 29 '15 at 15:04
  • I don't know why you're so opposed to annotations and you didn't really point that out in your original question, but you can use [mix-ins](http://wiki.fasterxml.com/JacksonMixInAnnotations) to keep the classes nice and tidy but you'll end up writing way more code because you'll have to create a mix-in class to mirror each class that gets deserialized into, and yes, those mix-in classes will need annotations. – Erik Gillespie Apr 29 '15 at 15:25
  • Yes, you're right, I could have been clearer on that point. The reason I'm not content with annotations here is that I'm opposed to violating the DRY principle. Me or my colleagues should not have to think about having to do something extra every time we add `Optional`'s to a new or existing Class. I want to be able to say, "make every Optional field in every Class I deserialize into, pre-filled with `Absent`", only once, and be done with it. – László van den Hoek Apr 29 '15 at 15:43
1

Your value object should initialize these values as absent. That's the way to ensure that default values have no nulls. Guava module's Optional handler really should only deserialize them as "absent" (even with explicit JSON nulls), and never as nulls, with later versions. But since Jackson only operates on JSON properties that exist (and not on things that could exist but do not), POJO still needs to have default absent assignment.

StaxMan
  • 113,358
  • 34
  • 211
  • 239