Десериализовать JSON в полиморфный POJO с помощью JSON-B / Yasson

У меня есть конечная точка PATCH в классе ресурсов с абстрактным классом в качестве тела запроса. Я получаю следующую ошибку:

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

Похоже, что из-за того, что модель тела, которую я объявляю в качестве аргумента, является абстрактной, она не может десериализоваться.

Я ожидаю получить Element_A или Element_B

Как объявить тело полиморфным?

Это иерархия элементов

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

это класс ресурсов с моей конечной точкой

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

Это тело запроса, которое я отправляю в запросе PATCH.

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

Я также пытался добавить @JsonbTypeDeserializer с настраиваемым десериализатором, который не работал

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

Это новый запрос, который я отправил:

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

Выдает эту ошибку:

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

Мой pom.xml включает:

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

1 ответ

Решение

JSON-B пока не имеет идеальной поддержки полиморфной десериализации, поэтому в лучшем случае вы можете найти своего рода обходной путь. Здесь у нас есть проблема с дизайном: https://github.com/eclipse-ee4j/jsonb-api/issues/147, поэтому, пожалуйста, проголосуйте за него, поставив+1!

У вас есть правильная идея с настраиваемым десериализатором, но проблема в том, что вы не можете проанализировать текущий JsonObject а потом позвоните позже context.deserialize()с тем же синтаксическим анализатором, потому что синтаксический анализатор уже прошел состояние, в котором он прочитал объект JSON. (JSONParser - это синтаксический анализатор, работающий только в прямом направлении, поэтому нет возможности "перемотать" его назад)

Итак, вы все еще можете позвонить parser.getObject() в вашем пользовательском десериализаторе, но тогда вам нужно использовать отдельный Jsonb экземпляр, чтобы проанализировать эту конкретную строку JSON после определения ее конкретного типа.

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

Боковые примечания: вам необходимо изменить базовую объектную модель на следующее:

  @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. Предполагая, что вы не можете или не хотите изменять схему JSON, вы можете либо изменить имена полей Java (start а также end) для соответствия именам полей JSON (startTime а также endTime) ИЛИ вы можете использовать @JsonbProperty аннотация, чтобы переназначить их (что я сделал выше)
  2. Поскольку ваши временные метки находятся в формате ISO_LOCAL_DATE_TIME, я бы предложил использовать java.time.LocalDateTime вместо того java.sql.Timestamp потому как LocalDateTime обрабатывает часовые пояса, но в конечном итоге будет работать в любом случае, учитывая предоставленные вами образцы данных
Другие вопросы по тегам