diff --git a/src/Exceptionless.Web/Utility/OpenApi/RequiredPropertySchemaTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/RequiredPropertySchemaTransformer.cs index 13fa0bd7e7..2bf798f302 100644 --- a/src/Exceptionless.Web/Utility/OpenApi/RequiredPropertySchemaTransformer.cs +++ b/src/Exceptionless.Web/Utility/OpenApi/RequiredPropertySchemaTransformer.cs @@ -5,23 +5,24 @@ namespace Exceptionless.Web.Utility.OpenApi; /// -/// 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. /// /// /// /// Why this exists: 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. /// /// -/// This transformer inspects C# nullability context and marks properties as required when: +/// This transformer marks properties as required when: /// +/// The property has the required modifier (C# 11+) /// The property type is a non-nullable value type (e.g., int, bool, DateTime) -/// The property type is a non-nullable reference type in a nullable-enabled context /// /// /// -/// The [Required] attribute is also respected for explicit marking. +/// Non-nullable reference types are NOT marked as required unless they have the explicit required modifier. +/// This correctly handles properties with default initializers (e.g., public MyClass Prop { get; init; } = new();). /// /// /// This transformer resolves property names using the effective JSON property name @@ -82,6 +83,12 @@ 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(); + if (requiredMemberAttribute is not null) + return true; + // Non-nullable value types are always required (except when wrapped in Nullable) if (propertyType.IsValueType) { @@ -89,16 +96,13 @@ private static bool IsPropertyRequired(PropertyInfo property, NullabilityInfoCon 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; } }