1

I am struggling while I am trying to read a response from a webservice (a Page object from Spring Data) with a deserializer.

I would like to be able to retrieve my response, parsing it to a Page object, and defining the type of the content part. This is my deserializer

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.type.CollectionType;

public class PageJsonDeserializer extends StdDeserializer<Page<?>> implements ContextualDeserializer {

    public PageJsonDeserializer() {
        this(Page.class);
    }

    protected PageJsonDeserializer(final Class<?> vc) {
        super(vc);
    }

    private static final String CONTENT = "content";
    private static final String NUMBER = "number";
    private static final String SIZE = "size";
    private static final String TOTAL_ELEMENTS = "totalElements";
    private JavaType valueType;

    @Override
    public Page<?> deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException {
        final CollectionType valuesListType = ctxt.getTypeFactory().constructCollectionType(List.class, getValueType());

        List<?> list = new ArrayList();
        int pageNumber = 0;
        int pageSize = 0;
        long total = 0;
        if (p.isExpectedStartObjectToken()) {
            p.nextToken();
            if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
                String propName = p.getCurrentName();
                do {
                    p.nextToken();
                    switch (propName) {
                    case CONTENT:
                        list = ctxt.readValue(p, valuesListType);
                        break;
                    case NUMBER:
                        pageNumber = ctxt.readValue(p, Integer.class);
                        break;
                    case SIZE:
                        pageSize = ctxt.readValue(p, Integer.class);
                        break;
                    case TOTAL_ELEMENTS:
                        total = ctxt.readValue(p, Long.class);
                        break;
                    default:
                        p.skipChildren();
                        break;
                    }
                } while (((propName = p.nextFieldName())) != null);
            } else {
                ctxt.handleUnexpectedToken(handledType(), p);
            }
        } else {
            ctxt.handleUnexpectedToken(handledType(), p);
        }

        // Note that Sort field of Page is ignored here.
        // Feel free to add more switch cases above to deserialize it as well.
        return new PageImpl<>(list, PageRequest.of(pageNumber, pageSize), total);
    }

    /**
     * This is the main point here.
     * The PageDeserializer is created for each specific deserialization with concrete generic parameter type of Page.
     */
    @Override
    public JsonDeserializer<?> createContextual(final DeserializationContext ctxt, final BeanProperty property) {
        // This is the Page actually
        final JavaType wrapperType = ctxt.getContextualType();
        final PageJsonDeserializer deserializer = new PageJsonDeserializer();
        // This is the parameter of Page
        deserializer.valueType = wrapperType.containedType(0);
        return deserializer;
    }
}

And this is how i try to read my entity

public Page<EtudeDto> findEtudeWithFilterCriteria(final String nomEtude, final Pageable page) {
        try {
            final var response = etudeWebService.findEtudeWithFilterCriteria(nomEtude, page);
            if (Response.Status.Family.SUCCESSFUL == response.getStatusInfo().getFamily()) {
                return response.readEntity(Page.class);
            }
            log.error("Erreur lors de l'appel au webclient findEtudeWithFilterCriteria nom {}", nomEtude);
        } catch (final Exception e) {
            log.error("Erreur lors de l'appel au webclient findEtudeWithFilterCriteria nom {}", nomEtude, e);
        }
        return new PageImpl<>(List.of());
    }

Right now, my main problem is in createContextual method.deserializer.valueType = wrapperType.containedType(0); return null, and I don't know how to tell him which kind of object is in Content. And without it, it can't build my Page object. The specific thing is I don't want to specify the value Type manually in the deserializer, because sometimes the Page content is not a "EtudeDto" object, but an "EtudeOptique". I would like to do it in a generic way.

This is how my json response looks like on postman

{
    "content": [
        {
            "id": 1,
            ...
            "dateMiseAJour": "2022-07-06T00:00:00"
        }, 
        ...
    ],
    "pageable": {
        "sort": {
            "empty": false,
            "sorted": true,
            "unsorted": false
        },
        "offset": 0,
        "pageSize": 10,
        "pageNumber": 0,
        "unpaged": false,
        "paged": true
    },
    "last": false,
    "totalElements": 21,
    "totalPages": 3,
    "number": 0,
    "size": 10,
    "sort": {
        "empty": false,
        "sorted": true,
        "unsorted": false
    },
    "first": true,
    "numberOfElements": 10,
    "empty": false
}

Do you have any idea how can I resolve this ? Thanks !

Robyn.D
  • 339
  • 2
  • 20
  • Let me propose easier approach for deserializing spring page. Instead of writing custom deserializer, make classes extending spring's `PageImpl`, `PageRequest`, etc. and annotate constructors and parameters, jackson will take care of the rest. You can check my answer [here](https://stackoverflow.com/questions/71022383/how-to-link-a-vaadin-grid-with-the-result-of-spring-mono-webclient-data/71033265#71033265) for detailed explanation how to do it. – Chaosfire Jul 21 '22 at 08:15

1 Answers1

0

So, i figure it out following another response from a topic : Spring RestTemplate with paginated API

I removed my Page serializer and deserizalizer, and created a RestResponsePage object

import java.util.ArrayList;
import java.util.List;

import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;

@JsonIgnoreProperties(ignoreUnknown = true)
public class RestResponsePage<T> extends PageImpl<T> {

    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public RestResponsePage(@JsonProperty("content") final List<T> content,
                            @JsonProperty("number") final int number,
                            @JsonProperty("size") final int size,
                            @JsonProperty("totalElements") final Long totalElements,
                            @JsonProperty("pageable") final JsonNode pageable,
                            @JsonProperty("last") final boolean last,
                            @JsonProperty("totalPages") final int totalPages,
                            @JsonProperty("sort") final JsonNode sort,
                            @JsonProperty("first") final boolean first,
                            @JsonProperty("numberOfElements") final int numberOfElements) {

        super(content, PageRequest.of(number, size), totalElements);
    }

    public RestResponsePage(final List<T> content, final Pageable pageable, final long total) {
        super(content, pageable, total);
    }

    public RestResponsePage(final List<T> content) {
        super(content);
    }

    public RestResponsePage() {
        super(new ArrayList<>());
    }
}

And then, i've read my response as a RestResponsePage, and return a Page from my Webclient

public Page<EtudeDto> findEtudeWithFilterCriteria(final String nomEtude, final Pageable page) {
        try {
            final var response = etudeWebService.findEtudeWithFilterCriteria(nomEtude, page);
            if (Response.Status.Family.SUCCESSFUL == response.getStatusInfo().getFamily()) {
                  return response.readEntity(new GenericType<RestResponsePage<EtudeDto>>() {});
            }
            log.error("Erreur lors de l'appel au webclient findEtudeWithFilterCriteria nom {}", nomEtude);
        } catch (final Exception e) {
            log.error("Erreur lors de l'appel au webclient findEtudeWithFilterCriteria nom {}", nomEtude, e);
        }
        return new PageImpl<>(List.of());
    }
Robyn.D
  • 339
  • 2
  • 20