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