Дженерик и проблема с рекурсией и десериализацией

Рейтинг: 0Ответов: 1Опубликовано: 19.08.2023

У меня есть две DTO

public class PersonDto {
    private Integer id;
    private String name;
    List<PersonDto> childs;
}

и

public class PersonFullDto extends PersonDto {
    private String parentPhone;
}

Хочу что бы когда в метод поступает строка содержащая json, то можно было проверить есть ли там значения parentPhone?

Если есть, то возвращать PersonFullDto иначе PersonDto используя для чтения com.fasterxml.jackson.databind.ObjectMapper Что-то вроде этого:

T result;
if (json.contains("parentPhone")){
    result = objectMapper.readValue(json, PersonFullDto.class);
    log.info(result.toString());
} else {
    result = objectMapper.readValue(json, PersonDto.class);
    log.info(result.toString());
}

Пробовал использовать Bounded Type Parameter,

public class PersonDto<T extends PersonDto> {
    private Integer id;
    private String name;
    List<T> childs;
}

Но из-за наличия поля childs типа PersonDto или PersonFullDto возникала рекурсия, проблемы с которой я не смог решить

А если сделать вот так

public class PersonDto {
    private Integer id;
    private String name;
    List<? extends PersonDto> childs;
}

то ObjectMapper ломается и даже как написать Custom Deserializer для такого случая не нашёл

Вот полный код для теста: https://github.com/AlexeyOs/GenericProblem

Ответы

▲ 3Принят

Основная задача здесь - сделать правильный десериализатор, ключевые моменты прокомментированы в коде:

public class PersonDeserializer extends JsonDeserializer<PersonDto> {
    public static final String PARENT_PHONE_NODE_NAME = "parentPhone";
    public static final String ID_NODE_NAME = "id";
    public static final String NAME_NODE_NAME = "name";
    public static final String CHILDS_NODE_NAME = "childs";

    @Override
    public PersonDto deserialize(@NotNull JsonParser jsonParser, DeserializationContext ctx) throws IOException {
        JsonNode node = jsonParser.readValueAsTree();

        PersonDto result;
        // разбираемся: если есть поля от "дочернего" объекта - создадим его, иначе - создадим родительский.
        if (node.hasNonNull(PARENT_PHONE_NODE_NAME)) {
            result = new PersonFullDto();
            // и заполним здесь поля, которые не являются унаследованными
            ((PersonFullDto) result).setParentPhone(node.get(PARENT_PHONE_NODE_NAME).asText());
        } else {
            result = new PersonDto();
        }

        // Здесь заполняем все "общие" поля, которые присутствуют в базовом классе.
        result.setId(node.get(ID_NODE_NAME).asInt());
        result.setName(node.get(NAME_NODE_NAME).asText());

        // напоследок - самое интересное
        // заполняем список вложенных элементов, рекурсивно вызывая этот же десериализатор.
        JsonNode childNodes = node.get(CHILDS_NODE_NAME);
        if (Objects.nonNull(childNodes))
            if (childNodes.isArray())
                result.setChilds(
                        StreamSupport.stream(childNodes.spliterator(), false)
                                .map(jsonNode -> {
                                    try {
                                        return ctx.readTreeAsValue(jsonNode, PersonDto.class);
                                    } catch (IOException e) {
                                        throw new RuntimeException(e);
                                    }
                                })
                                .toList()
                );

        return result;
    }

Теперь остается только пометить базовую dto аннотацией десериализатора:

@Getter
@Setter
@JsonDeserialize(using=PersonDeserializer.class)
public class PersonDto {
    private Integer id;
    private String name;
    List<PersonDto> childs;
}

И можно вызывать слегка измененный (для облегчения работы юнит-тестам) метод сервиса:

public PersonDto jsonParser(String json) throws JsonProcessingException {
        return objectMapper.readValue(json, PersonDto.class);
   }

Собственно, сам тест для проверки (поленился - воспользовался уже созданным классом и скопировал в него json-ы из контроллера):

    @Test
    void contextLoads() throws JsonProcessingException {
        PersonDto value = service.jsonParser(personDto);
        assertInstanceOf(PersonDto.class, value);
        assertFalse(value instanceof PersonFullDto);

        value = service.jsonParser(personFullDto);
        assertInstanceOf(PersonFullDto.class, value);
    }