Skip to content

Commit 87795c7

Browse files
committed
preserving the stricter constraint when both bounds are defined. (#22943)
1 parent 1b6f408 commit 87795c7

4 files changed

Lines changed: 201 additions & 8 deletions

File tree

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,16 +1891,42 @@ private void normalizeExclusiveMinMax31(Schema<?> schema) {
18911891

18921892
// OAS 3.1 numeric exclusiveMinimum
18931893
BigDecimal exclusiveMinValue = schema.getExclusiveMinimumValue();
1894-
if (schema.getMinimum() == null && exclusiveMinValue != null) {
1895-
schema.setMinimum(exclusiveMinValue);
1896-
schema.setExclusiveMinimum(Boolean.TRUE);
1894+
if (exclusiveMinValue != null) {
1895+
BigDecimal minimum = schema.getMinimum();
1896+
1897+
if (minimum == null) {
1898+
schema.setMinimum(exclusiveMinValue);
1899+
schema.setExclusiveMinimum(Boolean.TRUE);
1900+
} else {
1901+
int cmp = exclusiveMinValue.compareTo(minimum);
1902+
1903+
if (cmp > 0) {
1904+
schema.setMinimum(exclusiveMinValue);
1905+
schema.setExclusiveMinimum(Boolean.TRUE);
1906+
} else if (cmp == 0) {
1907+
schema.setExclusiveMinimum(Boolean.TRUE);
1908+
}
1909+
}
18971910
}
18981911

18991912
// OAS 3.1 numeric exclusiveMaximum
19001913
BigDecimal exclusiveMaxValue = schema.getExclusiveMaximumValue();
1901-
if (schema.getMaximum() == null && exclusiveMaxValue != null) {
1902-
schema.setMaximum(exclusiveMaxValue);
1903-
schema.setExclusiveMaximum(Boolean.TRUE);
1914+
if (exclusiveMaxValue != null) {
1915+
BigDecimal maximum = schema.getMaximum();
1916+
1917+
if (maximum == null) {
1918+
schema.setMaximum(exclusiveMaxValue);
1919+
schema.setExclusiveMaximum(Boolean.TRUE);
1920+
} else {
1921+
int cmp = exclusiveMaxValue.compareTo(maximum);
1922+
1923+
if (cmp < 0) {
1924+
schema.setMaximum(exclusiveMaxValue);
1925+
schema.setExclusiveMaximum(Boolean.TRUE);
1926+
} else if (cmp == 0) {
1927+
schema.setExclusiveMaximum(Boolean.TRUE);
1928+
}
1929+
}
19041930
}
19051931
}
19061932

modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ public void testNormalize31Parameters() {
621621
}
622622

623623
@Test
624-
public void testNormalize31ExclusiveMinMaxNumeric() {
624+
public void testNormalize31ExclusiveMinMaxNumericOnly() {
625625
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
626626

627627
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
@@ -634,13 +634,116 @@ public void testNormalize31ExclusiveMinMaxNumeric() {
634634
.get(0)
635635
.getSchema();
636636

637+
// exclusiveMinimum: 0
638+
assertEquals(new BigDecimal("0"), schema.getExclusiveMinimumValue());
637639
assertEquals(new BigDecimal("0"), schema.getMinimum());
638640
assertEquals(Boolean.TRUE, schema.getExclusiveMinimum());
639641

642+
// exclusiveMaximum: 10
643+
assertEquals(new BigDecimal("10"), schema.getExclusiveMaximumValue());
640644
assertEquals(new BigDecimal("10"), schema.getMaximum());
641645
assertEquals(Boolean.TRUE, schema.getExclusiveMaximum());
642646
}
643647

648+
@Test
649+
public void testNormalize31ExclusiveMinMaxStricterThanMinMax() {
650+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
651+
652+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
653+
n.normalize();
654+
655+
Schema<?> schema = openAPI.getPaths()
656+
.get("/foo")
657+
.getGet()
658+
.getParameters()
659+
.get(0)
660+
.getSchema();
661+
662+
assertEquals(new BigDecimal("1"), schema.getExclusiveMinimumValue());
663+
assertEquals(new BigDecimal("1"), schema.getMinimum());
664+
assertEquals(Boolean.TRUE, schema.getExclusiveMinimum());
665+
666+
assertEquals(new BigDecimal("10"), schema.getExclusiveMaximumValue());
667+
assertEquals(new BigDecimal("10"), schema.getMaximum());
668+
assertEquals(Boolean.TRUE, schema.getExclusiveMaximum());
669+
}
670+
671+
@Test
672+
public void testNormalize31ExclusiveMinMaxEqualToMinMax() {
673+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
674+
675+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
676+
n.normalize();
677+
678+
Schema<?> schema = openAPI.getPaths()
679+
.get("/bar")
680+
.getGet()
681+
.getParameters()
682+
.get(0)
683+
.getSchema();
684+
685+
// minimum: 0 + exclusiveMinimum: 0 → must remain exclusive
686+
assertEquals(new BigDecimal("0"), schema.getExclusiveMinimumValue());
687+
assertEquals(new BigDecimal("0"), schema.getMinimum());
688+
assertEquals(Boolean.TRUE, schema.getExclusiveMinimum());
689+
690+
// maximum: 10 + exclusiveMaximum: 10 → must remain exclusive
691+
assertEquals(new BigDecimal("10"), schema.getExclusiveMaximumValue());
692+
assertEquals(new BigDecimal("10"), schema.getMaximum());
693+
assertEquals(Boolean.TRUE, schema.getExclusiveMaximum());
694+
}
695+
696+
@Test
697+
public void testNormalize31ExclusiveMinMaxInclusiveStricterThanExclusiveValue() {
698+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
699+
700+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
701+
n.normalize();
702+
703+
Schema<?> schema = openAPI.getPaths()
704+
.get("/baz")
705+
.getGet()
706+
.getParameters()
707+
.get(0)
708+
.getSchema();
709+
710+
// minimum: 5 is stricter than exclusiveMinimum: 0 (x >= 5 dominates x > 0)
711+
assertEquals(new BigDecimal("0"), schema.getExclusiveMinimumValue());
712+
assertEquals(new BigDecimal("5"), schema.getMinimum());
713+
assertNull(schema.getExclusiveMinimum());
714+
715+
// maximum: 10 is stricter than exclusiveMaximum: 11 (x <= 10 dominates x < 11)
716+
assertEquals(new BigDecimal("11"), schema.getExclusiveMaximumValue());
717+
assertEquals(new BigDecimal("10"), schema.getMaximum());
718+
assertNull(schema.getExclusiveMaximum());
719+
}
720+
721+
@Test
722+
public void testNormalize31ExclusiveMinMaxBooleanExclusiveAlreadySet() {
723+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
724+
725+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
726+
n.normalize();
727+
728+
Schema<?> schema = openAPI.getPaths()
729+
.get("/old")
730+
.getGet()
731+
.getParameters()
732+
.get(0)
733+
.getSchema();
734+
735+
// 3.0-style boolean exclusive flags should remain intact
736+
assertEquals(new BigDecimal("0"), schema.getMinimum());
737+
assertNull(schema.getExclusiveMinimum());
738+
739+
assertEquals(new BigDecimal("10"), schema.getMaximum());
740+
assertNull(schema.getExclusiveMaximum());
741+
742+
// Ensure numeric 3.1 value fields are not unexpectedly set by normalization
743+
assertNull(schema.getExclusiveMinimumValue());
744+
assertNull(schema.getExclusiveMaximumValue());
745+
}
746+
644747

645748
@Test
646749
public void testRemoveXInternal() {

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,7 @@ public void shouldGenerateExclusiveMinMaxForOAS31() throws IOException {
961961
files.keySet().stream().sorted().forEach(System.out::println);
962962

963963

964-
File apiFile = files.get("XApi.java"); // oder XApi.java je nach Tag/operation grouping
964+
File apiFile = files.get("XApi.java");
965965
assertThat(apiFile).isNotNull();
966966

967967
String content = Files.readString(apiFile.toPath());

modules/openapi-generator/src/test/resources/3_1/exclusive-min-max.yaml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,67 @@ paths:
1515
responses:
1616
"200":
1717
description: ok
18+
/foo:
19+
get:
20+
operationId: getFoo
21+
parameters:
22+
- name: foo
23+
in: query
24+
required: true
25+
schema:
26+
type: number
27+
minimum: 0
28+
exclusiveMinimum: 1
29+
maximum: 11
30+
exclusiveMaximum: 10
31+
responses:
32+
"200":
33+
description: ok
34+
/bar:
35+
get:
36+
operationId: getBar
37+
parameters:
38+
- name: bar
39+
in: query
40+
required: true
41+
schema:
42+
type: number
43+
minimum: 0
44+
exclusiveMinimum: 0
45+
maximum: 10
46+
exclusiveMaximum: 10
47+
responses:
48+
"200":
49+
description: ok
50+
/baz:
51+
get:
52+
operationId: getBaz
53+
parameters:
54+
- name: baz
55+
in: query
56+
required: true
57+
schema:
58+
type: number
59+
minimum: 5
60+
exclusiveMinimum: 0
61+
maximum: 10
62+
exclusiveMaximum: 11
63+
responses:
64+
"200":
65+
description: ok
66+
/old:
67+
get:
68+
operationId: getOld
69+
parameters:
70+
- name: old
71+
in: query
72+
required: true
73+
schema:
74+
type: number
75+
minimum: 0
76+
exclusiveMinimum: true
77+
maximum: 10
78+
exclusiveMaximum: true
79+
responses:
80+
"200":
81+
description: ok

0 commit comments

Comments
 (0)