From 351ecc2745e4fb2116adc7dbda2e3e343294a138 Mon Sep 17 00:00:00 2001 From: Lucas Lustosa Madureira Date: Tue, 23 Jun 2026 20:53:29 -0300 Subject: [PATCH 1/5] Adding support for PageableDefault Adding unit test Update Readme solves #4 --- README.md | 3 +- .../resolver/JsonApiPageArgumentResolver.java | 26 ++-- .../simple/response/PageDefaultsTest.java | 114 ++++++++++++++++++ .../jsonapi/simple/response/PageSortTest.java | 6 +- .../jsonapi/simple/response/PageTest.java | 29 +++-- 5 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 src/test/java/com/slmdev/jsonapi/simple/response/PageDefaultsTest.java diff --git a/README.md b/README.md index 4d17ea4..15598c0 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,8 @@ And for the page size are: - **page[limit]** #### Request page always starts from 0 for compatible with spring repositories and etc.! -#### If request page number < 1 resolver always return number = 0 and if size < 1 it always returns default value = 25! +#### If request page number < 1 resolver always return number = 0 and if size < 1 it always returns default value = 10! +#### Those default values can be overridden by annotating the method parameter with ```@PageableDefault``` ### Sorting diff --git a/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java b/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java index a8d6641..d8fdc44 100644 --- a/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java +++ b/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java @@ -5,6 +5,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -21,7 +23,10 @@ *

By default using {@code page} param name and key names in square brackets, * for example {@code page[number]=3&page[size]=15}. * - * After parsing will be created new spring {@link org.springframework.data.domain.Pageable} object. + * Optionally, if provided with a {@link org.springframework.data.web.PageableDefault}, it uses those + * values as the defaults when parsing the request parameters. + * + * After the parsing, a new spring {@link org.springframework.data.domain.Pageable} object will be created. * *

