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..66eb13364 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 @@ -47,6 +47,7 @@ * The Spring Data Page type model converter. * * @author Claudio Nave + * @author dpkass */ public class PageOpenAPIConverter implements ModelConverter { @@ -77,6 +78,15 @@ public class PageOpenAPIConverter implements ModelConverter { "empty" ); + private static final List PAGED_MODEL_REQUIRED_PROPERTIES = List.of("content", "page"); + + private static final List PAGE_METADATA_REQUIRED_PROPERTIES = List.of( + "size", + "number", + "totalElements", + "totalPages" + ); + /** * The Spring doc object mapper. */ @@ -110,9 +120,11 @@ public PageOpenAPIConverter(boolean replacePageWithPagedModel, ObjectMapperProvi public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator chain) { JavaType javaType = springDocObjectMapper.jsonMapper().constructType(type.getType()); boolean isPageType = false; + boolean isPagedModelType = false; if (javaType != null) { Class cls = javaType.getRawClass(); isPageType = PAGE_TO_REPLACE.equals(cls.getCanonicalName()); + isPagedModelType = PagedModel.class.isAssignableFrom(cls); if (replacePageWithPagedModel && isPageType) { if (!type.isSchemaProperty()) type = resolvePagedModelType(javaType, type); @@ -124,6 +136,8 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato if (isPageType && !replacePageWithPagedModel) sortPageSchemaProperties(schema, context); + else if (isPagedModelType || isPageType) + requirePagedModelProperties(schema, context); return schema; } @@ -181,6 +195,29 @@ private void sortPageSchemaProperties(Schema schema, ModelConverterContext conte pageSchema.setProperties(sortedProperties); } + /** + * Require properties emitted by Spring Data's stable page representation. + * + * @param schema the schema + * @param context the context + */ + private void requirePagedModelProperties(Schema schema, ModelConverterContext context) { + Schema pagedModelSchema = resolveReferencedSchema(schema, context); + if (pagedModelSchema == null || pagedModelSchema.getProperties() == null + || !pagedModelSchema.getProperties().keySet().containsAll(PAGED_MODEL_REQUIRED_PROPERTIES)) + return; + + pagedModelSchema.setRequired(PAGED_MODEL_REQUIRED_PROPERTIES); + + Schema metadataSchema = resolveReferencedSchema( + (Schema) pagedModelSchema.getProperties().get("page"), context); + if (metadataSchema == null || metadataSchema.getProperties() == null + || !metadataSchema.getProperties().keySet().containsAll(PAGE_METADATA_REQUIRED_PROPERTIES)) + return; + + metadataSchema.setRequired(PAGE_METADATA_REQUIRED_PROPERTIES); + } + /** * Resolve referenced schema. * 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..26c68c3a4 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,10 @@ 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.PagedModelString.required", + containsInAnyOrder("content", "page"))) + .andExpect(jsonPath("$.components.schemas.PageMetadata.required", + containsInAnyOrder("size", "number", "totalElements", "totalPages"))) .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..5bba83aa3 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 @@ -213,7 +213,13 @@ "type": "integer", "format": "int64" } - } + }, + "required": [ + "number", + "size", + "totalElements", + "totalPages" + ] }, "PagedModelString": { "type": "object", @@ -227,7 +233,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "PagedModel": { "type": "object", @@ -241,7 +251,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "DummyListString": { "type": "object", @@ -266,7 +280,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "PageString": { "type": "object", diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-via_dto.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-via_dto.json index ed88f14ce..87aff1e7b 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-via_dto.json +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-via_dto.json @@ -213,7 +213,13 @@ "type": "integer", "format": "int64" } - } + }, + "required": [ + "number", + "size", + "totalElements", + "totalPages" + ] }, "PagedModelString": { "type": "object", @@ -227,7 +233,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "PagedModel": { "type": "object", @@ -241,7 +251,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "DummyListString": { "type": "object", @@ -266,7 +280,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "PagedModelUserDto": { "type": "object", @@ -280,7 +298,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "UserDto": { "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..996d86bb1 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 @@ -193,7 +193,13 @@ "type": "integer", "format": "int64" } - } + }, + "required": [ + "number", + "size", + "totalElements", + "totalPages" + ] }, "PagedModelString": { "type": "object", @@ -207,7 +213,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "PagedModel": { "type": "object", @@ -219,7 +229,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "DummyListString": { "type": "object", @@ -244,7 +258,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "PageString": { "type": "object", diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-via_dto.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-via_dto.json index d8d18c9cf..dc3ffb1cd 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-via_dto.json +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-via_dto.json @@ -193,7 +193,13 @@ "type": "integer", "format": "int64" } - } + }, + "required": [ + "number", + "size", + "totalElements", + "totalPages" + ] }, "PagedModelString": { "type": "object", @@ -207,7 +213,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "PagedModel": { "type": "object", @@ -219,7 +229,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "DummyListString": { "type": "object", @@ -244,7 +258,11 @@ "page": { "$ref": "#/components/schemas/PageMetadata" } - } + }, + "required": [ + "content", + "page" + ] }, "DummyPageString": { "type": "object",