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 @@ -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
Expand Down
27 changes: 15 additions & 12 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
121 changes: 121 additions & 0 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, IOpenApiMediaType>()
{
["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<ILogger<KiotaBuilder>>();
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<CodeClass>("primitiveRequestBuilder");
Assert.NotNull(requestBuilder);
var method = requestBuilder.GetChildElements(true).OfType<CodeMethod>().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<IOpenApiParameter> {
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<ILogger<KiotaBuilder>>();
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<CodeClass>("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()
{
Expand Down
Loading