Skip to content

Commit 669c5a5

Browse files
committed
openapi: purge/clear unused schemas from output fix #3862
1 parent 010b0c2 commit 669c5a5

File tree

5 files changed

+235
-176
lines changed

5 files changed

+235
-176
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.openapi;
7+
8+
import java.util.*;
9+
10+
import io.swagger.v3.oas.models.OpenAPI;
11+
import io.swagger.v3.oas.models.media.Schema;
12+
13+
public class SchemaPurger {
14+
15+
public static void purgeUnused(OpenAPI openAPI) {
16+
if (openAPI == null
17+
|| openAPI.getComponents() == null
18+
|| openAPI.getComponents().getSchemas() == null) {
19+
return;
20+
}
21+
22+
Set<String> visitedSchemas = new HashSet<>();
23+
Queue<String> queue = new LinkedList<>();
24+
25+
// 1. Gather Roots (Using your visitor/parser)
26+
// Scan Paths, Parameters, Responses, and RequestBodies for $refs pointing to schemas.
27+
Set<String> rootSchemaNames = extractRootSchemaNames(openAPI);
28+
29+
for (String schemaName : rootSchemaNames) {
30+
if (visitedSchemas.add(schemaName)) {
31+
queue.add(schemaName);
32+
}
33+
}
34+
35+
Map<String, Schema> allSchemas = openAPI.getComponents().getSchemas();
36+
37+
// 2. Traverse Graph (BFS)
38+
while (!queue.isEmpty()) {
39+
String currentName = queue.poll();
40+
Schema<?> currentSchema = allSchemas.get(currentName);
41+
42+
if (currentSchema == null) continue;
43+
44+
// Scan this schema for nested $refs (properties, items, allOf, anyOf, oneOf)
45+
Set<String> nestedSchemaNames = extractSchemaNamesFromSchema(currentSchema);
46+
47+
for (String nestedName : nestedSchemaNames) {
48+
// CIRCULAR REFERENCE CHECK:
49+
// visitedSchemas.add() returns false if the element is already present.
50+
// If it's already visited, we ignore it, breaking the cycle.
51+
if (visitedSchemas.add(nestedName)) {
52+
queue.add(nestedName);
53+
}
54+
}
55+
}
56+
57+
// 3. Purge Unused
58+
// retainAll efficiently drops any key from the components map that isn't in our visited set.
59+
allSchemas.keySet().retainAll(visitedSchemas);
60+
}
61+
62+
// --- Helper Methods (To be integrated with your OpenAPI visitor) ---
63+
64+
private static Set<String> extractRootSchemaNames(OpenAPI openAPI) {
65+
Set<String> roots = new HashSet<>();
66+
67+
// 1. Scan Paths for schemas used in operations
68+
if (openAPI.getPaths() != null) {
69+
openAPI
70+
.getPaths()
71+
.values()
72+
.forEach(
73+
pathItem -> {
74+
75+
// Check path-level parameters
76+
if (pathItem.getParameters() != null) {
77+
pathItem
78+
.getParameters()
79+
.forEach(
80+
param -> roots.addAll(extractSchemaNamesFromSchema(param.getSchema())));
81+
}
82+
83+
if (pathItem.readOperations() != null) {
84+
pathItem
85+
.readOperations()
86+
.forEach(
87+
operation -> {
88+
89+
// Check operation parameters (Query, Path, Header, etc.)
90+
if (operation.getParameters() != null) {
91+
operation
92+
.getParameters()
93+
.forEach(
94+
param ->
95+
roots.addAll(
96+
extractSchemaNamesFromSchema(param.getSchema())));
97+
}
98+
99+
// Check Request Bodies
100+
if (operation.getRequestBody() != null
101+
&& operation.getRequestBody().getContent() != null) {
102+
operation
103+
.getRequestBody()
104+
.getContent()
105+
.values()
106+
.forEach(
107+
mediaType ->
108+
roots.addAll(
109+
extractSchemaNamesFromSchema(mediaType.getSchema())));
110+
}
111+
112+
// Check Responses
113+
if (operation.getResponses() != null) {
114+
operation
115+
.getResponses()
116+
.values()
117+
.forEach(
118+
response -> {
119+
if (response.getContent() != null) {
120+
response
121+
.getContent()
122+
.values()
123+
.forEach(
124+
mediaType ->
125+
roots.addAll(
126+
extractSchemaNamesFromSchema(
127+
mediaType.getSchema())));
128+
}
129+
});
130+
}
131+
});
132+
}
133+
});
134+
}
135+
136+
// 2. Scan Components for shared non-schema objects that reference schemas
137+
if (openAPI.getComponents() != null) {
138+
139+
// Shared Parameters
140+
if (openAPI.getComponents().getParameters() != null) {
141+
openAPI
142+
.getComponents()
143+
.getParameters()
144+
.values()
145+
.forEach(param -> roots.addAll(extractSchemaNamesFromSchema(param.getSchema())));
146+
}
147+
148+
// Shared Responses
149+
if (openAPI.getComponents().getResponses() != null) {
150+
openAPI
151+
.getComponents()
152+
.getResponses()
153+
.values()
154+
.forEach(
155+
response -> {
156+
if (response.getContent() != null) {
157+
response
158+
.getContent()
159+
.values()
160+
.forEach(
161+
mediaType ->
162+
roots.addAll(extractSchemaNamesFromSchema(mediaType.getSchema())));
163+
}
164+
});
165+
}
166+
167+
// Shared RequestBodies
168+
if (openAPI.getComponents().getRequestBodies() != null) {
169+
openAPI
170+
.getComponents()
171+
.getRequestBodies()
172+
.values()
173+
.forEach(
174+
requestBody -> {
175+
if (requestBody.getContent() != null) {
176+
requestBody
177+
.getContent()
178+
.values()
179+
.forEach(
180+
mediaType ->
181+
roots.addAll(extractSchemaNamesFromSchema(mediaType.getSchema())));
182+
}
183+
});
184+
}
185+
}
186+
187+
return roots;
188+
}
189+
190+
private static Set<String> extractSchemaNamesFromSchema(Schema<?> schema) {
191+
Set<String> refs = new HashSet<>();
192+
193+
// 1. Check direct ref
194+
if (schema.get$ref() != null) {
195+
refs.add(extractName(schema.get$ref()));
196+
}
197+
198+
// 2. Check properties
199+
if (schema.getProperties() != null) {
200+
schema
201+
.getProperties()
202+
.values()
203+
.forEach(prop -> refs.addAll(extractSchemaNamesFromSchema(prop)));
204+
}
205+
206+
// 3. Check arrays (items)
207+
if (schema.getItems() != null) {
208+
refs.addAll(extractSchemaNamesFromSchema(schema.getItems()));
209+
}
210+
211+
// 4. Check compositions
212+
if (schema.getAllOf() != null)
213+
schema.getAllOf().forEach(s -> refs.addAll(extractSchemaNamesFromSchema(s)));
214+
if (schema.getAnyOf() != null)
215+
schema.getAnyOf().forEach(s -> refs.addAll(extractSchemaNamesFromSchema(s)));
216+
if (schema.getOneOf() != null)
217+
schema.getOneOf().forEach(s -> refs.addAll(extractSchemaNamesFromSchema(s)));
218+
219+
// 5. Check additionalProperties (Maps)
220+
if (schema.getAdditionalProperties() instanceof Schema) {
221+
refs.addAll(extractSchemaNamesFromSchema((Schema<?>) schema.getAdditionalProperties()));
222+
}
223+
224+
return refs;
225+
}
226+
227+
private static String extractName(String ref) {
228+
return ref != null ? ref.substring(ref.lastIndexOf('/') + 1) : null;
229+
}
230+
}

modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,9 @@ public OpenAPIGenerator() {}
326326
return openapi;
327327
}
328328

329-
private void finish(OpenAPIExt openapi) {}
329+
private void finish(OpenAPIExt openapi) {
330+
SchemaPurger.purgeUnused(openapi);
331+
}
330332

331333
ObjectMapper yamlMapper() {
332334
return specVersion == SpecVersion.V30 ? Yaml.mapper() : Yaml31.mapper();

modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -161,21 +161,7 @@ public void shouldGenerateMinApp(OpenAPIResult result) {
161161
+ " type: integer\n"
162162
+ " format: int64\n"
163163
+ " name:\n"
164-
+ " type: string\n"
165-
+ " PetQuery:\n"
166-
+ " type: object\n"
167-
+ " properties:\n"
168-
+ " id:\n"
169-
+ " type: integer\n"
170-
+ " format: int64\n"
171-
+ " name:\n"
172-
+ " type: string\n"
173-
+ " start:\n"
174-
+ " type: integer\n"
175-
+ " format: int32\n"
176-
+ " max:\n"
177-
+ " type: integer\n"
178-
+ " format: int32\n",
164+
+ " type: string\n",
179165
result.toYaml());
180166
}
181167

@@ -326,21 +312,7 @@ public void shouldGenerateKtMinApp(OpenAPIResult result) {
326312
+ " type: integer\n"
327313
+ " format: int64\n"
328314
+ " name:\n"
329-
+ " type: string\n"
330-
+ " PetQuery:\n"
331-
+ " type: object\n"
332-
+ " properties:\n"
333-
+ " id:\n"
334-
+ " type: integer\n"
335-
+ " format: int64\n"
336-
+ " name:\n"
337-
+ " type: string\n"
338-
+ " start:\n"
339-
+ " type: integer\n"
340-
+ " format: int32\n"
341-
+ " max:\n"
342-
+ " type: integer\n"
343-
+ " format: int32\n",
315+
+ " type: string\n",
344316
result.toYaml());
345317
}
346318

modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -566,21 +566,6 @@ private void checkResult(OpenAPIResult result) {
566566
description: Published books.
567567
items:
568568
$ref: "#/components/schemas/Book"
569-
BookQuery:
570-
type: object
571-
properties:
572-
title:
573-
type: string
574-
description: Book's title.
575-
author:
576-
type: string
577-
description: Book's author. Optional.
578-
isbn:
579-
type: array
580-
description: Book's isbn. Optional.
581-
items:
582-
type: string
583-
description: Query books by complex filters.
584569
Address:
585570
type: object
586571
properties:

0 commit comments

Comments
 (0)