Skip to content

Commit a329910

Browse files
committed
Schema replaced by String when using @ApiResponse with RepresentationModel (Hateoas links). Fixes #2902
1 parent cdfaf63 commit a329910

File tree

7 files changed

+307
-13
lines changed

7 files changed

+307
-13
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/HateoasLinksConverter.java

+25-13
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929

3030
import java.util.Iterator;
31+
import java.util.Optional;
3132

3233
import com.fasterxml.jackson.databind.JavaType;
3334
import io.swagger.v3.core.converter.ModelConverter;
@@ -43,7 +44,7 @@
4344

4445
/**
4546
* The type Hateoas links converter.
46-
*
47+
*
4748
* @author bnasslahsen
4849
*/
4950
public class HateoasLinksConverter implements ModelConverter {
@@ -70,19 +71,30 @@ public Schema<?> resolve(
7071
) {
7172
JavaType javaType = springDocObjectMapper.jsonMapper().constructType(type.getType());
7273
if (javaType != null && RepresentationModel.class.isAssignableFrom(javaType.getRawClass())) {
73-
Schema<?> schema = chain.next().resolve(type, context, chain);
74-
String schemaName = schema.get$ref().substring(Components.COMPONENTS_SCHEMAS_REF.length());
75-
Schema original = context.getDefinedModels().get(schemaName);
76-
Object links = original.getProperties().get("_links");
77-
if(links instanceof JsonSchema jsonSchema) {
78-
jsonSchema.set$ref(AnnotationsUtils.COMPONENTS_REF + "Links");
79-
jsonSchema.setType(null);
80-
jsonSchema.setItems(null);
81-
jsonSchema.setTypes(null);
82-
} else if (links instanceof ArraySchema arraySchema){
83-
arraySchema.set$ref(AnnotationsUtils.COMPONENTS_REF + "Links");
74+
Schema<?> schema = chain.next().resolve(type, context, chain);
75+
if (schema != null) {
76+
String schemaName = Optional.ofNullable(schema.get$ref())
77+
.filter(ref -> ref.startsWith(Components.COMPONENTS_SCHEMAS_REF))
78+
.map(ref -> ref.substring(Components.COMPONENTS_SCHEMAS_REF.length()))
79+
.orElse(schema.getName());
80+
if(schemaName != null) {
81+
Schema original = context.getDefinedModels().get(schemaName);
82+
if (original == null || original.getProperties() == null) {
83+
return schema;
84+
}
85+
Object links = original.getProperties().get("_links");
86+
if (links instanceof JsonSchema jsonSchema) {
87+
jsonSchema.set$ref(AnnotationsUtils.COMPONENTS_REF + "Links");
88+
jsonSchema.setType(null);
89+
jsonSchema.setItems(null);
90+
jsonSchema.setTypes(null);
91+
}
92+
else if (links instanceof ArraySchema arraySchema) {
93+
arraySchema.set$ref(AnnotationsUtils.COMPONENTS_REF + "Links");
94+
}
8495
}
85-
return schema;
96+
}
97+
return schema;
8698
}
8799
return chain.hasNext() ? chain.next().resolve(type, context, chain) : null;
88100
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * *
7+
* * * * * * Copyright 2019-2025 the original author or authors.
8+
* * * * * *
9+
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
10+
* * * * * * you may not use this file except in compliance with the License.
11+
* * * * * * You may obtain a copy of the License at
12+
* * * * * *
13+
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
14+
* * * * * *
15+
* * * * * * Unless required by applicable law or agreed to in writing, software
16+
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
17+
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* * * * * * See the License for the specific language governing permissions and
19+
* * * * * * limitations under the License.
20+
* * * * *
21+
* * * *
22+
* * *
23+
* *
24+
*
25+
*/
26+
27+
package test.org.springdoc.api.v31.app11;
28+
29+
import test.org.springdoc.api.v31.AbstractSpringDocTest;
30+
31+
import org.springframework.boot.autoconfigure.SpringBootApplication;
32+
33+
public class SpringDocApp11Test extends AbstractSpringDocTest {
34+
35+
@SpringBootApplication
36+
static class SpringDocTestApp {
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package test.org.springdoc.api.v31.app11.configuration;
2+
3+
import java.util.List;
4+
5+
import com.fasterxml.jackson.annotation.JsonInclude;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
import org.springframework.http.MediaType;
11+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
12+
13+
@Configuration
14+
public class WebMvcConfiguration {
15+
16+
@Bean
17+
MappingJackson2HttpMessageConverter getMappingJacksonHttpMessageConverter() {
18+
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
19+
converter.setSupportedMediaTypes(List.of(MediaType.APPLICATION_JSON));
20+
converter.setObjectMapper(new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)
21+
);
22+
23+
return converter;
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package test.org.springdoc.api.v31.app11.controllers;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.media.Content;
5+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
6+
import test.org.springdoc.api.v31.app11.model.Cat;
7+
8+
import org.springframework.hateoas.MediaTypes;
9+
import org.springframework.hateoas.RepresentationModel;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.ResponseStatus;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
@RestController
17+
@RequestMapping(path = "/")
18+
public class BasicController {
19+
20+
@GetMapping("/cat")
21+
@ResponseStatus(HttpStatus.OK)
22+
@Operation(summary = "get", description = "Provides an animal.")
23+
public String get(Cat cat) {
24+
return cat != null ? cat.getName() : "";
25+
}
26+
27+
@GetMapping("/test")
28+
@ResponseStatus(HttpStatus.OK)
29+
@Operation(summary = "get", description = "Provides a response.")
30+
@ApiResponse(content = @Content(mediaType = MediaTypes.HAL_JSON_VALUE,
31+
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = Response.class)),
32+
responseCode = "200")
33+
public Response get() {
34+
return new Response("value");
35+
}
36+
37+
// dummy
38+
public static class Response extends RepresentationModel {
39+
public Response(String v) {}
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package test.org.springdoc.api.v31.app11.controllers;
2+
3+
import org.springdoc.core.customizers.SpringDocCustomizers;
4+
import org.springdoc.core.properties.SpringDocConfigProperties;
5+
import org.springdoc.core.providers.SpringDocProviders;
6+
import org.springdoc.core.service.AbstractRequestService;
7+
import org.springdoc.core.service.GenericResponseService;
8+
import org.springdoc.core.service.OpenAPIService;
9+
import org.springdoc.core.service.OperationService;
10+
import org.springdoc.webmvc.api.OpenApiWebMvcResource;
11+
12+
import org.springframework.beans.factory.ObjectFactory;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
@RestController
16+
public class CustomOpenApiWebMvcResource extends OpenApiWebMvcResource {
17+
18+
public CustomOpenApiWebMvcResource(ObjectFactory<OpenAPIService> openAPIBuilderObjectFactory,
19+
AbstractRequestService requestBuilder,
20+
GenericResponseService responseBuilder,
21+
OperationService operationParser,
22+
SpringDocConfigProperties springDocConfigProperties,
23+
SpringDocProviders springDocProviders,
24+
SpringDocCustomizers springDocCustomizers) {
25+
super(openAPIBuilderObjectFactory, requestBuilder, responseBuilder, operationParser, springDocConfigProperties, springDocProviders, springDocCustomizers);
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package test.org.springdoc.api.v31.app11.model;
2+
3+
import com.fasterxml.jackson.annotation.JsonUnwrapped;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
6+
@Schema(description = "Represents a Cat class.")
7+
public class Cat {
8+
9+
@JsonUnwrapped
10+
@Schema(description = "The name.", nullable = true)
11+
private String name;
12+
13+
public Cat(String name) {
14+
this.name = name;
15+
}
16+
17+
public String getName() {
18+
return name;
19+
}
20+
21+
public void setName(String name) {
22+
this.name = name;
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"servers": [
8+
{
9+
"url": "http://localhost",
10+
"description": "Generated server url"
11+
}
12+
],
13+
"paths": {
14+
"/test": {
15+
"get": {
16+
"tags": [
17+
"basic-controller"
18+
],
19+
"summary": "get",
20+
"description": "Provides a response.",
21+
"operationId": "get",
22+
"responses": {
23+
"200": {
24+
"description": "OK",
25+
"content": {
26+
"application/hal+json": {
27+
"schema": {
28+
"$ref": "#/components/schemas/Response"
29+
}
30+
}
31+
}
32+
}
33+
}
34+
}
35+
},
36+
"/cat": {
37+
"get": {
38+
"tags": [
39+
"basic-controller"
40+
],
41+
"summary": "get",
42+
"description": "Provides an animal.",
43+
"operationId": "get_1",
44+
"parameters": [
45+
{
46+
"name": "cat",
47+
"in": "query",
48+
"required": true,
49+
"schema": {
50+
"$ref": "#/components/schemas/Cat"
51+
}
52+
}
53+
],
54+
"responses": {
55+
"200": {
56+
"description": "OK",
57+
"content": {
58+
"*/*": {
59+
"schema": {
60+
"type": "string"
61+
}
62+
}
63+
}
64+
}
65+
}
66+
}
67+
}
68+
},
69+
"components": {
70+
"schemas": {
71+
"Response": {
72+
"type": "object",
73+
"properties": {
74+
"_links": {
75+
"$ref": "#/components/schemas/Links"
76+
}
77+
}
78+
},
79+
"Cat": {
80+
"type": "object",
81+
"description": "Represents a Cat class.",
82+
"properties": {
83+
"name": {
84+
"type": "string",
85+
"description": "The name."
86+
}
87+
}
88+
},
89+
"Link": {
90+
"type": "object",
91+
"properties": {
92+
"href": {
93+
"type": "string"
94+
},
95+
"hreflang": {
96+
"type": "string"
97+
},
98+
"title": {
99+
"type": "string"
100+
},
101+
"type": {
102+
"type": "string"
103+
},
104+
"deprecation": {
105+
"type": "string"
106+
},
107+
"profile": {
108+
"type": "string"
109+
},
110+
"name": {
111+
"type": "string"
112+
},
113+
"templated": {
114+
"type": "boolean"
115+
}
116+
}
117+
},
118+
"Links": {
119+
"type": "object",
120+
"additionalProperties": {
121+
"$ref": "#/components/schemas/Link"
122+
}
123+
}
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)