diff --git a/src/Kiota.Builder/CodeDOM/CodeProperty.cs b/src/Kiota.Builder/CodeDOM/CodeProperty.cs index c5f155828d..4beb6e0edd 100644 --- a/src/Kiota.Builder/CodeDOM/CodeProperty.cs +++ b/src/Kiota.Builder/CodeDOM/CodeProperty.cs @@ -122,6 +122,14 @@ public bool IsPrimaryErrorMessage { get; set; } + /// + /// Indicates that this property appeared in the parent schema's required array. + /// Set during Code DOM construction in KiotaBuilder; should not be modified by refiners. + /// + public bool IsRequired + { + get; set; + } public object Clone() { @@ -143,8 +151,8 @@ public object Clone() OriginalPropertyFromBaseType = OriginalPropertyFromBaseType?.Clone() as CodeProperty, Deprecation = Deprecation, IsPrimaryErrorMessage = IsPrimaryErrorMessage, + IsRequired = IsRequired, }; return property; } } - diff --git a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs index e82f1079d2..e84bf7d84d 100644 --- a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs +++ b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs @@ -65,6 +65,10 @@ public bool UsesBackingStore { get; set; } + public bool MakeRequiredPropertiesNonNullable + { + get; set; + } = true; public bool ExcludeBackwardCompatible { get; set; @@ -184,6 +188,7 @@ public object Clone() DisableSSLValidation = DisableSSLValidation, ExportPublicApi = ExportPublicApi, PluginAuthInformation = PluginAuthInformation, + MakeRequiredPropertiesNonNullable = MakeRequiredPropertiesNonNullable, }; } private static readonly StringIEnumerableDeepComparer comparer = new(); diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index 543e40916e..5c790929ee 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -80,6 +80,16 @@ public static bool HasAnyProperty(this IOpenApiSchema? schema) { return schema?.Properties is { Count: > 0 }; } + + internal static bool IsExplicitlyNullable(this IOpenApiSchema? schema) + { + if (schema is null) return false; + // OAS 3.0 nullable: true or OAS 3.1 type includes null + if ((schema.Type & JsonSchemaType.Null) is JsonSchemaType.Null) return true; + // OAS 3.1 anyOf [ { type: null } ] pattern + return schema.AnyOf?.Any(static x => + (x.Type & JsonSchemaType.Null) is JsonSchemaType.Null && !x.HasAnyProperty()) ?? false; + } public static bool IsInclusiveUnion(this IOpenApiSchema? schema, uint exclusiveMinimumNumberOfEntries = 1) { return schema?.AnyOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > exclusiveMinimumNumberOfEntries; diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 21c02e437c..4d5226ef71 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -661,7 +661,7 @@ public async Task ApplyLanguageRefinementAsync(GenerationConfiguration config, C public async Task CreateLanguageSourceFilesAsync(GenerationLanguage language, CodeNamespace generatedCode, CancellationToken cancellationToken) { - var languageWriter = LanguageWriter.GetLanguageWriter(language, config.OutputPath, config.ClientNamespaceName, config.UsesBackingStore, config.ExcludeBackwardCompatible); + var languageWriter = LanguageWriter.GetLanguageWriter(language, config.OutputPath, config.ClientNamespaceName, config.UsesBackingStore, config.ExcludeBackwardCompatible, config.MakeRequiredPropertiesNonNullable); var stopwatch = new Stopwatch(); stopwatch.Start(); var codeRenderer = CodeRenderer.GetCodeRender(config); @@ -1174,7 +1174,7 @@ private CodeIndexer[] CreateIndexer(string childIdentifier, string childType, Co } private static readonly StructuralPropertiesReservedNameProvider structuralPropertiesReservedNameProvider = new(); - private CodeProperty? CreateProperty(string childIdentifier, string childType, IOpenApiSchema? propertySchema = null, CodeTypeBase? existingType = null, CodePropertyKind kind = CodePropertyKind.Custom) + private CodeProperty? CreateProperty(string childIdentifier, string childType, IOpenApiSchema? propertySchema = null, CodeTypeBase? existingType = null, CodePropertyKind kind = CodePropertyKind.Custom, bool isRequired = false) { var propertyName = childIdentifier.CleanupSymbolName(); if (structuralPropertiesReservedNameProvider.ReservedNames.Contains(propertyName)) @@ -1196,6 +1196,7 @@ private CodeIndexer[] CreateIndexer(string childIdentifier, string childType, Co ReadOnly = propertySchema?.ReadOnly ?? false, Type = resultType, Deprecation = propertySchema?.GetDeprecationInformation(), + IsRequired = isRequired, IsPrimaryErrorMessage = kind == CodePropertyKind.Custom && propertySchema is { Extensions: not null } && propertySchema.Extensions.TryGetValue(OpenApiPrimaryErrorMessageExtension.Name, out var openApiExtension) && @@ -1213,6 +1214,23 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten !"null".Equals(stringDefaultValue, StringComparison.OrdinalIgnoreCase)) prop.DefaultValue = $"\"{stringDefaultValue}\""; + // If the property is required and the schema explicitly does not allow null, + // mark the type as non-nullable. We clone existingType to avoid mutating a shared reference. + // Collections are excluded: IsNullable on a collection type controls both the outer + // collection ? AND the enum element ? (e.g. List vs List). Setting + // IsNullable = false on a required collection breaks the serialization API which always + // expects IEnumerable?. The outer collection ? is suppressed via IsRequired in + // CodePropertyWriter instead. + var isCollection = existingType != null + ? existingType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None + : propertySchema.IsArray(); + if (kind == CodePropertyKind.Custom && isRequired && !propertySchema.IsExplicitlyNullable() && !isCollection) + { + if (existingType != null) + prop.Type = (CodeTypeBase)existingType.Clone(); + prop.Type.IsNullable = false; + } + if (existingType == null) { prop.Type.CollectionKind = propertySchema.IsArray() ? CodeTypeBase.CodeTypeCollectionKind.Complex : default; @@ -2418,7 +2436,8 @@ private void CreatePropertiesForModelClass(OpenApiUrlTreeNode currentNode, IOpen LogOmittedPropertyInvalidSchema(x.Key, model.Name, currentNode.Path); return null; } - return CreateProperty(x.Key, definition.Name, propertySchema: propertySchema, existingType: definition); + var isRequired = schema.Required?.Contains(x.Key) ?? false; + return CreateProperty(x.Key, definition.Name, propertySchema: propertySchema, existingType: definition, isRequired: isRequired); }) .OfType() .ToArray() ?? []; diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 371f54068a..cdb017d9c8 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -136,6 +136,7 @@ protected static void MakeEnumPropertiesNullable(CodeElement currentElement) if (currentElement is CodeClass currentClass && currentClass.IsOfKind(CodeClassKind.Model)) currentClass.Properties .Where(x => x.Type is CodeType propType && propType.TypeDefinition is CodeEnum) + .Where(x => !x.IsRequired) .ToList() .ForEach(x => x.Type.IsNullable = true); CrawlTree(currentElement, MakeEnumPropertiesNullable); diff --git a/src/Kiota.Builder/Writers/CSharp/CSharpConventionService.cs b/src/Kiota.Builder/Writers/CSharp/CSharpConventionService.cs index 9b9497f37a..f833307582 100644 --- a/src/Kiota.Builder/Writers/CSharp/CSharpConventionService.cs +++ b/src/Kiota.Builder/Writers/CSharp/CSharpConventionService.cs @@ -244,6 +244,20 @@ _ when NullableTypes.Contains(typeName) => true, _ => false, }; } + internal bool IsValueType(CodeTypeBase type) + { + if (type is not CodeType codeType) return false; + // Collections are reference types regardless of element type — never use .Value on them + if (codeType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None) return false; + if (codeType.TypeDefinition is CodeEnum) return true; + var typeName = TranslateType(codeType); + return NullableTypes.Contains(typeName); + } + /// + /// When true (default), required non-nullable OAS properties are generated as non-nullable C# types. + /// Set to false to revert to the previous all-nullable behavior. + /// + public bool MakeRequiredPropertiesNonNullable { get; set; } = true; public override string GetParameterSignature(CodeParameter parameter, CodeElement targetElement, LanguageWriter? writer = null) { ArgumentNullException.ThrowIfNull(parameter); diff --git a/src/Kiota.Builder/Writers/CSharp/CSharpWriter.cs b/src/Kiota.Builder/Writers/CSharp/CSharpWriter.cs index 9b393a7e40..fd6a5d3e9d 100644 --- a/src/Kiota.Builder/Writers/CSharp/CSharpWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CSharpWriter.cs @@ -4,10 +4,13 @@ namespace Kiota.Builder.Writers.CSharp; public class CSharpWriter : LanguageWriter { - public CSharpWriter(string rootPath, string clientNamespaceName) + public CSharpWriter(string rootPath, string clientNamespaceName, bool makeRequiredPropertiesNonNullable = true) { PathSegmenter = new CSharpPathSegmenter(rootPath, clientNamespaceName); - var conventionService = new CSharpConventionService(); + var conventionService = new CSharpConventionService + { + MakeRequiredPropertiesNonNullable = makeRequiredPropertiesNonNullable + }; AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); AddOrReplaceCodeElementWriter(new CodeBlockEndWriter(conventionService)); AddOrReplaceCodeElementWriter(new CodeEnumWriter(conventionService)); diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 5e1d20b7c4..78e637b3e2 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -359,7 +359,10 @@ private void WriteDeserializerBodyForInheritedModel(bool shouldHide, CodeMethod .Where(static x => !x.ExistsInBaseType) .OrderBy(static x => x.Name, StringComparer.Ordinal)) { - writer.WriteLine($"{{ \"{otherProp.WireName.SanitizeDoubleQuote()}\", n => {{ {otherProp.Name.ToFirstCharacterUpperCase()} = n.{GetDeserializationMethodName(otherProp.Type, codeElement)}; }} }},"); + // When a property is required and non-nullable, the C# property type is T (not T?). + // Parse-node methods for value types and enums return T?, so we must unwrap with .Value. + var deserializeSuffix = !otherProp.Type.IsNullable && conventions.IsValueType(otherProp.Type) ? ".Value" : string.Empty; + writer.WriteLine($"{{ \"{otherProp.WireName.SanitizeDoubleQuote()}\", n => {{ {otherProp.Name.ToFirstCharacterUpperCase()} = n.{GetDeserializationMethodName(otherProp.Type, codeElement)}{deserializeSuffix}; }} }},"); } writer.CloseBlock("};"); } diff --git a/src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs index 7159c9d63b..33e14caf44 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs @@ -14,9 +14,11 @@ public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter w if (codeElement.ExistsInExternalBaseType) return; var propertyType = conventions.GetTypeString(codeElement.Type, codeElement); var isNullableReferenceType = !propertyType.EndsWith('?') + && codeElement.Type.IsNullable + && !(conventions.MakeRequiredPropertiesNonNullable && codeElement.IsRequired) && codeElement.IsOfKind( CodePropertyKind.Custom, - CodePropertyKind.QueryParameter);// Other property types are appropriately constructor initialized + CodePropertyKind.QueryParameter); conventions.WriteShortDescription(codeElement, writer); conventions.WriteDeprecationAttribute(codeElement, writer); if (isNullableReferenceType) @@ -26,7 +28,7 @@ public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter w CSharpConventionService.WriteNullableMiddle(writer); } - WritePropertyInternal(codeElement, writer, propertyType);// Always write the normal way + WritePropertyInternal(codeElement, writer, propertyType); if (isNullableReferenceType) CSharpConventionService.WriteNullableClosing(writer); diff --git a/src/Kiota.Builder/Writers/LanguageWriter.cs b/src/Kiota.Builder/Writers/LanguageWriter.cs index 22313267dd..7853ba22f4 100644 --- a/src/Kiota.Builder/Writers/LanguageWriter.cs +++ b/src/Kiota.Builder/Writers/LanguageWriter.cs @@ -178,11 +178,11 @@ protected void AddOrReplaceCodeElementWriter(ICodeElementWriter writer) wh Writers[typeof(T)] = writer; } private readonly Dictionary Writers = []; // we have to type as object because dotnet doesn't have type capture i.e eq for `? extends CodeElement` - public static LanguageWriter GetLanguageWriter(GenerationLanguage language, string outputPath, string clientNamespaceName, bool usesBackingStore = false, bool excludeBackwardCompatible = false) + public static LanguageWriter GetLanguageWriter(GenerationLanguage language, string outputPath, string clientNamespaceName, bool usesBackingStore = false, bool excludeBackwardCompatible = false, bool makeRequiredPropertiesNonNullable = true) { return language switch { - GenerationLanguage.CSharp => new CSharpWriter(outputPath, clientNamespaceName), + GenerationLanguage.CSharp => new CSharpWriter(outputPath, clientNamespaceName, makeRequiredPropertiesNonNullable), GenerationLanguage.Java => new JavaWriter(outputPath, clientNamespaceName), GenerationLanguage.TypeScript => new TypeScriptWriter(outputPath, clientNamespaceName), GenerationLanguage.Ruby => new RubyWriter(outputPath, clientNamespaceName), diff --git a/src/kiota/Handlers/KiotaGenerateCommandHandler.cs b/src/kiota/Handlers/KiotaGenerateCommandHandler.cs index 4d38b7edf8..6bd091b2f9 100644 --- a/src/kiota/Handlers/KiotaGenerateCommandHandler.cs +++ b/src/kiota/Handlers/KiotaGenerateCommandHandler.cs @@ -86,6 +86,7 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio bool excludeBackwardCompatible = parseResult.GetValue(ExcludeBackwardCompatibleOption); bool clearCache = parseResult.GetValue(ClearCacheOption); bool disableSSLValidation = parseResult.GetValue(DisableSSLValidationOption); + bool makeRequiredPropertiesNonNullable = parseResult.GetValue(MakeRequiredPropertiesNonNullableOption); bool includeAdditionalData = parseResult.GetValue(AdditionalDataOption); string? className = parseResult.GetValue(ClassOption); AccessModifier typeAccessModifier = parseResult.GetValue(TypeAccessModifierOption); @@ -152,6 +153,7 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio Configuration.Generation.CleanOutput = cleanOutput; Configuration.Generation.ClearCache = clearCache; Configuration.Generation.DisableSSLValidation = disableSSLValidation; + Configuration.Generation.MakeRequiredPropertiesNonNullable = makeRequiredPropertiesNonNullable; var (loggerFactory, logger) = GetLoggerAndFactory(parseResult, Configuration.Generation.OutputPath); using (loggerFactory) @@ -233,6 +235,10 @@ public required Option DisableSSLValidationOption { get; init; } + public required Option MakeRequiredPropertiesNonNullableOption + { + get; init; + } private static void CreateTelemetryTags(ActivitySource? activitySource, GenerationLanguage language, bool backingStore, bool excludeBackwardCompatible, bool clearCache, bool disableSslValidation, bool cleanOutput, string? output, diff --git a/src/kiota/KiotaHost.cs b/src/kiota/KiotaHost.cs index b5e17e2d9f..b95a5312bb 100644 --- a/src/kiota/KiotaHost.cs +++ b/src/kiota/KiotaHost.cs @@ -589,6 +589,8 @@ private static Command GetGenerateCommand(IServiceProvider serviceProvider) var disableSSLValidationOption = GetDisableSSLValidationOption(defaultConfiguration.DisableSSLValidation); + var makeRequiredPropertiesNonNullableOption = GetMakeRequiredPropertiesNonNullableOption(defaultConfiguration.MakeRequiredPropertiesNonNullable); + var command = new Command("generate", "Generates a REST HTTP API client from an OpenAPI description file.") { descriptionOption, manifestOption, @@ -610,6 +612,7 @@ private static Command GetGenerateCommand(IServiceProvider serviceProvider) dvrOption, clearCacheOption, disableSSLValidationOption, + makeRequiredPropertiesNonNullableOption, }; command.Action = new KiotaGenerateCommandHandler { @@ -633,6 +636,7 @@ private static Command GetGenerateCommand(IServiceProvider serviceProvider) DisabledValidationRulesOption = dvrOption, ClearCacheOption = clearCacheOption, DisableSSLValidationOption = disableSSLValidationOption, + MakeRequiredPropertiesNonNullableOption = makeRequiredPropertiesNonNullableOption, ServiceProvider = serviceProvider, }; return command; @@ -708,6 +712,16 @@ private static Option GetClearCacheOption(bool defaultValue) return clearCacheOption; } + internal static Option GetMakeRequiredPropertiesNonNullableOption(bool defaultValue = true) + { + var option = new Option("--make-required-properties-non-nullable") + { + DefaultValueFactory = _ => defaultValue, + Description = "When enabled (default), properties marked as required in the OpenAPI description and not explicitly nullable are generated as non-nullable types. Set to false to revert to the previous behavior where all properties are nullable, useful for specs that incorrectly mark fields as required.", + }; + option.Aliases.Add("--mrpnn"); + return option; + } private static Option GetDisableSSLValidationOption(bool defaultValue) { var disableSSLValidationOption = new Option("--disable-ssl-validation") diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 85f2c151f9..07002cdaea 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -11062,6 +11062,457 @@ public async Task GeneratesNavigationPropertiesForActionPathsSiblingToCollection Assert.NotNull(replyWithQuoteRb); } + #region Issue-3911 — required/nullable OAS properties → IsNullable / IsRequired + + [Fact] + public async Task RequiredNonNullableStringProperty_IsNullableFalse_IsRequiredTrue() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + required: + - name + properties: + name: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var nameProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("name", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(nameProp); + Assert.False(nameProp.Type.IsNullable); + Assert.True(nameProp.IsRequired); + } + + [Fact] + public async Task RequiredNullableStringProperty_IsNullableTrue_IsRequiredTrue() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + required: + - name + properties: + name: + type: string + nullable: true"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var nameProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("name", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(nameProp); + Assert.True(nameProp.Type.IsNullable); + Assert.True(nameProp.IsRequired); + } + + [Fact] + public async Task OptionalNonNullableStringProperty_IsNullableTrue_IsRequiredFalse() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + properties: + name: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var nameProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("name", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(nameProp); + Assert.True(nameProp.Type.IsNullable); + Assert.False(nameProp.IsRequired); + } + + [Fact] + public async Task OptionalNullableStringProperty_IsNullableTrue_IsRequiredFalse() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + properties: + name: + type: string + nullable: true"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var nameProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("name", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(nameProp); + Assert.True(nameProp.Type.IsNullable); + Assert.False(nameProp.IsRequired); + } + + [Fact] + public async Task RequiredNonNullableIntegerProperty_IsNullableFalse_IsRequiredTrue() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + required: + - count + properties: + count: + type: integer"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var countProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("count", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(countProp); + Assert.False(countProp.Type.IsNullable); + Assert.True(countProp.IsRequired); + } + + [Fact] + public async Task RequiredNonNullableObjectRefProperty_IsNullableFalse_IsRequiredTrue() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + required: + - owner + properties: + owner: + $ref: '#/components/schemas/Owner' + Owner: + type: object + properties: + id: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var ownerProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("owner", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(ownerProp); + Assert.False(ownerProp.Type.IsNullable); + Assert.True(ownerProp.IsRequired); + } + + [Fact] + public async Task RequiredNonNullableEnumProperty_IsNullableFalse_IsRequiredTrue() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + required: + - status + properties: + status: + $ref: '#/components/schemas/Status' + Status: + type: string + enum: + - active + - inactive"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var statusProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("status", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(statusProp); + Assert.False(statusProp.Type.IsNullable); + Assert.True(statusProp.IsRequired); + } + + [Fact] + public async Task RequiredNonNullableCollectionProperty_IsNullableFalse_IsRequiredTrue() + { + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + required: + - tags + properties: + tags: + type: array + items: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var tagsProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("tags", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(tagsProp); + // Collections keep IsNullable = true so enum element types stay T? (required by serializer API). + // The outer collection ? is suppressed in the C# writer via IsRequired. + Assert.True(tagsProp.Type.IsNullable); + Assert.True(tagsProp.IsRequired); + } + + [Fact] + public async Task RequiredNonNullableProperty_FlagFalse_IsNullableTrue() + { + // When MakeRequiredPropertiesNonNullable = false, required non-nullable properties keep IsNullable = true + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + required: + - name + properties: + name: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, MakeRequiredPropertiesNonNullable = false }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var nameProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("name", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(nameProp); + // Flag off: IsNullable stays true (old behavior) + Assert.True(nameProp.Type.IsNullable); + // IsRequired is always set accurately regardless of flag + Assert.True(nameProp.IsRequired); + } + + [Fact] + public async Task RequiredNonNullableProperty_FlagTrue_IsNullableFalse() + { + // When MakeRequiredPropertiesNonNullable = true (default), required non-nullable properties get IsNullable = false + var tempFilePath = Path.GetTempFileName(); + await using var fs = await GetDocumentStreamAsync(@"openapi: 3.0.1 +info: + title: Test + version: 1.0.0 +servers: + - url: https://example.com/v1.0 +paths: + /items: + get: + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + required: + - name + properties: + name: + type: string"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, MakeRequiredPropertiesNonNullable = true }, _httpClient); + var document = await builder.CreateOpenApiDocumentAsync(fs, cancellationToken: TestContext.Current.CancellationToken); + var node = builder.CreateUriSpace(document); + var codeModel = builder.CreateSourceModel(node); + var modelsNS = codeModel.FindNamespaceByName("ApiSdk.models"); + Assert.NotNull(modelsNS); + var item = modelsNS.FindChildByName("Item", false); + Assert.NotNull(item); + var nameProp = item.Properties.FirstOrDefault(static p => p.Name.Equals("name", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(nameProp); + // Flag on: required non-nullable scalar gets IsNullable = false + Assert.False(nameProp.Type.IsNullable); + Assert.True(nameProp.IsRequired); + } + + #endregion + /// /// Regression test for https://github.com/microsoft/kiota/issues/7292 /// Required query parameters on one operation must not leak into the path-item URL template diff --git a/tests/Kiota.Builder.Tests/Writers/CSharp/CodePropertyWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/CodePropertyWriterTests.cs index f46d93933e..55eafeb834 100644 --- a/tests/Kiota.Builder.Tests/Writers/CSharp/CodePropertyWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/CodePropertyWriterTests.cs @@ -271,5 +271,72 @@ public void WritesMessageOverrideOnPrimary() // Then Assert.Contains("public override string Message { get => Prop1 ?? string.Empty; }", result); } + + [Fact] + public void WritesRequiredNonNullableCustomProperty_NoNullableBlock() + { + // A required, non-nullable reference-type property should NOT emit #nullable enable / ? + property.Kind = CodePropertyKind.Custom; + property.Type.IsNullable = false; + property.IsRequired = true; + writer.Write(property); + var result = tw.ToString(); + Assert.Contains($"{TypeName} {PropertyName}", result); + Assert.DoesNotContain("#nullable enable", result); + Assert.DoesNotContain($"{TypeName}?", result); + } + + [Fact] + public void WritesNullableCustomProperty_HasNullableBlock() + { + // A nullable reference-type property (the default) should still emit #nullable enable / ? + property.Kind = CodePropertyKind.Custom; + property.Type.IsNullable = true; + writer.Write(property); + var result = tw.ToString(); + Assert.Contains("#nullable enable", result); + Assert.Contains($"{TypeName}?", result); + } + + [Fact] + public void WritesRequiredNonNullableEnumProperty_NoNullableBlock() + { + var enumDef = rootNamespace.AddClass(new CodeClass { Name = "MyEnum" }).First(); + property.Kind = CodePropertyKind.Custom; + property.Type = new CodeType + { + Name = "MyEnum", + TypeDefinition = new CodeEnum { Name = "MyEnum" }, + IsNullable = false, + }; + property.IsRequired = true; + parentClass.AddProperty(property); + writer.Write(property); + var result = tw.ToString(); + Assert.DoesNotContain("#nullable enable", result); + Assert.DoesNotContain("MyEnum?", result); + } + + [Fact] + public void WritesRequiredProperty_FlagFalse_HasNullableBlock() + { + // When MakeRequiredPropertiesNonNullable = false, a required property with IsNullable = true + // should still get the #nullable enable block (old all-nullable behavior). + var flagOffWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.CSharp, DefaultPath, DefaultName, makeRequiredPropertiesNonNullable: false); + using var sw = new StringWriter(); + flagOffWriter.SetTextWriter(sw); + + property.Kind = CodePropertyKind.Custom; + property.Type.IsNullable = true; // flag=false: stays nullable + property.IsRequired = true; // IsRequired is still set accurately + writer.Write(property); // control: default writer suppresses ? for required + var defaultResult = tw.ToString(); + Assert.DoesNotContain("#nullable enable", defaultResult); // flag=true suppresses it + + flagOffWriter.Write(property); // flag=false: should emit nullable block + var flagOffResult = sw.ToString(); + Assert.Contains("#nullable enable", flagOffResult); // opt-out restores it + Assert.Contains($"{TypeName}?", flagOffResult); + } } diff --git a/tests/Kiota.Builder.Tests/Writers/Java/CodePropertyWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Java/CodePropertyWriterTests.cs index 21e75ec198..c65ac53c8b 100644 --- a/tests/Kiota.Builder.Tests/Writers/Java/CodePropertyWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Java/CodePropertyWriterTests.cs @@ -124,4 +124,17 @@ public void WritesCollectionFlagEnumsAsOneDimensionalArray() Assert.Contains("List<", result); Assert.DoesNotContain("EnumSet", result); } + + [Fact] + public void WritesRequiredNonNullable_NonnullAnnotation() + { + // A required, non-nullable custom property must emit @Nonnull (not @Nullable) + property.Kind = CodePropertyKind.Custom; + property.Type.IsNullable = false; + property.IsRequired = true; + writer.Write(property); + var result = tw.ToString(); + Assert.Contains("@jakarta.annotation.Nonnull", result); + Assert.DoesNotContain("@jakarta.annotation.Nullable", result); + } } diff --git a/tests/Kiota.Builder.Tests/Writers/Php/CodePropertyWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Php/CodePropertyWriterTests.cs index fc7ca327c2..5ccfd90f71 100644 --- a/tests/Kiota.Builder.Tests/Writers/Php/CodePropertyWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Php/CodePropertyWriterTests.cs @@ -290,4 +290,27 @@ public void WritePropertyWithDescription() var result = stringWriter.ToString(); Assert.DoesNotContain("/*/*", result); } + + [Fact] + public void WritesRequiredNonNullableProperty_NoNullPrefix_NoNullDefault() + { + // A required, non-nullable custom property should have no '?' type prefix and no '= null' default + var property = new CodeProperty + { + Name = "Email", + Access = AccessModifier.Public, + Kind = CodePropertyKind.Custom, + IsRequired = true, + Type = new CodeType + { + Name = "emailAddress", + IsNullable = false, + } + }; + parentClass.AddProperty(property); + propertyWriter.WriteCodeElement(property, languageWriter); + var result = stringWriter.ToString(); + Assert.DoesNotContain("?EmailAddress", result); + Assert.DoesNotContain("= null", result); + } } diff --git a/tests/Kiota.Builder.Tests/Writers/Python/CodePropertyWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Python/CodePropertyWriterTests.cs index ce0163df05..45180038a5 100644 --- a/tests/Kiota.Builder.Tests/Writers/Python/CodePropertyWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Python/CodePropertyWriterTests.cs @@ -149,4 +149,17 @@ public void WritePrimaryErrorMessagePropertyOption2() var result = tw.ToString(); Assert.Contains("return '' if self.error.message is None else self.error.message", result); } + + [Fact] + public void WritesRequiredNonNullableCustomProperty_NoOptional_NoNoneDefault() + { + // A required, non-nullable custom property must not be wrapped in Optional[...] and must have no '= None' default + property.Kind = CodePropertyKind.Custom; + property.Type.IsNullable = false; + property.IsRequired = true; + writer.Write(property); + var result = tw.ToString(); + Assert.DoesNotContain("Optional[", result); + Assert.DoesNotContain("= None", result); + } } diff --git a/vscode/packages/microsoft-kiota/package.json b/vscode/packages/microsoft-kiota/package.json index e48d8cec0c..0a91551599 100644 --- a/vscode/packages/microsoft-kiota/package.json +++ b/vscode/packages/microsoft-kiota/package.json @@ -38,6 +38,11 @@ "default": true, "description": "%kiota.generate.includeAdditionalData.description%" }, + "kiota.generate.makeRequiredPropertiesNonNullable.enabled": { + "type": "boolean", + "default": true, + "description": "%kiota.generate.makeRequiredPropertiesNonNullable.description%" + }, "kiota.generate.backingStore.enabled": { "type": "boolean", "default": false, diff --git a/vscode/packages/microsoft-kiota/package.nls.json b/vscode/packages/microsoft-kiota/package.nls.json index d1c0f02166..c00c6142db 100644 --- a/vscode/packages/microsoft-kiota/package.nls.json +++ b/vscode/packages/microsoft-kiota/package.nls.json @@ -26,10 +26,11 @@ "kiota.generate.deserializer.description": "The fully qualified class names for deserializers", "kiota.generate.structuredMimeTypes.description": "The MIME types and preference to use for structured data model generation. As per RFC9110 Accept header notation.", "kiota.generate.includeAdditionalData.description": "Will include the 'AdditionalData' property for models", + "kiota.generate.makeRequiredPropertiesNonNullable.description": "When enabled (default), properties marked as required in the OpenAPI description and not explicitly nullable are generated as non-nullable types. Disable to revert to the previous behavior where all properties are nullable.", "kiota.openApiExplorer.openFile.title": "Open file", "kiota.workspace.name": "My Workspace", "kiota.openApiExplorer.regenerateButton.title": "Re-generate", "kiota.openApiExplorer.editPaths.title": "Select", "kiota.openApiExplorer.refresh.title": "Refresh", "kiota.migrateClients.title": "Migrate API clients" -} +} \ No newline at end of file diff --git a/vscode/packages/microsoft-kiota/src/commands/generate/generateClientCommand.ts b/vscode/packages/microsoft-kiota/src/commands/generate/generateClientCommand.ts index 8fdfb73e4a..d519d99ca6 100644 --- a/vscode/packages/microsoft-kiota/src/commands/generate/generateClientCommand.ts +++ b/vscode/packages/microsoft-kiota/src/commands/generate/generateClientCommand.ts @@ -306,6 +306,7 @@ export class GenerateClientCommand extends Command { deserializers: settings.languagesSerializationConfiguration[language].deserializers, structuredMimeTypes: settings.structuredMimeTypes, includeAdditionalData: settings.includeAdditionalData, + makeRequiredPropertiesNonNullable: settings.makeRequiredPropertiesNonNullable, operation: ConsumerOperation.Add, workingDirectory: config.workingDirectory ? config.workingDirectory : getWorkspaceJsonDirectory() }); diff --git a/vscode/packages/microsoft-kiota/src/commands/regenerate/regenerate.service.ts b/vscode/packages/microsoft-kiota/src/commands/regenerate/regenerate.service.ts index c2d35196be..11b51a1742 100644 --- a/vscode/packages/microsoft-kiota/src/commands/regenerate/regenerate.service.ts +++ b/vscode/packages/microsoft-kiota/src/commands/regenerate/regenerate.service.ts @@ -47,6 +47,7 @@ export class RegenerateService { deserializers: settings.languagesSerializationConfiguration[language].deserializers, structuredMimeTypes: clientObjectItem.structuredMimeTypes ? clientObjectItem.structuredMimeTypes : settings.structuredMimeTypes, includeAdditionalData: clientObjectItem.includeAdditionalData ? clientObjectItem.includeAdditionalData : settings.includeAdditionalData, + makeRequiredPropertiesNonNullable: clientObjectItem.makeRequiredPropertiesNonNullable !== undefined ? clientObjectItem.makeRequiredPropertiesNonNullable : settings.makeRequiredPropertiesNonNullable, operation: ConsumerOperation.Edit, workingDirectory: getWorkspaceJsonDirectory() }); diff --git a/vscode/packages/microsoft-kiota/src/types/extensionSettings.ts b/vscode/packages/microsoft-kiota/src/types/extensionSettings.ts index 218f7c3cd5..fa23e4ce2c 100644 --- a/vscode/packages/microsoft-kiota/src/types/extensionSettings.ts +++ b/vscode/packages/microsoft-kiota/src/types/extensionSettings.ts @@ -4,6 +4,7 @@ import * as vscode from "vscode"; export function getExtensionSettings(extensionId: string): ExtensionSettings { return { includeAdditionalData: getBooleanConfiguration(extensionId, "generate.includeAdditionalData.enabled"), + makeRequiredPropertiesNonNullable: getBooleanConfiguration(extensionId, "generate.makeRequiredPropertiesNonNullable.enabled"), backingStore: getBooleanConfiguration(extensionId, "generate.backingStore.enabled"), excludeBackwardCompatible: getBooleanConfiguration(extensionId, "generate.excludeBackwardCompatible.enabled"), cleanOutput: getBooleanConfiguration(extensionId, "cleanOutput.enabled"), @@ -44,6 +45,7 @@ export interface ExtensionSettings { disableValidationRules: string[]; structuredMimeTypes: string[]; includeAdditionalData: boolean; + makeRequiredPropertiesNonNullable: boolean; languagesSerializationConfiguration: Record; } diff --git a/vscode/packages/npm-package/lib/generateClient.ts b/vscode/packages/npm-package/lib/generateClient.ts index f4f68c4f79..1e0a09ab06 100644 --- a/vscode/packages/npm-package/lib/generateClient.ts +++ b/vscode/packages/npm-package/lib/generateClient.ts @@ -18,6 +18,7 @@ export interface ClientGenerationOptions { excludeBackwardCompatible?: boolean; excludePatterns?: string[]; includeAdditionalData?: boolean; + makeRequiredPropertiesNonNullable?: boolean; includePatterns?: string[]; clearCache?: boolean; cleanOutput?: boolean; @@ -75,6 +76,7 @@ export async function generateClient(clientGenerationOptions: ClientGenerationOp excludeBackwardCompatible: clientGenerationOptions.excludeBackwardCompatible ?? false, excludePatterns: clientGenerationOptions.excludePatterns ?? [], includeAdditionalData: clientGenerationOptions.includeAdditionalData ?? false, + makeRequiredPropertiesNonNullable: clientGenerationOptions.makeRequiredPropertiesNonNullable ?? true, cleanOutput: clientGenerationOptions.cleanOutput ?? false, clearCache: clientGenerationOptions.clearCache ?? false, includePatterns: clientGenerationOptions.includePatterns ?? [], diff --git a/vscode/packages/npm-package/types.ts b/vscode/packages/npm-package/types.ts index eaed5534a9..5a25984426 100644 --- a/vscode/packages/npm-package/types.ts +++ b/vscode/packages/npm-package/types.ts @@ -246,6 +246,7 @@ export interface GenerationConfiguration { excludeBackwardCompatible: boolean; excludePatterns: string[]; includeAdditionalData: boolean; + makeRequiredPropertiesNonNullable: boolean; includePatterns: string[]; language: KiotaGenerationLanguage; openAPIFilePath: string; @@ -278,6 +279,7 @@ export interface ClientObjectProperties extends WorkspaceObjectProperties { clientNamespaceName: string; usesBackingStore: boolean; includeAdditionalData: boolean; + makeRequiredPropertiesNonNullable: boolean; excludeBackwardCompatible: boolean; disabledValidationRules: string[]; } @@ -300,7 +302,7 @@ export interface KiotaResult extends KiotaLoggedResult { isSuccess: boolean; } -export interface ValidateOpenApiResult extends KiotaLoggedResult {} +export interface ValidateOpenApiResult extends KiotaLoggedResult { } export interface GeneratePluginResult extends KiotaResult { aiPlugin: string;