Skip to content

Add @JsonAlias support to SimplePageable in PageJacksonModule for snake_case compatibility with global SNAKE_CASE naming strategy #1324

@weslyvinicius

Description

@weslyvinicius

Problem Description

When using PageJacksonModule from Spring Cloud OpenFeign to deserialize org.springframework.data.domain.Page<T> responses, deserialization fails if the application uses a global Jackson PropertyNamingStrategies.SNAKE_CASE configuration.

Example of global configuration:

  • Via builder:
JsonMapper.builder().propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
  • Or via properties:
spring.jackson.property-naming-strategy=SNAKE_CASE

The SimplePageable record uses hardcoded @JsonProperty annotations with camelCase names:

// Current (simplified)
record SimplePageable(
    @JsonProperty("pageNumber") int number,
    @JsonProperty("pageSize") int size,
    @JsonProperty("sort") Sort sort
) { ... }

With a global SNAKE_CASE strategy active:

  • Jackson prioritizes the naming strategy and looks for page_number / page_size in the JSON.
  • The @JsonProperty("pageNumber") and @JsonProperty("pageSize") mappings are conflicted.
  • Fields like size default to 0 (invalid).
  • This causes the exception:
java.lang.IllegalArgumentException: Page size must not be less than one

Thrown from PageRequest.of(...) inside the SimplePageable constructor.


Reproduction with Minimal Example

A minimal reproducible project is available here:

https://github.com/weslyvinicius/bug-fix-page-jackson-module


Key Reproduction Steps

1. Global Jackson config with SNAKE_CASE

@Configuration
public class JsonMapperConfig {

    @Bean
    @Primary
    public JsonMapper jacksonMapper() {
        return JsonMapper.builder()
                .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
                .addModule(new PageJacksonModule())
                .addModule(new SortJacksonModule())
                .build();
    }
}

2. WireMock stub returns paginated JSON in snake_case

{
  "content": [ ... ],
  "pageable": {
    "page_number": 0,
    "page_size": 10
  },
  "total_elements": 10,
  "total_pages": 1
}

3. Run integration test

@SpringBootTest
@AutoConfigureMockMvc
@EnableWireMock({
    @ConfigureWireMock(
            name = "person-service",
            baseUrlProperties = "http://localhost",
            port = 8081 )
})
class PersonControllerTest {

    @Autowired
    public MockMvc mockMvc;

    @Test
    void getAllPersons() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/person"))
                .andDo(print())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content").isArray());
    }
}

Expected Behavior

The test passes and Page<Person> is correctly deserialized.


Actual Behavior

Deserialization fails during response extraction with:

Page size must not be less than one

Proposed Fix

Add @JsonAlias to accept snake_case variants while keeping camelCase as primary:

static class SimplePageable implements Pageable {
       private final PageRequest delegate;
-     @JsonProperty("pageNumber") int number,
+     @JsonProperty("pageNumber")
+     @JsonAlias({"page-number", "page_number", "pagenumber", "PageNumber"})
+     int number,

-     @JsonProperty("pageSize") int size,
+     @JsonProperty("pageSize")
+     @JsonAlias({"page-size", "page_size", "pagesize", "PageSize"})
+     int size,

      @JsonProperty("sort") Sort sort
) { ... }

Optionally, add more aliases for consistency with other fields.


Demonstration of Fix in the Example Repo

In the linked repo, a CustomPageJacksonModule was created by copying/extending the original PageJacksonModule and adding @JsonAlias to SimplePageable.

To verify:

  1. Comment out:
.addModule(new PageJacksonModule())
  1. Uncomment:
.addModule(new CustomPageJacksonModule())
  1. Rerun the test — it passes successfully.

Benefits

  • Fully backward compatible.
  • Allows global SNAKE_CASE without custom mappers or per-client overrides.
  • Supports common snake_case paginated APIs.
  • Avoids forcing camelCase changes in external or legacy APIs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status

    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions