From 13e1e52a1ad5675196e64e32adb07b42e335a846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Stadtm=C3=BCller?= Date: Fri, 30 Jan 2026 08:56:52 +0100 Subject: [PATCH 1/2] bugfix for #7339 Fix discriminator keys for oneOf with allOf-inherited types GetDiscriminatorMappings() now resolves OpenApiSchemaReference in allOf to find base type discriminator mappings, instead of falling back to using schema names as keys. --- .../Extensions/OpenApiSchemaExtensions.cs | 23 ++- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 131 ++++++++++++++++++ 2 files changed, 148 insertions(+), 6 deletions(-) diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index 8d8f93bf25..7e0d868491 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -412,20 +412,31 @@ internal static IEnumerable> GetDiscriminatorMappin if (schema == null) return []; if (schema.Discriminator?.Mapping is not { Count: > 0 }) + { if (schema.OneOf is { Count: > 0 }) return schema.OneOf.SelectMany(x => GetDiscriminatorMappings(x, inheritanceIndex)); - else if (schema.AnyOf is { Count: > 0 }) + if (schema.AnyOf is { Count: > 0 }) return schema.AnyOf.SelectMany(x => GetDiscriminatorMappings(x, inheritanceIndex)); - else if (schema.IsInherited() && schema.AllOf?.OfType().FirstOrDefault(allOfEvaluatorForMappings) is { } allOfEntry) + if (schema.IsInherited()) + { // ensure we're in an inheritance context and get the discriminator from the parent when available - return GetDiscriminatorMappings(allOfEntry, inheritanceIndex); - else if (schema.GetReferenceId() is string refId && !string.IsNullOrEmpty(refId)) + // First check inline schemas + if (schema.AllOf?.OfType().FirstOrDefault(allOfEvaluatorForMappings) is { } allOfEntry) + return GetDiscriminatorMappings(allOfEntry, inheritanceIndex); + // Then check schema references and resolve them to find discriminator mappings + if (schema.AllOf?.OfType() + .Select(static x => x.RecursiveTarget) + .OfType() + .FirstOrDefault(allOfEvaluatorForMappings) is { } allOfRefTarget) + return GetDiscriminatorMappings(allOfRefTarget, inheritanceIndex); + } + if (schema.GetReferenceId() is string refId && !string.IsNullOrEmpty(refId)) return GetAllInheritanceSchemaReferences(refId, inheritanceIndex) .Where(static x => !string.IsNullOrEmpty(x)) .Select(x => KeyValuePair.Create(x, x)) .Union([KeyValuePair.Create(refId, refId)]); - else - return []; + return []; + } return schema.Discriminator .Mapping diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index a332cf63c1..f7cf86054a 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -3529,6 +3529,137 @@ public async Task AddsDiscriminatorMappingsAllOfImplicitAsync() Assert.Empty(userClass.DiscriminatorInformation.DiscriminatorMappings); } + [Fact] + public async Task AddsDiscriminatorMappingsForOneOfWithDerivedTypesFromBaseWithMappingAsync() + { + // Test case: oneOf contains derived types (via allOf) where the discriminator mapping is defined on the base type + // Expected: The discriminator keys from the base type's mapping should be used, not the schema names + var baseSchema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary { + { + "id", new OpenApiSchema { + Type = JsonSchemaType.String + } + }, + { + "$type", new OpenApiSchema { + Type = JsonSchemaType.String + } + } + }, + Required = new HashSet { + "$type" + }, + Discriminator = new() + { + PropertyName = "$type", + Mapping = new Dictionary { + { + "typeA", new OpenApiSchemaReference("ResultTypeA") + }, + { + "typeB", new OpenApiSchemaReference("ResultTypeB") + } + } + }, + }; + var resultTypeASchema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + AllOf = new List { + new OpenApiSchemaReference("OperationResult"), + new OpenApiSchema { + Properties = new Dictionary { + { + "dataA", new OpenApiSchema { + Type = JsonSchemaType.String + } + } + }, + } + }, + }; + var resultTypeBSchema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + AllOf = new List { + new OpenApiSchemaReference("OperationResult"), + new OpenApiSchema { + Properties = new Dictionary { + { + "dataB", new OpenApiSchema { + Type = JsonSchemaType.String + } + } + }, + } + }, + }; + var oneOfResponseSchema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + OneOf = new List { + new OpenApiSchemaReference("ResultTypeA"), + new OpenApiSchemaReference("ResultTypeB") + }, + }; + var document = new OpenApiDocument + { + Paths = new OpenApiPaths + { + ["objects"] = new OpenApiPathItem + { + Operations = new() + { + [NetHttpMethod.Get] = new OpenApiOperation + { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponseReference("OperationResponse"), + } + } + } + } + }, + }; + var operationResponse = new OpenApiResponse + { + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchemaReference("OneOfResponse", document) + } + }, + }; + document.AddComponent("OperationResult", baseSchema); + document.AddComponent("ResultTypeA", resultTypeASchema); + document.AddComponent("ResultTypeB", resultTypeBSchema); + document.AddComponent("OneOfResponse", oneOfResponseSchema); + document.AddComponent("OperationResponse", operationResponse); + document.SetReferenceHostDocument(); + var mockLogger = new Mock>(); + var config = new GenerationConfiguration { ClientClassName = "TestClient", ApiRootUrl = "https://localhost" }; + var builder = new KiotaBuilder(mockLogger.Object, config, _httpClient); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + await builder.ApplyLanguageRefinementAsync(config, codeModel, CancellationToken.None); + + var oneOfResponseClass = codeModel.FindChildByName("OneOfResponse"); + Assert.NotNull(oneOfResponseClass); + + // The discriminator mappings on the oneOf wrapper class should use the keys from OperationResult's discriminator mapping + var mappings = oneOfResponseClass.DiscriminatorInformation.DiscriminatorMappings.ToList(); + Assert.Equal(2, mappings.Count); + // Bug fix verification: keys should be "typeA" and "typeB" (from base type mapping), not "ResultTypeA" and "ResultTypeB" (schema names) + Assert.Contains("typeA", mappings.Select(static x => x.Key)); + Assert.Contains("typeB", mappings.Select(static x => x.Key)); + Assert.DoesNotContain("ResultTypeA", mappings.Select(static x => x.Key)); + Assert.DoesNotContain("ResultTypeB", mappings.Select(static x => x.Key)); + } + [Fact] public async Task AddsDiscriminatorMappingsAllOfImplicitWithParentHavingMappingsWhileChildDoesNotAsync() { From ae592d6d7b9cb3de7d7aba830c1cb11932bfbd37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Stadtm=C3=BCller?= Date: Mon, 2 Feb 2026 07:24:31 +0100 Subject: [PATCH 2/2] added entry to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 095b991cf8..2bcfe9031f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Fixed a bug where discriminator mappings for oneOf types with allOf-inherited schemas would incorrectly use schema names as keys instead of resolving the base type discriminator mappings. [#7339](https://github.com/microsoft/kiota/issues/7339) - Fixed TypeScript enum imports to use `import type` for type aliases to support `verbatimModuleSyntax`. [#7332](https://github.com/microsoft/kiota/pull/7332) ## [1.30.0] - 2026-01-26