I am using Jackson to deserialize a number of different implementations of the Product
interface. These product implementations have different fields, but all have an InsuredAmount
field. This InsuredAmount
class has a value field and an IAType
field. The IAType
is a marker interface with different enums as implementations.
Now here's the problem: The enum implementations of the IAType
interface correspond to a certain implementation of the Product
interface. How can I make a generic implementation and tell Jackson to find the correct implementation of thee IAType? Should I use a generic parameter on the Product and the IAType interface identifying the product implementation? Should I use a Productable
functional interface on the classes identifying the product implementation? How can I tell Jackson to use that implementation?
I hope the code below clarifies the problem, I chose to implement a Productable
interface here, but a bettere structure to handle this problem would also be welcome.
@JsonPropertyOrder({"type", "someInfo"})
public class InsuredAmount implements Productable, Serializable {
private static final long serialVersionUID = 1L;
private IAType type;
private String someInfo;
public InsuredAmount() {
}
public InsuredAmount(IAType typeA, String someInfo) {
this.type = typeA;
this.someInfo = someInfo;
}
/* This should be on the product level, but if I can solve this problem,
the next level will just be more of the same.
*/
@JsonIgnore
@Override
public Product getProduct() {
return Product.PROD_A;
}
// Getters, setters, equals, etc. omitted.
}
--
public interface Productable {
public Product getProduct();
}
--
public enum Product {
PROD_A, PROD_B;
}
--
@JsonDeserialize(using = IATypeDeserializer.class)
public interface IAType extends Productable {
}
--
public enum IATypeA implements IAType {
FOO, BAR;
@Override
public Product getProduct() {
return Product.PROD_A;
}
}
--
public class IATypeDeserializer extends StdDeserializer<IAType> {
private static final long serialVersionUID = 1L;
public IATypeDeserializer() {
this(null);
}
public IATypeDeserializer(Class<?> vc) {
super(vc);
}
@Override
public IAType deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
JsonNode node = parser.getCodec().readTree(parser);
/* How to find out that the class calling the deserialization is InsuredAmountA, which
has getProduct() method that returns PROD_A, and matches the IATypeA that also returns
PROD_A, so I know to deserialize IATypeA, instead of other implementations of the IAType
interface?
*/
return IATypeA.valueOf(node.asText());
}
}
--
public class InsuredAmountTest {
private final ObjectMapper mapper = new ObjectMapper();
@Test
public void test01() throws IOException {
InsuredAmount iaA = new InsuredAmount(IATypeA.FOO, "test it");
String json = mapper.writeValueAsString(iaA);
assertThat(json, is("{\"type\":\"FOO\",\"someInfo\":\"test it\"}"));
InsuredAmount iaA2 = mapper.readValue(json, InsuredAmount.class);
IAType type = iaA2.getType();
assertThat(type, is(IATypeA.FOO));
assertThat(type.getProduct(), is(Product.PROD_A));
assertThat(iaA, is(iaA2));
}
@Test
public void test02() throws IOException {
InsuredAmount iaA = new InsuredAmount(IATypeA.BAR, "test it");
String json = mapper.writeValueAsString(iaA);
assertThat(json, is("{\"type\":\"BAR\",\"someInfo\":\"test it\"}"));
InsuredAmount iaA2 = mapper.readValue(json, InsuredAmount.class);
assertThat(iaA, is(iaA2));
}
}
Jackson handles the serialization of enums with minimal fuss, so all you need to do is annotate the IAType
field with @JsonTypeInfo
:
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
private IAType type;
Then a test:
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(new InsuredAmount(IATypeA.FOO, "info"));
System.out.println(json);
InsuredAmount ia = mapper.readValue(json, InsuredAmount.class);
System.out.println("Type is: " + ia.getType());
}
results in the output:
{"type":[".IATypeA","FOO"],"someInfo":"info"}
Type is: FOO
To get a more compact representation you will have to use custom serialization. Assuming that there are no overlaps in your enum namespace, you can serialize the type field as the enum name.
The deserializer will need to know which types are available for construction, either by class path discovery or, as in the following example, simply hard-coding the references:
public class IATest {
public static class IATypeSerializer extends JsonSerializer<IAType> {
@Override
public void serialize(IAType value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(((Enum) value).name());
}
}
public static class IATypeDeserializer extends JsonDeserializer<IAType> {
@Override
public IAType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.readValueAs(String.class);
try {
return IATypeA.valueOf(value);
} catch (IllegalArgumentException e) {
// fall through
}
try {
return IATypeB.valueOf(value);
} catch (IllegalArgumentException e) {
// fall through
}
throw new JsonMappingException(p, "Unknown type '" + value + "'");
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
// Register a module to handle serialization of IAType implementations
SimpleModule module = new SimpleModule();
module.addSerializer(IAType.class, new IATypeSerializer());
module.addDeserializer(IAType.class, new IATypeDeserializer());
mapper.registerModule(module);
// Test
String json = mapper.writeValueAsString(new InsuredAmount(IATypeA.FOO, "info"));
System.out.println(json);
InsuredAmount ia = mapper.readValue(json, InsuredAmount.class);
System.out.println("Type is: " + ia.getType());
}
}
Which outputs:
{"type":"FOO","someInfo":"info"}
Type is: FOO
I ended up with using JsonCreator
annotation on a special constructor.
@JsonCreator
public InsuredAmountA(
@JsonProperty("type") String type,
@JsonProperty("someInfo") String someInfo) throws IOException {
switch (getProduct()) {
case PROD_A:
try {
this.type = IATypeA.valueOf(type);
break;
} catch (IllegalArgumentException ex) {
// Throw IOException in the default.
}
// case PROD_B:
// this.type = (IATypeB) typeA;
// break;
default:
throw new IOException(String.format("Cannot parse value %s as type.", type));
}
this.someInfo = someInfo;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With