Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How deserialize based on information available in the parent class

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));
    }

}
like image 246
Martijn Burger Avatar asked Feb 03 '17 11:02

Martijn Burger


2 Answers

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 
like image 104
teppic Avatar answered Sep 25 '22 17:09

teppic


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;
    }
like image 29
Martijn Burger Avatar answered Sep 21 '22 17:09

Martijn Burger