From 2c73a4ec7aad5815493e6ddb9ad74f6c9545260c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=89=E5=AE=97=E5=8E=9F=20TsungYuan=20Yeh?= Date: Wed, 25 Feb 2026 10:28:46 +0800 Subject: [PATCH 1/2] test: add test case for `ExclusiveMinMax Test` (#22943) --- .geminiignore | 64 ++++++ .../codegen/ExclusiveMinMaxTest.java | 199 ++++++++++++++++++ .../test/resources/3_0/exclusive-min-max.yaml | 131 ++++++++++++ .../test/resources/3_1/exclusive-min-max.yaml | 138 +++++++++++- 4 files changed, 525 insertions(+), 7 deletions(-) create mode 100644 .geminiignore create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/ExclusiveMinMaxTest.java create mode 100644 modules/openapi-generator/src/test/resources/3_0/exclusive-min-max.yaml diff --git a/.geminiignore b/.geminiignore new file mode 100644 index 000000000000..44b80e07ea1b --- /dev/null +++ b/.geminiignore @@ -0,0 +1,64 @@ +# .geminiignore +# This file specifies intentionally untracked files that Gemini CLI should ignore. +# It uses the same pattern matching rules as .gitignore. + +# Build artifacts +target/ +build/ +*.jar +*.war +*.ear +*.class +*.log + +# IDE and editor files +.idea/ +.vscode/ +*.iml +*.ipr +*.iws + +# Maven/Gradle wrapper directories +.mvn/wrapper/ +.gradle/ + +# Node.js dependencies for website +website/node_modules/ +website/build/ + +# Generated sources by OpenAPI Generator (usually not to be touched directly) +# This includes sample outputs which are generated code for various languages. +samples/ + +# Temporary files +tmp/ +.DS_Store +# Eclipse +.classpath +.project +.settings + +# IntelliJ IDEA +.idea/ +*.iml +*.iws +*.iwp +.vscode/ + +# MacOS +.DS_Store + +# ReSharper +*.resharper + +# Visual Studio +.vs/ +*.user +*.suo +*.sln.docstates + +# Other +*.bak +*.swp +*~ +.#* diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/ExclusiveMinMaxTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/ExclusiveMinMaxTest.java new file mode 100644 index 000000000000..ca738dcb2887 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/ExclusiveMinMaxTest.java @@ -0,0 +1,199 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Map; + +import org.testng.annotations.Test; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; + +public class ExclusiveMinMaxTest { + + @Test + public void testCodegen31ExclusiveMinMaxNumericOnly() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml"); + + OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")); + n.normalize(); + + DefaultCodegen config = new DefaultCodegen(); + config.setOpenAPI(openAPI); + + Schema schema = openAPI.getPaths().get("/x").getGet().getParameters().get(0).getSchema(); + + CodegenProperty cp = config.fromProperty("price", schema); + + // exclusiveMinimum: 0 + assertEquals(cp.getMinimum(), "0"); + assertTrue(cp.getExclusiveMinimum()); + + // exclusiveMaximum: 10 + assertEquals(cp.getMaximum(), "10"); + assertTrue(cp.getExclusiveMaximum()); + } + + @Test + public void testCodegen31ExclusiveMinMaxStricterThanMinMax() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml"); + + OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")); + n.normalize(); + + DefaultCodegen config = new DefaultCodegen(); + config.setOpenAPI(openAPI); + + Schema schema = openAPI.getPaths().get("/foo").getGet().getParameters().get(0).getSchema(); + CodegenProperty cp = config.fromProperty("foo", schema); + + assertEquals(cp.getMinimum(), "1"); + assertTrue(cp.getExclusiveMinimum()); + + assertEquals(cp.getMaximum(), "10"); + assertTrue(cp.getExclusiveMaximum()); + } + + @Test + public void testCodegen31ExclusiveMinMaxEqualToMinMax() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml"); + + OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")); + n.normalize(); + + DefaultCodegen config = new DefaultCodegen(); + config.setOpenAPI(openAPI); + + Schema schema = openAPI.getPaths().get("/bar").getGet().getParameters().get(0).getSchema(); + CodegenProperty cp = config.fromProperty("bar", schema); + + // minimum: 0 + exclusiveMinimum: 0 → must remain exclusive + assertEquals(cp.getMinimum(), "0"); + assertTrue(cp.getExclusiveMinimum()); + + // maximum: 10 + exclusiveMaximum: 10 → must remain exclusive + assertEquals(cp.getMaximum(), "10"); + assertTrue(cp.getExclusiveMaximum()); + } + + @Test + public void testCodegen31ExclusiveMinMaxInclusiveStricterThanExclusiveValue() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml"); + + OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")); + n.normalize(); + + DefaultCodegen config = new DefaultCodegen(); + config.setOpenAPI(openAPI); + + Schema schema = openAPI.getPaths().get("/baz").getGet().getParameters().get(0).getSchema(); + CodegenProperty cp = config.fromProperty("baz", schema); + + // minimum: 5 is stricter than exclusiveMinimum: 0 (x >= 5 dominates x > 0) + assertEquals(cp.getMinimum(), "5"); + assertFalse(Boolean.TRUE.equals(cp.getExclusiveMinimum())); + + // maximum: 10 is stricter than exclusiveMaximum: 11 (x <= 10 dominates x < 11) + assertEquals(cp.getMaximum(), "10"); + assertFalse(Boolean.TRUE.equals(cp.getExclusiveMaximum())); + } + + @Test + public void testCodegen31ExclusiveMinMaxBooleanExclusiveAlreadySet() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml"); + OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")); + n.normalize(); + + DefaultCodegen config = new DefaultCodegen(); + config.setOpenAPI(openAPI); + + Schema schema = openAPI.getPaths().get("/old").getGet().getParameters().get(0).getSchema(); + CodegenProperty cp = config.fromProperty("old", schema); + + // 3.0-style boolean exclusive flags should remain intact + assertEquals(cp.getMinimum(), "0"); + assertFalse(Boolean.TRUE.equals(cp.getExclusiveMinimum())); + + assertEquals(cp.getMaximum(), "10"); + assertFalse(Boolean.TRUE.equals(cp.getExclusiveMaximum())); + } + + @Test + public void testCodegenModelWithAllOfOas30() { + // Load OAS 3.0 spec (using standard boolean syntax) + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/exclusive-min-max.yaml"); + + // Normalize (should not affect existing 3.0 boolean flags) + OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of()); + n.normalize(); + + final Schema priceDto3Schema = openAPI.getComponents().getSchemas().get("PriceDto3"); + final CodegenConfig config = new DefaultCodegen(); + config.setOpenAPI(openAPI); + final CodegenModel cm = config.fromModel("PriceDto3", priceDto3Schema); + + final CodegenProperty historyPrice2Prop = cm + .getVars() + .stream() + .filter(p -> "historyPrice2".equals(p.baseName)) + .findFirst() + .orElse(null); + + assertNotNull(historyPrice2Prop, "OAS 3.0: PriceDto3 should contain historyPrice2"); + + // Verify that under OAS 3.0, allOf composition correctly preserves the + // exclusiveMinimum flag + assertEquals(historyPrice2Prop.getMinimum(), "3001"); + assertTrue(historyPrice2Prop.getExclusiveMinimum(), "OAS 3.0: historyPrice2 exclusiveMinimum should be true"); + } + + @Test + public void testCodegenModelWithAllOfNormalization() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/exclusive-min-max.yaml"); + + // 1. Run normalization + OpenAPINormalizer n = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")); + n.normalize(); + + // 2. Simulate Codegen processing PriceDto3 (which uses allOf to reference + // PriceDto2) + final Schema priceDto3Schema = openAPI.getComponents().getSchemas().get("PriceDto3"); + final CodegenConfig config = new DefaultCodegen(); + config.setOpenAPI(openAPI); + final CodegenModel cm = config.fromModel("PriceDto3", priceDto3Schema); + + // 3. Check historyPrice2 property inherited from PriceDto2 + final CodegenProperty historyPrice2Prop = cm + .getVars() + .stream() + .filter(p -> "historyPrice2".equals(p.baseName)) + .findFirst() + .orElse(null); + + assertNotNull(historyPrice2Prop, "PriceDto3 should contain inherited historyPrice2 property from PriceDto2"); + + // Verify that CodegenProperty received normalized values and flags + assertEquals(historyPrice2Prop.getMinimum(), "3101", "historyPrice2 minimum should be 3101"); + assertTrue(historyPrice2Prop.getExclusiveMinimum(), "historyPrice2 exclusiveMinimum should be true"); + } + +} diff --git a/modules/openapi-generator/src/test/resources/3_0/exclusive-min-max.yaml b/modules/openapi-generator/src/test/resources/3_0/exclusive-min-max.yaml new file mode 100644 index 000000000000..5e226bf63c52 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/exclusive-min-max.yaml @@ -0,0 +1,131 @@ +openapi: 3.0.0 +info: + title: ExclusiveMinMax Test OAS 3.0 + version: 1.0.0 +paths: + /savePrice: + post: + summary: '' + operationId: postPrice + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + price: + type: number + minimum: 0 + exclusiveMinimum: true + maximum: 10 + exclusiveMaximum: true + sellingPrice: + type: number + minimum: 0 + maximum: 10 + historys: + type: array + items: + type: object + properties: + historyPrice0: + type: number + minimum: 0 + exclusiveMinimum: true + maximum: 10 + exclusiveMaximum: true + historySellingPrice0: + type: number + minimum: 0 + maximum: 10 + required: + - historyPrice0 + - historySellingPrice0 + required: + - price + - sellingPrice + description: '' + put: + summary: '' + operationId: putPrice + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PriceDto1' +components: + schemas: + PriceDto1: + title: PriceDto1 + type: object + properties: + price: + type: number + minimum: 0 + exclusiveMinimum: true + maximum: 10 + exclusiveMaximum: true + sellingPrice: + type: number + minimum: 0 + maximum: 10 + historys: + type: array + items: + type: object + properties: + historyPrice1: + type: number + minimum: 0 + exclusiveMinimum: true + maximum: 10 + exclusiveMaximum: true + historySellingPrice1: + type: number + minimum: 0 + maximum: 10 + required: + - historyPrice1 + - historySellingPrice1 + historys2: + type: array + items: + $ref: '#/components/schemas/PriceDto2' + historys3: + type: array + items: + $ref: '#/components/schemas/PriceDto3' + required: + - price + - sellingPrice + PriceDto2: + title: PriceDto2 + type: object + properties: + historyPrice2: + type: number + minimum: 3001 + exclusiveMinimum: true + maximum: 3002 + exclusiveMaximum: true + historySellingPrice2: + type: number + minimum: 3003 + maximum: 3004 + required: + - historyPrice2 + - historySellingPrice2 + PriceDto3: + title: PriceDto3 + allOf: + - type: object + properties: + id: + type: string + - $ref: '#/components/schemas/PriceDto2' diff --git a/modules/openapi-generator/src/test/resources/3_1/exclusive-min-max.yaml b/modules/openapi-generator/src/test/resources/3_1/exclusive-min-max.yaml index 8bfefd24d83b..10c5048de6c5 100644 --- a/modules/openapi-generator/src/test/resources/3_1/exclusive-min-max.yaml +++ b/modules/openapi-generator/src/test/resources/3_1/exclusive-min-max.yaml @@ -1,5 +1,7 @@ openapi: 3.1.0 -info: { title: t, version: 1.0.0 } +info: + title: t + version: 1.0.0 paths: /x: get: @@ -13,7 +15,7 @@ paths: exclusiveMinimum: 0 exclusiveMaximum: 10 responses: - "200": + '200': description: ok /foo: get: @@ -29,7 +31,7 @@ paths: maximum: 11 exclusiveMaximum: 10 responses: - "200": + '200': description: ok /bar: get: @@ -45,7 +47,7 @@ paths: maximum: 10 exclusiveMaximum: 10 responses: - "200": + '200': description: ok /baz: get: @@ -61,7 +63,7 @@ paths: maximum: 10 exclusiveMaximum: 11 responses: - "200": + '200': description: ok /old: get: @@ -77,5 +79,127 @@ paths: maximum: 10 exclusiveMaximum: true responses: - "200": - description: ok \ No newline at end of file + '200': + description: ok + /savePrice: + post: + summary: '' + operationId: postPrice + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + type: object + properties: + price: + type: number + exclusiveMinimum: 0 + exclusiveMaximum: 10 + sellingPrice: + type: number + minimum: 0 + maximum: 10 + historys: + type: array + items: + type: object + properties: + historyPrice0: + type: number + exclusiveMinimum: 0 + exclusiveMaximum: 10 + historySellingPrice0: + type: number + minimum: 0 + maximum: 10 + required: + - historyPrice0 + - historySellingPrice0 + required: + - price + - sellingPrice + description: '' + put: + summary: '' + operationId: putPrice + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PriceDto1' +components: + schemas: + PriceDto1: + title: PriceDto1 + x-stoplight: + id: 6c700sj3ukk3o + type: object + properties: + price: + type: number + exclusiveMinimum: 0 + exclusiveMaximum: 10 + sellingPrice: + type: number + minimum: 0 + maximum: 10 + historys: + type: array + items: + type: object + properties: + historyPrice1: + type: number + exclusiveMinimum: 0 + exclusiveMaximum: 10 + historySellingPrice1: + type: number + minimum: 0 + maximum: 10 + required: + - historyPrice1 + - historySellingPrice1 + historys2: + type: array + items: + $ref: '#/components/schemas/PriceDto2' + historys3: + type: array + items: + $ref: '#/components/schemas/PriceDto3' + required: + - price + - sellingPrice + PriceDto2: + title: PriceDto2 + x-stoplight: + id: xl2koq12tdte0 + type: object + properties: + historyPrice2: + type: number + exclusiveMinimum: 3101 + exclusiveMaximum: 3102 + historySellingPrice2: + type: number + minimum: 3103 + maximum: 3104 + required: + - historyPrice2 + - historySellingPrice2 + PriceDto3: + title: PriceDto3 + x-stoplight: + id: gvpk3358r329k + allOf: + - type: object + properties: + id: + type: string + - $ref: '#/components/schemas/PriceDto2' From 2469074f4c06cac241e599fe285d33c20000dda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=89=E5=AE=97=E5=8E=9F=20TsungYuan=20Yeh?= Date: Wed, 25 Feb 2026 10:29:14 +0800 Subject: [PATCH 2/2] feat: Compatibility with exclusiveMinimum in OpenAPI 3.0.0 vs. 3.1.0 (#22943) --- .../codegen/utils/ModelUtils.java | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index a8f62b1b8331..3e355713e7f9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -835,6 +835,8 @@ public static boolean hasValidation(Schema sc) { sc.getMaximum() != null || sc.getExclusiveMaximum() != null || sc.getExclusiveMinimum() != null || + sc.getExclusiveMaximumValue() != null || + sc.getExclusiveMinimumValue() != null || sc.getUniqueItems() != null ); } @@ -1869,15 +1871,44 @@ public static void syncValidationProperties(Schema schema, IJsonSchemaValidation if (multipleOf != null) vSB.withMultipleOf(); BigDecimal minimum = schema.getMinimum(); - if (minimum != null) vSB.withMinimum(); - BigDecimal maximum = schema.getMaximum(); - if (maximum != null) vSB.withMaximum(); - Boolean exclusiveMinimum = schema.getExclusiveMinimum(); - if (exclusiveMinimum != null) vSB.withExclusiveMinimum(); - Boolean exclusiveMaximum = schema.getExclusiveMaximum(); + + // === START: Added code to handle OpenAPI 3.1.0+ numeric exclusiveMinimum/exclusiveMaximum === + // Logic synced from OpenAPINormalizer#normalizeExclusiveMinMax31() + BigDecimal exclusiveMinValue = schema.getExclusiveMinimumValue(); + if (exclusiveMinValue != null) { + if (minimum == null) { + minimum = exclusiveMinValue; + exclusiveMinimum = Boolean.TRUE; + } else { + int cmp = exclusiveMinValue.compareTo(minimum); + if (cmp >= 0) { + minimum = exclusiveMinValue; + exclusiveMinimum = Boolean.TRUE; + } + } + } + + BigDecimal exclusiveMaxValue = schema.getExclusiveMaximumValue(); + if (exclusiveMaxValue != null) { + if (maximum == null) { + maximum = exclusiveMaxValue; + exclusiveMaximum = Boolean.TRUE; + } else { + int cmp = exclusiveMaxValue.compareTo(maximum); + if (cmp <= 0) { + maximum = exclusiveMaxValue; + exclusiveMaximum = Boolean.TRUE; + } + } + } + // === END: Added code === + + if (minimum != null) vSB.withMinimum(); + if (maximum != null) vSB.withMaximum(); + if (exclusiveMinimum != null) vSB.withExclusiveMinimum(); if (exclusiveMaximum != null) vSB.withExclusiveMaximum(); LinkedHashSet setValidations = vSB.build(); @@ -2207,6 +2238,7 @@ public static boolean hasCommonAttributesDefined(Schema schema) { if (schema.getNullable() != null || schema.getDefault() != null || schema.getMinimum() != null || schema.getMaximum() != null || schema.getExclusiveMaximum() != null || schema.getExclusiveMinimum() != null || + schema.getExclusiveMaximumValue() != null || schema.getExclusiveMinimumValue() != null || schema.getMinLength() != null || schema.getMaxLength() != null || schema.getMinItems() != null || schema.getMaxItems() != null || schema.getReadOnly() != null || schema.getWriteOnly() != null ||