Skip to content

Commit 1536f24

Browse files
authoredFeb 25, 2025
Merge pull request #2909 from wingsofovnia/fix/2879-2
Check both SerDe `BeanPropertyDefinition` for `@JsonUnwrapped`/`@Schema`
2 parents 70057d0 + 6e43467 commit 1536f24

File tree

13 files changed

+452
-17
lines changed

13 files changed

+452
-17
lines changed
 

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

+104-17
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@
2626

2727
package org.springdoc.core.converters;
2828

29-
import java.lang.reflect.Field;
29+
import java.lang.annotation.Annotation;
3030
import java.lang.reflect.Modifier;
3131
import java.util.ArrayList;
3232
import java.util.Collection;
3333
import java.util.Collections;
3434
import java.util.HashSet;
3535
import java.util.Iterator;
3636
import java.util.List;
37+
import java.util.Map;
3738
import java.util.Set;
3839

3940
import com.fasterxml.jackson.annotation.JsonUnwrapped;
40-
import com.fasterxml.jackson.databind.BeanDescription;
4141
import com.fasterxml.jackson.databind.JavaType;
4242
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
4343
import io.swagger.v3.core.converter.AnnotatedType;
@@ -50,9 +50,11 @@
5050
import io.swagger.v3.oas.models.media.ObjectSchema;
5151
import io.swagger.v3.oas.models.media.Schema;
5252
import org.apache.commons.lang3.ArrayUtils;
53-
import org.apache.commons.lang3.reflect.FieldUtils;
5453
import org.springdoc.core.providers.ObjectMapperProvider;
5554

