From c35786642f942d207f082b901a072db13ad15694 Mon Sep 17 00:00:00 2001 From: Padraic Slattery Date: Wed, 18 Feb 2026 14:45:34 +0100 Subject: [PATCH] Fix #17754: Convert OpenAPI types to Python types in anyOf/oneOf - Add toPythonType() helper method to convert OpenAPI types (string, array) to Python types (str, List) - Override postProcessModels() to convert anyOf/oneOf types in AbstractPythonCodegen and AbstractPythonPydanticV1Codegen - Add test case to verify anyOf with string and array types generates correct Python types Before: anyOf schemas contained ['array[string]', 'string'] After: anyOf schemas contain ['List[str]', 'str'] --- .../languages/AbstractPythonCodegen.java | 63 +++++++++++++++++-- .../AbstractPythonPydanticV1Codegen.java | 52 ++++++++++++++- .../python/PythonClientCodegenTest.java | 26 ++++++++ .../3_0/issue_17754_anyof_string_array.yaml | 27 ++++++++ 4 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/issue_17754_anyof_string_array.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 0f43180be5a0..20aa1af57937 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 @@ -829,6 +829,63 @@ public GeneratorLanguage generatorLanguage() { return GeneratorLanguage.PYTHON; } + private String toPythonType(String openApiType) { + if (openApiType == null) { + return "Any"; + } + if (openApiType.startsWith("array[")) { + String innerType = openApiType.substring(6, openApiType.length() - 1); + return "List[" + toPythonType(innerType) + "]"; + } + if (openApiType.startsWith("map[")) { + String innerType = openApiType.substring(4, openApiType.length() - 1); + return "Dict[" + innerType + "]"; + } + switch (openApiType) { + case "string": + return "str"; + case "array": + return "List"; + case "object": + return "Dict"; + case "number": + return "float"; + case "integer": + return "int"; + case "boolean": + return "bool"; + case "binary": + return "bytes"; + default: + return openApiType; + } + } + + /** + * Post-process the codegen models to convert OpenAPI types to Python types in anyOf/oneOf. + */ + @Override + public ModelsMap postProcessModels(ModelsMap objs) { + for (ModelMap mo : objs.getModels()) { + CodegenModel model = mo.getModel(); + if (model.anyOf != null && !model.anyOf.isEmpty()) { + Set pythonTypes = new LinkedHashSet<>(); + for (String type : model.anyOf) { + pythonTypes.add(toPythonType(type)); + } + model.anyOf = pythonTypes; + } + if (model.oneOf != null && !model.oneOf.isEmpty()) { + Set pythonTypes = new LinkedHashSet<>(); + for (String type : model.oneOf) { + pythonTypes.add(toPythonType(type)); + } + model.oneOf = pythonTypes; + } + } + return postProcessModelsEnum(objs); + } + @Override public Map postProcessAllModels(Map objs) { final Map processed = super.postProcessAllModels(objs); @@ -1389,12 +1446,6 @@ public String addRegularExpressionDelimiter(String pattern) { return pattern; } - @Override - public ModelsMap postProcessModels(ModelsMap objs) { - // process enum in models - return postProcessModelsEnum(objs); - } - @Override public String toEnumVarName(String name, String datatype) { if ("int".equals(datatype) || "float".equals(datatype)) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonPydanticV1Codegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonPydanticV1Codegen.java index 7591c1df7c37..09b8707e9cc2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonPydanticV1Codegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonPydanticV1Codegen.java @@ -1937,10 +1937,60 @@ public String addRegularExpressionDelimiter(String pattern) { return pattern; } + private String toPythonType(String openApiType) { + if (openApiType == null) { + return "Any"; + } + if (openApiType.startsWith("array[")) { + String innerType = openApiType.substring(6, openApiType.length() - 1); + return "List[" + toPythonType(innerType) + "]"; + } + if (openApiType.startsWith("map[")) { + String innerType = openApiType.substring(4, openApiType.length() - 1); + return "Dict[" + innerType + "]"; + } + switch (openApiType) { + case "string": + return "str"; + case "array": + return "List"; + case "object": + return "Dict"; + case "number": + return "float"; + case "integer": + return "int"; + case "boolean": + return "bool"; + case "binary": + return "bytes"; + default: + return openApiType; + } + } + @Override public ModelsMap postProcessModels(ModelsMap objs) { // process enum in models - return postProcessModelsEnum(objs); + ModelsMap processed = postProcessModelsEnum(objs); + for (ModelMap modelMap : processed.getModels()) { + CodegenModel model = modelMap.getModel(); + if (model.anyOf != null && !model.anyOf.isEmpty()) { + Set pythonTypes = new LinkedHashSet<>(); + for (String type : model.anyOf) { + pythonTypes.add(toPythonType(type)); + } + model.anyOf = pythonTypes; + } + if (model.oneOf != null && !model.oneOf.isEmpty()) { + Set pythonTypes = new LinkedHashSet<>(); + for (String type : model.oneOf) { + pythonTypes.add(toPythonType(type)); + } + model.oneOf = pythonTypes; + } + } + return processed; } @Override 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 a18f2f40d11b..e215e0a19468 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 @@ -685,4 +685,30 @@ public void testNonPoetry1LicenseFormat() throws IOException { // Verify it does NOT use the legacy string format TestUtils.assertFileNotContains(pyprojectPath, "license = \"BSD-3-Clause\""); } + + @Test(description = "Test anyOf with string and array types - issue #17754") + public void testAnyOfStringArrayTypes() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("python") + .setInputSpec("src/test/resources/3_0/issue_17754_anyof_string_array.yaml") + .setOutputDir(output.getAbsolutePath()); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + // The anyOf creates an inline model - check that model + Path valueModelPath = Paths.get(output.getAbsolutePath(), "openapi_client", "models", "key_value_pair_value.py"); + TestUtils.assertFileExists(valueModelPath); + + String content = Files.readAllLines(valueModelPath).stream().collect(Collectors.joining("\n")); + + // The anyOf schemas should contain Python types (str, List[str]) not OpenAPI types (string, array) + // Check that ANY_OF_SCHEMAS contains "List[str]" and "str" + Assert.assertTrue(content.contains("KEYVALUEPAIRVALUE_ANY_OF_SCHEMAS = [\"List[str]\", \"str\"]"), + "Expected anyOf schemas to contain Python types: List[str], str"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_17754_anyof_string_array.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_17754_anyof_string_array.yaml new file mode 100644 index 000000000000..285b690da311 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_17754_anyof_string_array.yaml @@ -0,0 +1,27 @@ +openapi: 3.1.0 +info: + title: Test anyOf with string and array + version: 1.0.0 +paths: + /: + get: + responses: + '200': + description: Request successful + content: + application/json: + schema: + $ref: '#/components/schemas/KeyValuePair' +components: + schemas: + KeyValuePair: + properties: + key: + type: string + value: + anyOf: + - items: + type: string + type: array + - type: string + type: object