diff --git a/modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache b/modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache index 312cda2f1ccd..4bf1ce40276d 100644 --- a/modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache +++ b/modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache @@ -14,13 +14,43 @@ 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 { + {{#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}} @@ -39,5 +69,6 @@ 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..c49411f3309d 100644 --- a/modules/openapi-generator/src/main/resources/swift6/modelOneOf.mustache +++ b/modules/openapi-generator/src/main/resources/swift6/modelOneOf.mustache @@ -14,13 +14,43 @@ 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 { + {{#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}} @@ -39,5 +69,6 @@ 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(); + } + } } diff --git a/samples/client/petstore/swift5/oneOf/PetstoreClient/Classes/OpenAPIs/Models/Fruit.swift b/samples/client/petstore/swift5/oneOf/PetstoreClient/Classes/OpenAPIs/Models/Fruit.swift index 0a7c58c949d8..ca95d10ffb06 100644 --- a/samples/client/petstore/swift5/oneOf/PetstoreClient/Classes/OpenAPIs/Models/Fruit.swift +++ b/samples/client/petstore/swift5/oneOf/PetstoreClient/Classes/OpenAPIs/Models/Fruit.swift @@ -27,6 +27,7 @@ public enum Fruit: Codable, JSONEncodable, Hashable { } } + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let value = try? container.decode(Apple.self) { diff --git a/samples/client/petstore/swift6/oneOf/PetstoreClient/Classes/OpenAPIs/Models/Fruit.swift b/samples/client/petstore/swift6/oneOf/PetstoreClient/Classes/OpenAPIs/Models/Fruit.swift index 7d60ea96189a..20c9683e14fb 100644 --- a/samples/client/petstore/swift6/oneOf/PetstoreClient/Classes/OpenAPIs/Models/Fruit.swift +++ b/samples/client/petstore/swift6/oneOf/PetstoreClient/Classes/OpenAPIs/Models/Fruit.swift @@ -24,6 +24,7 @@ public enum Fruit: Sendable, Codable, ParameterConvertible, Hashable { } } + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let value = try? container.decode(Apple.self) {