diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs
index 2da593d7628634..3560d8a1190c2d 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs
@@ -8,7 +8,12 @@ namespace System.Reflection
///
/// A class that represents nullability info
///
- public sealed class NullabilityInfo
+#if NET
+ public
+#else
+ internal
+#endif
+ sealed class NullabilityInfo
{
internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState writeState,
NullabilityInfo? elementType, NullabilityInfo[] typeArguments)
@@ -46,7 +51,12 @@ internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState
///
/// An enum that represents nullability state
///
- public enum NullabilityState
+#if NET
+ public
+#else
+ internal
+#endif
+ enum NullabilityState
{
///
/// Nullability context not enabled (oblivious)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs
index 9411bad737716e..5c56ef25197591 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs
@@ -11,7 +11,12 @@ namespace System.Reflection
/// Provides APIs for populating nullability information/context from reflection members:
/// , , and .
///
- public sealed class NullabilityInfoContext
+#if NET
+ public
+#else
+ internal
+#endif
+ sealed class NullabilityInfoContext
{
private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices";
private readonly Dictionary _publicOnlyModules = new();
@@ -65,7 +70,11 @@ private enum NotAnnotatedStatus
///
public NullabilityInfo Create(ParameterInfo parameterInfo)
{
+#if NET
ArgumentNullException.ThrowIfNull(parameterInfo);
+#else
+ NetstandardHelpers.ThrowIfNull(parameterInfo, nameof(parameterInfo));
+#endif
EnsureIsSupported();
@@ -190,7 +199,11 @@ private static void CheckNullabilityAttributes(NullabilityInfo nullability, ILis
///
public NullabilityInfo Create(PropertyInfo propertyInfo)
{
+#if NET
ArgumentNullException.ThrowIfNull(propertyInfo);
+#else
+ NetstandardHelpers.ThrowIfNull(propertyInfo, nameof(propertyInfo));
+#endif
EnsureIsSupported();
@@ -212,7 +225,9 @@ public NullabilityInfo Create(PropertyInfo propertyInfo)
if (setter != null)
{
- CheckNullabilityAttributes(nullability, setter.GetParametersAsSpan()[^1].GetCustomAttributesData());
+ ReadOnlySpan parameters = setter.GetParametersAsSpan();
+ ParameterInfo parameter = parameters[parameters.Length - 1];
+ CheckNullabilityAttributes(nullability, parameter.GetCustomAttributesData());
}
else
{
@@ -243,7 +258,11 @@ private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method)
///
public NullabilityInfo Create(EventInfo eventInfo)
{
+#if NET
ArgumentNullException.ThrowIfNull(eventInfo);
+#else
+ NetstandardHelpers.ThrowIfNull(eventInfo, nameof(eventInfo));
+#endif
EnsureIsSupported();
@@ -260,7 +279,11 @@ public NullabilityInfo Create(EventInfo eventInfo)
///
public NullabilityInfo Create(FieldInfo fieldInfo)
{
+#if NET
ArgumentNullException.ThrowIfNull(fieldInfo);
+#else
+ NetstandardHelpers.ThrowIfNull(fieldInfo, nameof(fieldInfo));
+#endif
EnsureIsSupported();
@@ -497,7 +520,11 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T
Debug.Assert(genericParameter.IsGenericParameter);
if (reflectedType is not null
+#if NET
&& !genericParameter.IsGenericMethodParameter
+#else
+ && !genericParameter.IsGenericMethodParameter()
+#endif
&& TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType))
{
return true;
@@ -528,7 +555,12 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T
private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(NullabilityInfo nullability, Type genericParameter, Type context, Type reflectedType)
{
- Debug.Assert(genericParameter.IsGenericParameter && !genericParameter.IsGenericMethodParameter);
+ Debug.Assert(genericParameter.IsGenericParameter &&
+#if NET
+ !genericParameter.IsGenericMethodParameter);
+#else
+ !genericParameter.IsGenericMethodParameter());
+#endif
Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context;
if (genericParameter.DeclaringType == contextTypeDefinition)
@@ -666,4 +698,54 @@ public bool ParseNullableState(int index, ref NullabilityState state)
}
}
}
+
+#if !NET
+ internal static class NetstandardHelpers
+ {
+ public static void ThrowIfNull(object? argument, string paramName)
+ {
+ if (argument is null)
+ {
+ Throw(paramName);
+ static void Throw(string paramName) => throw new ArgumentNullException(paramName);
+ }
+ }
+
+ [Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
+ Justification = "This is finding the MemberInfo with the same MetadataToken as specified MemberInfo. If the specified MemberInfo " +
+ "exists and wasn't trimmed, then the current Type's MemberInfo couldn't have been trimmed.")]
+ public static MemberInfo GetMemberWithSameMetadataDefinitionAs(this Type type, MemberInfo member)
+ {
+ ThrowIfNull(member, nameof(member));
+
+ const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
+ foreach (MemberInfo myMemberInfo in type.GetMembers(all))
+ {
+ if (myMemberInfo.HasSameMetadataDefinitionAs(member))
+ {
+ return myMemberInfo;
+ }
+ }
+
+ throw new MissingMemberException(type.FullName, member.Name);
+ }
+
+ private static bool HasSameMetadataDefinitionAs(this MemberInfo info, MemberInfo other)
+ {
+ if (info.MetadataToken != other.MetadataToken)
+ return false;
+
+ if (!info.Module.Equals(other.Module))
+ return false;
+
+ return true;
+ }
+
+ public static bool IsGenericMethodParameter(this Type type)
+ => type.IsGenericParameter && type.DeclaringMethod is not null;
+
+ public static ReadOnlySpan GetParametersAsSpan(this MethodBase metaMethod)
+ => metaMethod.GetParameters();
+ }
+#endif
}
diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs
index f1c76e7674f07a..8fd1e3353dc4f8 100644
--- a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs
+++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs
@@ -115,6 +115,11 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults)
///
public JsonCommentHandling ReadCommentHandling { get; set; }
+ ///
+ /// Specifies the default value of when set.
+ ///
+ public bool RespectNullableAnnotations { get; set; }
+
///
/// Specifies the default value of when set.
///
diff --git a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
index 3f3ecb506fd83d..5512321b0e2f04 100644
--- a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
+++ b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs
@@ -50,6 +50,12 @@ public static ITypeSymbol EraseCompileTimeMetadata(this Compilation compilation,
type = type.WithNullableAnnotation(NullableAnnotation.None);
}
+ if (type is IArrayTypeSymbol arrayType)
+ {
+ ITypeSymbol elementType = compilation.EraseCompileTimeMetadata(arrayType.ElementType);
+ return compilation.CreateArrayTypeSymbol(elementType, arrayType.Rank);
+ }
+
if (type is INamedTypeSymbol namedType)
{
if (namedType.IsTupleType)
@@ -189,6 +195,9 @@ SpecialType.System_Byte or SpecialType.System_UInt16 or SpecialType.System_UInt3
SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal;
}
+ public static bool IsNullableType(this ITypeSymbol type)
+ => !type.IsValueType || type.OriginalDefinition.SpecialType is SpecialType.System_Nullable_T;
+
public static bool IsNullableValueType(this ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? elementType)
{
if (type.IsValueType && type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T })
@@ -269,5 +278,107 @@ public static string GetTypeKindKeyword(this TypeDeclarationSyntax typeDeclarati
return null;
}
}
+
+ public static void ResolveNullabilityAnnotations(this IFieldSymbol field, out bool isGetterNonNullable, out bool isSetterNonNullable)
+ {
+ if (field.Type.IsNullableType())
+ {
+ // Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters,
+ // (e.g. the same metadata is being used for both KeyValuePair and KeyValuePair),
+ // we derive nullability annotations from the original definition of the field and not its instantiation.
+ // This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader.
+ field = field.OriginalDefinition;
+
+ isGetterNonNullable = IsOutputTypeNonNullable(field, field.Type);
+ isSetterNonNullable = IsInputTypeNonNullable(field, field.Type);
+ }
+ else
+ {
+ isGetterNonNullable = isSetterNonNullable = false;
+ }
+ }
+
+ public static void ResolveNullabilityAnnotations(this IPropertySymbol property, out bool isGetterNonNullable, out bool isSetterNonNullable)
+ {
+ if (property.Type.IsNullableType())
+ {
+ // Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters,
+ // (e.g. the same metadata is being used for both KeyValuePair and KeyValuePair),
+ // we derive nullability annotations from the original definition of the field and not its instantiation.
+ // This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader.
+ property = property.OriginalDefinition;
+
+ isGetterNonNullable = property.GetMethod != null && IsOutputTypeNonNullable(property, property.Type);
+ isSetterNonNullable = property.SetMethod != null && IsInputTypeNonNullable(property, property.Type);
+ }
+ else
+ {
+ isGetterNonNullable = isSetterNonNullable = false;
+ }
+ }
+
+ public static bool IsNullable(this IParameterSymbol parameter)
+ {
+ if (parameter.Type.IsNullableType())
+ {
+ // Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters,
+ // (e.g. the same metadata is being used for both KeyValuePair and KeyValuePair),
+ // we derive nullability annotations from the original definition of the field and not instation.
+ // This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader.
+ parameter = parameter.OriginalDefinition;
+ return !IsInputTypeNonNullable(parameter, parameter.Type);
+ }
+
+ return false;
+ }
+
+ private static bool IsOutputTypeNonNullable(this ISymbol symbol, ITypeSymbol returnType)
+ {
+ if (symbol.HasCodeAnalysisAttribute("MaybeNullAttribute"))
+ {
+ return false;
+ }
+
+ if (symbol.HasCodeAnalysisAttribute("NotNullAttribute"))
+ {
+ return true;
+ }
+
+ if (returnType is ITypeParameterSymbol { HasNotNullConstraint: false })
+ {
+ return false;
+ }
+
+ return returnType.NullableAnnotation is NullableAnnotation.NotAnnotated;
+ }
+
+ private static bool IsInputTypeNonNullable(this ISymbol symbol, ITypeSymbol inputType)
+ {
+ Debug.Assert(inputType.IsNullableType());
+
+ if (symbol.HasCodeAnalysisAttribute("AllowNullAttribute"))
+ {
+ return false;
+ }
+
+ if (symbol.HasCodeAnalysisAttribute("DisallowNullAttribute"))
+ {
+ return true;
+ }
+
+ if (inputType is ITypeParameterSymbol { HasNotNullConstraint: false })
+ {
+ return false;
+ }
+
+ return inputType.NullableAnnotation is NullableAnnotation.NotAnnotated;
+ }
+
+ private static bool HasCodeAnalysisAttribute(this ISymbol symbol, string attributeName)
+ {
+ return symbol.GetAttributes().Any(attr =>
+ attr.AttributeClass?.Name == attributeName &&
+ attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis");
+ }
}
}
diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs
index 06d15bd3b030b9..99a07d9e40d68d 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs
@@ -30,6 +30,9 @@ private static class ExceptionMessages
public const string InvalidSerializablePropertyConfiguration =
"Invalid serializable-property configuration specified for type '{0}'. For more information, see 'JsonSourceGenerationMode.Serialization'.";
+
+ public const string PropertyGetterDisallowNull =
+ "The property or field '{0}' on type '{1}' doesn't allow getting null values. Consider updating its nullability annotation.";
};
}
}
diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
index 12173a0e917de9..febf787afeef8a 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
@@ -41,6 +41,7 @@ private sealed partial class Emitter
// global::fully.qualified.name for referenced types
private const string InvalidOperationExceptionTypeRef = "global::System.InvalidOperationException";
+ private const string JsonExceptionTypeRef = "global::System.Text.Json.JsonException";
private const string TypeTypeRef = "global::System.Type";
private const string UnsafeTypeRef = "global::System.Runtime.CompilerServices.Unsafe";
private const string EqualityComparerTypeRef = "global::System.Collections.Generic.EqualityComparer";
@@ -672,6 +673,15 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe
writer.WriteLine($"properties[{i}].Order = {property.Order};");
}
+ if (property.IsGetterNonNullableAnnotation)
+ {
+ writer.WriteLine($"properties[{i}].IsGetNullable = false;");
+ }
+ if (property.IsSetterNonNullableAnnotation)
+ {
+ writer.WriteLine($"properties[{i}].IsSetNullable = false;");
+ }
+
writer.WriteLine();
}
@@ -682,33 +692,34 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe
private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, string ctorParamMetadataInitMethodName, TypeGenerationSpec typeGenerationSpec)
{
- const string parametersVarName = "parameters";
-
ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs;
ImmutableEquatableArray propertyInitializers = typeGenerationSpec.PropertyInitializerSpecs;
int paramCount = parameters.Count + propertyInitializers.Count(propInit => !propInit.MatchesConstructorParameter);
Debug.Assert(paramCount > 0);
- writer.WriteLine($"private static {JsonParameterInfoValuesTypeRef}[] {ctorParamMetadataInitMethodName}()");
+ writer.WriteLine($"private static {JsonParameterInfoValuesTypeRef}[] {ctorParamMetadataInitMethodName}() => new {JsonParameterInfoValuesTypeRef}[]");
writer.WriteLine('{');
writer.Indentation++;
- writer.WriteLine($"var {parametersVarName} = new {JsonParameterInfoValuesTypeRef}[{paramCount}];");
- writer.WriteLine();
-
+ int i = 0;
foreach (ParameterGenerationSpec spec in parameters)
{
writer.WriteLine($$"""
- {{parametersVarName}}[{{spec.ParameterIndex}}] = new()
+ new()
{
Name = {{FormatStringLiteral(spec.Name)}},
ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}),
Position = {{spec.ParameterIndex}},
HasDefaultValue = {{FormatBoolLiteral(spec.HasDefaultValue)}},
- DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}}
- };
-
+ DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}},
+ IsNullable = {{FormatBoolLiteral(spec.IsNullable)}},
+ },
""");
+
+ if (++i < paramCount)
+ {
+ writer.WriteLine();
+ }
}
foreach (PropertyInitializerGenerationSpec spec in propertyInitializers)
@@ -719,20 +730,22 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin
}
writer.WriteLine($$"""
- {{parametersVarName}}[{{spec.ParameterIndex}}] = new()
+ new()
{
Name = {{FormatStringLiteral(spec.Name)}},
ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}),
Position = {{spec.ParameterIndex}},
- };
-
+ },
""");
- }
- writer.WriteLine($"return {parametersVarName};");
+ if (++i < paramCount)
+ {
+ writer.WriteLine();
+ }
+ }
writer.Indentation--;
- writer.WriteLine('}');
+ writer.WriteLine("};");
}
private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGenerationSpec contextSpec, string serializeMethodName, TypeGenerationSpec typeGenSpec)
@@ -760,6 +773,8 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio
writer.WriteLine($"{WriterVarName}.WriteStartObject();");
writer.WriteLine();
+ bool generateDisallowNullThrowHelper = false;
+
// Provide generation logic for each prop.
foreach (int i in typeGenSpec.FastPathPropertyIndices)
{
@@ -784,7 +799,7 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio
Debug.Assert(!_propertyNames.TryGetValue(effectiveJsonPropertyName, out string? existingName) || existingName == propertyNameFieldName);
_propertyNames.TryAdd(effectiveJsonPropertyName, propertyNameFieldName);
- DefaultCheckType defaultCheckType = GetDefaultCheckType(contextSpec, propertyGenSpec);
+ SerializedValueCheckType defaultCheckType = GetCheckType(contextSpec, propertyGenSpec);
// For properties whose declared type differs from that of the serialized type
// perform an explicit cast -- this is to account for hidden properties or diamond ambiguity.
@@ -793,7 +808,7 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio
: ValueVarName;
string propValueExpr;
- if (defaultCheckType != DefaultCheckType.None)
+ if (defaultCheckType != SerializedValueCheckType.None)
{
// Use temporary variable to evaluate property value only once
string localVariableName = $"__value_{propertyGenSpec.NameSpecifiedInSourceCode.TrimStart('@')}";
@@ -807,25 +822,44 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio
switch (defaultCheckType)
{
- case DefaultCheckType.Null:
+ case SerializedValueCheckType.IgnoreWhenNull:
writer.WriteLine($"if ({propValueExpr} != null)");
writer.WriteLine('{');
writer.Indentation++;
+
+ GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr);
+
+ writer.Indentation--;
+ writer.WriteLine('}');
break;
- case DefaultCheckType.Default:
+ case SerializedValueCheckType.IgnoreWhenDefault:
writer.WriteLine($"if (!{EqualityComparerTypeRef}<{propertyGenSpec.PropertyType.FullyQualifiedName}>.Default.Equals(default, {propValueExpr}))");
writer.WriteLine('{');
writer.Indentation++;
+
+ GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr);
+
+ writer.Indentation--;
+ writer.WriteLine('}');
break;
- }
- GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr);
+ case SerializedValueCheckType.DisallowNull:
+ writer.WriteLine($$"""
+ if ({{propValueExpr}} is null)
+ {
+ ThrowPropertyNullException({{FormatStringLiteral(propertyGenSpec.EffectiveJsonPropertyName)}});
+ }
- if (defaultCheckType != DefaultCheckType.None)
- {
- writer.Indentation--;
- writer.WriteLine('}');
+ """);
+
+ GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr);
+ generateDisallowNullThrowHelper = true;
+ break;
+
+ default:
+ GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr);
+ break;
}
}
@@ -839,6 +873,17 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio
writer.WriteLine($"((global::{JsonConstants.IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();");
}
+ if (generateDisallowNullThrowHelper)
+ {
+ writer.WriteLine();
+ writer.WriteLine($$"""
+ static void ThrowPropertyNullException(string propertyName)
+ {
+ throw new {{JsonExceptionTypeRef}}(string.Format("{{ExceptionMessages.PropertyGetterDisallowNull}}", propertyName, {{FormatStringLiteral(typeGenSpec.TypeRef.Name)}}));
+ }
+ """);
+ }
+
writer.Indentation--;
writer.WriteLine('}');
}
@@ -978,20 +1023,22 @@ private static void GenerateSerializePropertyStatement(SourceWriter writer, Type
}
}
- private enum DefaultCheckType
+ private enum SerializedValueCheckType
{
None,
- Null,
- Default,
+ IgnoreWhenNull,
+ IgnoreWhenDefault,
+ DisallowNull,
}
- private static DefaultCheckType GetDefaultCheckType(ContextGenerationSpec contextSpec, PropertyGenerationSpec propertySpec)
+ private static SerializedValueCheckType GetCheckType(ContextGenerationSpec contextSpec, PropertyGenerationSpec propertySpec)
{
return (propertySpec.DefaultIgnoreCondition ?? contextSpec.GeneratedOptionsSpec?.DefaultIgnoreCondition) switch
{
- JsonIgnoreCondition.WhenWritingNull => propertySpec.PropertyType.CanBeNull ? DefaultCheckType.Null : DefaultCheckType.None,
- JsonIgnoreCondition.WhenWritingDefault => propertySpec.PropertyType.CanBeNull ? DefaultCheckType.Null : DefaultCheckType.Default,
- _ => DefaultCheckType.None,
+ JsonIgnoreCondition.WhenWritingNull => propertySpec.PropertyType.CanBeNull ? SerializedValueCheckType.IgnoreWhenNull : SerializedValueCheckType.None,
+ JsonIgnoreCondition.WhenWritingDefault => propertySpec.PropertyType.CanBeNull ? SerializedValueCheckType.IgnoreWhenNull : SerializedValueCheckType.IgnoreWhenDefault,
+ _ when propertySpec.IsGetterNonNullableAnnotation && contextSpec.GeneratedOptionsSpec?.RespectNullableAnnotations is true => SerializedValueCheckType.DisallowNull,
+ _ => SerializedValueCheckType.None,
};
}
@@ -1135,6 +1182,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti
if (optionsSpec.DictionaryKeyPolicy is JsonKnownNamingPolicy dictionaryKeyPolicy)
writer.WriteLine($"DictionaryKeyPolicy = {FormatNamingPolicy(dictionaryKeyPolicy)},");
+ if (optionsSpec.RespectNullableAnnotations is bool respectNullableAnnotations)
+ writer.WriteLine($"RespectNullableAnnotations = {FormatBoolLiteral(respectNullableAnnotations)},");
+
if (optionsSpec.IgnoreReadOnlyFields is bool ignoreReadOnlyFields)
writer.WriteLine($"IgnoreReadOnlyFields = {FormatBoolLiteral(ignoreReadOnlyFields)},");
diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
index 5d94f860cd785f..66d40939ca4233 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
@@ -268,6 +268,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
int? defaultBufferSize = null;
JsonIgnoreCondition? defaultIgnoreCondition = null;
JsonKnownNamingPolicy? dictionaryKeyPolicy = null;
+ bool? respectNullableAnnotations = null;
bool? ignoreReadOnlyFields = null;
bool? ignoreReadOnlyProperties = null;
bool? includeFields = null;
@@ -329,6 +330,10 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
dictionaryKeyPolicy = (JsonKnownNamingPolicy)namedArg.Value.Value!;
break;
+ case nameof(JsonSourceGenerationOptionsAttribute.RespectNullableAnnotations):
+ respectNullableAnnotations = (bool)namedArg.Value.Value!;
+ break;
+
case nameof(JsonSourceGenerationOptionsAttribute.IgnoreReadOnlyFields):
ignoreReadOnlyFields = (bool)namedArg.Value.Value!;
break;
@@ -412,6 +417,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN
Converters = converters?.ToImmutableEquatableArray(),
DefaultIgnoreCondition = defaultIgnoreCondition,
DictionaryKeyPolicy = dictionaryKeyPolicy,
+ RespectNullableAnnotations = respectNullableAnnotations,
IgnoreReadOnlyFields = ignoreReadOnlyFields,
IgnoreReadOnlyProperties = ignoreReadOnlyProperties,
IncludeFields = includeFields,
@@ -1120,7 +1126,9 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
out bool canUseGetter,
out bool canUseSetter,
out bool hasJsonIncludeButIsInaccessible,
- out bool setterIsInitOnly);
+ out bool setterIsInitOnly,
+ out bool isGetterNonNullable,
+ out bool isSetterNonNullable);
if (hasJsonIncludeButIsInaccessible)
{
@@ -1184,6 +1192,8 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
PropertyType = propertyTypeRef,
DeclaringType = declaringType,
ConverterType = converterType,
+ IsGetterNonNullableAnnotation = isGetterNonNullable,
+ IsSetterNonNullableAnnotation = isSetterNonNullable,
};
}
@@ -1301,7 +1311,9 @@ private void ProcessMember(
out bool canUseGetter,
out bool canUseSetter,
out bool hasJsonIncludeButIsInaccessible,
- out bool isSetterInitOnly)
+ out bool isSetterInitOnly,
+ out bool isGetterNonNullable,
+ out bool isSetterNonNullable)
{
isAccessible = false;
isReadOnly = false;
@@ -1310,6 +1322,8 @@ private void ProcessMember(
canUseSetter = false;
hasJsonIncludeButIsInaccessible = false;
isSetterInitOnly = false;
+ isGetterNonNullable = false;
+ isSetterNonNullable = false;
switch (memberInfo)
{
@@ -1358,6 +1372,8 @@ private void ProcessMember(
{
isReadOnly = true;
}
+
+ propertyInfo.ResolveNullabilityAnnotations(out isGetterNonNullable, out isSetterNonNullable);
break;
case IFieldSymbol fieldInfo:
isReadOnly = fieldInfo.IsReadOnly;
@@ -1380,6 +1396,8 @@ private void ProcessMember(
{
hasJsonIncludeButIsInaccessible = hasJsonInclude;
}
+
+ fieldInfo.ResolveNullabilityAnnotations(out isGetterNonNullable, out isSetterNonNullable);
break;
default:
Debug.Fail("Method given an invalid symbol type.");
@@ -1430,6 +1448,7 @@ private void ProcessMember(
HasDefaultValue = parameterInfo.HasExplicitDefaultValue,
DefaultValue = parameterInfo.HasExplicitDefaultValue ? parameterInfo.ExplicitDefaultValue : null,
ParameterIndex = i,
+ IsNullable = parameterInfo.IsNullable(),
};
}
}
diff --git a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs
index 68e32d01531569..2dea67e2159ccb 100644
--- a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs
+++ b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs
@@ -32,5 +32,6 @@ public sealed record ParameterGenerationSpec
// so it always satisfies the structural equality requirement for the record.
public required object? DefaultValue { get; init; }
public required int ParameterIndex { get; init; }
+ public required bool IsNullable { get; init; }
}
}
diff --git a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs
index 214c32b4d19e21..a003a9d74d48c2 100644
--- a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs
+++ b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs
@@ -97,6 +97,16 @@ public sealed record PropertyGenerationSpec
///
public required bool CanUseSetter { get; init; }
+ ///
+ /// Whether the property getter returns a nullable type with a non-nullable annotation.
+ ///
+ public required bool IsGetterNonNullableAnnotation { get; init; }
+
+ ///
+ /// Whether the property setter accepts a nullable type with a non-nullable annotation.
+ ///
+ public required bool IsSetterNonNullableAnnotation { get; init; }
+
///
/// The for the property.
///
diff --git a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs
index e16bebb6ceb49e..9b7ecc8ea6920a 100644
--- a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs
+++ b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs
@@ -28,6 +28,8 @@ public sealed record SourceGenerationOptionsSpec
public required JsonKnownNamingPolicy? DictionaryKeyPolicy { get; init; }
+ public required bool? RespectNullableAnnotations { get; init; }
+
public required bool? IgnoreReadOnlyFields { get; init; }
public required bool? IgnoreReadOnlyProperties { get; init; }
diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
index 3fd6af13262bf1..f50564ba081fc8 100644
--- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs
+++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs
@@ -401,6 +401,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } }
public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } }
+ public bool RespectNullableAnnotations { get { throw null; } set { } }
public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver? TypeInfoResolver { get { throw null; } set { } }
public System.Collections.Generic.IList TypeInfoResolverChain { get { throw null; } }
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
@@ -1091,6 +1092,7 @@ public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefau
public bool PropertyNameCaseInsensitive { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonKnownNamingPolicy PropertyNamingPolicy { get { throw null; } set { } }
public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
+ public bool RespectNullableAnnotations { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } set { } }
public bool UseStringEnumConverter { get { throw null; } set { } }
@@ -1274,6 +1276,7 @@ public sealed partial class JsonParameterInfoValues
public JsonParameterInfoValues() { }
public object? DefaultValue { get { throw null; } init { } }
public bool HasDefaultValue { get { throw null; } init { } }
+ public bool IsNullable { get { throw null; } init { } }
public string Name { get { throw null; } init { } }
public System.Type ParameterType { get { throw null; } init { } }
public int Position { get { throw null; } init { } }
@@ -1294,7 +1297,9 @@ internal JsonPropertyInfo() { }
public System.Text.Json.Serialization.JsonConverter? CustomConverter { get { throw null; } set { } }
public System.Func