55+
import static java.util.function.Function.identity;
56+
import static java.util.stream.Collectors.toMap;
57+
5658
/**
5759
* The type Polymorphic model converter.
5860
*
@@ -122,28 +124,18 @@ else if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getSi
122124
public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
123125
JavaType javaType = springDocObjectMapper.jsonMapper().constructType(type.getType());
124126
if (javaType != null) {
125-
BeanDescription javaTypeIntrospection = springDocObjectMapper.jsonMapper().getDeserializationConfig().introspect(javaType);
126-
for (BeanPropertyDefinition property : javaTypeIntrospection.findProperties()) {
127-
boolean isUnwrapped = (property.getField() != null && property.getField().hasAnnotation(JsonUnwrapped.class)) ||
128-
(property.getGetter() != null && property.getGetter().hasAnnotation(JsonUnwrapped.class));
129-
130-
if (isUnwrapped) {
127+
for (BeanPropertyBiDefinition propertyDef : introspectBeanProperties(javaType)) {
128+
if (propertyDef.isAnyAnnotated(JsonUnwrapped.class)) {
131129
if (!TypeNameResolver.std.getUseFqn())
132130
PARENT_TYPES_TO_IGNORE.add(javaType.getRawClass().getSimpleName());
133131
else
134132
PARENT_TYPES_TO_IGNORE.add(javaType.getRawClass().getName());
135133
}
136134
else {
137-
io.swagger.v3.oas.annotations.media.Schema declaredSchema = null;
138-
if (property.getField() != null) {
139-
declaredSchema = property.getField().getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
140-
} else if (property.getGetter() != null) {
141-
declaredSchema = property.getGetter().getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
142-
}
143-
135+
io.swagger.v3.oas.annotations.media.Schema declaredSchema = propertyDef.getAnyAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
144136
if (declaredSchema != null &&
145137
(ArrayUtils.isNotEmpty(declaredSchema.oneOf()) || ArrayUtils.isNotEmpty(declaredSchema.allOf()))) {
146-
TYPES_TO_SKIP.add(property.getPrimaryType().getRawClass().getSimpleName());
138+
TYPES_TO_SKIP.add(propertyDef.getPrimaryType().getRawClass().getSimpleName());
147139
}
148140
}
149141
}
@@ -227,4 +219,99 @@ private boolean isConcreteClass(AnnotatedType type) {
227219
Class<?> clazz = javaType.getRawClass();
228220
return !Modifier.isAbstract(clazz.getModifiers()) && !clazz.isInterface();
229221
}
222+
223+
/**
224+
* Introspects the properties of the given Java type based on serialization and deserialization configurations.
225+
* This method identifies properties present in both JSON serialization and deserialization views,
226+
* and pairs them into a list of {@code BeanPropertyBiDefinition}.
227+
*/
228+
private List<BeanPropertyBiDefinition> introspectBeanProperties(JavaType javaType) {
229+
Map<String, BeanPropertyDefinition> forSerializationProps =
230+
springDocObjectMapper.jsonMapper()
231+
.getSerializationConfig()
232+
.introspect(javaType)
233+
.findProperties()
234+
.stream()
235+
.collect(toMap(BeanPropertyDefinition::getName, identity()));
236+
Map<String, BeanPropertyDefinition> forDeserializationProps =
237+
springDocObjectMapper.jsonMapper()
238+
.getDeserializationConfig()
239+
.introspect(javaType)
240+
.findProperties()
241+
.stream()
242+
.collect(toMap(BeanPropertyDefinition::getName, identity()));
243+
244+
return forSerializationProps.keySet().stream()
245+
.map(key -> new BeanPropertyBiDefinition(forSerializationProps.get(key), forDeserializationProps.get(key)))
246+
.toList();
247+
}
248+
249+
/**
250+
* A record representing the bi-definition of a bean property, combining both
251+
* serialization and deserialization property views.
252+
*/
253+
private record BeanPropertyBiDefinition(
254+
BeanPropertyDefinition forSerialization,
255+
BeanPropertyDefinition forDeserialization
256+
) {
257+
258+
/**
259+
* Retrieves an annotation of the specified type from either the serialization or
260+
* deserialization property definition (field, getter, setter), returning the first available match.
261+
*/
262+
public <A extends Annotation> A getAnyAnnotation(Class<A> acls) {
263+
A anyForSerializationAnnotation = getAnyAnnotation(forSerialization, acls);
264+
A anyForDeserializationAnnotation = getAnyAnnotation(forDeserialization, acls);
265+
266+
return anyForSerializationAnnotation != null ? anyForSerializationAnnotation : anyForDeserializationAnnotation;
267+
}
268+
269+
/**
270+
* Checks if any annotation of the specified type exists across serialization
271+
* or deserialization property definitions.
272+
*/
273+
public <A extends Annotation> boolean isAnyAnnotated(Class<A> acls) {
274+
return getAnyAnnotation(acls) != null;
275+
}
276+
277+
/**
278+
* Type determined from the primary member for the property being built.
279+
*/
280+
public JavaType getPrimaryType() {
281+
JavaType forSerializationType = null;
282+
if (forSerialization != null) {
283+
forSerializationType = forSerialization.getPrimaryType();
284+
}
285+
286+
JavaType forDeserializationType = null;
287+
if (forDeserialization != null) {
288+
forDeserializationType = forDeserialization.getPrimaryType();
289+
}
290+
291+
if (forSerializationType != null && forDeserializationType != null && forSerializationType != forDeserializationType) {
292+
throw new IllegalStateException("The property " + forSerialization.getName() + " has different types for serialization and deserialization: "
293+
+ forSerializationType + " and " + forDeserializationType);
294+
}
295+
296+
return forSerializationType != null ? forSerializationType : forDeserializationType;
297+
}
298+
299+
private <A extends Annotation> A getAnyAnnotation(BeanPropertyDefinition prop, Class<A> acls) {
300+
if (prop == null) {
301+
return null;
302+
}
303+
304+
if (prop.getField() != null) {
305+
return prop.getField().getAnnotation(acls);
306+
}
307+
if (prop.getGetter() != null) {
308+
return prop.getGetter().getAnnotation(acls);
309+
}
310+
if (prop.getSetter() != null) {
311+
return prop.getSetter().getAnnotation(acls);
312+
}
313+
314+
return null;
315+
}
316+
}
230317
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package test.org.springdoc.api.v30.app242;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.annotation.JsonUnwrapped;
5+
6+
public class RootModel {
7+
8+
private Integer rootProperty;
9+
10+
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
11+
@JsonUnwrapped
12+
private UnwrappedModelOne unwrappedModelOne;
13+
14+
private UnwrappedModelTwo unwrappedModelTwo;
15+
16+
public Integer getRootProperty() {
17+
return rootProperty;
18+
}
19+
20+
public void setRootProperty(Integer rootProperty) {
21+
this.rootProperty = rootProperty;
22+
}
23+
24+
public UnwrappedModelOne getUnwrappedModelOne() {
25+
return unwrappedModelOne;
26+
}
27+
28+
public void setUnwrappedModelOne(UnwrappedModelOne unwrappedModelOne) {
29+
this.unwrappedModelOne = unwrappedModelOne;
30+
}
31+
32+
public UnwrappedModelTwo getUnwrappedModelTwo() {
33+
return unwrappedModelTwo;
34+
}
35+
36+
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
37+
@JsonUnwrapped
38+
public void setUnwrappedModelTwo(UnwrappedModelTwo unwrappedModelTwo) {
39+
this.unwrappedModelTwo = unwrappedModelTwo;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2024 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.v30.app242;
26+
27+
import org.springframework.boot.autoconfigure.SpringBootApplication;
28+
import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
29+
30+
public class SpringDocApp242Test extends AbstractSpringDocV30Test {
31+
32+
@SpringBootApplication
33+
static class SpringDocTestApp {}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package test.org.springdoc.api.v30.app242;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.RequestMapping;
5+
import org.springframework.web.bind.annotation.RestController;
6+
7+
@RestController
8+
@RequestMapping("/api")
9+
public class TestController {
10+
11+
@GetMapping
12+
public RootModel getRootModel() {
13+
return new RootModel();
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package test.org.springdoc.api.v30.app242;
2+
3+
public class UnwrappedModelOne {
4+
5+
private Integer unwrappedOneProperty;
6+
7+
public Integer getUnwrappedOneProperty() {
8+
return unwrappedOneProperty;
9+
}
10+
11+
public void setUnwrappedOneProperty(Integer unwrappedOneProperty) {
12+
this.unwrappedOneProperty = unwrappedOneProperty;
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package test.org.springdoc.api.v30.app242;
2+
3+
public class UnwrappedModelTwo {
4+
5+
private Integer unwrappedTwoProperty;
6+
7+
public Integer getUnwrappedTwoProperty() {
8+
return unwrappedTwoProperty;
9+
}
10+
11+
public void setUnwrappedTwoProperty(Integer unwrappedTwoProperty) {
12+
this.unwrappedTwoProperty = unwrappedTwoProperty;
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package test.org.springdoc.api.v31.app242;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.annotation.JsonUnwrapped;
5+
6+
public class RootModel {
7+
8+
private Integer rootProperty;
9+
10+
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
11+
@JsonUnwrapped
12+
private UnwrappedModelOne unwrappedModelOne;
13+
14+
private UnwrappedModelTwo unwrappedModelTwo;
15+
16+
public Integer getRootProperty() {
17+
return rootProperty;
18+
}
19+
20+
public void setRootProperty(Integer rootProperty) {
21+
this.rootProperty = rootProperty;
22+
}
23+
24+
public UnwrappedModelOne getUnwrappedModelOne() {
25+
return unwrappedModelOne;
26+
}
27+
28+
public void setUnwrappedModelOne(UnwrappedModelOne unwrappedModelOne) {
29+
this.unwrappedModelOne = unwrappedModelOne;
30+
}
31+
32+
public UnwrappedModelTwo getUnwrappedModelTwo() {
33+
return unwrappedModelTwo;
34+
}
35+
36+
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
37+
@JsonUnwrapped
38+
public void setUnwrappedModelTwo(UnwrappedModelTwo unwrappedModelTwo) {
39+
this.unwrappedModelTwo = unwrappedModelTwo;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2024 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.v31.app242;
26+
27+
import org.springframework.boot.autoconfigure.SpringBootApplication;
28+
import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
29+
30+
public class SpringDocApp242Test extends AbstractSpringDocV30Test {
31+
32+
@SpringBootApplication
33+
static class SpringDocTestApp {}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package test.org.springdoc.api.v31.app242;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.RequestMapping;
5+
import org.springframework.web.bind.annotation.RestController;
6+
7+
@RestController
8+
@RequestMapping("/api")
9+
public class TestController {
10+
11+
@GetMapping
12+
public RootModel getRootModel() {
13+
return new RootModel();
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package test.org.springdoc.api.v31.app242;
2+
3+
public class UnwrappedModelOne {
4+
5+
private Integer unwrappedOneProperty;
6+
7+
public Integer getUnwrappedOneProperty() {
8+
return unwrappedOneProperty;
9+
}
10+
11+
public void setUnwrappedOneProperty(Integer unwrappedOneProperty) {
12+
this.unwrappedOneProperty = unwrappedOneProperty;
13+
}
14+
}

0 commit comments

Comments
 (0)
Failed to load comments.