10

I need to call an endpoint which expects a Pageable field:

@GetMapping
public Page<ProductDTO> listProducts(Pageable pageable) {
    return productService.findProducts(pageable); 
}

In my test I have this code:

MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add("page", String.valueOf(0));
URI url = defaultURI(port, "/products", parameters);

ParameterizedTypeReference<RestResponsePage<ProductDTO>> type = new ParameterizedTypeReference<RestResponsePage<ProductDTO>>() {};
ResponseEntity<RestResponsePage<ProductDTO>> response = restTemplate.exchange(url.toString(), HttpMethod.GET, httpEntity, type);

PageImpl contains no default constructor so to avoid that problem I created a class like the following one to pass to the ParameterizedTypeReference:

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

    private static final long serialVersionUID = 3844794233375694591L;

    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public RestResponsePage(@JsonProperty("content") List<T> content,
                        @JsonProperty("number") int page,
                        @JsonProperty("size") int size,
                        @JsonProperty("totalElements") long totalElements) {
        super(content, new PageRequest(page, size), totalElements);
    }

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

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

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

The problem is that I still get the following error:

Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of org.springframework.data.domain.Pageable: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: java.io.PushbackInputStream@75564689; line: 38, column: 16] (through reference chain: com.shaunyl.util.ResponsePageImpl["pageable"])

Why it keeps saying that I am passing an abstract class? ResponsePageImpl is a class not an abstract class.

Thank you

Shaunyl
  • 527
  • 2
  • 11
  • 24
  • One problem may be that your RestResponsePage class is an abstract type. If you look at https://stackoverflow.com/a/44895867/3362244, you'll notice that the HelperPage class is not generically typed. Instead, it is a concrete implementation of a generically typed abstract class, which Jackson can deserialize. This is one of the key reasons why it works. – teuber789 Jun 14 '18 at 20:45
  • @jtcotton63 no dice, I got the same error. – Shaunyl Jun 16 '18 at 09:02
  • Have you tried removing `implements Serializable` from class declaration? – masterfly Aug 07 '18 at 05:50
  • @Venky For me that din't work. – Lokesh Aug 27 '18 at 09:27

2 Answers2

16
package com.td.support;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

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

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

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

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

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

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

spring boot 2.0 may be ok above..

steven
  • 377
  • 3
  • 14
  • 1
    For anyone who needs more context, the constructor needs to have all these parameters even though they're not used. I had this issue when upgrading from spring boot 1.5.x to 2.x – Younes El Ouarti Jul 22 '20 at 08:13
  • I am facing exact same issue with my test class and my spring-boot version is 2.3.2.RELEASE, any one solved this issue or any clues? – mramsath Feb 17 '21 at 12:15
  • @YounesElOuarti I hear that you can leave out unknown/unineresting properties by using `@JsonIgnoreProperties(ignoreUnknown = true)` on the class. – toolforger Aug 18 '22 at 12:22
1

i have found a more elegant way to resovle this problem. It's a problem of jackson serializing ,in fact.

At first, define a module of jackson:

public class PageJacksonModule extends Module {

    @Override
    public String getModuleName() {
        return "PageJacksonModule";
    }

    @Override
    public Version version() {
        return new Version(0,1,0, "", null,null);
    }

    @Override
    public void setupModule(SetupContext context) {
        context.setMixInAnnotations(Page.class, PageMixIn.class);
    }

    @JsonDeserialize(as = SimplePageImpl.class)
    private interface PageMixIn{ }


    static class SimplePageImpl<T> implements Page<T> {

        private final Page<T> delegate;

        public SimplePageImpl(
                @JsonProperty("content") List<T> content,
                @JsonProperty("page")int number,
                @JsonProperty("size") int size,
                @JsonProperty("totalElements") long totalElements){
            delegate = new PageImpl<>(content, PageRequest.of(number, size), totalElements);
        }


        @JsonProperty
        @Override
        public int getTotalPages() {
            return delegate.getTotalPages();
        }

        @JsonProperty
        @Override
        public long getTotalElements() {
            return delegate.getTotalElements();
        }

        @JsonProperty("page")
        @Override
        public int getNumber() {
            return delegate.getNumber();
        }

