Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 17 additions & 6 deletions src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,20 +412,31 @@ internal static IEnumerable<KeyValuePair<string, string>> 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<OpenApiSchema>().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<OpenApiSchema>().FirstOrDefault(allOfEvaluatorForMappings) is { } allOfEntry)
return GetDiscriminatorMappings(allOfEntry, inheritanceIndex);
// Then check schema references and resolve them to find discriminator mappings
if (schema.AllOf?.OfType<OpenApiSchemaReference>()
.Select(static x => x.RecursiveTarget)
.OfType<OpenApiSchema>()
.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
Expand Down
131 changes: 131 additions & 0 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, IOpenApiSchema> {
{
"id", new OpenApiSchema {
Type = JsonSchemaType.String
}
},
{
"$type", new OpenApiSchema {
Type = JsonSchemaType.String
}
}
},
Required = new HashSet<string> {
"$type"
},
Discriminator = new()
{
PropertyName = "$type",
Mapping = new Dictionary<string, OpenApiSchemaReference> {
{
"typeA", new OpenApiSchemaReference("ResultTypeA")
},
{
"typeB", new OpenApiSchemaReference("ResultTypeB")
}
}
},
};
var resultTypeASchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
AllOf = new List<IOpenApiSchema> {
new OpenApiSchemaReference("OperationResult"),
new OpenApiSchema {
Properties = new Dictionary<string, IOpenApiSchema> {
{
"dataA", new OpenApiSchema {
Type = JsonSchemaType.String
}
}
},
}
},
};
var resultTypeBSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
AllOf = new List<IOpenApiSchema> {
new OpenApiSchemaReference("OperationResult"),
new OpenApiSchema {
Properties = new Dictionary<string, IOpenApiSchema> {
{
"dataB", new OpenApiSchema {
Type = JsonSchemaType.String
}
}
},
}
},
};
var oneOfResponseSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
OneOf = new List<IOpenApiSchema> {
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<string, IOpenApiMediaType>()
{
["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<ILogger<KiotaBuilder>>();
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<CodeClass>("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()
{
Expand Down
Loading