javaresteasyquarkusjsonb-apiyasson

Deserialize JSON into polymorphic POJO with JSON-B / Yasson


I have a PATCH endpoint in a resource class with an abstract class as a request body. I'm getting the following error:

22:59:30 SEVERE [or.ec.ya.in.Unmarshaller] (on Line: 64) (executor-thread-63) Can't create instance

It seems that because the body model that i'm declaring as an argument is abstract so it fail to deserialize.

I'm expecting to get either Element_A or Element_B

How can I declare the body to be polymorphic?

This is the elements hierarchy

public abstract class BaseElement {
    public String name;
    public Timestamp start;
    public Timestamp end;
}

public class Element_A extends BaseElement{
    public String A_data;
}

public class Element_B extends BaseElement{
    public long B_data;
}

this is the resource class with my endpoint

@Path("/myEndpoint")
public class ResourceClass {

    @PATCH
    @Path("/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.TEXT_PLAIN)
    public Response updateEvent(@Valid BaseElement element, @Context UriInfo uriInfo, @PathParam long id) {
        if (element instanceof Element_A) {
            // Element_A logic
        } else if (element instanceof Element_B) {
            // Element_B logic
        }
        return Response.status(Response.Status.OK).entity("working").type(MediaType.TEXT_PLAIN).build();
    }
}

This is the request body that I'm sending in a PATCH request

{
  "name": "test",
  "startTime": "2020-02-05T17:50:55",
  "endTime": "2020-02-05T17:51:55",
  "A_data": "it's my data"
}

I've also tried to add @JsonbTypeDeserializer with a custom deserializer which didn't work

@JsonbTypeDeserializer(CustomDeserialize.class)
public abstract class BaseElement {
    public String type;
    public String name;
    public Timestamp start;
    public Timestamp end;
}
public class CustomDeserialize implements JsonbDeserializer<BaseElement> {

    @Override
    public BaseElement deserialize(JsonParser parser, DeserializationContext context, Type rtType) {
        JsonObject jsonObj = parser.getObject();
        String type = jsonObj.getString("type");

        switch (type) {
            case "A":
                return context.deserialize(Element_A.class, parser);
            case "B":
                return context.deserialize(Element_B.class, parser);
        }

        return null;
    }
}

This is the new request I sent:

{
  "type": "A"
  "name": "test",
  "startTime": "2020-02-05T17:50:55",
  "endTime": "2020-02-05T17:51:55",
  "A_data": "it's my data"
}

Throws this error:

02:33:10 SEVERE [or.ec.ya.in.Unmarshaller] (executor-thread-67) null
02:33:10 SEVERE [or.ec.ya.in.Unmarshaller] (executor-thread-67) Internal error: null

My pom.xml include:

<dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>

Solution

  • JSON-B doesn't have ideal support for polymorphic deserialization yet, so at best you can do a sort of workaround for now. We have a design issue open for it here: https://github.com/eclipse-ee4j/jsonb-api/issues/147, so please vote for it by giving it a +1!

    You have the right idea with a custom deserializer, but the issue is that you cannot parse the current JsonObject and then later call context.deserialize() with the same parser, because the parser has already advanced past the state where it read the JSON object. (The JSONParser is a forward-only parser so there isn't a way to "rewind" it)

    So, you can still call parser.getObject() in your custom deserializer, but then you need to use a separate Jsonb instance to parse that particular JSON string once you have determined its concrete type.

    public static class CustomDeserialize implements JsonbDeserializer<BaseElement> {
    
      private static final Jsonb jsonb = JsonbBuilder.create();
    
      @Override
      public BaseElement deserialize(JsonParser parser, DeserializationContext context, Type rtType) {
    
          JsonObject jsonObj = parser.getObject();
          String jsonString = jsonObj.toString();
          String type = jsonObj.getString("type");
    
          switch (type) {
            case "A":
              return jsonb.fromJson(jsonString, Element_A.class);
            case "B":
              return jsonb.fromJson(jsonString, Element_B.class);
            default:
              throw new JsonbException("Unknown type: " + type);
          }
      }
    }
    

    Side notes: You need to change the base object model to the following:

      @JsonbTypeDeserializer(CustomDeserialize.class)
      public abstract static class BaseElement {
        public String type;
        public String name;
        @JsonbProperty("startTime")
        public LocalDateTime start;
        @JsonbProperty("endTime")
        public LocalDateTime end;
      }
    
    1. Assuming you can't or don't want to change your JSON schema, you can either change your java field names (start and end) to match the JSON field names (startTime and endTime) OR you can use the @JsonbProperty annotation to remap them (which I have done above)
    2. Since your timestamps are in the ISO_LOCAL_DATE_TIME format, I would suggest using java.time.LocalDateTime instead of java.sql.Timestamp because LocalDateTime handles time zones, but ultimately either way will work given the sample data you provided