4

I'm trying to combine a few features of Jackson such that I can deserialize a {type,value} pair in json into a Union type in java representing all the information but can't work out how to do it. Help would be greatly appreciated.

Here is what I'm working with:

  1. The java Union type enforces that only a single value can be set at any one time, think of this like an enum but with dynamic data.
  2. The value of the union can take any number of types, some scalar and some object or collection.
  3. Multiple options for the union can share the same type, i.e. noyes and offon are both boolean types in the example below.
  4. I'm not in control of the json structure in any way (it's the data passed to/from an external API) so can't change it at all.
  5. The java classes are generated code from Thrift idl so I can't add annotations to them or adjust their structure drastically. I am in control of the idl though, but would like to keep it fairly clean and free of leaky patterns like the {type, value} that is needed for json but not strongly typed languages.
  6. The type names used in the json (and some of the field names) conflict with java (and other language) keywords which is why each value is suffixed: noyes -> noYesValue, float -> floatValue

A concrete example

I have a json document that looks like this:

{
  "obj": [
    {
      "type": "noyes",
      "value": true,
      "id": 1
    }, {
      "type": "offon",
      "value": false,
      "id": 2
    }, {
      "type": "text",
      "value": "hello",
      "id": 3
    }, {
      "type": "float",
      "value": 1.2,
      "id": 4
    }, {
      "type": "times",
      "value": [{"s": "12:22", "e": "16:00"}]
      "id": 5
    }
  ]
}

And java classes that look like this:

class Response {
  List<Item> obj;
}

class Item {
  int id;
  Value value;
}

class Value {
  enum Fields { NO_YES_VALUE, OFF_ON_VALUE, TEXT_VALUE, FLOAT_VALUE, TIMES_VALUE }

  static Item noYesValue(boolean noYes) {...}
  static Item offOnValue(boolean offOn) {...}
  static Item textValue(String text) {...}
  static Item floatValue(float value) {...}
  static Item timesValue(List<TimesValue> times) {...}

  Fields setField;
  Object fieldValue;

  Value() {}
  Value(Fields field, Object value) {setFieldValue(field, value);}

  Fields getSetField() { return setField; }
  Object getFieldValue() { return fieldValue; }
  void setFieldValue(Fields field, Object value) {
    // checkType(field, value);
    this.setField = field;
    this.fieldValue = value;
  }
  
  // these do have checks for the set field, types, null, etc
  boolean getNoYesValue() { return (Boolean) fieldValue; }
  void setNoYesValue(boolean v) { setField = NO_YES_VALUE; fieldValue = v; }
  boolean getOffOnValue() { return (Boolean) fieldValue; }
  void setOffOnValue(boolean v) { setField = OFF_ON_VALUE; fieldValue = v; }
  // ...
}

class TimesValue {
  String startTime;
  String endTime;
}

With the java classes the following are equivalent:

Value.noYesValue(false);
new Value().setNoYesValue(false);
new Value().setFieldValue(NO_YES_VALUE, false);
new Value(NO_YES_VALUE, false);

Similarly the following are equivalent:

value.getNoYesValue();
(Boolean) value.getFieldValue();

Partially working code (does what I want but only with simple types)

After some more digging and trial and error I've managed to get farther than I have before.

First things first, I flatten out the Value into the Item

interface ItemMixin {
  @JsonUnwrapped Value getValue();
}

This hoists all my Value properties so they become part of the Item class, the equivalent of {"value": {"type": "offon", "value": false}} becoming {"type": "offon", "value": false}

Next I need to deal with that Fields enum, to do this I wrote a custom Deserializer which looked like this and register it with a module

new StdDeserializer<Value.Fields>(Value.Fields.class) {
  @Override
  public Value.Fields deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
    String type = p.getText();
    switch (type) {
      case "noyes": return Value.Fields.NO_YES_VALUE;
      case "offon": return Value.Fields.OFF_ON_VALUE;
      case "float": return Value.Fields.FLOAT_VALUE;
      case "text": return Value.Fields.TEXT_VALUE;
      case "times": return Value.Fields.TIMES_VALUE;
      // ...
    }
    ctxt.handleWeirdStringValue(Value.Fields.class, type, "Unsupported Value type");
    return null;
  }
}

Finally I tell the Value type to use the constructor to create the object

static abstract class ValueMixin {
  @JsonCreator
  ValueMixin(@JsonProperty("type") Value.Fields type, @JsonProperty("value") Object value) {}
}

This gives me 90% of what I need, however the issue I now have is that this only works for primitive types (boolean, String, etc), my times field just gets deserialized as a String causing exceptions in my code.

I've tried using @JsonTypeInfo and @JsonSubTypes on the value parameter, creating a property for that field and putting the annotations there but can't get the creator to correctly resolve the type needed.

caeus
  • 3,084
  • 1
  • 22
  • 36
Matt
  • 722
  • 10
  • 20
  • a similiar question exists: https://stackoverflow.com/questions/20143114/jackson-deserialization-of-type-with-different-objects. Maybe similiar approach will help you. – piradian Jul 24 '18 at 13:15
  • @piradian that is similar, the reason I can't do it that way is due to 3) in my list above. Only the `type` field can tell me how to deserialise the `value` field, and that requires both at the same time. – Matt Jul 24 '18 at 13:55
  • I Don't undestand why can't you do it as it was proposed in mentione question. Use "untyped" mapping first. E.g. class RawItem{ String type; Object value; ...} and then use Factory to get Item instance based on type. – piradian Jul 24 '18 at 14:43
  • A couple of reasons. 1. The types Result, Item, Value, etc are all generated classes from a thrift idl so I can't really modify them to change, for example, the Result.obj list to be of type RawItem instead of Item. 2. If I were to continue using these classes as-is and create a companion set of types where raw values are used where they're needed then I'd just have to implement all the mapping by hand anyway, not only for the problematic Value type but all the other properties in there too. At this point I may as well just parse the whole thing as a tree and do it by hand from the start. – Matt Jul 24 '18 at 14:58

0 Answers0