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