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:
JsonMapper.builder().propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
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:
- Comment out:
.addModule(new PageJacksonModule())
- Uncomment:
.addModule(new CustomPageJacksonModule())
- 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.
Problem Description
When using
PageJacksonModulefrom Spring Cloud OpenFeign to deserializeorg.springframework.data.domain.Page<T>responses, deserialization fails if the application uses a global JacksonPropertyNamingStrategies.SNAKE_CASEconfiguration.Example of global configuration:
spring.jackson.property-naming-strategy=SNAKE_CASEThe
SimplePageablerecord uses hardcoded@JsonPropertyannotations with camelCase names:With a global SNAKE_CASE strategy active:
page_number/page_sizein the JSON.@JsonProperty("pageNumber")and@JsonProperty("pageSize")mappings are conflicted.sizedefault to0(invalid).Thrown from
PageRequest.of(...)inside theSimplePageableconstructor.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
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
Expected Behavior
The test passes and
Page<Person>is correctly deserialized.Actual Behavior
Deserialization fails during response extraction with:
Proposed Fix
Add
@JsonAliasto 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
CustomPageJacksonModulewas created by copying/extending the originalPageJacksonModuleand adding@JsonAliastoSimplePageable.To verify:
Benefits