Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@
namespace Exceptionless.Web.Utility.OpenApi;

/// <summary>
/// Schema transformer that marks non-nullable properties as required in OpenAPI schemas.
/// Schema transformer that marks required properties in OpenAPI schemas based on C# required modifiers and value types.
/// </summary>
/// <remarks>
/// <para>
/// <b>Why this exists:</b> Microsoft.AspNetCore.OpenApi doesn't consistently detect
/// required properties from C# nullability annotations. This causes all properties to
/// become optional in generated schemas, even when they're non-nullable in the C# model.
/// required properties from C# modifiers. This transformer ensures properties are marked as required
/// when appropriate for the OpenAPI schema.
/// </para>
/// <para>
/// This transformer inspects C# nullability context and marks properties as required when:
/// This transformer marks properties as required when:
/// <list type="bullet">
/// <item>The property has the <c>required</c> modifier (C# 11+)</item>
/// <item>The property type is a non-nullable value type (e.g., <c>int</c>, <c>bool</c>, <c>DateTime</c>)</item>
/// <item>The property type is a non-nullable reference type in a nullable-enabled context</item>
/// </list>
/// </para>
/// <para>
/// The <c>[Required]</c> attribute is also respected for explicit marking.
/// Non-nullable reference types are NOT marked as required unless they have the explicit <c>required</c> modifier.
/// This correctly handles properties with default initializers (e.g., <c>public MyClass Prop { get; init; } = new();</c>).
/// </para>
/// <para>
/// This transformer resolves property names using the effective JSON property name
Expand Down Expand Up @@ -82,23 +83,26 @@ private static bool IsPropertyRequired(PropertyInfo property, NullabilityInfoCon
{
var propertyType = property.PropertyType;

// Check if the property is marked with the 'required' modifier (C# 11+)
// This takes precedence over other heuristics
var requiredMemberAttribute = property.GetCustomAttribute<System.Runtime.CompilerServices.RequiredMemberAttribute>();
if (requiredMemberAttribute is not null)
return true;

// Non-nullable value types are always required (except when wrapped in Nullable<T>)
if (propertyType.IsValueType)
{
// Nullable<T> is optional, plain value types are required
return Nullable.GetUnderlyingType(propertyType) is null;
}

// For reference types, check nullability annotations
try
{
var nullabilityInfo = nullabilityContext.Create(property);
return nullabilityInfo.WriteState == NullabilityState.NotNull;
}
catch
{
// If we can't determine nullability, default to not required
return false;
}
// For reference types with default initializers (e.g., "= new()"), we should NOT mark them as required
// since they can be omitted during construction. However, we can't reliably detect initializers via reflection.
// Instead, we only mark reference types as required if they have the 'required' modifier (checked above).
// This means non-nullable reference types without 'required' are treated as optional.

// For backwards compatibility and to match expected behavior, we do NOT mark non-nullable
// reference types as required unless they have the explicit 'required' modifier.
return false;
}
}