        @JsonProperty
        @Override
        public int getSize() {
            return delegate.getSize();
        }

        @JsonProperty
        @Override
        public int getNumberOfElements() {
            return delegate.getNumberOfElements();
        }

        @JsonProperty
        @Override
        public List<T> getContent() {
            return delegate.getContent();
        }

        @JsonProperty
        @Override
        public boolean hasContent() {
            return delegate.hasContent();
        }

        @JsonIgnore
        @Override
        public Sort getSort() {
            return delegate.getSort();
        }

        @JsonProperty
        @Override
        public boolean isFirst() {
            return delegate.isFirst();
        }

        @JsonProperty
        @Override
        public boolean isLast() {
            return delegate.isLast();
        }

        @JsonIgnore
        @Override
        public boolean hasNext() {
            return delegate.hasNext();
        }

        @JsonIgnore
        @Override
        public boolean hasPrevious() {
            return delegate.hasPrevious();
        }

        @JsonIgnore
        @Override
        public Pageable nextPageable() {
            return delegate.nextPageable();
        }
        
        @JsonIgnore
        @Override
        public Pageable previousPageable() {
            return delegate.previousPageable();
        }
        
        @JsonIgnore
        @Override
        public <U> Page<U> map(Function<? super T, ? extends U> converter) {
            return delegate.map(converter);
        }

        @JsonIgnore
        @Override
        public Iterator<T> iterator() {
            return delegate.iterator();
        }
    }
}

then inject it:

@Configuration
public class JacksonConfig {

    @Bean
    public Module pageJacksonModule() {
        return new PageJacksonModule();
    }
}

finally, you can use Page object.

test, of course:

@RunWith(SpringJUnit4ClassRunner.class)
@JsonTest
public class PageImplTest {

        @Autowired
        ObjectMapper mapper;

        @Test
        public  void page() throws IOException {
                String inputPageStr = "{\"content\":[{\"name\":\"n1\",\"gender\":\"boy\",\"age\":23},{\"name\":\"n2\",\"gender\":\"girl\",\"age\":20}],\"pageable\":\"INSTANCE\",\"totalPages\":1,\"last\":true,\"totalElements\":2,\"sort\":{\"sorted\":false,\"unsorted\":true,\"empty\":true},\"first\":true,\"numberOfElements\":2,\"size\":2,\"number\":0,\"empty\":false}";
                Page<PageItem> pageItems = mapper
                        .readValue(inputPageStr, new TypeReference<Page<PageItem>>() {});
                Assert.assertNotNull(pageItems);
        }

        static class PageItem {

                private String name;
                private String gender;
                private int age;

                public PageItem() {
                }

                public PageItem(String name, String gender, int age) {
                        this.name = name;
                        this.gender = gender;
                        this.age = age;
                }

                public String getName() {
                        return name;
                }

                public void setName(String name) {
                        this.name = name;
                }

                public String getGender() {
                        return gender;
                }

                public void setGender(String gender) {
                        this.gender = gender;
                }

                public int getAge() {
                        return age;
                }

                public void setAge(int age) {
                        this.age = age;
                }
        }

        @Configuration
        static class TestConfig{
                @Bean
                public Module pageJacksonModule() {
                        return new PageJacksonModule();
                }
        }
}

steven
  • 377
  • 3
  • 14
  • Very clean ,thanks ! This way it is possible to still use the Page interface in the feign class, Jakson will automatically use the SimplePageImpl for deserialization. I just needed to change "new PageImpl<>(content, PageRequest.of(number, size), totalElements);" to "new PageImpl<>(content == null ? List.of() : content, PageRequest.of(number, size), totalElements);" to manage the "Content is null" error when the returned page is empty. It seems that Feign (spring-cloud-openfeign-core-3.1.0) and Page (spring-data-commons-2.6.4) do not work well together. – Laurent Aug 26 '22 at 08:41
  • And also for injecting the bean the solution you propose do not work for me but the following work : @Bean public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.findAndRegisterModules(); objectMapper.registerModule(new PageJacksonModule()); return objectMapper; } – Laurent Aug 26 '22 at 12:20