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:
- 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.
- The value of the union can take any number of types, some scalar and some object or collection.
- Multiple options for the union can share the same type, i.e.
noyes
andoffon
are bothboolean
types in the example below. - 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.
- 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. - 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.