This resolver must be registered in Spring application. */ @@ -37,11 +42,12 @@ public Pageable resolveArgument(final MethodParameter methodParameter, final ModelAndViewContainer modelAndViewContainer, final NativeWebRequest nativeWebRequest, final WebDataBinderFactory webDataBinderFactory) { - final RequestJsonApiPage RequestJsonApiPage = methodParameter.getParameterAnnotation(RequestJsonApiPage.class); - final String pageKeyStart = RequestJsonApiPage.name() + REQUEST_PAGE_KEY_BRACKET_START; - final Sort sort = parseSortField(nativeWebRequest); - int page = 0; - int size = 25; + final RequestJsonApiPage requestJsonApiPage = methodParameter.getParameterAnnotation(RequestJsonApiPage.class); + final PageableDefault pageableDefault = methodParameter.getParameterAnnotation(PageableDefault.class); + final String pageKeyStart = requestJsonApiPage.name() + REQUEST_PAGE_KEY_BRACKET_START; + final Sort sort = parseSortField(nativeWebRequest, pageableDefault); + int page = pageableDefault == null ? 0 : pageableDefault.page(); + int size = pageableDefault == null ? 10 : pageableDefault.size(); final List> entries = nativeWebRequest.getParameterMap() .entrySet() @@ -101,10 +107,16 @@ private List valueToList(final String[] values) { return valueItems; } - private Sort parseSortField(final NativeWebRequest nativeWebRequest) { + private Sort parseSortField(final NativeWebRequest nativeWebRequest, @Nullable final PageableDefault pageableDefault) { if (CollectionUtils.isEmpty(nativeWebRequest.getParameterMap()) || nativeWebRequest.getParameterMap().get("sort") == null) { + + if (pageableDefault != null && pageableDefault.sort().length > 0) { + return Sort.by(pageableDefault.direction(), pageableDefault.sort()); + } + return null; + } final var sortOrders = Arrays.stream(nativeWebRequest.getParameterMap() .get("sort")) diff --git a/src/test/java/com/slmdev/jsonapi/simple/response/PageDefaultsTest.java b/src/test/java/com/slmdev/jsonapi/simple/response/PageDefaultsTest.java new file mode 100644 index 0000000..e670d5f --- /dev/null +++ b/src/test/java/com/slmdev/jsonapi/simple/response/PageDefaultsTest.java @@ -0,0 +1,114 @@ +package com.slmdev.jsonapi.simple.response; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashMap; +import java.util.List; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.context.request.NativeWebRequest; + +import com.slmdev.jsonapi.simple.annotation.RequestJsonApiPage; +import com.slmdev.jsonapi.simple.resolver.JsonApiPageArgumentResolver; + +public class PageDefaultsTest extends BaseTest { + private static final String REQUEST_PAGE_ARGUMENT_NAME = "page"; + + private JsonApiPageArgumentResolver jsonApiPageArgumentResolver; + + @Mock + private MethodParameter methodParameter; + @Mock + private RequestJsonApiPage requestJsonApiPage; + @Mock + private PageableDefault pageableDefault; + @Mock + private NativeWebRequest nativeWebRequest; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + + Mockito.when(methodParameter.getParameterAnnotation(RequestJsonApiPage.class)) + .thenReturn(requestJsonApiPage); + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(pageableDefault); + Mockito.when(requestJsonApiPage.name()) + .thenReturn(REQUEST_PAGE_ARGUMENT_NAME); + + jsonApiPageArgumentResolver = new JsonApiPageArgumentResolver(); + } + + @Test + public void shouldParsePageWithPage2AndSize10AndNoSortParams() { + Mockito.when(pageableDefault.size()) + .thenReturn(2); + Mockito.when(pageableDefault.page()) + .thenReturn(10); + Mockito.when(pageableDefault.sort()) + .thenReturn(new String[0]); + shouldParsePageWithSizeAndPageAndSort(); + } + + @Test + public void shouldParsePageWithPage2AndSize10AndAscSortParams() { + Mockito.when(pageableDefault.size()) + .thenReturn(2); + Mockito.when(pageableDefault.page()) + .thenReturn(10); + Mockito.when(pageableDefault.direction()) + .thenReturn(Direction.ASC); + Mockito.when(pageableDefault.sort()) + .thenReturn(new String[]{"foo", "bar"}); + shouldParsePageWithSizeAndPageAndSort(); + } + + @Test + public void shouldParsePageWithPage2AndSize10AndDescSortParams() { + Mockito.when(pageableDefault.size()) + .thenReturn(2); + Mockito.when(pageableDefault.page()) + .thenReturn(10); + Mockito.when(pageableDefault.direction()) + .thenReturn(Direction.DESC); + Mockito.when(pageableDefault.sort()) + .thenReturn(new String[]{"foo", "bar"}); + shouldParsePageWithSizeAndPageAndSort(); + } + + private void shouldParsePageWithSizeAndPageAndSort() { + + // No page parameters should be passed in order for pageableDefault to take effect + final var pageParams = new HashMap(); + + Mockito.when(nativeWebRequest.getParameterMap()) + .thenReturn(pageParams); + + final Pageable page = jsonApiPageArgumentResolver.resolveArgument(methodParameter, null, nativeWebRequest, null); + + assertEquals(page.getPageSize(), pageableDefault.size()); + assertEquals(page.getPageNumber(), pageableDefault.page()); + + List pageOrders = page.getSort().toList(); + assertEquals(pageOrders.size(), pageableDefault.sort().length); + assertAll("all orders match", IntStream.range(0, page.getSort().toList().size()).mapToObj(i -> + () -> { + assertEquals(pageOrders.get(i).getDirection(), pageableDefault.direction()); + assertEquals(pageOrders.get(i).getProperty(), pageableDefault.sort()[i]); + } + )); + + } + +} diff --git a/src/test/java/com/slmdev/jsonapi/simple/response/PageSortTest.java b/src/test/java/com/slmdev/jsonapi/simple/response/PageSortTest.java index 5e4ec66..910a6b2 100644 --- a/src/test/java/com/slmdev/jsonapi/simple/response/PageSortTest.java +++ b/src/test/java/com/slmdev/jsonapi/simple/response/PageSortTest.java @@ -9,6 +9,7 @@ import org.mockito.MockitoAnnotations; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.web.context.request.NativeWebRequest; import java.util.HashMap; @@ -18,7 +19,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; public class PageSortTest extends BaseTest { private static final String REQUEST_PAGE_ARGUMENT_NAME = "page"; @@ -43,8 +43,10 @@ public class PageSortTest extends BaseTest { public void setUp() { MockitoAnnotations.openMocks(this); - Mockito.when(methodParameter.getParameterAnnotation(any())) + Mockito.when(methodParameter.getParameterAnnotation(RequestJsonApiPage.class)) .thenReturn(requestJsonApiPage); + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(null); Mockito.when(requestJsonApiPage.name()) .thenReturn(REQUEST_PAGE_ARGUMENT_NAME); diff --git a/src/test/java/com/slmdev/jsonapi/simple/response/PageTest.java b/src/test/java/com/slmdev/jsonapi/simple/response/PageTest.java index 19e730f..ef3c6f9 100644 --- a/src/test/java/com/slmdev/jsonapi/simple/response/PageTest.java +++ b/src/test/java/com/slmdev/jsonapi/simple/response/PageTest.java @@ -1,7 +1,13 @@ package com.slmdev.jsonapi.simple.response; -import com.slmdev.jsonapi.simple.annotation.RequestJsonApiPage; -import com.slmdev.jsonapi.simple.resolver.JsonApiPageArgumentResolver; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.isNotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -10,27 +16,22 @@ import org.springframework.core.MethodParameter; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.web.context.request.NativeWebRequest; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isNotNull; +import com.slmdev.jsonapi.simple.annotation.RequestJsonApiPage; +import com.slmdev.jsonapi.simple.resolver.JsonApiPageArgumentResolver; public class PageTest extends BaseTest { private static final String REQUEST_PAGE_ARGUMENT_NAME = "page"; private static final String REQUEST_PAGE_NUMBER_KEY = "number"; private static final String REQUEST_PAGE_SIZE_KEY = "size"; private static final int DEFAULT_PAGE_NUMBER = 0; - private static final int DEFAULT_PAGE_SIZE = 25; + private static final int DEFAULT_PAGE_SIZE = 10; private static final int TEST_PAGE_NUMBER_1 = 1; private static final int TEST_PAGE_NUMBER_2 = 2; private static final int TEST_PAGE_SIZE_10 = 10; - private static final int TEST_PAGE_SIZE_25 = DEFAULT_PAGE_SIZE; + private static final int TEST_PAGE_SIZE_25 = 25; private JsonApiPageArgumentResolver jsonApiPageArgumentResolver; @@ -45,8 +46,10 @@ public class PageTest extends BaseTest { public void setUp() { MockitoAnnotations.openMocks(this); - Mockito.when(methodParameter.getParameterAnnotation(any())) + Mockito.when(methodParameter.getParameterAnnotation(RequestJsonApiPage.class)) .thenReturn(requestJsonApiPage); + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(null); Mockito.when(requestJsonApiPage.name()) .thenReturn(REQUEST_PAGE_ARGUMENT_NAME); From c5ab6abf961c0b3f4f7277d48dc5982273f4d9e6 Mon Sep 17 00:00:00 2001 From: Lucas Lustosa Madureira Date: Wed, 24 Jun 2026 17:55:10 -0300 Subject: [PATCH 2/5] Changing JsonApiPageArgumentResolver to implement PageableArgumentResolver instead of HandlerMethodArgumentResolver The former is more sematically aligned, IMHO --- .../simple/resolver/JsonApiPageArgumentResolver.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java b/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java index d8fdc44..3f05087 100644 --- a/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java +++ b/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java @@ -5,12 +5,13 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableArgumentResolver; import org.springframework.data.web.PageableDefault; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import java.util.*; @@ -30,7 +31,7 @@ * *

This resolver must be registered in Spring application. */ -public class JsonApiPageArgumentResolver implements HandlerMethodArgumentResolver { +public class JsonApiPageArgumentResolver implements PageableArgumentResolver { private static final String REQUEST_PAGE_KEY_BRACKET_START = "["; private static final String REQUEST_PAGE_KEY_BRACKET_END = "]"; @@ -38,6 +39,8 @@ public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterAnnotation(RequestJsonApiPage.class) != null; } + @NonNull + @Override public Pageable resolveArgument(final MethodParameter methodParameter, final ModelAndViewContainer modelAndViewContainer, final NativeWebRequest nativeWebRequest, From e3dd00508bb3af7754364a890a77bf003f9127f5 Mon Sep 17 00:00:00 2001 From: Lucas Lustosa Madureira Date: Thu, 25 Jun 2026 18:30:57 -0300 Subject: [PATCH 3/5] Adding support for SortDefault Adding unit test Update Readme --- README.md | 76 +++++- .../resolver/JsonApiPageArgumentResolver.java | 70 +++++- .../simple/response/PageDefaultsTest.java | 230 +++++++++++++++++- 3 files changed, 356 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 15598c0..b33c6f3 100644 --- a/README.md +++ b/README.md @@ -354,18 +354,18 @@ And for the page size are: - **page[size]** - **page[limit]** -#### Request page always starts from 0 for compatible with spring repositories and etc.! -#### If request page number < 1 resolver always return number = 0 and if size < 1 it always returns default value = 10! +#### Request page always starts from 0 for compatibility with spring repositories and etc.! +#### If request page number < 1, resolver always return number = 0, and if size < 1, it always returns default value of 10! #### Those default values can be overridden by annotating the method parameter with ```@PageableDefault``` ### Sorting See documentation part: [fetching-sorting](https://jsonapi.org/format/#fetching-sorting) -If you want to use request sort with annotation ```@RequestJsonApiPage``` add argument resolver in your configuration +If you want to use request sort with annotation ```@RequestJsonApiPage```, add the argument resolver in your configuration as described in section [Pagination](#pagination). -Then you can use annotation ```@RequestJsonApiPage``` in controllers and get standard spring Pageable object. +Then you can use the annotation ```@RequestJsonApiPage``` in controllers and get a standard spring Pageable object. For example: ```java @@ -385,12 +385,78 @@ public class RestController { } ``` -Now if request will be contained list of fields like ```sort=field1,field2,-field3``` we can get them in the ```Pageable``` object. +Now if the request contains a list of fields like ```sort=field1,field2,-field3``` we can get them in the ```Pageable``` object. For ASC order we just put fields name as is, for example: ```sort=name,age,...``` (see JSON:API spec). For DESC order we should put fields name with prefix **-**, for example: ```sort=-name,age,...``` (see JSON:API spec). +To provide a default sort in case the request doesn't specify one, you can use either the one of the ```@SortDefault``` or +```@SortDefaults``` annotations, or the property **sort** and **direction** of the ```@PageableDefault``` +annotation. See the examples bellow: + +```java +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping(value = "/app", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class RestController { + @GetMapping + public Response get( + @sortDefault(sort={"name", "age"}, direction=Sort.Direction.ASC) + final @RequestJsonApiPage Pageable page) throws Exception { + final Sort sort = page.getSort(); + // do anything with sort + + return Response.builder() + .build(); + } +} +``` + +```java +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping(value = "/app", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class RestController { + @GetMapping + public Response get( + @SortDefault.SortDefaults({ + @SortDefault({sort={"name", "age"}, direction=Sort.Direction.ASC), + @SortDefault({sort={"updateAt"}, direction=Sort.Direction.DESC) + }) + final @RequestJsonApiPage Pageable page) throws Exception { + final Sort sort = page.getSort(); + // do anything with sort + + return Response.builder() + .build(); + } +} +``` + +```java +@Slf4j +@RestController +@AllArgsConstructor +@RequestMapping(value = "/app", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class RestController { + @GetMapping + public Response get( + @PageableDefault( + sort={"name", "age"}, direction=Sort.Direction.ASC + ) + final @RequestJsonApiPage Pageable page) throws Exception { + final Sort sort = page.getSort(); + // do anything with sort + + return Response.builder() + .build(); + } +} +``` + ### Other response examples Example response with one data object: ```java diff --git a/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java b/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java index 3f05087..b897e78 100644 --- a/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java +++ b/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java @@ -1,12 +1,21 @@ package com.slmdev.jsonapi.simple.resolver; -import com.slmdev.jsonapi.simple.annotation.RequestJsonApiPage; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.springframework.core.MethodParameter; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableArgumentResolver; import org.springframework.data.web.PageableDefault; +import org.springframework.data.web.SortDefault; +import org.springframework.data.web.SortDefault.SortDefaults; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -14,9 +23,7 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import com.slmdev.jsonapi.simple.annotation.RequestJsonApiPage; /** * Spring resolver using for extract page values from the request. @@ -36,7 +43,8 @@ public class JsonApiPageArgumentResolver implements PageableArgumentResolver { private static final String REQUEST_PAGE_KEY_BRACKET_END = "]"; public boolean supportsParameter(MethodParameter parameter) { - return parameter.getParameterAnnotation(RequestJsonApiPage.class) != null; + return Pageable.class.equals(parameter.getParameterType()) && + parameter.getParameterAnnotation(RequestJsonApiPage.class) != null; } @NonNull @@ -45,10 +53,29 @@ public Pageable resolveArgument(final MethodParameter methodParameter, final ModelAndViewContainer modelAndViewContainer, final NativeWebRequest nativeWebRequest, final WebDataBinderFactory webDataBinderFactory) { - final RequestJsonApiPage requestJsonApiPage = methodParameter.getParameterAnnotation(RequestJsonApiPage.class); + + final SortDefault sortDefault = methodParameter.getParameterAnnotation(SortDefault.class); + final SortDefaults sortDefaults = methodParameter.getParameterAnnotation(SortDefaults.class); final PageableDefault pageableDefault = methodParameter.getParameterAnnotation(PageableDefault.class); + + if (sortDefault != null && sortDefaults != null) { + throw new IllegalArgumentException( + String.format( + "Cannot use both @%s and @%s on parameter %s; Move %s into %s to define sorting order", + SortDefaults.class.getSimpleName(), + SortDefault.class.getSimpleName(), + methodParameter.toString(), + SortDefault.class.getSimpleName(), + SortDefaults.class.getSimpleName() + ) + ); + } + + final RequestJsonApiPage requestJsonApiPage = methodParameter.getParameterAnnotation(RequestJsonApiPage.class); + final String pageKeyStart = requestJsonApiPage.name() + REQUEST_PAGE_KEY_BRACKET_START; - final Sort sort = parseSortField(nativeWebRequest, pageableDefault); + + final Sort sort = parseSortField(nativeWebRequest, sortDefault, sortDefaults, pageableDefault); int page = pageableDefault == null ? 0 : pageableDefault.page(); int size = pageableDefault == null ? 10 : pageableDefault.size(); @@ -110,11 +137,36 @@ private List valueToList(final String[] values) { return valueItems; } - private Sort parseSortField(final NativeWebRequest nativeWebRequest, @Nullable final PageableDefault pageableDefault) { + private Sort parseSortField( + final NativeWebRequest nativeWebRequest, + @Nullable final SortDefault sortDefault, + @Nullable final SortDefaults sortDefaults, + @Nullable final PageableDefault pageableDefault + ) { + if (CollectionUtils.isEmpty(nativeWebRequest.getParameterMap()) || nativeWebRequest.getParameterMap().get("sort") == null) { - if (pageableDefault != null && pageableDefault.sort().length > 0) { + SortDefault[] sortDefaultArray = + sortDefaults != null ? + sortDefaults.value() : + sortDefault != null ? + new SortDefault[]{sortDefault} : + new SortDefault[0]; + + // Takes the sort from sortDefalt(s) + if (sortDefaultArray.length > 0) { + return Arrays.stream(sortDefaultArray).map( + currentSortDefault -> Sort.by( + currentSortDefault.direction(), + currentSortDefault.sort() + ) + ).reduce( + Sort.unsorted(), + Sort::and + ); + // Takes the sort from pageableDefault + } else if (pageableDefault != null && pageableDefault.sort().length > 0) { return Sort.by(pageableDefault.direction(), pageableDefault.sort()); } diff --git a/src/test/java/com/slmdev/jsonapi/simple/response/PageDefaultsTest.java b/src/test/java/com/slmdev/jsonapi/simple/response/PageDefaultsTest.java index e670d5f..fbb0b39 100644 --- a/src/test/java/com/slmdev/jsonapi/simple/response/PageDefaultsTest.java +++ b/src/test/java/com/slmdev/jsonapi/simple/response/PageDefaultsTest.java @@ -2,9 +2,12 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import java.util.Arrays; import java.util.HashMap; import java.util.List; + import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; @@ -17,6 +20,8 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.data.web.PageableDefault; +import org.springframework.data.web.SortDefault; +import org.springframework.data.web.SortDefault.SortDefaults; import org.springframework.web.context.request.NativeWebRequest; import com.slmdev.jsonapi.simple.annotation.RequestJsonApiPage; @@ -24,6 +29,8 @@ public class PageDefaultsTest extends BaseTest { private static final String REQUEST_PAGE_ARGUMENT_NAME = "page"; + private static final int DEFAULT_PAGE_NUMBER = 0; + private static final int DEFAULT_PAGE_SIZE = 10; private JsonApiPageArgumentResolver jsonApiPageArgumentResolver; @@ -32,6 +39,10 @@ public class PageDefaultsTest extends BaseTest { @Mock private RequestJsonApiPage requestJsonApiPage; @Mock + private SortDefault sortDefault; + @Mock + private SortDefaults sortDefaults; + @Mock private PageableDefault pageableDefault; @Mock private NativeWebRequest nativeWebRequest; @@ -42,8 +53,6 @@ public void setUp() { Mockito.when(methodParameter.getParameterAnnotation(RequestJsonApiPage.class)) .thenReturn(requestJsonApiPage); - Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) - .thenReturn(pageableDefault); Mockito.when(requestJsonApiPage.name()) .thenReturn(REQUEST_PAGE_ARGUMENT_NAME); @@ -52,17 +61,34 @@ public void setUp() { @Test public void shouldParsePageWithPage2AndSize10AndNoSortParams() { + + // Mocks the PageableDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(pageableDefault); Mockito.when(pageableDefault.size()) .thenReturn(2); Mockito.when(pageableDefault.page()) .thenReturn(10); Mockito.when(pageableDefault.sort()) .thenReturn(new String[0]); - shouldParsePageWithSizeAndPageAndSort(); + + // Mocks the SortDefault and SortDefaults annotations + Mockito.when(methodParameter.getParameterAnnotation(SortDefault.class)) + .thenReturn(null); + + // Mocks the SortDefaults annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefaults.class)) + .thenReturn(null); + + shouldParsePageFromPageableDefault(); } @Test public void shouldParsePageWithPage2AndSize10AndAscSortParams() { + + // Mocks the PageableDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(pageableDefault); Mockito.when(pageableDefault.size()) .thenReturn(2); Mockito.when(pageableDefault.page()) @@ -71,11 +97,24 @@ public void shouldParsePageWithPage2AndSize10AndAscSortParams() { .thenReturn(Direction.ASC); Mockito.when(pageableDefault.sort()) .thenReturn(new String[]{"foo", "bar"}); - shouldParsePageWithSizeAndPageAndSort(); + + // Mocks the SortDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefault.class)) + .thenReturn(null); + + // Mocks the SortDefaults annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefaults.class)) + .thenReturn(null); + + shouldParsePageFromPageableDefault(); } @Test public void shouldParsePageWithPage2AndSize10AndDescSortParams() { + + // Mocks the PageableDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(pageableDefault); Mockito.when(pageableDefault.size()) .thenReturn(2); Mockito.when(pageableDefault.page()) @@ -84,10 +123,133 @@ public void shouldParsePageWithPage2AndSize10AndDescSortParams() { .thenReturn(Direction.DESC); Mockito.when(pageableDefault.sort()) .thenReturn(new String[]{"foo", "bar"}); - shouldParsePageWithSizeAndPageAndSort(); + + // Mocks the SortDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefault.class)) + .thenReturn(null); + + // Mocks the SortDefaults annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefaults.class)) + .thenReturn(null); + + shouldParsePageFromPageableDefault(); + } + + @Test + public void shouldParsePageWithAscSortParams() { + + // Mocks the PageableDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(null); + + // Mocks the SortDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefault.class)) + .thenReturn(sortDefault); + Mockito.when(sortDefault.sort()) + .thenReturn(new String[]{"foo", "bar"}); + Mockito.when(sortDefault.direction()) + .thenReturn(Direction.DESC); + + // Mocks the SortDefaults annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefaults.class)) + .thenReturn(null); + + shouldParsePageFromSortDefault(); + } + + @Test + public void shouldParsePageWithDescSortParams() { + + // Mocks the PageableDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(null); + + // Mocks the SortDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefault.class)) + .thenReturn(sortDefault); + Mockito.when(sortDefault.sort()) + .thenReturn(new String[]{"foo", "bar"}); + Mockito.when(sortDefault.direction()) + .thenReturn(Direction.ASC); + + // Mocks the SortDefaults annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefaults.class)) + .thenReturn(null); + + shouldParsePageFromSortDefault(); + } + + @Test + public void shouldParsePageWithTwoAscAndOneDescSortParams() { + + // Mocks the PageableDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(null); + + // Mocks the SortDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefault.class)) + .thenReturn(null); + + // Mocks the SortDefaults annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefaults.class)) + .thenReturn(sortDefaults); + + SortDefault sortDefaultMock1 = mock(SortDefault.class); + Mockito.when(sortDefaultMock1.sort()) + .thenReturn(new String[]{"foo", "bar"}); + Mockito.when(sortDefaultMock1.direction()) + .thenReturn(Direction.ASC); + + SortDefault sortDefaultMock2 = mock(SortDefault.class); + Mockito.when(sortDefaultMock2.sort()) + .thenReturn(new String[]{"baz"}); + Mockito.when(sortDefaultMock2.direction()) + .thenReturn(Direction.DESC); + + SortDefault[] sSortDefaultArrayMock = new SortDefault[] { sortDefaultMock1, sortDefaultMock2 }; + + Mockito.when(sortDefaults.value()) + .thenReturn(sSortDefaultArrayMock); + + shouldParsePageFromSortDefaults(); } - private void shouldParsePageWithSizeAndPageAndSort() { + @Test + public void shouldParsePageWithOneAscAndTwoDescSortParams() { + + // Mocks the PageableDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(PageableDefault.class)) + .thenReturn(null); + + // Mocks the SortDefault annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefault.class)) + .thenReturn(null); + + // Mocks the SortDefaults annotation + Mockito.when(methodParameter.getParameterAnnotation(SortDefaults.class)) + .thenReturn(sortDefaults); + + SortDefault sortDefaultMock1 = mock(SortDefault.class); + Mockito.when(sortDefaultMock1.sort()) + .thenReturn(new String[]{"foo"}); + Mockito.when(sortDefaultMock1.direction()) + .thenReturn(Direction.ASC); + + SortDefault sortDefaultMock2 = mock(SortDefault.class); + Mockito.when(sortDefaultMock2.sort()) + .thenReturn(new String[]{"bar", "baz"}); + Mockito.when(sortDefaultMock2.direction()) + .thenReturn(Direction.DESC); + + SortDefault[] sSortDefaultArrayMock = new SortDefault[] { sortDefaultMock1, sortDefaultMock2 }; + + Mockito.when(sortDefaults.value()) + .thenReturn(sSortDefaultArrayMock); + + shouldParsePageFromSortDefaults(); + } + + private void shouldParsePageFromPageableDefault() { // No page parameters should be passed in order for pageableDefault to take effect final var pageParams = new HashMap(); @@ -111,4 +273,60 @@ private void shouldParsePageWithSizeAndPageAndSort() { } + private void shouldParsePageFromSortDefault() { + + // No page parameters should be passed in order for SortDefault to take effect + final var pageParams = new HashMap(); + + Mockito.when(nativeWebRequest.getParameterMap()) + .thenReturn(pageParams); + + final Pageable page = jsonApiPageArgumentResolver.resolveArgument(methodParameter, null, nativeWebRequest, null); + + assertEquals(page.getPageSize(), DEFAULT_PAGE_SIZE); + assertEquals(page.getPageNumber(), DEFAULT_PAGE_NUMBER); + + List pageOrders = page.getSort().toList(); + assertEquals(pageOrders.size(), sortDefault.sort().length); + assertAll("all orders match", IntStream.range(0, page.getSort().toList().size()).mapToObj(i -> + () -> { + assertEquals(pageOrders.get(i).getDirection(), sortDefault.direction()); + assertEquals(pageOrders.get(i).getProperty(), sortDefault.sort()[i]); + } + )); + + } + + private void shouldParsePageFromSortDefaults() { + + // No page parameters should be passed in order for SortDefaults to take effect + final var pageParams = new HashMap(); + + Mockito.when(nativeWebRequest.getParameterMap()) + .thenReturn(pageParams); + + final Pageable page = jsonApiPageArgumentResolver.resolveArgument(methodParameter, null, nativeWebRequest, null); + + assertEquals(page.getPageSize(), DEFAULT_PAGE_SIZE); + assertEquals(page.getPageNumber(), DEFAULT_PAGE_NUMBER); + + List pageOrders = page.getSort().toList(); + SortDefault[] sortDefaultsArray = sortDefaults.value(); + String[] properties = Arrays.stream(sortDefaultsArray).map(SortDefault::sort).flatMap(Arrays::stream).toArray(String[]::new); + Direction[] directions = Arrays.stream(sortDefaultsArray).map(sortDefault -> { + int n = sortDefault.sort().length; + Direction[] sortDefaultDirections = new Direction[n]; + Arrays.fill(sortDefaultDirections, sortDefault.direction()); + return sortDefaultDirections; + }).flatMap(Arrays::stream).toArray(Direction[]::new); + assertEquals(pageOrders.size(), properties.length); + assertAll("all orders match", IntStream.range(0, page.getSort().toList().size()).mapToObj(i -> + () -> { + assertEquals(pageOrders.get(i).getDirection(), directions[i]); + assertEquals(pageOrders.get(i).getProperty(), properties[i]); + } + )); + + } + } From 0e1f6e96294e53bf426e5b481be75a53e810317b Mon Sep 17 00:00:00 2001 From: Lucas Lustosa Madureira Date: Thu, 25 Jun 2026 18:56:50 -0300 Subject: [PATCH 4/5] Typo fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b33c6f3..43f5e82 100644 --- a/README.md +++ b/README.md @@ -423,8 +423,8 @@ public class RestController { @GetMapping public Response get( @SortDefault.SortDefaults({ - @SortDefault({sort={"name", "age"}, direction=Sort.Direction.ASC), - @SortDefault({sort={"updateAt"}, direction=Sort.Direction.DESC) + @SortDefault(sort={"name", "age"}, direction=Sort.Direction.ASC), + @SortDefault(sort={"updateAt"}, direction=Sort.Direction.DESC) }) final @RequestJsonApiPage Pageable page) throws Exception { final Sort sort = page.getSort(); From 8fb83f71471e2995b6ddff69a8a24b91b6e0ab37 Mon Sep 17 00:00:00 2001 From: Lucas Lustosa Madureira Date: Thu, 25 Jun 2026 19:31:29 -0300 Subject: [PATCH 5/5] Mark method as nullable --- .../jsonapi/simple/resolver/JsonApiPageArgumentResolver.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java b/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java index b897e78..7931b61 100644 --- a/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java +++ b/src/main/java/com/slmdev/jsonapi/simple/resolver/JsonApiPageArgumentResolver.java @@ -137,6 +137,7 @@ private List valueToList(final String[] values) { return valueItems; } + @Nullable private Sort parseSortField( final NativeWebRequest nativeWebRequest, @Nullable final SortDefault sortDefault,