diff --git a/modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache b/modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache index 312cda2f1ccd..ec8c1bde0459 100644 --- a/modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache +++ b/modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache @@ -14,14 +14,41 @@ try container.encode(value) {{/oneOf}} {{#oneOfUnknownDefaultCase}} - case unknownDefaultOpenApi(let type): + case .unknownDefaultOpenApi: try container.encodeNil() {{/oneOfUnknownDefaultCase}} } } +{{#discriminator}} + + private enum DiscriminatorCodingKey: String, CodingKey { + case {{discriminator.propertyName}} = "{{discriminator.propertyBaseName}}" + } +{{/discriminator}} public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() +{{#discriminator}} let keyedContainer = try decoder.container(keyedBy: DiscriminatorCodingKey.self) + let discriminatorValue = try keyedContainer.decode(String.self, forKey: .{{discriminator.propertyName}}) + + switch discriminatorValue { + {{#discriminator.mappedModels}} + case "{{mappingName}}": + self = .type{{modelName}}(try {{modelName}}(from: decoder)) + {{/discriminator.mappedModels}} + default: + {{#oneOfUnknownDefaultCase}} + self = .unknownDefaultOpenApi + {{/oneOfUnknownDefaultCase}} + {{^oneOfUnknownDefaultCase}} + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown discriminator value '\(discriminatorValue)' for {{classname}}" + ) + ) + {{/oneOfUnknownDefaultCase}} + } +{{/discriminator}}{{^discriminator}} let container = try decoder.singleValueContainer() {{#oneOf}} {{#-first}} if let value = try? container.decode({{.}}.self) { @@ -39,5 +66,5 @@ throw DecodingError.typeMismatch(Self.Type.self, .init(codingPath: decoder.codingPath, debugDescription: "Unable to decode instance of {{classname}}")) {{/oneOfUnknownDefaultCase}} } - } +{{/discriminator}} } } diff --git a/modules/openapi-generator/src/main/resources/swift6/modelOneOf.mustache b/modules/openapi-generator/src/main/resources/swift6/modelOneOf.mustache index 06205f18249c..abcf9782c7fc 100644 --- a/modules/openapi-generator/src/main/resources/swift6/modelOneOf.mustache +++ b/modules/openapi-generator/src/main/resources/swift6/modelOneOf.mustache @@ -14,14 +14,41 @@ try container.encode(value) {{/oneOf}} {{#oneOfUnknownDefaultCase}} - case unknownDefaultOpenApi(let type): + case .unknownDefaultOpenApi: try container.encodeNil() {{/oneOfUnknownDefaultCase}} } } +{{#discriminator}} + + private enum DiscriminatorCodingKey: String, CodingKey { + case {{discriminator.propertyName}} = "{{discriminator.propertyBaseName}}" + } +{{/discriminator}} public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() +{{#discriminator}} let keyedContainer = try decoder.container(keyedBy: DiscriminatorCodingKey.self) + let discriminatorValue = try keyedContainer.decode(String.self, forKey: .{{discriminator.propertyName}}) + + switch discriminatorValue { + {{#discriminator.mappedModels}} + case "{{mappingName}}": + self = .type{{#transformArrayType}}{{modelName}}{{/transformArrayType}}(try {{modelName}}(from: decoder)) + {{/discriminator.mappedModels}} + default: + {{#oneOfUnknownDefaultCase}} + self = .unknownDefaultOpenApi + {{/oneOfUnknownDefaultCase}} + {{^oneOfUnknownDefaultCase}} + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown discriminator value '\(discriminatorValue)' for {{classname}}" + ) + ) + {{/oneOfUnknownDefaultCase}} + } +{{/discriminator}}{{^discriminator}} let container = try decoder.singleValueContainer() {{#oneOf}} {{#-first}} if let value = try? container.decode({{.}}.self) { @@ -39,5 +66,5 @@ throw DecodingError.typeMismatch(Self.Type.self, .init(codingPath: decoder.codingPath, debugDescription: "Unable to decode instance of {{classname}}")) {{/oneOfUnknownDefaultCase}} } - } +{{/discriminator}} } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java index 8ce28b3467e4..712b86e1e2e9 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java @@ -317,4 +317,42 @@ public void oneOfFormParameterTest() { } + @Test(description = "test oneOf with discriminator generates discriminator-first decoding", enabled = true) + public void oneOfDiscriminatorFirstDecodingTest() throws IOException { + Path target = Files.createTempDirectory("test"); + File output = target.toFile(); + try { + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("swift5") + .setInputSpec("src/test/resources/3_0/oneOfDiscriminator.yaml") + .setOutputDir(target.toAbsolutePath().toString()); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(false); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + List files = generator.opts(clientOptInput).generate(); + + File modelFile = files.stream() + .filter(f -> f.getName().equals("FruitOneOfEnumMappingDisc.swift")) + .findFirst() + .orElseThrow(() -> new RuntimeException("FruitOneOfEnumMappingDisc.swift not found")); + + String content = Files.readString(modelFile.toPath()); + + // Verify discriminator-first decoding pattern + Assert.assertTrue(content.contains("private enum DiscriminatorCodingKey: String, CodingKey")); + Assert.assertTrue(content.contains("let keyedContainer = try decoder.container(keyedBy: DiscriminatorCodingKey.self)")); + Assert.assertTrue(content.contains("switch discriminatorValue")); + Assert.assertTrue(content.contains("case \"APPLE\":")); + Assert.assertTrue(content.contains("self = .typeAppleOneOfEnumMappingDisc(try AppleOneOfEnumMappingDisc(from: decoder))")); + Assert.assertFalse(content.contains("if let value = try? container.decode(AppleOneOfEnumMappingDisc.self)")); + + } finally { + output.deleteOnExit(); + } + } + } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java index 4fcfe0d9728f..1bba4234b30f 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java @@ -363,4 +363,42 @@ public void oneOfArrayTypeNamesTest() throws IOException { output.deleteOnExit(); } } + + @Test(description = "test oneOf with discriminator generates discriminator-first decoding", enabled = true) + public void oneOfDiscriminatorFirstDecodingTest() throws IOException { + Path target = Files.createTempDirectory("test"); + File output = target.toFile(); + try { + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("swift6") + .setInputSpec("src/test/resources/3_0/oneOfDiscriminator.yaml") + .setOutputDir(target.toAbsolutePath().toString()); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(false); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + List files = generator.opts(clientOptInput).generate(); + + File modelFile = files.stream() + .filter(f -> f.getName().equals("FruitOneOfEnumMappingDisc.swift")) + .findFirst() + .orElseThrow(() -> new RuntimeException("FruitOneOfEnumMappingDisc.swift not found")); + + String content = Files.readString(modelFile.toPath()); + + // Verify discriminator-first decoding pattern + Assert.assertTrue(content.contains("private enum DiscriminatorCodingKey: String, CodingKey")); + Assert.assertTrue(content.contains("let keyedContainer = try decoder.container(keyedBy: DiscriminatorCodingKey.self)")); + Assert.assertTrue(content.contains("switch discriminatorValue")); + Assert.assertTrue(content.contains("case \"APPLE\":")); + Assert.assertTrue(content.contains("self = .typeAppleOneOfEnumMappingDisc(try AppleOneOfEnumMappingDisc(from: decoder))")); + Assert.assertFalse(content.contains("if let value = try? container.decode(AppleOneOfEnumMappingDisc.self)")); + + } finally { + output.deleteOnExit(); + } + } }