From e4e1b9758b569d061efda51ccad3ac35bf4b8d83 Mon Sep 17 00:00:00 2001 From: Andrey Markelov Date: Wed, 30 Sep 2026 13:25:10 -0700 Subject: [PATCH 1/3] fix(python): validate string enum values excluded by not --- .../languages/AbstractPythonCodegen.java | 4 +++ .../languages/PythonClientCodegen.java | 14 ++++++++ .../resources/python/model_generic.mustache | 20 +++++++++++ .../python/PythonClientCodegenTest.java | 29 +++++++++++++++ .../resources/3_1/python/oneof-not-enum.yaml | 35 +++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java index b08a6f518719..0a061f851be3 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java @@ -1119,6 +1119,10 @@ private ModelsMap postProcessModelsMap(ModelsMap objs) { readOnlyFields.add(cp.name); } + if (cp.vendorExtensions.containsKey("x-python-not-string-enum-values")) { + moduleImports.add(PYDANTIC, "field_validator"); + } + String typing = pydantic.generatePythonType(cp); cp.vendorExtensions.put(X_PY_TYPING, typing); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java index 61936225db59..b466306749da 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java @@ -1032,6 +1032,20 @@ public void postProcessParameter(CodegenParameter parameter) { @Override public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { super.postProcessModelProperty(model, property); + + if (property.getComposedSchemas() != null + && property.getComposedSchemas().getNot() != null) { + CodegenProperty notProperty = property.getComposedSchemas().getNot(); + if (notProperty.isEnum + && notProperty.isString + && notProperty.allowableValues != null + && notProperty.allowableValues.get("values") != null) { + property.vendorExtensions.put( + "x-python-not-string-enum-values", + notProperty.allowableValues.get("values")); + } + } + if (hasOneOf(model) || hasAnyOf(model)) { return; } diff --git a/modules/openapi-generator/src/main/resources/python/model_generic.mustache b/modules/openapi-generator/src/main/resources/python/model_generic.mustache index 3d1758ac7ec6..55548223c2a9 100644 --- a/modules/openapi-generator/src/main/resources/python/model_generic.mustache +++ b/modules/openapi-generator/src/main/resources/python/model_generic.mustache @@ -133,6 +133,26 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb {{/isContainer}} return value {{/isEnum}} + {{#vendorExtensions.x-python-not-string-enum-values}} + @field_validator('{{{name}}}') + def {{{name}}}_validate_not_enum(cls, value): + """Validates values excluded by not""" + {{^required}} + if value is None: + return value + + {{/required}} + {{#required}} + {{#isNullable}} + if value is None: + return value + + {{/isNullable}} + {{/required}} + if value in set([{{#vendorExtensions.x-python-not-string-enum-values}}{{#lambda.pythonStringLiteral}}{{{.}}}{{/lambda.pythonStringLiteral}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-python-not-string-enum-values}}]): + raise ValueError("must not be one of excluded enum values") + return value + {{/vendorExtensions.x-python-not-string-enum-values}} {{/vars}} {{#vendorExtensions.x-py-preprocesses-input-names}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java index 7ae88fae560b..f543d8408470 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java @@ -142,6 +142,35 @@ public void testInitialConfigValues() throws Exception { Assert.assertNull(codegen.additionalProperties().get(CodegenConstants.SOURCE_FOLDER)); } + @Test + public void testOneOfNotEnumValidation() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("python") + .setInputSpec("src/test/resources/3_1/python/oneof-not-enum.yaml") + .setOutputDir(output.getAbsolutePath()); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path other = Paths.get( + output.getAbsolutePath(), + "openapi_client", + "models", + "other.py"); + + TestUtils.assertFileContains( + other, + "from pydantic import BaseModel, ConfigDict, StrictStr, field_validator", + "@field_validator('kind')", + "def kind_validate_not_enum(cls, value):", + "if value in set([\"known\"]):", + "raise ValueError(\"must not be one of excluded enum values\")"); + } + @Test public void testSettersForConfigValues() throws Exception { final PythonClientCodegen codegen = new PythonClientCodegen(); diff --git a/modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml b/modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml new file mode 100644 index 000000000000..8d54fe6de8eb --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml @@ -0,0 +1,35 @@ +openapi: 3.1.0 + +info: + title: oneOf not enum repro + version: 1.0.0 + +paths: {} + +components: + schemas: + Known: + type: object + required: + - kind + properties: + kind: + type: string + enum: + - known + + Other: + type: object + required: + - kind + properties: + kind: + type: string + not: + enum: + - known + + TaggedUnion: + oneOf: + - $ref: '#/components/schemas/Known' + - $ref: '#/components/schemas/Other' From fdb06adbafe3fab94016be69f3bc797d30af024b Mon Sep 17 00:00:00 2001 From: Andrey Markelov Date: Wed, 30 Sep 2026 14:34:27 -0700 Subject: [PATCH 2/3] fix(python): emit not enum validator once --- .../resources/python/model_generic.mustache | 4 ++-- .../codegen/python/PythonClientCodegenTest.java | 17 ++++++++++------- .../resources/3_1/python/oneof-not-enum.yaml | 1 + 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/python/model_generic.mustache b/modules/openapi-generator/src/main/resources/python/model_generic.mustache index 55548223c2a9..67ee274208cf 100644 --- a/modules/openapi-generator/src/main/resources/python/model_generic.mustache +++ b/modules/openapi-generator/src/main/resources/python/model_generic.mustache @@ -133,7 +133,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb {{/isContainer}} return value {{/isEnum}} - {{#vendorExtensions.x-python-not-string-enum-values}} + {{#vendorExtensions.x-python-not-string-enum-values.size}} @field_validator('{{{name}}}') def {{{name}}}_validate_not_enum(cls, value): """Validates values excluded by not""" @@ -152,7 +152,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb if value in set([{{#vendorExtensions.x-python-not-string-enum-values}}{{#lambda.pythonStringLiteral}}{{{.}}}{{/lambda.pythonStringLiteral}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-python-not-string-enum-values}}]): raise ValueError("must not be one of excluded enum values") return value - {{/vendorExtensions.x-python-not-string-enum-values}} + {{/vendorExtensions.x-python-not-string-enum-values.size}} {{/vars}} {{#vendorExtensions.x-py-preprocesses-input-names}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java index f543d8408470..bdf7e8337eb2 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java @@ -162,13 +162,16 @@ public void testOneOfNotEnumValidation() throws IOException { "models", "other.py"); - TestUtils.assertFileContains( - other, - "from pydantic import BaseModel, ConfigDict, StrictStr, field_validator", - "@field_validator('kind')", - "def kind_validate_not_enum(cls, value):", - "if value in set([\"known\"]):", - "raise ValueError(\"must not be one of excluded enum values\")"); + String otherContent = Files.readString(other); + + Assert.assertTrue(otherContent.contains( + "if value in set([\"known\", \"reserved\"]):")); + Assert.assertTrue(otherContent.contains( + "raise ValueError(\"must not be one of excluded enum values\")")); + + Assert.assertEquals( + otherContent.split("def kind_validate_not_enum", -1).length - 1, + 1); } @Test diff --git a/modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml b/modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml index 8d54fe6de8eb..baee94e2394e 100644 --- a/modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml +++ b/modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml @@ -28,6 +28,7 @@ components: not: enum: - known + - reserved TaggedUnion: oneOf: From 1f1b1a7b3d4894b465847765db1527b398a8ea83 Mon Sep 17 00:00:00 2001 From: Andrey Markelov Date: Wed, 30 Sep 2026 14:44:14 -0700 Subject: [PATCH 3/3] test(python): verify not enum validator registration --- .../org/openapitools/codegen/python/PythonClientCodegenTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java index bdf7e8337eb2..9fb6172292bb 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java @@ -168,6 +168,7 @@ public void testOneOfNotEnumValidation() throws IOException { "if value in set([\"known\", \"reserved\"]):")); Assert.assertTrue(otherContent.contains( "raise ValueError(\"must not be one of excluded enum values\")")); + Assert.assertTrue(otherContent.contains("@field_validator('kind')")); Assert.assertEquals( otherContent.split("def kind_validate_not_enum", -1).length - 1,