Skip to content

Commit 2404ff8

Browse files
authored
improve swagger annotations to force schemas (#50)
* improve swagger annotations to force schemas * ignore checkstyle
1 parent 32e3454 commit 2404ff8

4 files changed

Lines changed: 229 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: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
@SuppressWarnings("checkstyle:FinalClass")
33+
private static class PrivateInnerClass {
34+
public String privateField;
35+
}
36+
}
37+
38+
@ForceSwaggerSchema(includeSubTypes = false)
39+
public static class AnnotatedClassWithoutSubTypes {
40+
public String field;
41+
42+
public static class InnerClass {
43+
public String innerField;
44+
}
45+
}
46+
47+
@ForceSwaggerSchema(includeSubTypes = true)
48+
public static class BaseClass {
49+
}
50+
51+
public static class SubClass extends BaseClass {
52+
public static class SubInnerClass {
53+
}
54+
}
55+
56+
@ForceSwaggerSchema(includeSubTypes = true)
57+
public static class ClassWithIgnoredMembers {
58+
public String field;
59+
60+
@ForceSwaggerSchemaIgnore
61+
public static class IgnoredInnerClass {
62+
public String innerField;
63+
}
64+
65+
public static class NotIgnoredInnerClass {
66+
public String innerField;
67+
}
68+
}
69+
70+
@ForceSwaggerSchema(includeSubTypes = true)
71+
public static class BaseWithIgnoredSubClass {
72+
}
73+
74+
@ForceSwaggerSchemaIgnore
75+
public static class IgnoredSubClass extends BaseWithIgnoredSubClass {
76+
}
77+
78+
public static class NotIgnoredSubClass extends BaseWithIgnoredSubClass {
79+
}
80+
81+
@ForceSwaggerSchema
82+
@ForceSwaggerSchemaIgnore
83+
public static class AnnotatedAndIgnored {
84+
}
85+
86+
@Test
87+
void shouldIncludeSubTypesWhenEnabled() {
88+
var classScanner = mock(ClassScannerUtil.ClassScanner.class);
89+
when(classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class)).thenReturn(Set.of(
90+
AnnotatedClass.class,
91+
BaseClass.class
92+
));
93+
when(classScanner.getSubTypesOf(BaseClass.class)).thenReturn(Set.of(SubClass.class));
94+
95+
var modelResolver = new ModelResolver(new ObjectMapper());
96+
var customizer = new ForceSchemaCustomizer(modelResolver, classScanner);
97+
var openApi = new OpenAPI();
98+
99+
customizer.customise(openApi);
100+
101+
var schemas = openApi.getComponents().getSchemas();
102+
assertThat(schemas).containsKey(AnnotatedClass.class.getSimpleName());
103+
assertThat(schemas).containsKey(AnnotatedClass.InnerClass.class.getSimpleName());
104+
assertThat(schemas).containsKey(AnnotatedClass.InnerRecord.class.getSimpleName());
105+
assertThat(schemas).doesNotContainKey(AnnotatedClass.PrivateInnerClass.class.getSimpleName());
106+
107+
assertThat(schemas).containsKey(BaseClass.class.getSimpleName());
108+
assertThat(schemas).containsKey(SubClass.class.getSimpleName());
109+
assertThat(schemas).containsKey(SubClass.SubInnerClass.class.getSimpleName());
110+
}
111+
112+
@Test
113+
void shouldNotIncludeSubTypesWhenDisabled() {
114+
var classScanner = mock(ClassScannerUtil.ClassScanner.class);
115+
when(classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class)).thenReturn(Set.of(
116+
AnnotatedClassWithoutSubTypes.class));
117+
118+
var modelResolver = new ModelResolver(new ObjectMapper());
119+
var customizer = new ForceSchemaCustomizer(modelResolver, classScanner);
120+
var openApi = new OpenAPI();
121+
122+
customizer.customise(openApi);
123+
124+
var schemas = openApi.getComponents().getSchemas();
125+
assertThat(schemas).containsKey(AnnotatedClassWithoutSubTypes.class.getSimpleName());
126+
assertThat(schemas).doesNotContainKey(AnnotatedClassWithoutSubTypes.InnerClass.class.getSimpleName());
127+
}
128+
129+
@Test
130+
void shouldExcludeIgnoredClasses() {
131+
var classScanner = mock(ClassScannerUtil.ClassScanner.class);
132+
when(classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class)).thenReturn(Set.of(
133+
ClassWithIgnoredMembers.class,
134+
BaseWithIgnoredSubClass.class
135+
));
136+
when(classScanner.getSubTypesOf(BaseWithIgnoredSubClass.class)).thenReturn(Set.of(
137+
IgnoredSubClass.class,
138+
NotIgnoredSubClass.class
139+
));
140+
141+
var modelResolver = new ModelResolver(new ObjectMapper());
142+
var customizer = new ForceSchemaCustomizer(modelResolver, classScanner);
143+
var openApi = new OpenAPI();
144+
145+
customizer.customise(openApi);
146+
147+
var schemas = openApi.getComponents().getSchemas();
148+
assertThat(schemas).containsKey(ClassWithIgnoredMembers.class.getSimpleName());
149+
assertThat(schemas).containsKey(ClassWithIgnoredMembers.NotIgnoredInnerClass.class.getSimpleName());
150+
assertThat(schemas).doesNotContainKey(ClassWithIgnoredMembers.IgnoredInnerClass.class.getSimpleName());
151+
152+
assertThat(schemas).containsKey(BaseWithIgnoredSubClass.class.getSimpleName());
153+
assertThat(schemas).containsKey(NotIgnoredSubClass.class.getSimpleName());
154+
assertThat(schemas).doesNotContainKey(IgnoredSubClass.class.getSimpleName());
155+
}
156+
157+
@Test
158+
void shouldExcludeClassWhenBothAnnotatedAndIgnored() {
159+
var classScanner = mock(ClassScannerUtil.ClassScanner.class);
160+
when(classScanner.getClassesAnnotatedWith(ForceSwaggerSchema.class)).thenReturn(Set.of(
161+
AnnotatedAndIgnored.class
162+
));
163+
164+
var modelResolver = new ModelResolver(new ObjectMapper());
165+
var customizer = new ForceSchemaCustomizer(modelResolver, classScanner);
166+
var openApi = new OpenAPI();
167+
168+
customizer.customise(openApi);
169+
170+
var schemas = openApi.getComponents().getSchemas();
171+
assertThat(schemas).doesNotContainKey(AnnotatedAndIgnored.class.getSimpleName());
172+
}
173+
}

0 commit comments

Comments
 (0)