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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added an opt-in `--make-required-properties-non-nullable` (`--mrpnn`) generation option that generates properties marked as required in the OpenAPI description (and not explicitly nullable) as non-nullable and non-optional. Currently supported for TypeScript, Python, and Java. Disabled by default to preserve existing output. [#7795](https://github.com/microsoft/kiota/pull/7795)

### Changed

- Removed support for specifying dependency install commands through the `x-ms-kiota-info` OpenAPI description extension.
Expand Down
1 change: 1 addition & 0 deletions specs/cli/client-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Once the `workspace.json` file is generated and the OpenAPI description file is
| `--namespace-name \| -n` | No | Contoso.GraphApp | The namespace of the client class. Defaults to `Microsoft.Graph`. | Yes, without its value |
| `--backing-store \| -b` | No | | Defaults to `false` | Yes |
| `--exclude-backward-compatible \| --ebc` | No | | Whether to exclude the code generated only for backward compatibility reasons or not. Defaults to `false`. | Yes |
| `--make-required-properties-non-nullable \| --mrpnn` | No | | When enabled, properties marked as required and not explicitly nullable are generated as non-nullable (and non-optional, for languages that distinguish the two). Currently supported for TypeScript, Python, and Java. Defaults to `false`. | Yes |
| `--structured-media-types \| -m` | No | `application/json` | Any valid media type which will match a request body type or a response type in the OpenAPI description. Default are documented [here](https://learn.microsoft.com/en-us/openapi/kiota/using#--structured-mime-types--m). | Yes |
| `--skip-generation \| --sg` | No | true | When specified, the generation would be skipped. Defaults to false. |Yes |
| `--output \| -o` | No | ./generated/graph/csharp | The output directory or file path for the generated code files. This is relative to the location of `workspace.json`. Defaults to `./output`. | Yes, without its value |
Expand Down
1 change: 1 addition & 0 deletions specs/cli/client-edit.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Once the `workspace.json` file and the API Manifest are updated, the code genera
| `--namespace-name \| -n` | No | Contoso.GraphApp | The namespace of the client class. Defaults to `Microsoft.Graph`. | Yes, without its value |
| `--backing-store \| -b` | No | | Defaults to `false` | Yes |
| `--exclude-backward-compatible \| --ebc` | No | | Whether to exclude the code generated only for backward compatibility reasons or not. Defaults to `false`. | Yes |
| `--make-required-properties-non-nullable \| --mrpnn` | No | | When enabled, properties marked as required and not explicitly nullable are generated as non-nullable (and non-optional, for languages that distinguish the two). Currently supported for TypeScript, Python, and Java. Defaults to `false`. | Yes |
| `--structured-media-types \| -m` | No | `application/json` |Any valid media type which will match a request body type or a response type in the OpenAPI description. Default are documented [here](https://learn.microsoft.com/en-us/openapi/kiota/using#--structured-mime-types--m). | Yes |
| `--skip-generation \| --sg` | No | true | When specified, the generation would be skipped. Defaults to false. | Yes |
| `--output \| -o` | No | ./generated/graph/csharp | The output directory or file path for the generated code files. Defaults to `./output`. | Yes, without its value |
Expand Down
3 changes: 3 additions & 0 deletions specs/schemas/workspace.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
},
"includeAdditionalData": {
"type": "boolean"
},
"makeRequiredPropertiesNonNullable": {
"type": "boolean"
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/Kiota.Builder/CodeDOM/CodeProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ public bool IsPrimaryErrorMessage
{
get; set;
}
/// <summary>
/// Indicates that this property appeared in the parent schema's <c>required</c> array.
/// Set during Code DOM construction in KiotaBuilder; should not be modified by refiners.
/// </summary>
public bool IsRequired
{
get; set;
}

public object Clone()
{
Expand All @@ -143,6 +151,7 @@ public object Clone()
OriginalPropertyFromBaseType = OriginalPropertyFromBaseType?.Clone() as CodeProperty,
Deprecation = Deprecation,
IsPrimaryErrorMessage = IsPrimaryErrorMessage,
IsRequired = IsRequired,
};
return property;
}
Expand Down
10 changes: 10 additions & 0 deletions src/Kiota.Builder/Configuration/GenerationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ public bool UsesBackingStore
{
get; set;
}
/// <summary>
/// When enabled, properties marked as required in the OpenAPI description and not explicitly nullable
/// are generated as non-nullable (and non-optional, for languages that distinguish the two).
/// Defaults to false to preserve the historical all-nullable behavior for existing clients.
/// </summary>
public bool MakeRequiredPropertiesNonNullable
{
get; set;
}
public bool ExcludeBackwardCompatible
{
get; set;
Expand Down Expand Up @@ -184,6 +193,7 @@ public object Clone()
DisableSSLValidation = DisableSSLValidation,
ExportPublicApi = ExportPublicApi,
PluginAuthInformation = PluginAuthInformation,
MakeRequiredPropertiesNonNullable = MakeRequiredPropertiesNonNullable,
};
}
private static readonly StringIEnumerableDeepComparer comparer = new();
Expand Down
15 changes: 15 additions & 0 deletions src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ public static bool HasAnyProperty(this IOpenApiSchema? schema)
{
return schema?.Properties is { Count: > 0 };
}
/// <summary>
/// Indicates whether the schema explicitly permits null.
/// Covers OAS 3.0 <c>nullable: true</c> and OAS 3.1 <c>type: [..., "null"]</c>,
/// as well as the OAS 3.1 <c>anyOf: [{ type: null }]</c> pattern.
/// </summary>
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/oneOf [ { type: null }, ... ] patterns
static bool HasNullMember(IList<IOpenApiSchema>? members) => members?.Any(static x =>
(x.Type & JsonSchemaType.Null) is JsonSchemaType.Null && !x.HasAnyProperty()) ?? false;
return HasNullMember(schema.AnyOf) || HasNullMember(schema.OneOf);
}
public static bool IsInclusiveUnion(this IOpenApiSchema? schema, uint exclusiveMinimumNumberOfEntries = 1)
{
return schema?.AnyOf?.Count(static x => IsSemanticallyMeaningful(x, true)) > exclusiveMinimumNumberOfEntries;
Expand Down
29 changes: 26 additions & 3 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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))
Expand All @@ -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) &&
Expand All @@ -1221,6 +1222,22 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten
prop.DefaultValue = stringDefaultJsonValue2.ToString();
}

// Required, non-explicitly-nullable properties are marked non-nullable so writers can drop the optional/nullable markers.
// Collections are excluded: their IsNullable also drives element nullability, which the serialization APIs rely on.
// Scoped to the languages whose writers honor this safely (TypeScript, Python, Java); other languages keep the
// historical nullable rendering so their output is unchanged until support is added and validated for them.
// Clone first to avoid mutating a shared/cached type reference.
var isCollection = existingType != null
? existingType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None
: propertySchema.IsArray();
var languageSupportsNonNullableRequired = config.Language is GenerationLanguage.TypeScript or GenerationLanguage.Python or GenerationLanguage.Java;
if (config.MakeRequiredPropertiesNonNullable && languageSupportsNonNullableRequired && 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;
Expand Down Expand Up @@ -2434,7 +2451,13 @@ 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);
// Required-ness is read from the (already allOf-merged) schema's own `required` array. Note that
// MergeIntersectionSchemaEntries merges member *properties* but not their `required` arrays, so a
// property declared required only inside an allOf member is treated as not-required here. This is a
// safe under-application for MakeRequiredPropertiesNonNullable (such a property stays nullable) and
// matches the pre-existing required handling; widening it would be a separate, cross-cutting change.
var isRequired = schema.Required?.Contains(x.Key) ?? false;
return CreateProperty(x.Key, definition.Name, propertySchema: propertySchema, existingType: definition, isRequired: isRequired);
})
.OfType<CodeProperty>()
.ToArray() ?? [];
Expand Down
10 changes: 10 additions & 0 deletions src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ public bool ExcludeBackwardCompatible
get; set;
}
/// <summary>
/// Whether required, non-explicitly-nullable properties were generated as non-nullable for this client.
/// </summary>
public bool MakeRequiredPropertiesNonNullable
{
get; set;
}
/// <summary>
/// The OpenAPI validation rules to disable during the generation.
/// </summary>
public HashSet<string> DisabledValidationRules { get; set; } = new(StringComparer.OrdinalIgnoreCase);
Expand All @@ -73,6 +80,7 @@ public ApiClientConfiguration(GenerationConfiguration config) : base(config)
ClientNamespaceName = config.ClientNamespaceName;
UsesBackingStore = config.UsesBackingStore;
ExcludeBackwardCompatible = config.ExcludeBackwardCompatible;
MakeRequiredPropertiesNonNullable = config.MakeRequiredPropertiesNonNullable;
IncludeAdditionalData = config.IncludeAdditionalData;
StructuredMimeTypes = config.StructuredMimeTypes.ToList();
DisabledValidationRules = config.DisabledValidationRules.ToHashSet(StringComparer.OrdinalIgnoreCase);
Expand All @@ -94,6 +102,7 @@ public void UpdateGenerationConfigurationFromApiClientConfiguration(GenerationCo
config.TypeAccessModifier = parsedTypeAccessModifier;
config.UsesBackingStore = UsesBackingStore;
config.ExcludeBackwardCompatible = ExcludeBackwardCompatible;
config.MakeRequiredPropertiesNonNullable = MakeRequiredPropertiesNonNullable;
config.IncludeAdditionalData = IncludeAdditionalData;
config.StructuredMimeTypes = new(StructuredMimeTypes);
config.DisabledValidationRules = DisabledValidationRules.ToHashSet(StringComparer.OrdinalIgnoreCase);
Expand All @@ -111,6 +120,7 @@ public object Clone()
UsesBackingStore = UsesBackingStore,
IncludeAdditionalData = IncludeAdditionalData,
ExcludeBackwardCompatible = ExcludeBackwardCompatible,
MakeRequiredPropertiesNonNullable = MakeRequiredPropertiesNonNullable,
DisabledValidationRules = new(DisabledValidationRules, StringComparer.OrdinalIgnoreCase),
};
CloneBase(result);
Expand Down
4 changes: 2 additions & 2 deletions src/Kiota.Builder/Writers/LanguageWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,13 @@ protected void AddOrReplaceCodeElementWriter<T>(ICodeElementWriter<T> writer) wh
Writers[typeof(T)] = writer;
}
private readonly Dictionary<Type, object> 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 = false)
{
return language switch
{
GenerationLanguage.CSharp => new CSharpWriter(outputPath, clientNamespaceName),
GenerationLanguage.Java => new JavaWriter(outputPath, clientNamespaceName),
GenerationLanguage.TypeScript => new TypeScriptWriter(outputPath, clientNamespaceName),
GenerationLanguage.TypeScript => new TypeScriptWriter(outputPath, clientNamespaceName, makeRequiredPropertiesNonNullable),
GenerationLanguage.Ruby => new RubyWriter(outputPath, clientNamespaceName),
GenerationLanguage.PHP => new PhpWriter(outputPath, clientNamespaceName, usesBackingStore),
GenerationLanguage.Python => new PythonWriter(outputPath, clientNamespaceName, usesBackingStore),
Expand Down
20 changes: 18 additions & 2 deletions src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,15 @@ private void WriteSetterAccessProperties(CodeClass parentClass, LanguageWriter w
}
}
private const string NoneKeyword = "None";
private static string GetNonNullableDefaultValue(string pythonType) => pythonType switch
{
"str" => "''",
"int" => "0",
"float" => "0.0",
"bool" => "False",
"bytes" => "b''",
_ => NoneKeyword,
};
private void WriteSetterAccessPropertiesWithoutDefaults(CodeClass parentClass, LanguageWriter writer)
{
foreach (var propWithoutDefault in parentClass.GetPropertiesOfKind(SetterAccessProperties)
Expand All @@ -463,10 +472,17 @@ private void WriteSetterAccessPropertiesWithoutDefaults(CodeClass parentClass, L
{
var returnType = conventions.GetTypeString(propWithoutDefault.Type, propWithoutDefault, true, writer);
conventions.WriteInLineDescription(propWithoutDefault, writer);
// A required, non-nullable property (set only when MakeRequiredPropertiesNonNullable is on) is rendered
// without Optional[...]. A dataclass still needs a default for no-arg construction, so use a type-correct
// zero-value for primitives (str -> '', int -> 0, ...) instead of None. For non-primitive types (objects,
// enums) no zero-value literal exists, so None is kept — an advisory non-null, matching Java's @Nonnull.
var defaultValue = !propWithoutDefault.Type.IsNullable && propWithoutDefault.IsRequired
? GetNonNullableDefaultValue(returnType)
: NoneKeyword;
if (parentClass.IsOfKind(CodeClassKind.Model))
writer.WriteLine($"{propWithoutDefault.Name}: {(propWithoutDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithoutDefault.Type.IsNullable ? "]" : string.Empty)} = {NoneKeyword}");
writer.WriteLine($"{propWithoutDefault.Name}: {(propWithoutDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithoutDefault.Type.IsNullable ? "]" : string.Empty)} = {defaultValue}");
else
writer.WriteLine($"self.{conventions.GetAccessModifier(propWithoutDefault.Access)}{propWithoutDefault.NamePrefix}{propWithoutDefault.Name}: {(propWithoutDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithoutDefault.Type.IsNullable ? "]" : string.Empty)} = {NoneKeyword}");
writer.WriteLine($"self.{conventions.GetAccessModifier(propWithoutDefault.Access)}{propWithoutDefault.NamePrefix}{propWithoutDefault.Name}: {(propWithoutDefault.Type.IsNullable ? "Optional[" : string.Empty)}{returnType}{(propWithoutDefault.Type.IsNullable ? "]" : string.Empty)} = {defaultValue}");
}
}
private static void WriteSetterBody(CodeMethod codeElement, LanguageWriter writer, CodeClass parentClass)
Expand Down
7 changes: 7 additions & 0 deletions src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ public override void WriteCodeElement(CodeFunction codeElement, LanguageWriter w
var composedType = GetOriginalComposedType(codeMethod.ReturnType);
var isComposedOfPrimitives = composedType is not null && composedType.IsComposedOfPrimitives(IsPrimitiveType);

// A primitive-composed factory's body is `parseNode?.get*Value()`, i.e. always `T | undefined`, so its
// declared return type must stay nullable for the `| undefined` suffix to be emitted (read by
// CodeMethodWriter.WriteMethodPrototypeInternal). MakeRequiredPropertiesNonNullable can otherwise flip it
// non-nullable via a required scalar-alias reference; no-op when the flag is off (already nullable).
if (codeMethod.Kind is CodeMethodKind.Factory && isComposedOfPrimitives)
codeMethod.ReturnType.IsNullable = true;

var returnType = codeMethod.Kind is CodeMethodKind.Factory && !isComposedOfPrimitives ?
FactoryMethodReturnType :
GetTypescriptTypeString(codeMethod.ReturnType, codeElement, inlineComposedTypeString: true);
Expand Down
Loading