Skip to content
Open
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 @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Fixed a bug where required query parameters from one HTTP operation were leaking into the path-item-level URL template, making them appear required for sibling operations on the same path. [#7292](https://github.com/microsoft/kiota/issues/7292)
- Fixed incorrect TypeScript deserialization for properties typed as a named discriminated union (oneOf with discriminator): the generated code now uses the base type's factory method (e.g. `createWeatherForecastFromDiscriminatorValue`) instead of chaining individual subtype factories with `??`. [#6615](https://github.com/microsoft/kiota/issues/6615)
- Fixed a potential NullReferenceException in union model discriminator factory methods when a discriminator mapping key is null or empty across C#, Dart, Go, Java, PHP, and Python writers. [#7641](https://github.com/microsoft/kiota/pull/7641)
- Fixed `kiota download` returning exit code 0 (success) when no results are found or multiple ambiguous matches exist. [#7643](https://github.com/microsoft/kiota/pull/7643)
- Fixed incorrect command hints and telemetry in `kiota plugin generate` handler referencing "client" instead of "plugin". [#7642](https://github.com/microsoft/kiota/pull/7642)
Expand Down
21 changes: 19 additions & 2 deletions src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -725,8 +725,17 @@ private void WritePropertyDeserializationBlock(CodeProperty otherProp, CodeParam
}
else if (GetOriginalComposedType(otherProp.Type) is { } composedType)
{
var expression = string.Join(" ?? ", SortTypesByInheritance(composedType.Types).Select(codeType => $"n.{conventions.GetDeserializationMethodName(codeType, codeFile, composedType.IsCollection)}"));
writer.WriteLine($"\"{otherProp.WireName.SanitizeDoubleQuote()}\": n => {{ {paramName}.{propName} = {expression};{suffix} }},");
if (TypeHasBasicDiscriminatorInformation(otherProp.Type))
{
var deserializationMethodName = conventions.GetDeserializationMethodName(otherProp.Type, codeFile);
var defaultValueSuffix = GetDefaultValueSuffix(otherProp);
writer.WriteLine($"\"{otherProp.WireName.SanitizeDoubleQuote()}\": n => {{ {paramName}.{propName} = n.{deserializationMethodName}{defaultValueSuffix};{suffix} }},");
}
else
{
var expression = string.Join(" ?? ", SortTypesByInheritance(composedType.Types).Select(codeType => $"n.{conventions.GetDeserializationMethodName(codeType, codeFile, composedType.IsCollection)}"));
writer.WriteLine($"\"{otherProp.WireName.SanitizeDoubleQuote()}\": n => {{ {paramName}.{propName} = {expression};{suffix} }},");
}
}
else
{
Expand All @@ -742,6 +751,14 @@ private static string GetDefaultValueSuffix(CodeProperty otherProp)
return !string.IsNullOrEmpty(defaultValue) && !defaultValue.EqualsIgnoreCase("\"null\"") ? $" ?? {defaultValue}" : string.Empty;
}

private static bool TypeHasBasicDiscriminatorInformation(CodeTypeBase codeType) =>
codeType switch
{
CodeType { TypeDefinition: CodeInterface codeInterface } => codeInterface.OriginalClass?.DiscriminatorInformation.HasBasicDiscriminatorInformation == true,
CodeType { TypeDefinition: CodeClass codeClass } => codeClass.DiscriminatorInformation.HasBasicDiscriminatorInformation,
_ => false,
};

private static string GetDefaultValueLiteralForProperty(CodeProperty codeProperty)
{
if (string.IsNullOrEmpty(codeProperty.DefaultValue)) return string.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Kiota.Builder.Tests.OpenApiSampleFiles;

public static class DiscriminatedUnionPropertySample
{
/**
* An OpenAPI 3.0.0 sample document with a named oneOf schema (WeatherForecast) that uses a
* discriminator. WeatherSummary is a container model that references WeatherForecast as both
* a collection property and a single-object property. This validates that the TypeScript
* deserializer uses the base type factory (createWeatherForecastFromDiscriminatorValue) rather
* than chaining subtype factories with ??.
*/
public static readonly string OpenApiYaml = @"
openapi: 3.0.0
info:
title: Forecast API
version: 1.0.0
paths:
/forecast:
get:
summary: Get forecast summary
responses:
'200':
description: A weather summary
content:
application/json:
schema:
$ref: '#/components/schemas/WeatherSummary'
components:
schemas:
WeatherForecast:
oneOf:
- $ref: '#/components/schemas/RainyDayForecast'
- $ref: '#/components/schemas/SunnyDayForecast'
discriminator:
propertyName: forecastType
mapping:
rain: '#/components/schemas/RainyDayForecast'
sunny: '#/components/schemas/SunnyDayForecast'
RainyDayForecast:
type: object
properties:
forecastType:
type: string
rainAmount:
type: number
SunnyDayForecast:
type: object
properties:
forecastType:
type: string
uvIndex:
type: number
WeatherSummary:
type: object
properties:
forecasts:
type: array
items:
$ref: '#/components/schemas/WeatherForecast'
primaryForecast:
$ref: '#/components/schemas/WeatherForecast'";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2009,5 +2009,92 @@ public async Task WritesOneOfWithInheritanceDeserializationAsync()
Assert.True(deviceIndex > 0, "Should contain Device deserialization");
Assert.True(managedDeviceIndex < deviceIndex, "ManagedPrivilegedDevice should appear before Device in the deserialization chain");
}

[Fact]
public async Task Writes_DiscriminatedUnionPropertyType_UsesBaseFactoryMethodAsync()
{
var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript };
var modelNameSpace = root.AddNamespace("ApiSdk.models");

var baseClass = TestHelper.CreateModelClass(modelNameSpace, "WeatherForecast");
baseClass.DiscriminatorInformation.DiscriminatorPropertyName = "forecastType";
var rainyClass = TestHelper.CreateModelClass(modelNameSpace, "RainyDayForecast");
var sunnyClass = TestHelper.CreateModelClass(modelNameSpace, "SunnyDayForecast");
baseClass.DiscriminatorInformation.AddDiscriminatorMapping("rain", new CodeType { Name = "RainyDayForecast", TypeDefinition = rainyClass });
baseClass.DiscriminatorInformation.AddDiscriminatorMapping("sunny", new CodeType { Name = "SunnyDayForecast", TypeDefinition = sunnyClass });

var composedType = new CodeUnionType { Name = "WeatherForecast" };
composedType.AddType(
new CodeType { Name = "RainyDayForecast", TypeDefinition = rainyClass },
new CodeType { Name = "SunnyDayForecast", TypeDefinition = sunnyClass });
baseClass.OriginalComposedType = composedType;

var containerClass = TestHelper.CreateModelClass(modelNameSpace, "WeatherSummary");
containerClass.AddProperty(new CodeProperty
{
Name = "forecasts",
Type = new CodeType
{
Name = "WeatherForecast",
TypeDefinition = baseClass,
CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array,
},
});
containerClass.AddProperty(new CodeProperty
{
Name = "primaryForecast",
Type = new CodeType
{
Name = "WeatherForecast",
TypeDefinition = baseClass,
},
});

TestHelper.AddSerializationPropertiesToModelClass(containerClass);
await ILanguageRefiner.RefineAsync(generationConfiguration, root, cancellationToken: TestContext.Current.CancellationToken);

var deserializerFunction = root.FindChildByName<CodeFunction>($"DeserializeIntoWeatherSummary");
Assert.NotNull(deserializerFunction);
var parentNS = deserializerFunction.GetImmediateParentOfType<CodeNamespace>();
Assert.NotNull(parentNS);
parentNS.TryAddCodeFile("foo", deserializerFunction);
writer.Write(deserializerFunction);
var result = tw.ToString();

Assert.Contains("n.getCollectionOfObjectValues<WeatherForecast>(createWeatherForecastFromDiscriminatorValue)", result);
Assert.Contains("n.getObjectValue<WeatherForecast>(createWeatherForecastFromDiscriminatorValue)", result);
Assert.DoesNotContain("createRainyDayForecastFromDiscriminatorValue", result);
Assert.DoesNotContain("createSunnyDayForecastFromDiscriminatorValue", result);
AssertExtensions.CurlyBracesAreClosed(result, 1);
}

[Fact]
public async Task Writes_DiscriminatedUnionPropertyType_UsesBaseFactoryMethod_IntegrationAsync()
{
var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript };
var tempFilePath = Path.GetTempFileName();
await File.WriteAllTextAsync(tempFilePath, DiscriminatedUnionPropertySample.OpenApiYaml, cancellationToken: TestContext.Current.CancellationToken);
var mockLogger = new Mock<ILogger<KiotaBuilder>>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Forecast", Serializers = ["none"], Deserializers = ["none"] }, _httpClient);
await using var fs = new FileStream(tempFilePath, FileMode.Open);
var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken);
var node = builder.CreateUriSpace(document);
builder.SetApiRootUrl();
var codeModel = builder.CreateSourceModel(node);
var rootNS = codeModel.FindNamespaceByName("ApiSdk");
Assert.NotNull(rootNS);
await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS, cancellationToken: TestContext.Current.CancellationToken);
File.Delete(tempFilePath);

var deserializerFunction = rootNS.FindChildByName<CodeFunction>("deserializeIntoWeatherSummary");
Assert.NotNull(deserializerFunction);
writer.Write(deserializerFunction);
var result = tw.ToString();

Assert.Contains("createWeatherForecastFromDiscriminatorValue", result);
Assert.DoesNotContain("createRainyDayForecastFromDiscriminatorValue", result);
Assert.DoesNotContain("createSunnyDayForecastFromDiscriminatorValue", result);
AssertExtensions.CurlyBracesAreClosed(result, 1);
}
}