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..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,6 +133,26 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb {{/isContainer}} return value {{/isEnum}} + {{#vendorExtensions.x-python-not-string-enum-values.size}} + @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.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 7ae88fae560b..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 @@ -142,6 +142,39 @@ 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"); + + 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.assertTrue(otherContent.contains("@field_validator('kind')")); + + Assert.assertEquals( + otherContent.split("def kind_validate_not_enum", -1).length - 1, + 1); + } + @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..baee94e2394e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/python/oneof-not-enum.yaml @@ -0,0 +1,36 @@ +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 + - reserved + + TaggedUnion: + oneOf: + - $ref: '#/components/schemas/Known' + - $ref: '#/components/schemas/Other'