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
    No fields configured for issues without a type.

    Projects

    Status

    In Progress

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions