Skip to content

Commit 2bd53a9

Browse files
committed
feat(validation): warn when schema default value is not in enum
Signed-off-by: Zhiwei Liang <zhiwei.liang@zliang.me>
1 parent 4f6817e commit 2bd53a9

4 files changed

Lines changed: 357 additions & 1 deletion

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,39 @@ public static List<Schema> getAllSchemas(OpenAPI openAPI) {
10931093
return allSchemas;
10941094
}
10951095

1096+
/**
1097+
* Return the list of all schemas in the entire OpenAPI document, including inline schemas
1098+
* defined in path operations (request bodies, responses, parameters, headers, callbacks)
1099+
* and schemas under components/schemas. Results are deduplicated by identity.
1100+
* This is a superset of {@link #getAllSchemas(OpenAPI)}.
1101+
*
1102+
* @param openAPI specification
1103+
* @return schemas a deduplicated list of all schemas in the document
1104+
*/
1105+
public static List<Schema> getAllSchemasInDocument(OpenAPI openAPI) {
1106+
List<Schema> allSchemas = new ArrayList<Schema>();
1107+
Set<Schema> seen = Collections.newSetFromMap(new IdentityHashMap<>());
1108+
1109+
// Visit schemas reachable from paths (inline + $ref targets)
1110+
visitOpenAPI(openAPI, (s, mimeType) -> {
1111+
if (seen.add(s)) {
1112+
allSchemas.add(s);
1113+
}
1114+
});
1115+
1116+
// Also visit components/schemas entries not reachable from any path
1117+
List<String> refSchemas = new ArrayList<String>();
1118+
getSchemas(openAPI).forEach((key, schema) -> {
1119+
visitSchema(openAPI, schema, null, refSchemas, (s, mimeType) -> {
1120+
if (seen.add(s)) {
1121+
allSchemas.add(s);
1122+
}
1123+
});
1124+
});
1125+
1126+
return allSchemas;
1127+
}
1128+
10961129
/**
10971130
* If a RequestBody contains a reference to another RequestBody with '$ref', returns the referenced RequestBody if it is found or the actual RequestBody in the other cases.
10981131
*
@@ -1131,7 +1164,7 @@ public static RequestBody getRequestBody(OpenAPI openAPI, String name) {
11311164
*/
11321165
public static ApiResponse getReferencedApiResponse(OpenAPI openAPI, ApiResponse apiResponse) {
11331166
if (apiResponse != null && StringUtils.isNotEmpty(apiResponse.get$ref())) {
1134-
String name = getSimpleRef(apiResponse.get$ref());
1167+
String name = getSimpleRef(apiResponse.get$ref(schemas));
11351168
ApiResponse referencedApiResponse = getApiResponse(openAPI, name);
11361169
if (referencedApiResponse != null) {
11371170
return referencedApiResponse;

modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiEvaluator.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ public ValidationResult validate(OpenAPI specification) {
5656
validationResult.consume(schemaValidations.validate(wrapper));
5757
});
5858

59+
// Per-occurrence check: default value not in enum.
60+
// Uses getAllSchemasInDocument to also cover inline schemas in path operations.
61+
if (ruleConfiguration.isEnableRecommendations()
62+
&& ruleConfiguration.isEnableDefaultNotInEnumRecommendation()) {
63+
ValidationRule defaultNotInEnumRule = ValidationRule.create(Severity.WARNING,
64+
"Schema has default value not in enum",
65+
"While technically valid, a default outside the enum may cause "
66+
+ "generators to emit incorrect default values.",
67+
s -> ValidationRule.Pass.empty());
68+
for (Schema schema : ModelUtils.getAllSchemasInDocument(specification)) {
69+
List<?> enumList = schema.getEnum();
70+
Object defaultValue = schema.getDefault();
71+
if (enumList != null && !enumList.isEmpty()
72+
&& defaultValue != null
73+
&& !enumList.contains(defaultValue)) {
74+
validationResult.addResult(Validated.invalid(defaultNotInEnumRule,
75+
String.format(Locale.ROOT,
76+
"Schema has default value '%s' not in enum %s",
77+
defaultValue, enumList)));
78+
}
79+
}
80+
}
81+
5982
List<Parameter> parameters = new ArrayList<>(50);
6083

6184
Paths paths = specification.getPaths();

modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/RuleConfiguration.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,20 @@ public class RuleConfiguration {
138138
* @param enableApiRequestUriWithBodyRecommendation <code>true</code> to enable, <code>false</code> to disable
139139
*/
140140
private boolean enableApiRequestUriWithBodyRecommendation = defaultedBoolean(propertyPrefix + ".anti-patterns.uri-unexpected-body", true);
141+
/**
142+
* -- GETTER --
143+
* Gets whether the recommendation check for default values not in enum is enabled.
144+
* <p>
145+
* JSON Schema treats 'default' as an annotation keyword — it is RECOMMENDED to validate
146+
* against the schema but not required. A default outside the enum is technically valid
147+
* but causes generators to emit incorrect default values.
148+
*
149+
* @return <code>true</code> if enabled, <code>false</code> if disabled
150+
* -- SETTER --
151+
* Enable or Disable the recommendation check for default values not in enum.
152+
* @param enableDefaultNotInEnumRecommendation <code>true</code> to enable, <code>false</code> to disable
153+
*/
154+
private boolean enableDefaultNotInEnumRecommendation = defaultedBoolean(propertyPrefix + ".default-not-in-enum", true);
141155

142156
@SuppressWarnings("SameParameterValue")
143157
private static boolean defaultedBoolean(String key, boolean defaultValue) {
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package org.openapitools.codegen.validations.oas;
2+
3+
import io.swagger.v3.oas.models.Components;
4+
import io.swagger.v3.oas.models.OpenAPI;
5+
import io.swagger.v3.oas.models.PathItem;
6+
import io.swagger.v3.oas.models.Paths;
7+
import io.swagger.v3.oas.models.Operation;
8+
import io.swagger.v3.oas.models.media.Content;
9+
import io.swagger.v3.oas.models.media.IntegerSchema;
10+
import io.swagger.v3.oas.models.media.MediaType;
11+
import io.swagger.v3.oas.models.media.ObjectSchema;
12+
import io.swagger.v3.oas.models.media.StringSchema;
13+
import io.swagger.v3.oas.models.parameters.RequestBody;
14+
import io.swagger.v3.oas.models.responses.ApiResponse;
15+
import io.swagger.v3.oas.models.responses.ApiResponses;
16+
import org.openapitools.codegen.validation.Invalid;
17+
import org.openapitools.codegen.validation.ValidationResult;
18+
import org.testng.Assert;
19+
import org.testng.annotations.Test;
20+
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
24+
25+
public class OpenApiEvaluatorTest {
26+
27+
private static OpenAPI buildSpecWithEnumDefault(List<?> enumValues, Object defaultValue) {
28+
OpenAPI openAPI = new OpenAPI();
29+
openAPI.openapi("3.0.1");
30+
Components components = new Components();
31+
ObjectSchema obj = new ObjectSchema();
32+
StringSchema prop = new StringSchema();
33+
prop.setEnum(enumValues.stream()
34+
.filter(v -> v instanceof String)
35+
.map(v -> (String) v)
36+
.collect(Collectors.toList()));
37+
prop.setDefault(defaultValue);
38+
obj.addProperty("protocol", prop);
39+
components.addSchemas("Config", obj);
40+
openAPI.setComponents(components);
41+
return openAPI;
42+
}
43+
44+
private static List<Invalid> getDefaultNotInEnumWarnings(ValidationResult result) {
45+
return result.getWarnings().stream()
46+
.filter(i -> i.getMessage().contains("not in enum"))
47+
.collect(Collectors.toList());
48+
}
49+
50+
@Test(description = "warn when default is not in enum")
51+
public void testDefaultNotInEnum() {
52+
RuleConfiguration config = new RuleConfiguration();
53+
config.setEnableRecommendations(true);
54+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
55+
56+
OpenAPI openAPI = buildSpecWithEnumDefault(Arrays.asList("udp", "tcp"), "http");
57+
ValidationResult result = evaluator.validate(openAPI);
58+
59+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
60+
Assert.assertEquals(warnings.size(), 1);
61+
Assert.assertTrue(warnings.get(0).getMessage().contains("'http'"));
62+
Assert.assertTrue(warnings.get(0).getMessage().contains("[udp, tcp]"));
63+
}
64+
65+
@Test(description = "no warning when default is in enum")
66+
public void testDefaultInEnum() {
67+
RuleConfiguration config = new RuleConfiguration();
68+
config.setEnableRecommendations(true);
69+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
70+
71+
OpenAPI openAPI = buildSpecWithEnumDefault(Arrays.asList("http", "https"), "http");
72+
ValidationResult result = evaluator.validate(openAPI);
73+
74+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
75+
Assert.assertEquals(warnings.size(), 0);
76+
}
77+
78+
@Test(description = "no warning when rule is disabled individually")
79+
public void testDefaultNotInEnumDisabledRule() {
80+
RuleConfiguration config = new RuleConfiguration();
81+
config.setEnableRecommendations(true);
82+
config.setEnableDefaultNotInEnumRecommendation(false);
83+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
84+
85+
OpenAPI openAPI = buildSpecWithEnumDefault(Arrays.asList("udp"), "http");
86+
ValidationResult result = evaluator.validate(openAPI);
87+
88+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
89+
Assert.assertEquals(warnings.size(), 0);
90+
}
91+
92+
@Test(description = "no warning when all recommendations are disabled")
93+
public void testDefaultNotInEnumRecommendationsOff() {
94+
RuleConfiguration config = new RuleConfiguration();
95+
config.setEnableRecommendations(false);
96+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
97+
98+
OpenAPI openAPI = buildSpecWithEnumDefault(Arrays.asList("udp"), "http");
99+
ValidationResult result = evaluator.validate(openAPI);
100+
101+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
102+
Assert.assertEquals(warnings.size(), 0);
103+
}
104+
105+
@Test(description = "multiple schemas with default not in enum produce separate warnings")
106+
public void testDefaultNotInEnumMultipleOccurrences() {
107+
RuleConfiguration config = new RuleConfiguration();
108+
config.setEnableRecommendations(true);
109+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
110+
111+
OpenAPI openAPI = new OpenAPI();
112+
openAPI.openapi("3.0.1");
113+
Components components = new Components();
114+
115+
ObjectSchema udpConfig = new ObjectSchema();
116+
StringSchema proto1 = new StringSchema();
117+
proto1.setEnum(Arrays.asList("udp"));
118+
proto1.setDefault("http");
119+
udpConfig.addProperty("protocol", proto1);
120+
121+
ObjectSchema tcpConfig = new ObjectSchema();
122+
StringSchema proto2 = new StringSchema();
123+
proto2.setEnum(Arrays.asList("tcp"));
124+
proto2.setDefault("http");
125+
tcpConfig.addProperty("protocol", proto2);
126+
127+
components.addSchemas("UdpConfig", udpConfig);
128+
components.addSchemas("TcpConfig", tcpConfig);
129+
openAPI.setComponents(components);
130+
131+
ValidationResult result = evaluator.validate(openAPI);
132+
133+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
134+
// Two property schemas with distinct enum values → two unique messages
135+
Assert.assertEquals(warnings.size(), 2);
136+
}
137+
138+
@Test(description = "warn for integer default not in integer enum")
139+
public void testDefaultNotInEnumInteger() {
140+
RuleConfiguration config = new RuleConfiguration();
141+
config.setEnableRecommendations(true);
142+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
143+
144+
OpenAPI openAPI = new OpenAPI();
145+
openAPI.openapi("3.0.1");
146+
Components components = new Components();
147+
ObjectSchema obj = new ObjectSchema();
148+
IntegerSchema prop = new IntegerSchema();
149+
prop.setEnum(Arrays.asList(1, 2, 3));
150+
prop.setDefault(99);
151+
obj.addProperty("code", prop);
152+
components.addSchemas("Config", obj);
153+
openAPI.setComponents(components);
154+
155+
ValidationResult result = evaluator.validate(openAPI);
156+
157+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
158+
Assert.assertEquals(warnings.size(), 1);
159+
Assert.assertTrue(warnings.get(0).getMessage().contains("'99'"));
160+
}
161+
162+
@Test(description = "no warning when schema has no enum")
163+
public void testNoEnum() {
164+
RuleConfiguration config = new RuleConfiguration();
165+
config.setEnableRecommendations(true);
166+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
167+
168+
OpenAPI openAPI = new OpenAPI();
169+
openAPI.openapi("3.0.1");
170+
Components components = new Components();
171+
ObjectSchema obj = new ObjectSchema();
172+
StringSchema prop = new StringSchema();
173+
prop.setDefault("http");
174+
obj.addProperty("protocol", prop);
175+
components.addSchemas("Config", obj);
176+
openAPI.setComponents(components);
177+
178+
ValidationResult result = evaluator.validate(openAPI);
179+
180+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
181+
Assert.assertEquals(warnings.size(), 0);
182+
}
183+
184+
@Test(description = "no warning when schema has no default")
185+
public void testNoDefault() {
186+
RuleConfiguration config = new RuleConfiguration();
187+
config.setEnableRecommendations(true);
188+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
189+
190+
OpenAPI openAPI = new OpenAPI();
191+
openAPI.openapi("3.0.1");
192+
Components components = new Components();
193+
ObjectSchema obj = new ObjectSchema();
194+
StringSchema prop = new StringSchema();
195+
prop.setEnum(Arrays.asList("udp", "tcp"));
196+
obj.addProperty("protocol", prop);
197+
components.addSchemas("Config", obj);
198+
openAPI.setComponents(components);
199+
200+
ValidationResult result = evaluator.validate(openAPI);
201+
202+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
203+
Assert.assertEquals(warnings.size(), 0);
204+
}
205+
206+
@Test(description = "warn for default not in enum in inline request body schema")
207+
public void testDefaultNotInEnumInlineRequestBody() {
208+
RuleConfiguration config = new RuleConfiguration();
209+
config.setEnableRecommendations(true);
210+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
211+
212+
OpenAPI openAPI = new OpenAPI();
213+
openAPI.openapi("3.0.1");
214+
215+
// Build an inline schema in a request body (not in components/schemas)
216+
ObjectSchema bodySchema = new ObjectSchema();
217+
StringSchema prop = new StringSchema();
218+
prop.setEnum(Arrays.asList("udp", "tcp"));
219+
prop.setDefault("http");
220+
bodySchema.addProperty("protocol", prop);
221+
222+
MediaType mediaType = new MediaType();
223+
mediaType.setSchema(bodySchema);
224+
Content content = new Content();
225+
content.addMediaType("application/json", mediaType);
226+
RequestBody requestBody = new RequestBody();
227+
requestBody.setContent(content);
228+
229+
Operation operation = new Operation();
230+
operation.setRequestBody(requestBody);
231+
operation.setResponses(new ApiResponses());
232+
233+
PathItem pathItem = new PathItem();
234+
pathItem.setPost(operation);
235+
Paths paths = new Paths();
236+
paths.addPathItem("/test", pathItem);
237+
openAPI.setPaths(paths);
238+
239+
ValidationResult result = evaluator.validate(openAPI);
240+
241+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
242+
Assert.assertEquals(warnings.size(), 1);
243+
Assert.assertTrue(warnings.get(0).getMessage().contains("'http'"));
244+
}
245+
246+
@Test(description = "warn for default not in enum in inline response schema")
247+
public void testDefaultNotInEnumInlineResponse() {
248+
RuleConfiguration config = new RuleConfiguration();
249+
config.setEnableRecommendations(true);
250+
OpenApiEvaluator evaluator = new OpenApiEvaluator(config);
251+
252+
OpenAPI openAPI = new OpenAPI();
253+
openAPI.openapi("3.0.1");
254+
255+
// Build an inline schema in a response (not in components/schemas)
256+
ObjectSchema responseSchema = new ObjectSchema();
257+
StringSchema prop = new StringSchema();
258+
prop.setEnum(Arrays.asList("tcp"));
259+
prop.setDefault("http");
260+
responseSchema.addProperty("protocol", prop);
261+
262+
MediaType mediaType = new MediaType();
263+
mediaType.setSchema(responseSchema);
264+
Content content = new Content();
265+
content.addMediaType("application/json", mediaType);
266+
ApiResponse apiResponse = new ApiResponse();
267+
apiResponse.setContent(content);
268+
ApiResponses responses = new ApiResponses();
269+
responses.addApiResponse("200", apiResponse);
270+
271+
Operation operation = new Operation();
272+
operation.setResponses(responses);
273+
274+
PathItem pathItem = new PathItem();
275+
pathItem.setGet(operation);
276+
Paths paths = new Paths();
277+
paths.addPathItem("/test", pathItem);
278+
openAPI.setPaths(paths);
279+
280+
ValidationResult result = evaluator.validate(openAPI);
281+
282+
List<Invalid> warnings = getDefaultNotInEnumWarnings(result);
283+
Assert.assertEquals(warnings.size(), 1);
284+
Assert.assertTrue(warnings.get(0).getMessage().contains("'http'"));
285+
}
286+
}

0 commit comments

Comments
 (0)