Thomas Betous Thomas Betous - 6 months ago 93
Java Question

How to deserialize an object containing a generic type with a builder Jackson?

I'm currently developing a small web application with dropwizard. Another application can send several types of message to my application. These messages have a common base and a specific content depending of the type of message.

Here is the common base of messages:

@JsonDeserialize(builder = BaseMessageBuilder.class)
public abstract class BaseMessage<T> {
private String commonFiled;
private T content;

protected BaseMessage(BaseMessageBuilder<T> builder) {
this.commonFiled = builder.getCommonField();
this.content = builder.getContent();
}

public String getCommonField() {
return commonFiled;
}

public T getContent() {
return content;
}
}


Here is the builder of the common base of messages:

@JsonPOJOBuilder
public class BaseMessageBuilder<T> {
private String commonFiled;
private T content;

public String getCommonField() {
return commonFiled;
}

public BaseMessageBuilder<T> withCommonField(String commonFiled) {
this.commonFiled = commonFiled;
return this;
}

public String getContent {
return participants;
}

public BaseMessageBuilder<T> withContent(T content) {
this.content = content;
return this;
}

public BaseMessage<T> build() {
return new BaseMessage<T>(this);
}
}


Here is a specific content:

@JsonDeserialize(builder = SpecificContentBuilder.class)
public abstract class SpecificContent{
private String field1;
private Long field2;

protected SpecificContent(SpecificMessageBuilder builder) {
this.field1 = builder.getField1();
this.field2 = builder.getField2();
}

public String getField1() {
return field1;
}

public Long getField2() {
return field2;
}
}


Here is the builder of a specific content:

@JsonPOJOBuilder
public class SpecificContentBuilder {
private String field1;
private Long field2;

public String getField1() {
return commonFiled;
}

public SpecificContentBuilder withField1(String field1) {
this.field1 = field1;
return this;
}

public String getField2 {
return field2;
}

public SpecificContentBuilder withField2(Long field2) {
this.field2 = field2;
return this;
}

public BaseMessage build() {
return new BaseMessage<T>(this);
}
}


And here is my resource:

@Path("/test")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class TestResource {

@POST
public void test(BaseMessage<SpecificContent> message) {
// ...
}

}


Currently when my resource get the object, the content message don't actually get the right type. When I debug it, it is automatically a
LinkedHashMap
. I guess there is something wrong. Can anyone help me ?

EDIT :

It is a Jackson issue (Look at pandaadb response). I have deleted my BaseMessageBuilder and I have used a static constructor in order to keep my object immutable.

public abstract class BaseMessage<T> {
private String commonFiled;
private T content;

@JsonCreator
public static <T> BaseMessage<T> newInstance(
@JsonProperty("commonFiled") String commonFiled,
@JsonProperty("content") T content) {
return new WebhookEventBuilder()
.withCommonFied(commonFiled)
.withContent(content)
.build();
}

protected BaseMessage(BaseMessageBuilder<T> builder) {
this.commonFiled = builder.getCommonField();
this.content = builder.getContent();
}

public String getCommonField() {
return commonFiled;
}

public T getContent() {
return content;
}

private static class BaseMessageBuilder<T> {
private String commonFiled;
private T content;

public WebhookEventType getCommonFiled() {
return commonFiled;
}

public BaseMessageBuilder withCommonFiled(String commonFiled) {
this.commonFiled = commonFiled;
return this;
}

public T getContent() {
return content;
}

public BaseMessageBuilder withContent(T content) {
this.content = content;
return this;
}

public BaseMessage build() {
return new BaseMessage(this);
}
}
}

Answer

tried this, this is currently not supported it seems:

https://github.com/FasterXML/jackson-databind/issues/921

However, for your example, this is unnecessary. Jackson does the right thing by default, for example, this code example works for me:

I have 1 BaseMessage:

public class BaseMessage<T> {

    @JsonProperty("val1")
    String val1;
    @JsonProperty("val2")
    T val2;
}

And 2 different Content types:

public class Content {

    @JsonProperty("val1")
    String val1;
    @JsonProperty("val2")
    long val2;

}

public class Content2 {

    @JsonProperty("val1")
    String val1;
    @JsonProperty("val2")
    String val2;

}

With this resource:

@Path("/builder")
@Produces(MediaType.APPLICATION_JSON)
public class BuilderResource {


    @POST
    @Path("/test")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response test(BaseMessage<Content> testContent) {

        System.out.println("hit normal content");
        return Response.ok().build();
    }


    @POST
    @Path("/test2")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response test2(BaseMessage<Content2> testContent) {

        System.out.println("hit String content");
        return Response.ok().build();
    }
}

Hitting the test urls, jackson successfully detects the type T of the base content and creates the correct instance.

The other alternative would be to write your own MessageBodyReader for the BaseMessage type

I hope that helps,

Artur