Skip to content

Commit ff6c87b

Browse files
committed
improve swagger annotations to force schemas
1 parent 32e3454 commit ff6c87b

4 files changed

Lines changed: 228 additions & 1 deletion

File tree

src/main/java/it/aboutbits/springboot/toolbox/swagger/annotation/ForceSwaggerSchema.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
@Target(ElementType.TYPE)
1212
@Retention(RetentionPolicy.RUNTIME)
1313
public @interface ForceSwaggerSchema {
14-
14+
boolean includeSubTypes() default true;
1515
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package it.aboutbits.springboot.toolbox.swagger.annotation;
2+
3+
import org.jspecify.annotations.NullMarked;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
/**
11+
* Annotation to mark classes that should be ignored when forcing Swagger schemas.
12+
*/
13+
@Target(ElementType.TYPE)
14+
@Retention(RetentionPolicy.RUNTIME)
15+
@NullMarked
16+
public @interface ForceSwaggerSchemaIgnore {
17+
}

src/main/java/it/aboutbits/springboot/toolbox/swagger/customization/force_schema/ForceSchemaCustomizer.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
import io.swagger.v3.oas.models.media.Schema;
88
import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil;
99
import it.aboutbits.springboot.toolbox.swagger.annotation.ForceSwaggerSchema;
10+
import it.aboutbits.springboot.toolbox.swagger.annotation.ForceSwaggerSchemaIgnore;
1011
import lombok.RequiredArgsConstructor;
1112
import lombok.extern.slf4j.Slf4j;
1213
import org.springdoc.core.customizers.OpenApiCustomizer;
1314

15+
import java.lang.reflect.Modifier;
1416
import java.util.Arrays;
17+
import java.util.HashSet;
1518
import java.util.LinkedHashMap;
19+
import java.util.Set;
1620

1721
@RequiredArgsConstructor
1822
@Slf4j
@@ -40,7 +44,31 @@ private void addAnnotatedSchemas(OpenAPI openAPI) {
4044
// Scan for classes with @ForceSwaggerSchema annotation
4145
var annotatedClasses = classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class);
4246

47+
var classesToProcess = new HashSet<Class<?>>();
4348
for (var clazz : annotatedClasses) {
49+
if (clazz.isAnnotationPresent(ForceSwaggerSchemaIgnore.class)) {
50+
continue;
51+
}
52+
classesToProcess.add(clazz);
53+
var annotation = clazz.getAnnotation(ForceSwaggerSchema.class);
54+
if (annotation != null && annotation.includeSubTypes()) {
55+
var subTypes = classScanner.getSubTypesOf(clazz);
56+
for (var subType : subTypes) {
57+
if (!subType.isAnnotationPresent(ForceSwaggerSchemaIgnore.class)) {
58+
classesToProcess.add(subType);
59+
}
60+
}
61+
62+
collectPublicNestedTypes(clazz, classesToProcess);
63+
for (var subType : subTypes) {
64+
if (classesToProcess.contains(subType)) {
65+
collectPublicNestedTypes(subType, classesToProcess);
66+
}
67+
}
68+
}
69+
}
70+
71+
for (var clazz : classesToProcess) {
4472
log.info("Forcing schema for class: {}", clazz.getName());
4573

4674
if (clazz.isEnum()) {
@@ -75,5 +103,15 @@ private void addAnnotatedSchemas(OpenAPI openAPI) {
75103
log.debug("Scanned packages: {}", String.join(", ", classScanner.getScannedPackages()));
76104
}
77105
}
106+
107+
private void collectPublicNestedTypes(Class<?> clazz, Set<Class<?>> collected) {
108+
for (var nested : clazz.getDeclaredClasses()) {
109+
if (Modifier.isPublic(nested.getModifiers()) && !nested.isAnnotationPresent(ForceSwaggerSchemaIgnore.class)) {
110+
if (collected.add(nested)) {
111+
collectPublicNestedTypes(nested, collected);
112+
}
113+
}
114+
}
115+
}
78116
}
79117

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package it.aboutbits.springboot.toolbox.swagger.customization.force_schema;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import io.swagger.v3.core.jackson.ModelResolver;
5+
import io.swagger.v3.oas.models.OpenAPI;
6+
import it.aboutbits.springboot.toolbox.reflection.util.ClassScannerUtil;
7+
import it.aboutbits.springboot.toolbox.swagger.annotation.ForceSwaggerSchema;
8+
import it.aboutbits.springboot.toolbox.swagger.annotation.ForceSwaggerSchemaIgnore;
9+
import org.jspecify.annotations.NullUnmarked;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.util.Set;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.mockito.Mockito.mock;
16+
import static org.mockito.Mockito.when;
17+
18+
@NullUnmarked
19+
class ForceSchemaCustomizerTest {
20+
21+
@ForceSwaggerSchema(includeSubTypes = true)
22+
public static class AnnotatedClass {
23+
public String field;
24+
25+
public static class InnerClass {
26+
public String innerField;
27+
}
28+
29+
public record InnerRecord(String recordField) {
30+
}
31+
32+
private static class PrivateInnerClass {
33+
public String privateField;
34+
}
35+
}
36+
37+
@ForceSwaggerSchema(includeSubTypes = false)
38+
public static class AnnotatedClassWithoutSubTypes {
39+
public String field;
40+
41+
public static class InnerClass {
42+
public String innerField;
43+
}
44+
}
45+
46+
@ForceSwaggerSchema(includeSubTypes = true)
47+
public static class BaseClass {
48+
}
49+
50+
public static class SubClass extends BaseClass {
51+
public static class SubInnerClass {
52+
}
53+
}
54+
55+
@ForceSwaggerSchema(includeSubTypes = true)
56+
public static class ClassWithIgnoredMembers {
57+
public String field;
58+
59+
@ForceSwaggerSchemaIgnore
60+
public static class IgnoredInnerClass {
61+
public String innerField;
62+
}
63+
64+
public static class NotIgnoredInnerClass {
65+
public String innerField;
66+
}
67+
}
68+
69+
@ForceSwaggerSchema(includeSubTypes = true)
70+
public static class BaseWithIgnoredSubClass {
71+
}
72+
73+
@ForceSwaggerSchemaIgnore
74+
public static class IgnoredSubClass extends BaseWithIgnoredSubClass {
75+
}
76+
77+
public static class NotIgnoredSubClass extends BaseWithIgnoredSubClass {
78+
}
79+
80+
@ForceSwaggerSchema
81+
@ForceSwaggerSchemaIgnore
82+
public static class AnnotatedAndIgnored {
83+
}
84+
85+
@Test
86+
void shouldIncludeSubTypesWhenEnabled() {
87+
var classScanner = mock(ClassScannerUtil.ClassScanner.class);
88+
when(classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class)).thenReturn(Set.of(
89+
AnnotatedClass.class,
90+
BaseClass.class
91+
));
92+
when(classScanner.getSubTypesOf(BaseClass.class)).thenReturn(Set.of(SubClass.class));
93+
94+
var modelResolver = new ModelResolver(new ObjectMapper());
95+
var customizer = new ForceSchemaCustomizer(modelResolver, classScanner);
96+
var openApi = new OpenAPI();
97+
98+
customizer.customise(openApi);
99+
100+
var schemas = openApi.getComponents().getSchemas();
101+
assertThat(schemas).containsKey(AnnotatedClass.class.getSimpleName());
102+
assertThat(schemas).containsKey(AnnotatedClass.InnerClass.class.getSimpleName());
103+
assertThat(schemas).containsKey(AnnotatedClass.InnerRecord.class.getSimpleName());
104+
assertThat(schemas).doesNotContainKey(AnnotatedClass.PrivateInnerClass.class.getSimpleName());
105+
106+
assertThat(schemas).containsKey(BaseClass.class.getSimpleName());
107+
assertThat(schemas).containsKey(SubClass.class.getSimpleName());
108+
assertThat(schemas).containsKey(SubClass.SubInnerClass.class.getSimpleName());
109+
}
110+
111+
@Test
112+
void shouldNotIncludeSubTypesWhenDisabled() {
113+
var classScanner = mock(ClassScannerUtil.ClassScanner.class);
114+
when(classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class)).thenReturn(Set.of(
115+
AnnotatedClassWithoutSubTypes.class));
116+
117+
var modelResolver = new ModelResolver(new ObjectMapper());
118+
var customizer = new ForceSchemaCustomizer(modelResolver, classScanner);
119+
var openApi = new OpenAPI();
120+
121+
customizer.customise(openApi);
122+
123+
var schemas = openApi.getComponents().getSchemas();
124+
assertThat(schemas).containsKey(AnnotatedClassWithoutSubTypes.class.getSimpleName());
125+
assertThat(schemas).doesNotContainKey(AnnotatedClassWithoutSubTypes.InnerClass.class.getSimpleName());
126+
}
127+
128+
@Test
129+
void shouldExcludeIgnoredClasses() {
130+
var classScanner = mock(ClassScannerUtil.ClassScanner.class);
131+
when(classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class)).thenReturn(Set.of(
132+
ClassWithIgnoredMembers.class,
133+
BaseWithIgnoredSubClass.class
134+
));
135+
when(classScanner.getSubTypesOf(BaseWithIgnoredSubClass.class)).thenReturn(Set.of(
136+
IgnoredSubClass.class,
137+
NotIgnoredSubClass.class
138+
));
139+
140+
var modelResolver = new ModelResolver(new ObjectMapper());
141+
var customizer = new ForceSchemaCustomizer(modelResolver, classScanner);
142+
var openApi = new OpenAPI();
143+
144+
customizer.customise(openApi);
145+
146+
var schemas = openApi.getComponents().getSchemas();
147+
assertThat(schemas).containsKey(ClassWithIgnoredMembers.class.getSimpleName());
148+
assertThat(schemas).containsKey(ClassWithIgnoredMembers.NotIgnoredInnerClass.class.getSimpleName());
149+
assertThat(schemas).doesNotContainKey(ClassWithIgnoredMembers.IgnoredInnerClass.class.getSimpleName());
150+
151+
assertThat(schemas).containsKey(BaseWithIgnoredSubClass.class.getSimpleName());
152+
assertThat(schemas).containsKey(NotIgnoredSubClass.class.getSimpleName());
153+
assertThat(schemas).doesNotContainKey(IgnoredSubClass.class.getSimpleName());
154+
}
155+
156+
@Test
157+
void shouldExcludeClassWhenBothAnnotatedAndIgnored() {
158+
var classScanner = mock(ClassScannerUtil.ClassScanner.class);
159+
when(classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class)).thenReturn(Set.of(
160+
AnnotatedAndIgnored.class
161+
));
162+
163+
var modelResolver = new ModelResolver(new ObjectMapper());
164+
var customizer = new ForceSchemaCustomizer(modelResolver, classScanner);
165+
var openApi = new OpenAPI();
166+
167+
customizer.customise(openApi);
168+
169+
var schemas = openApi.getComponents().getSchemas();
170+
assertThat(schemas).doesNotContainKey(AnnotatedAndIgnored.class.getSimpleName());
171+
}
172+
}

0 commit comments

Comments
 (0)