Skip to content

Commit 1b6f408

Browse files
committed
fix: support numeric exclusiveMinimum/exclusiveMaximum in OpenAPI 3.1 (#22943)
1 parent 73dcdd6 commit 1b6f408

4 files changed

Lines changed: 116 additions & 0 deletions

File tree

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.slf4j.LoggerFactory;
3434

3535
import java.lang.reflect.Constructor;
36+
import java.math.BigDecimal;
3637
import java.util.*;
3738
import java.util.function.Function;
3839
import java.util.stream.Collectors;
@@ -1789,6 +1790,8 @@ protected Schema processNormalize31Spec(Schema schema, Set<Schema> visitedSchema
17891790
return null;
17901791
}
17911792

1793+
normalizeExclusiveMinMax31(schema);
1794+
17921795
if (schema instanceof JsonSchema &&
17931796
schema.get$schema() == null &&
17941797
schema.getTypes() == null && schema.getType() == null) {
@@ -1883,6 +1886,25 @@ protected Schema processNormalize31Spec(Schema schema, Set<Schema> visitedSchema
18831886
return schema;
18841887
}
18851888

1889+
private void normalizeExclusiveMinMax31(Schema<?> schema) {
1890+
if (schema == null || schema.get$ref() != null) return;
1891+
1892+
// OAS 3.1 numeric exclusiveMinimum
1893+
BigDecimal exclusiveMinValue = schema.getExclusiveMinimumValue();
1894+
if (schema.getMinimum() == null && exclusiveMinValue != null) {
1895+
schema.setMinimum(exclusiveMinValue);
1896+
schema.setExclusiveMinimum(Boolean.TRUE);
1897+
}
1898+
1899+
// OAS 3.1 numeric exclusiveMaximum
1900+
BigDecimal exclusiveMaxValue = schema.getExclusiveMaximumValue();
1901+
if (schema.getMaximum() == null && exclusiveMaxValue != null) {
1902+
schema.setMaximum(exclusiveMaxValue);
1903+
schema.setExclusiveMaximum(Boolean.TRUE);
1904+
}
1905+
}
1906+
1907+
18861908
// ===================== end of rules =====================
18871909

18881910
protected static class Filter {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.testng.annotations.Test;
2828

2929
import java.lang.reflect.Array;
30+
import java.math.BigDecimal;
3031
import java.util.*;
3132

3233
import static org.testng.Assert.*;
@@ -619,6 +620,28 @@ public void testNormalize31Parameters() {
619620
assertNotNull(pathItem.getDelete().getParameters().get(0).getSchema().getTypes());
620621
}
621622

623+
@Test
624+
public void testNormalize31ExclusiveMinMaxNumeric() {
625+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml");
626+
627+
OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true"));
628+
n.normalize();
629+
630+
Schema<?> schema = openAPI.getPaths()
631+
.get("/x")
632+
.getGet()
633+
.getParameters()
634+
.get(0)
635+
.getSchema();
636+
637+
assertEquals(new BigDecimal("0"), schema.getMinimum());
638+
assertEquals(Boolean.TRUE, schema.getExclusiveMinimum());
639+
640+
assertEquals(new BigDecimal("10"), schema.getMaximum());
641+
assertEquals(Boolean.TRUE, schema.getExclusiveMaximum());
642+
}
643+
644+
622645
@Test
623646
public void testRemoveXInternal() {
624647
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml");

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,60 @@ public void shouldApiNameSuffixForApiClassname() throws IOException {
934934
assertThat(notExisting).isNull();
935935
}
936936

937+
@Test
938+
public void shouldGenerateExclusiveMinMaxForOAS31() throws IOException {
939+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
940+
output.deleteOnExit();
941+
942+
OpenAPI openAPI = new OpenAPIParser()
943+
.readLocation("src/test/resources/3_1/exclusive-min-max.yaml", null, new ParseOptions())
944+
.getOpenAPI();
945+
946+
SpringCodegen codegen = new SpringCodegen();
947+
codegen.setLibrary(SPRING_CLOUD_LIBRARY);
948+
codegen.setOutputDir(output.getAbsolutePath());
949+
codegen.additionalProperties().put(INTERFACE_ONLY, "true");
950+
codegen.additionalProperties().put(CodegenConstants.API_PACKAGE, "xyz.controller");
951+
codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "xyz.model");
952+
953+
ClientOptInput input = new ClientOptInput().openAPI(openAPI).config(codegen);
954+
955+
DefaultGenerator generator = new DefaultGenerator();
956+
generator.setGenerateMetadata(false);
957+
Map<String, File> files = generator.opts(input).generate().stream()
958+
.collect(Collectors.toMap(File::getName, Function.identity()));
959+
960+
System.out.println("Generated files:");
961+
files.keySet().stream().sorted().forEach(System.out::println);
962+
963+
964+
File apiFile = files.get("XApi.java"); // oder XApi.java je nach Tag/operation grouping
965+
assertThat(apiFile).isNotNull();
966+
967+
String content = Files.readString(apiFile.toPath());
968+
969+
var param = openAPI.getPaths()
970+
.get("/x").getGet().getParameters().get(0);
971+
972+
var schema = (io.swagger.v3.oas.models.media.Schema<?>) param.getSchema();
973+
974+
System.out.println("minimum=" + schema.getMinimum());
975+
System.out.println("maximum=" + schema.getMaximum());
976+
System.out.println("exclusiveMinimum=" + schema.getExclusiveMinimum());
977+
System.out.println("exclusiveMaximum=" + schema.getExclusiveMaximum());
978+
System.out.println("exclusiveMinimum class=" + (schema.getExclusiveMinimum() == null ? null : schema.getExclusiveMinimum().getClass()));
979+
980+
System.out.println("schema extensions=" + schema.getExtensions());
981+
982+
assertThat(content).contains("@DecimalMin");
983+
assertThat(content).contains("\"0\"");
984+
assertThat(content).contains("@DecimalMax");
985+
assertThat(content).contains("\"10\"");
986+
assertThat(content).contains("inclusive = false");
987+
assertThat(content).doesNotContain("inclusive = true");
988+
}
989+
990+
937991
@Test
938992
public void shouldUseTagsForClassname() throws IOException {
939993
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
openapi: 3.1.0
2+
info: { title: t, version: 1.0.0 }
3+
paths:
4+
/x:
5+
get:
6+
operationId: getX
7+
parameters:
8+
- name: price
9+
in: query
10+
required: true
11+
schema:
12+
type: number
13+
exclusiveMinimum: 0
14+
exclusiveMaximum: 10
15+
responses:
16+
"200":
17+
description: ok

0 commit comments

Comments
 (0)