diff --git a/CHANGELOG.md b/CHANGELOG.md index 095b991cf8..edfe5f75a4 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 OpenAPI schemas with `format` but without `type` keyword would generate `UntypedNode` instead of proper types. [#7315](https://github.com/microsoft/kiota/issues/7315) - 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 diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index aa0851fbaa..13ec2cc24a 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1236,21 +1236,24 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten var format = typeSchema?.Format ?? typeSchema?.Items?.Format; return (typeName & ~JsonSchemaType.Null, format?.ToLowerInvariant()) switch { + // byte and binary can apply to any type (_, "byte") => new CodeType { Name = "base64", IsExternal = true }, (_, "binary") => new CodeType { Name = "binary", IsExternal = true }, - (JsonSchemaType.String, "base64url") => new CodeType { Name = "base64url", IsExternal = true }, - (JsonSchemaType.String, "duration") => new CodeType { Name = "TimeSpan", IsExternal = true }, - (JsonSchemaType.String, "time") => new CodeType { Name = "TimeOnly", IsExternal = true }, - (JsonSchemaType.String, "date") => new CodeType { Name = "DateOnly", IsExternal = true }, - (JsonSchemaType.String, "date-time") => new CodeType { Name = "DateTimeOffset", IsExternal = true }, - (JsonSchemaType.String, "uuid") => new CodeType { Name = "Guid", IsExternal = true }, + // String-based formats - handle both with explicit String type and without type (null) + (JsonSchemaType.String or null, "base64url") => new CodeType { Name = "base64url", IsExternal = true }, + (JsonSchemaType.String or null, "uuid") => new CodeType { Name = "Guid", IsExternal = true }, + (JsonSchemaType.String or null, "duration") => new CodeType { Name = "TimeSpan", IsExternal = true }, + (JsonSchemaType.String or null, "time") => new CodeType { Name = "TimeOnly", IsExternal = true }, + (JsonSchemaType.String or null, "date") => new CodeType { Name = "DateOnly", IsExternal = true }, + (JsonSchemaType.String or null, "date-time") => new CodeType { Name = "DateTimeOffset", IsExternal = true }, (JsonSchemaType.String, _) => new CodeType { Name = "string", IsExternal = true }, // covers commonmark and html - (JsonSchemaType.Number, "double" or "float" or "decimal") => new CodeType { Name = format.ToLowerInvariant(), IsExternal = true }, - (JsonSchemaType.Number or JsonSchemaType.Integer, "int8") => new CodeType { Name = "sbyte", IsExternal = true }, - (JsonSchemaType.Number or JsonSchemaType.Integer, "uint8") => new CodeType { Name = "byte", IsExternal = true }, - (JsonSchemaType.Number or JsonSchemaType.Integer, "int64") => new CodeType { Name = "int64", IsExternal = true }, - (JsonSchemaType.Number, "int16") => new CodeType { Name = "integer", IsExternal = true }, - (JsonSchemaType.Number, "int32") => new CodeType { Name = "integer", IsExternal = true }, + // Numeric formats - handle with explicit Number/Integer types and without type (null) + (JsonSchemaType.Number or null, "double" or "float" or "decimal") => new CodeType { Name = format.ToLowerInvariant(), IsExternal = true }, + (JsonSchemaType.Number or JsonSchemaType.Integer or null, "int8") => new CodeType { Name = "sbyte", IsExternal = true }, + (JsonSchemaType.Number or JsonSchemaType.Integer or null, "uint8") => new CodeType { Name = "byte", IsExternal = true }, + (JsonSchemaType.Number or JsonSchemaType.Integer or null, "int64") => new CodeType { Name = "int64", IsExternal = true }, + (JsonSchemaType.Number or JsonSchemaType.Integer or null, "int16") => new CodeType { Name = "integer", IsExternal = true }, + (JsonSchemaType.Number or JsonSchemaType.Integer or null, "int32") => new CodeType { Name = "integer", IsExternal = true }, (JsonSchemaType.Number, _) => new CodeType { Name = "double", IsExternal = true }, (JsonSchemaType.Integer, _) => new CodeType { Name = "integer", IsExternal = true }, (JsonSchemaType.Boolean, _) => new CodeType { Name = "boolean", IsExternal = true }, diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index a332cf63c1..50ec9e34b0 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -4329,6 +4329,127 @@ public void MapsQueryParameterTypes(JsonSchemaType type, string format, string e Assert.Equal(expected, property.Type.Name); Assert.True(property.Type.AllTypes.First().IsExternal); } + [InlineData("date-time", "DateTimeOffset")] + [InlineData("duration", "TimeSpan")] + [InlineData("time", "TimeOnly")] + [InlineData("date", "DateOnly")] + [InlineData("byte", "base64")] + [InlineData("binary", "binary")] + [InlineData("uuid", "Guid")] + [InlineData("base64url", "base64url")] + [InlineData("double", "double")] + [InlineData("float", "float")] + [InlineData("decimal", "decimal")] + [InlineData("int8", "sbyte")] + [InlineData("uint8", "byte")] + [InlineData("int16", "integer")] // int16 and int32 both map to generic "integer" type for backwards compatibility + [InlineData("int32", "integer")] + [InlineData("int64", "int64")] + [Theory] + public void MapsPrimitiveFormatsWithoutType(string format, string expected) + { + var document = new OpenApiDocument + { + Paths = new OpenApiPaths + { + ["primitive"] = new OpenApiPathItem + { + Operations = new() + { + [NetHttpMethod.Get] = new OpenApiOperation + { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + // Type is intentionally not set to simulate .NET 10 ASP.NET OpenAPI generator behavior + Format = format + } + } + } + }, + } + } + } + } + }, + }; + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }, _httpClient); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var requestBuilder = codeModel.FindChildByName("primitiveRequestBuilder"); + Assert.NotNull(requestBuilder); + var method = requestBuilder.GetChildElements(true).OfType().FirstOrDefault(x => x.IsOfKind(CodeMethodKind.RequestExecutor)); + Assert.NotNull(method); + Assert.Equal(expected, method.ReturnType.Name); + Assert.True(method.ReturnType.AllTypes.First().IsExternal); + } + [InlineData("date-time", "DateTimeOffset")] + [InlineData("duration", "TimeSpan")] + [InlineData("time", "TimeOnly")] + [InlineData("date", "DateOnly")] + [InlineData("byte", "base64")] + [InlineData("binary", "binary")] + [InlineData("uuid", "Guid")] + [InlineData("base64url", "base64url")] + [InlineData("double", "double")] + [InlineData("float", "float")] + [InlineData("decimal", "decimal")] + [InlineData("int8", "sbyte")] + [InlineData("uint8", "byte")] + [InlineData("int16", "integer")] // int16 and int32 both map to generic "integer" type for backwards compatibility + [InlineData("int32", "integer")] + [InlineData("int64", "int64")] + [Theory] + public void MapsQueryParameterTypesWithoutType(string format, string expected) + { + var document = new OpenApiDocument + { + Paths = new OpenApiPaths + { + ["primitive"] = new OpenApiPathItem + { + Operations = new() + { + [NetHttpMethod.Get] = new OpenApiOperation + { + Parameters = new List { + new OpenApiParameter() { + Name = "query", + In = ParameterLocation.Query, + Schema = new OpenApiSchema { + // Type is intentionally not set to simulate .NET 10 ASP.NET OpenAPI generator behavior + Format = format + } + } + }, + Responses = new OpenApiResponses + { + ["204"] = new OpenApiResponse() + } + } + } + } + }, + }; + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }, _httpClient); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var queryParameters = codeModel.FindChildByName("primitiveRequestBuilderGetQueryParameters"); + Assert.NotNull(queryParameters); + var property = queryParameters.Properties.First(static x => x.Name.Equals("query", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(property); + Assert.Equal(expected, property.Type.Name); + Assert.True(property.Type.AllTypes.First().IsExternal); + } [Fact] public void IncludesQueryParameterInUriTemplate() {