Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Numeric scalar unions with a format (e.g. `type: ["integer", "string"]` with `format: int32`, as emitted by ASP.NET Core's OpenAPI 3.1 generator under System.Text.Json's default `JsonNumberHandling.AllowReadingFromString`) now map to the numeric type instead of degrading to `UntypedNode`. [#6541](https://github.com/microsoft/kiota/issues/6541)
- golang: generated code now always uses LF line endings, so `gofmt` no longer reports formatting differences when generating on Windows.
- golang: make sure all generated code adheres to golangs coding standards
- Fixed non-deterministic model class descriptions when a component schema is referenced from multiple properties with differing reference-level descriptions. [#7927](https://github.com/microsoft/kiota/issues/7927)
Expand Down
13 changes: 12 additions & 1 deletion src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,7 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten
return prop;
}
private static readonly HashSet<JsonSchemaType> typeNamesToSkip = [JsonSchemaType.Object, JsonSchemaType.Array, JsonSchemaType.Object | JsonSchemaType.Null, JsonSchemaType.Array | JsonSchemaType.Null];
internal static readonly HashSet<string> numericFormats = new(StringComparer.OrdinalIgnoreCase) { "int8", "uint8", "int16", "uint16", "int32", "int64", "float", "double", "decimal" };
private static CodeType? GetPrimitiveType(IOpenApiSchema? typeSchema, string? childType = default)
{
if (typeSchema?.Items?.IsEnum() ?? false)
Expand All @@ -1297,7 +1298,17 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten
var typeName = typeNames.Find(static x => x is not null && !typeNamesToSkip.Contains(x.Value));

var format = typeSchema?.Format ?? typeSchema?.Items?.Format;
return (typeName & ~JsonSchemaType.Null, format?.ToLowerInvariant()) switch
var schemaType = typeName & ~JsonSchemaType.Null;
if (schemaType is { } schemaTypeValue &&
format is not null && numericFormats.Contains(format) &&
(schemaTypeValue & JsonSchemaType.String) is JsonSchemaType.String &&
(schemaTypeValue & (JsonSchemaType.Integer | JsonSchemaType.Number)) != 0)
// System.Text.Json's JsonNumberHandling.AllowReadingFromString (the ASP.NET Core default)
Comment thread
FabienDehopre marked this conversation as resolved.
// advertises numeric members as type ["integer"/"number", "string"] with a numeric format.
// Prefer the numeric type over a scalar union that would otherwise not be mappable and
// fall back to untyped. Unions with a non-numeric format keep the previous fallback.
schemaType = schemaTypeValue & ~JsonSchemaType.String;
Comment thread
baywet marked this conversation as resolved.
return (schemaType, format?.ToLowerInvariant()) switch
{
// byte and binary can apply to any type
(_, "byte") => new CodeType { Name = "base64", IsExternal = true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,13 @@ public InconsistentTypeFormatPair() : base(nameof(InconsistentTypeFormatPair), s
if (schema is null || !schema.Type.HasValue || string.IsNullOrEmpty(schema.Format) || KnownAndNotSupportedFormats.knownAndUnsupportedFormats.Contains(schema.Format) || escapedTypes.Contains(schema.Type.Value))
return;
var sanitizedType = schema.Type.Value & ~JsonSchemaType.Null;
// mirrors KiotaBuilder.GetPrimitiveType: numeric|string unions with a numeric format map to the numeric type
if ((sanitizedType & JsonSchemaType.String) is JsonSchemaType.String &&
(sanitizedType & (JsonSchemaType.Integer | JsonSchemaType.Number)) != 0 &&
KiotaBuilder.numericFormats.Contains(schema.Format))
sanitizedType &= ~JsonSchemaType.String;
if (!validPairs.TryGetValue(sanitizedType, out var validFormats) || !validFormats.Contains(schema.Format))
context.CreateWarning(nameof(InconsistentTypeFormatPair), $"The format {schema.Format} is not supported by Kiota for the type {sanitizedType} and the string type will be used.");
context.CreateWarning(nameof(InconsistentTypeFormatPair), $"The format {schema.Format} is not supported by Kiota for the type {sanitizedType} and will be ignored.");
})
{
}
Expand Down
70 changes: 70 additions & 0 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,76 @@ public void Object_Arrays_are_supported()
Assert.NotNull(unknownProp);
Assert.Equal(KiotaBuilder.UntypedNodeName, unknownProp.Type.Name);// left out property is an UntypedNode
}
[Theory]
[InlineData(JsonSchemaType.Integer | JsonSchemaType.String, "int32", "integer")]
[InlineData(JsonSchemaType.Integer | JsonSchemaType.String, "int64", "int64")]
[InlineData(JsonSchemaType.Integer | JsonSchemaType.String | JsonSchemaType.Null, "int32", "integer")]
[InlineData(JsonSchemaType.Number | JsonSchemaType.String, "double", "double")]
[InlineData(JsonSchemaType.Number | JsonSchemaType.String, "float", "float")]
[InlineData(JsonSchemaType.Number | JsonSchemaType.String | JsonSchemaType.Null, "double", "double")]
[InlineData(JsonSchemaType.Integer | JsonSchemaType.String, "uint16", "integer")]
// without a format, the scalar union keeps being generated as a composed type wrapper
[InlineData(JsonSchemaType.Integer | JsonSchemaType.String, null, "forecastGetResponse_temperature")]
[InlineData(JsonSchemaType.Number | JsonSchemaType.String, null, "forecastGetResponse_temperature")]
// with a non-numeric format, the union is not mappable and keeps falling back to untyped
[InlineData(JsonSchemaType.Integer | JsonSchemaType.String, "uuid", KiotaBuilder.UntypedNodeName)]
[InlineData(JsonSchemaType.Number | JsonSchemaType.String, "date-time", KiotaBuilder.UntypedNodeName)]
public void NumericStringScalarUnionsMapToNumericTypes(JsonSchemaType schemaType, string? format, string expectedTypeName)
{
// System.Text.Json's JsonNumberHandling.AllowReadingFromString (the ASP.NET Core default)
// advertises numeric members as type ["integer"/"number", "string"] with a digit pattern.
// https://github.com/microsoft/kiota/issues/6541
var document = new OpenApiDocument
{
Paths = new OpenApiPaths
{
["forecast"] = new OpenApiPathItem
{
Operations = new()
{
[NetHttpMethod.Get] = new OpenApiOperation
{
Responses = new OpenApiResponses
{
["200"] = new OpenApiResponse
{
Content = new Dictionary<string, IOpenApiMediaType>()
{
["application/json"] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Properties = new Dictionary<string, IOpenApiSchema> {
{
"temperature", new OpenApiSchema {
Type = schemaType,
Format = format,
Pattern = "^-?(?:0|[1-9]\\d*)$"
}
}
}
}
}
}
}
}
}
}
},
},
};
document.SetReferenceHostDocument();
var mockLogger = new CountLogger<KiotaBuilder>();
var builder = new KiotaBuilder(mockLogger, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }, _httpClient);
var node = builder.CreateUriSpace(document);
var codeModel = builder.CreateSourceModel(node);
var responseClass = codeModel.FindNamespaceByName("ApiSdk.forecast").FindChildByName<CodeClass>("ForecastGetResponse", false);
Assert.NotNull(responseClass);
var temperatureProp = responseClass.FindChildByName<CodeProperty>("temperature", false);
Assert.NotNull(temperatureProp);
Assert.Equal(expectedTypeName, temperatureProp.Type.Name);
}
[Fact]
public void TextPlainEndpointsAreSupported()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Kiota.Builder.Validation;
Expand Down Expand Up @@ -102,6 +103,60 @@ public async Task DoesntAddWarningOnNullable()
var diagnostic = await GetDiagnosticFromDocumentAsync(documentTxt);
Assert.Empty(diagnostic.Warnings);
}
[Theory]
[InlineData("[integer, string]", "int32")]
[InlineData("[integer, string, 'null']", "int64")]
[InlineData("[number, string]", "float")]
[InlineData("[number, string, 'null']", "double")]
[InlineData("[integer, string]", "uint16")]
public async Task DoesntAddAWarningWhenNumericStringUnionWithNumericFormat(string type, string format)
{
var documentTxt = $"""
openapi: 3.1.1
info:
title: OData Service for namespace microsoft.graph
description: This OData service is located at https://graph.microsoft.com/v1.0
version: 1.0.1
paths:
/enumeration:
get:
responses:
'200':
description: some description
content:
application/json:
schema:
type: {type}
format: {format}
""";
var diagnostic = await GetDiagnosticFromDocumentAsync(documentTxt);
Assert.Empty(diagnostic.Warnings);
}
[Fact]
public async Task AddsAWarningWhenNumericStringUnionWithUnsupportedFormat()
{
var documentTxt = """
openapi: 3.1.1
info:
title: OData Service for namespace microsoft.graph
description: This OData service is located at https://graph.microsoft.com/v1.0
version: 1.0.1
paths:
/enumeration:
get:
responses:
'200':
description: some description
content:
application/json:
schema:
type: [integer, string]
format: date-time
""";
var diagnostic = await GetDiagnosticFromDocumentAsync(documentTxt);
var warning = Assert.Single(diagnostic.Warnings);
Assert.EndsWith("will be ignored.", warning.Message, StringComparison.Ordinal);
}
private static async Task<OpenApiDiagnostic> GetDiagnosticFromDocumentAsync(string document)
{
var rule = new InconsistentTypeFormatPair();
Expand Down