diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OpenAPIService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OpenAPIService.java index 2eb56cb01..1d7c96f60 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OpenAPIService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OpenAPIService.java @@ -42,6 +42,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.core.jackson.TypeNameResolver; @@ -70,6 +71,7 @@ import org.springdoc.core.customizers.ServerBaseUrlCustomizer; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.providers.JavadocProvider; +import org.springdoc.core.providers.ObjectMapperProvider; import org.springdoc.core.utils.PropertyResolverUtils; import org.springframework.beans.BeansException; @@ -244,8 +246,11 @@ public OpenAPI build(Locale locale) { } else { try { - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = ObjectMapperProvider.createJson(springDocConfigProperties); calculatedOpenAPI = objectMapper.readValue(objectMapper.writeValueAsString(openAPI), OpenAPI.class); + objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + Map extensionsClone = objectMapper.readValue(objectMapper.writeValueAsString(openAPI.getExtensions()), Map.class); + calculatedOpenAPI.extensions(extensionsClone); } catch (JsonProcessingException e) { LOGGER.warn("Json Processing Exception occurred: {}", e.getMessage()); diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/FooConfiguration.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/FooConfiguration.java new file mode 100644 index 000000000..11065e4b9 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/FooConfiguration.java @@ -0,0 +1,57 @@ +package test.org.springdoc.api.app9; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.tags.Tag; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class FooConfiguration { + @Bean + OpenAPI customOpenApi() { + return new OpenAPI() + .components(new Components().addSecuritySchemes("bearerScheme", + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT")) + .addSchemas("FeedResponse", feedResponseSchema())) + .info(new Info() + .title("Response API") + .description("API for some response") + .version("0.0.1") + .contact(new Contact() + .name("EAlf91"))) + .tags(List.of(new Tag() + .name("ResponseTag") + .description("ResponseTag for API"), + new Tag() + .name("ResponseData") + .description("Version 2 ResponseApi"))); + + } + + private Schema feedResponseSchema() { + Schema schema = new Schema<>(); + schema.addProperty("_links", linkSchema()); + schema.addProperty("data", new ArraySchema().items(new Schema<>().$ref("#/components/schemas/ResponseData"))); + return schema; + } + + private Schema linkSchema() { + Schema linkSchema = new Schema<>(); + linkSchema.addProperty("next", new Schema<>().$ref("#/components/schemas/Link").example("http://localhost:8080/some-link")); + linkSchema.addProperty("self", new Schema<>().$ref("#/components/schemas/Link").example("http://localhost:8080/some-other-link")); + return linkSchema; + } + +} diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/SpringDocApp9Test.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/SpringDocApp9Test.java new file mode 100644 index 000000000..ba02d6e00 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/SpringDocApp9Test.java @@ -0,0 +1,30 @@ +/* + * + * * Copyright 2019-2020 the original author or authors. + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * https://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package test.org.springdoc.api.app9; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import test.org.springdoc.api.AbstractSpringDocTest; + +public class SpringDocApp9Test extends AbstractSpringDocTest { + + @SpringBootApplication + static class SpringDocTestApp { + } + +} \ No newline at end of file diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/FooController.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/FooController.java new file mode 100644 index 000000000..dd5864ae1 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/FooController.java @@ -0,0 +1,32 @@ +package test.org.springdoc.api.app9.application; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import test.org.springdoc.api.app9.application.dto.ResponseData; +import test.org.springdoc.api.app9.application.dto.FeedResponse; + +import java.util.List; +import java.util.UUID; +@Tag(name = "ResponseDataController") +@RestController +@RequestMapping(value = "/some-route", produces = MediaType.APPLICATION_JSON_VALUE) +@RequiredArgsConstructor +@Slf4j +public class FooController { + + + + + @Operation(summary = "Get all data", description = "Get all data") + @GetMapping(value = "foo/{id}") + @ResponseStatus(HttpStatus.OK) + public FeedResponse getFoo(@PathVariable("id") UUID id) { + var dataList = List.of(ResponseData.builder().dataId(id).build()); + return FeedResponse.createForResponseData(dataList, UUID.randomUUID()); + } +} \ No newline at end of file diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/dto/FeedResponse.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/dto/FeedResponse.java new file mode 100644 index 000000000..9fc1e3e66 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/dto/FeedResponse.java @@ -0,0 +1,35 @@ +package test.org.springdoc.api.app9.application.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.experimental.Accessors; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.RepresentationModel; +import test.org.springdoc.api.app9.application.FooController; + +import java.util.List; +import java.util.UUID; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +@Getter +@Accessors(fluent = true) +@Builder(access = AccessLevel.PACKAGE) +@EqualsAndHashCode(callSuper = true) +public class FeedResponse extends RepresentationModel { + @NotNull + @JsonProperty("data") + private final List data; + + public static FeedResponse createForResponseData(@NotNull List responseData, UUID uuid) { + var feedResponse = new FeedResponse(responseData); + feedResponse.add(linkTo(methodOn(FooController.class).getFoo(uuid)).withSelfRel()); + + + feedResponse.add(linkTo(methodOn(FooController.class).getFoo(uuid)).withRel(IanaLinkRelations.NEXT)); + + return feedResponse; + } +} diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/dto/ResponseData.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/dto/ResponseData.java new file mode 100644 index 000000000..7d0860484 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/app9/application/dto/ResponseData.java @@ -0,0 +1,20 @@ +package test.org.springdoc.api.app9.application.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +import java.time.LocalDate; +import java.util.UUID; + +@Builder +public record ResponseData( + @JsonProperty(value = "DATA_ID", required = true) + @NotNull + UUID dataId, + @JsonProperty(value = "DATE", required = true) + @NotNull + @Schema(example = "2024-03-27", format = "date") + LocalDate date +) {} diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/app9.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/app9.json new file mode 100644 index 000000000..f7dd3ac61 --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/app9.json @@ -0,0 +1,142 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Response API", + "description": "API for some response", + "contact": { + "name": "EAlf91" + }, + "version": "0.0.1" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "tags": [ + { + "name": "ResponseTag", + "description": "ResponseTag for API" + }, + { + "name": "ResponseData", + "description": "Version 2 ResponseApi" + } + ], + "paths": { + "/some-route/foo/{id}": { + "get": { + "tags": [ + "ResponseDataController" + ], + "summary": "Get all data", + "description": "Get all data", + "operationId": "getFoo", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "FeedResponse": { + "type": "object", + "properties": { + "_links": { + "type": "object", + "properties": { + "next": { + "$ref": "#/components/schemas/Link" + }, + "self": { + "$ref": "#/components/schemas/Link" + } + } + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseData" + } + } + } + }, + "ResponseData": { + "required": [ + "DATA_ID", + "DATE" + ], + "type": "object", + "properties": { + "DATA_ID": { + "type": "string", + "format": "uuid" + }, + "DATE": { + "type": "string", + "format": "date", + "example": "2024-03-27" + } + } + }, + "Link": { + "type": "object", + "properties": { + "href": { + "type": "string" + }, + "hreflang": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "deprecation": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "name": { + "type": "string" + }, + "templated": { + "type": "boolean" + } + } + } + }, + "securitySchemes": { + "bearerScheme": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + } +} \ No newline at end of file diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/app9wrong.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/app9wrong.json new file mode 100644 index 000000000..45fe97cdc --- /dev/null +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/app9wrong.json @@ -0,0 +1,122 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Response API", + "description": "API for some response", + "contact": { + "name": "EAlf91" + }, + "version": "0.0.1" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "tags": [ + { + "name": "ResponseTag", + "description": "ResponseTag for API" + }, + { + "name": "ResponseData", + "description": "Version 2 ResponseApi" + } + ], + "paths": { + "/some-route/foo/{id}": { + "get": { + "tags": [ + "ResponseDataController" + ], + "summary": "Get all data", + "description": "Get all data", + "operationId": "getFoo", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "FeedResponse": { + "properties": { + "_links": { + "properties": { + "next": { + "$ref": "#/components/schemas/Link" + }, + "self": { + "$ref": "#/components/schemas/Link" + } + } + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResponseData" + } + } + } + }, + "Link": { + "type": "object", + "properties": { + "href": { + "type": "string" + }, + "hreflang": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "deprecation": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "name": { + "type": "string" + }, + "templated": { + "type": "boolean" + } + } + } + }, + "securitySchemes": { + "bearerScheme": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + } +} \ No newline at end of file