From 6b4d766693c6fa181a19b40b13c2b488db40ff7d Mon Sep 17 00:00:00 2001 From: Taha El Amine Kassabi Date: Fri, 11 Sep 2026 12:21:49 +0200 Subject: [PATCH 1/2] Mark Spring Data Page properties as required Direct Spring Data Page serialization always includes the standard page fields. Reflect that runtime contract in generated schemas so client generators do not make every page property optional. --- .../core/converters/PageOpenAPIConverter.java | 7 ++- .../test/resources/results/3.0.1/app246.json | 13 ++++ .../test/resources/results/3.0.1/app249.json | 15 ++++- .../v30/app10/SpringDocApp10DirectTest.java | 4 ++ .../v31/app10/SpringDocApp10DirectTest.java | 4 ++ .../resources/results/3.0.1/app10-direct.json | 60 +++++++++++++++++-- .../resources/results/3.1.0/app10-direct.json | 45 +++++++++++++- 7 files changed, 137 insertions(+), 11 deletions(-) diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PageOpenAPIConverter.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PageOpenAPIConverter.java index a761a1c93..310f5f302 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PageOpenAPIConverter.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PageOpenAPIConverter.java @@ -123,7 +123,7 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato Schema schema = (chain.hasNext()) ? chain.next().resolve(type, context, chain) : null; if (isPageType && !replacePageWithPagedModel) - sortPageSchemaProperties(schema, context); + customizePageSchema(schema, context); return schema; } @@ -161,12 +161,12 @@ private Type pagedModelType(JavaType type) { } /** - * Sort page schema properties. + * Require and sort page schema properties. * * @param schema the schema * @param context the context */ - private void sortPageSchemaProperties(Schema schema, ModelConverterContext context) { + private void customizePageSchema(Schema schema, ModelConverterContext context) { Schema pageSchema = resolveReferencedSchema(schema, context); if (pageSchema == null || pageSchema.getProperties() == null) return; @@ -179,6 +179,7 @@ private void sortPageSchemaProperties(Schema schema, ModelConverterContext conte PAGE_PROPERTY_ORDER.forEach(property -> sortedProperties.put(property, properties.get(property))); properties.forEach(sortedProperties::putIfAbsent); pageSchema.setProperties(sortedProperties); + pageSchema.setRequired(PAGE_PROPERTY_ORDER); } /** diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app246.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app246.json index b0e9991ad..eae4e0a22 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app246.json +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app246.json @@ -79,6 +79,19 @@ } }, "PageHelloWorld_Hello": { + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ], "type": "object", "properties": { "totalElements": { diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app249.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app249.json index 42595c74b..ae27edcac 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app249.json +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app249.json @@ -105,6 +105,19 @@ } }, "PageHelloWorld_Hello": { + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ], "type": "object", "properties": { "totalPages": { @@ -166,4 +179,4 @@ } } } -} \ No newline at end of file +} diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app10/SpringDocApp10DirectTest.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app10/SpringDocApp10DirectTest.java index 55bdcf45f..9a96f9ef0 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app10/SpringDocApp10DirectTest.java +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app10/SpringDocApp10DirectTest.java @@ -33,6 +33,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.web.config.EnableSpringDataWebSupport; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -47,6 +48,9 @@ protected void testApp() throws Exception { mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)) .andExpect(status().isOk()) .andExpect(jsonPath("$.openapi", is("3.0.1"))) + .andExpect(jsonPath("$.components.schemas.PageString.required", containsInAnyOrder( + "totalPages", "totalElements", "size", "content", "number", "sort", "pageable", + "numberOfElements", "first", "last", "empty"))) .andExpect(content().json(getContent("results/3.0.1/app10-direct.json"), true)); } diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app10/SpringDocApp10DirectTest.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app10/SpringDocApp10DirectTest.java index e5222ec38..be6e45d37 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app10/SpringDocApp10DirectTest.java +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app10/SpringDocApp10DirectTest.java @@ -33,6 +33,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.web.config.EnableSpringDataWebSupport; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -47,6 +48,9 @@ protected void testApp() throws Exception { mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL)) .andExpect(status().isOk()) .andExpect(jsonPath("$.openapi", is("3.1.0"))) + .andExpect(jsonPath("$.components.schemas.PageString.required", containsInAnyOrder( + "totalPages", "totalElements", "size", "content", "number", "sort", "pageable", + "numberOfElements", "first", "last", "empty"))) .andExpect(content().json(getContent("results/3.1.0/app10-direct.json"), true)); } diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-direct.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-direct.json index 12f41a67f..b1411f5af 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-direct.json +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-direct.json @@ -312,7 +312,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "PageableObject": { "type": "object", @@ -398,7 +411,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "UserDto": { "type": "object", @@ -455,7 +481,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "PageDummyListString": { "type": "object", @@ -501,7 +540,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "DummyPageString": { "type": "object", diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-direct.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-direct.json index e3fa6c995..113baacac 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-direct.json +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-direct.json @@ -290,7 +290,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "PageableObject": { "type": "object", @@ -374,7 +387,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "PageDummyListString": { "type": "object", @@ -420,7 +446,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "DummyPageString": { "type": "object", From 7999bdc59be1f759b31236ca5ed1d5b1341b9494 Mon Sep 17 00:00:00 2001 From: Taha El Amine Kassabi Date: Fri, 11 Sep 2026 13:54:14 +0200 Subject: [PATCH 2/2] Update Spring Data REST Page fixtures --- .../src/test/resources/results/3.0.1/app24.json | 15 ++++++++++++++- .../src/test/resources/results/3.1.0/app24.json | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/resources/results/3.0.1/app24.json b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/resources/results/3.0.1/app24.json index 5447028dd..1aa83d04c 100644 --- a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/resources/results/3.0.1/app24.json +++ b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/resources/results/3.0.1/app24.json @@ -123,7 +123,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "PageableObject": { "type": "object", diff --git a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/resources/results/3.1.0/app24.json b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/resources/results/3.1.0/app24.json index a6e140662..caf092d9f 100644 --- a/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/resources/results/3.1.0/app24.json +++ b/springdoc-openapi-tests/springdoc-openapi-data-rest-tests/src/test/resources/results/3.1.0/app24.json @@ -123,7 +123,20 @@ "empty": { "type": "boolean" } - } + }, + "required": [ + "content", + "empty", + "first", + "last", + "number", + "numberOfElements", + "pageable", + "size", + "sort", + "totalElements", + "totalPages" + ] }, "PageableObject": { "type": "object",