Skip to content

Commit 5b89b47

Browse files
fix: core: preserve OAS 3.1 numeric exclusive validation constraints in composed schemas (#23053)
* test: add test case for `ExclusiveMinMax Test` (#22943) * feat: Compatibility with exclusiveMinimum in OpenAPI 3.0.0 vs. 3.1.0 (#22943) --------- Co-authored-by: 葉宗原 TsungYuan Yeh <tsungyuan.yeh@tpisoftware.com>
1 parent 20b18ad commit 5b89b47

5 files changed

Lines changed: 563 additions & 13 deletions

File tree

.geminiignore

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# .geminiignore
2+
# This file specifies intentionally untracked files that Gemini CLI should ignore.
3+
# It uses the same pattern matching rules as .gitignore.
4+
5+
# Build artifacts
6+
target/
7+
build/
8+
*.jar
9+
*.war
10+
*.ear
11+
*.class
12+
*.log
13+
14+
# IDE and editor files
15+
.idea/
16+
.vscode/
17+
*.iml
18+
*.ipr
19+
*.iws
20+
21+
# Maven/Gradle wrapper directories
22+
.mvn/wrapper/
23+
.gradle/
24+
25+
# Node.js dependencies for website
26+
website/node_modules/
27+
website/build/
28+
29+
# Generated sources by OpenAPI Generator (usually not to be touched directly)
30+
# This includes sample outputs which are generated code for various languages.
31+
samples/
32+
33+
# Temporary files
34+
tmp/
35+
.DS_Store
36+
# Eclipse
37+
.classpath
38+
.project
39+
.settings
40+
41+
# IntelliJ IDEA
42+
.idea/
43+
*.iml
44+
*.iws
45+
*.iwp
46+
.vscode/
47+
48+
# MacOS
49+
.DS_Store
50+
51+
# ReSharper
52+
*.resharper
53+
54+
# Visual Studio
55+
.vs/
56+
*.user
57+
*.suo
58+
*.sln.docstates
59+
60+
# Other
61+
*.bak
62+
*.swp
63+
*~
64+
.#*

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

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,8 @@ public static boolean hasValidation(Schema sc) {
835835
sc.getMaximum() != null ||
836836
sc.getExclusiveMaximum() != null ||
837837
sc.getExclusiveMinimum() != null ||
838+
sc.getExclusiveMaximumValue() != null ||
839+
sc.getExclusiveMinimumValue() != null ||
838840
sc.getUniqueItems() != null
839841
);
840842
}
@@ -1869,15 +1871,44 @@ public static void syncValidationProperties(Schema schema, IJsonSchemaValidation
18691871
if (multipleOf != null) vSB.withMultipleOf();
18701872

18711873
BigDecimal minimum = schema.getMinimum();
1872-
if (minimum != null) vSB.withMinimum();
1873-
18741874
BigDecimal maximum = schema.getMaximum();
1875-
if (maximum != null) vSB.withMaximum();
1876-
18771875
Boolean exclusiveMinimum = schema.getExclusiveMinimum();
1878-
if (exclusiveMinimum != null) vSB.withExclusiveMinimum();
1879-
18801876
Boolean exclusiveMaximum = schema.getExclusiveMaximum();
1877+
1878+
// === START: Added code to handle OpenAPI 3.1.0+ numeric exclusiveMinimum/exclusiveMaximum ===
1879+
// Logic synced from OpenAPINormalizer#normalizeExclusiveMinMax31()
1880+
BigDecimal exclusiveMinValue = schema.getExclusiveMinimumValue();
1881+
if (exclusiveMinValue != null) {
1882+
if (minimum == null) {
1883+
minimum = exclusiveMinValue;
1884+
exclusiveMinimum = Boolean.TRUE;
1885+
} else {
1886+
int cmp = exclusiveMinValue.compareTo(minimum);
1887+
if (cmp >= 0) {
1888+
minimum = exclusiveMinValue;
1889+
exclusiveMinimum = Boolean.TRUE;
1890+
}
1891+
}
1892+
}
1893+
1894+
BigDecimal exclusiveMaxValue = schema.getExclusiveMaximumValue();
1895+
if (exclusiveMaxValue != null) {
1896+
if (maximum == null) {
1897+
maximum = exclusiveMaxValue;
1898+
exclusiveMaximum = Boolean.TRUE;
1899+
} else {
1900+
int cmp = exclusiveMaxValue.compareTo(maximum);
1901+
if (cmp <= 0) {
1902+
maximum = exclusiveMaxValue;
1903+
exclusiveMaximum = Boolean.TRUE;
1904+
}
1905+
}
1906+
}
1907+
// === END: Added code ===
1908+
1909+
if (minimum != null) vSB.withMinimum();
1910+
if (maximum != null) vSB.withMaximum();
1911+
if (exclusiveMinimum != null) vSB.withExclusiveMinimum();
18811912
if (exclusiveMaximum != null) vSB.withExclusiveMaximum();
18821913

18831914
LinkedHashSet<String> setValidations = vSB.build();
@@ -2207,6 +2238,7 @@ public static boolean hasCommonAttributesDefined(Schema schema) {
22072238
if (schema.getNullable() != null || schema.getDefault() != null ||
22082239
schema.getMinimum() != null || schema.getMaximum() != null ||
22092240
schema.getExclusiveMaximum() != null || schema.getExclusiveMinimum() != null ||
2241+
schema.getExclusiveMaximumValue() != null || schema.getExclusiveMinimumValue() != null ||
22102242
schema.getMinLength() != null || schema.getMaxLength() != null ||
22112243
schema.getMinItems() != null || schema.getMaxItems() != null ||
22122244
schema.getReadOnly() != null || schema.getWriteOnly() != null ||
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.openapitools.codegen;
18+
19+
import static org.testng.Assert.assertEquals;
20+
import static org.testng.Assert.assertFalse;
21+
import static org.testng.Assert.assertNotNull;
22+
import static org.testng.Assert.assertTrue;
23+
24+
import java.util.Map;
25+
26+
import org.testng.annotations.Test;
27+
28+
import io.swagger.v3.oas.models.OpenAPI;
29+
import io.swagger.v3.oas.models.media.Schema;
30+
31+
public class ExclusiveMinMaxTest {
32+
33+
@Test
34+
public void testCodegen31ExclusiveMinMaxNumericOnly() {
35+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
36+
37+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
38+
n.normalize();
39+
40+
DefaultCodegen config = new DefaultCodegen();
41+
config.setOpenAPI(openAPI);
42+
43+
Schema<?> schema = openAPI.getPaths().get("/x").getGet().getParameters().get(0).getSchema();
44+
45+
CodegenProperty cp = config.fromProperty("price", schema);
46+
47+
// exclusiveMinimum: 0
48+
assertEquals(cp.getMinimum(), "0");
49+
assertTrue(cp.getExclusiveMinimum());
50+
51+
// exclusiveMaximum: 10
52+
assertEquals(cp.getMaximum(), "10");
53+
assertTrue(cp.getExclusiveMaximum());
54+
}
55+
56+
@Test
57+
public void testCodegen31ExclusiveMinMaxStricterThanMinMax() {
58+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
59+
60+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
61+
n.normalize();
62+
63+
DefaultCodegen config = new DefaultCodegen();
64+
config.setOpenAPI(openAPI);
65+
66+
Schema<?> schema = openAPI.getPaths().get("/foo").getGet().getParameters().get(0).getSchema();
67+
CodegenProperty cp = config.fromProperty("foo", schema);
68+
69+
assertEquals(cp.getMinimum(), "1");
70+
assertTrue(cp.getExclusiveMinimum());
71+
72+
assertEquals(cp.getMaximum(), "10");
73+
assertTrue(cp.getExclusiveMaximum());
74+
}
75+
76+
@Test
77+
public void testCodegen31ExclusiveMinMaxEqualToMinMax() {
78+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
79+
80+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
81+
n.normalize();
82+
83+
DefaultCodegen config = new DefaultCodegen();
84+
config.setOpenAPI(openAPI);
85+
86+
Schema<?> schema = openAPI.getPaths().get("/bar").getGet().getParameters().get(0).getSchema();
87+
CodegenProperty cp = config.fromProperty("bar", schema);
88+
89+
// minimum: 0 + exclusiveMinimum: 0 → must remain exclusive
90+
assertEquals(cp.getMinimum(), "0");
91+
assertTrue(cp.getExclusiveMinimum());
92+
93+
// maximum: 10 + exclusiveMaximum: 10 → must remain exclusive
94+
assertEquals(cp.getMaximum(), "10");
95+
assertTrue(cp.getExclusiveMaximum());
96+
}
97+
98+
@Test
99+
public void testCodegen31ExclusiveMinMaxInclusiveStricterThanExclusiveValue() {
100+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
101+
102+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
103+
n.normalize();
104+
105+
DefaultCodegen config = new DefaultCodegen();
106+
config.setOpenAPI(openAPI);
107+
108+
Schema<?> schema = openAPI.getPaths().get("/baz").getGet().getParameters().get(0).getSchema();
109+
CodegenProperty cp = config.fromProperty("baz", schema);
110+
111+
// minimum: 5 is stricter than exclusiveMinimum: 0 (x >= 5 dominates x > 0)
112+
assertEquals(cp.getMinimum(), "5");
113+
assertFalse(Boolean.TRUE.equals(cp.getExclusiveMinimum()));
114+
115+
// maximum: 10 is stricter than exclusiveMaximum: 11 (x <= 10 dominates x < 11)
116+
assertEquals(cp.getMaximum(), "10");
117+
assertFalse(Boolean.TRUE.equals(cp.getExclusiveMaximum()));
118+
}
119+
120+
@Test
121+
public void testCodegen31ExclusiveMinMaxBooleanExclusiveAlreadySet() {
122+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
123+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
124+
n.normalize();
125+
126+
DefaultCodegen config = new DefaultCodegen();
127+
config.setOpenAPI(openAPI);
128+
129+
Schema<?> schema = openAPI.getPaths().get("/old").getGet().getParameters().get(0).getSchema();
130+
CodegenProperty cp = config.fromProperty("old", schema);
131+
132+
// 3.0-style boolean exclusive flags should remain intact
133+
assertEquals(cp.getMinimum(), "0");
134+
assertFalse(Boolean.TRUE.equals(cp.getExclusiveMinimum()));
135+
136+
assertEquals(cp.getMaximum(), "10");
137+
assertFalse(Boolean.TRUE.equals(cp.getExclusiveMaximum()));
138+
}
139+
140+
@Test
141+
public void testCodegenModelWithAllOfOas30() {
142+
// Load OAS 3.0 spec (using standard boolean syntax)
143+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/exclusive-min-max.yaml");
144+
145+
// Normalize (should not affect existing 3.0 boolean flags)
146+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of());
147+
n.normalize();
148+
149+
final Schema<?> priceDto3Schema = openAPI.getComponents().getSchemas().get("PriceDto3");
150+
final CodegenConfig config = new DefaultCodegen();
151+
config.setOpenAPI(openAPI);
152+
final CodegenModel cm = config.fromModel("PriceDto3", priceDto3Schema);
153+
154+
final CodegenProperty historyPrice2Prop = cm
155+
.getVars()
156+
.stream()
157+
.filter(p -> "historyPrice2".equals(p.baseName))
158+
.findFirst()
159+
.orElse(null);
160+
161+
assertNotNull(historyPrice2Prop, "OAS 3.0: PriceDto3 should contain historyPrice2");
162+
163+
// Verify that under OAS 3.0, allOf composition correctly preserves the
164+
// exclusiveMinimum flag
165+
assertEquals(historyPrice2Prop.getMinimum(), "3001");
166+
assertTrue(historyPrice2Prop.getExclusiveMinimum(), "OAS 3.0: historyPrice2 exclusiveMinimum should be true");
167+
}
168+
169+
@Test
170+
public void testCodegenModelWithAllOfNormalization() {
171+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
172+
173+
// 1. Run normalization
174+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
175+
n.normalize();
176+
177+
// 2. Simulate Codegen processing PriceDto3 (which uses allOf to reference
178+
// PriceDto2)
179+
final Schema<?> priceDto3Schema = openAPI.getComponents().getSchemas().get("PriceDto3");
180+
final CodegenConfig config = new DefaultCodegen();
181+
config.setOpenAPI(openAPI);
182+
final CodegenModel cm = config.fromModel("PriceDto3", priceDto3Schema);
183+
184+
// 3. Check historyPrice2 property inherited from PriceDto2
185+
final CodegenProperty historyPrice2Prop = cm
186+
.getVars()
187+
.stream()
188+
.filter(p -> "historyPrice2".equals(p.baseName))
189+
.findFirst()
190+
.orElse(null);
191+
192+
assertNotNull(historyPrice2Prop, "PriceDto3 should contain inherited historyPrice2 property from PriceDto2");
193+
194+
// Verify that CodegenProperty received normalized values and flags
195+
assertEquals(historyPrice2Prop.getMinimum(), "3101", "historyPrice2 minimum should be 3101");
196+
assertTrue(historyPrice2Prop.getExclusiveMinimum(), "historyPrice2 exclusiveMinimum should be true");
197+
}
198+
199+
}

0 commit comments

Comments
 (0)