From 97a32755faff8564f0291ef5791c936a0d411965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Mon, 13 May 2024 01:28:42 -0500 Subject: [PATCH 01/19] Progress so far on nullability annotations --- .../src/System/Reflection/NullabilityInfo.cs | 14 +- .../Reflection/NullabilityInfoContext.cs | 106 +++- .../JsonSourceGenerationOptionsAttribute.cs | 5 + .../gen/JsonSourceGenerator.Emitter.cs | 15 +- .../gen/JsonSourceGenerator.Parser.cs | 55 +- .../gen/Model/ParameterGenerationSpec.cs | 1 + .../gen/Model/PropertyGenerationSpec.cs | 10 + .../gen/Model/SourceGenerationOptionsSpec.cs | 2 + .../System.Text.Json/ref/System.Text.Json.cs | 5 + .../src/Resources/Strings.resx | 6 + .../src/System.Text.Json.csproj | 2 + .../Text/Json/AppContextSwitchHelper.cs | 6 + ...ParameterizedConstructorConverter.Large.cs | 5 + ...ParameterizedConstructorConverter.Small.cs | 17 +- .../JsonSerializerOptions.Caching.cs | 2 + .../Serialization/JsonSerializerOptions.cs | 21 +- .../DefaultJsonTypeInfoResolver.Helpers.cs | 39 +- .../Metadata/JsonParameterInfo.cs | 4 + .../Metadata/JsonParameterInfoValues.cs | 6 + .../Metadata/JsonPropertyInfo.cs | 10 + .../Metadata/JsonPropertyInfoOfT.cs | 32 ++ .../Serialization/Metadata/JsonTypeInfo.cs | 2 + .../Text/Json/ThrowHelper.Serialization.cs | 6 + .../tests/Common/NullableAnnotationsTests.cs | 480 ++++++++++++++++++ .../Serialization/NullableAnnotationsTests.cs | 87 ++++ ...m.Text.Json.SourceGeneration.Tests.targets | 4 + .../Serialization/NullableAnnotationsTests.cs | 15 + .../Serialization/OptionsTests.cs | 27 + .../System.Text.Json.Tests.csproj | 2 + 29 files changed, 972 insertions(+), 14 deletions(-) create mode 100644 src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs create mode 100644 src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/NullableAnnotationsTests.cs 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..1737dd259e49ba 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 !NETCOREAPP + internal +#else + public +#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 !NETCOREAPP + internal +#else + public +#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..d547db4eb5f3d4 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 !NETCOREAPP + internal +#else + public +#endif + sealed class NullabilityInfoContext { private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices"; private readonly Dictionary _publicOnlyModules = new(); @@ -65,7 +70,14 @@ private enum NotAnnotatedStatus /// public NullabilityInfo Create(ParameterInfo parameterInfo) { +#if NETCOREAPP ArgumentNullException.ThrowIfNull(parameterInfo); +#else + if (parameterInfo is null) + { + throw new ArgumentNullException(nameof(parameterInfo)); + } +#endif EnsureIsSupported(); @@ -115,7 +127,11 @@ private void CheckParameterMetadataType(ParameterInfo parameter, NullabilityInfo private static ParameterInfo? GetMetaParameter(MethodBase metaMethod, ParameterInfo parameter) { +#if NETCOREAPP ReadOnlySpan parameters = metaMethod.GetParametersAsSpan(); +#else + ReadOnlySpan parameters = metaMethod.GetParameters(); +#endif for (int i = 0; i < parameters.Length; i++) { if (parameter.Position == i && @@ -190,7 +206,14 @@ private static void CheckNullabilityAttributes(NullabilityInfo nullability, ILis /// public NullabilityInfo Create(PropertyInfo propertyInfo) { +#if NETCOREAPP ArgumentNullException.ThrowIfNull(propertyInfo); +#else + if (propertyInfo is null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } +#endif EnsureIsSupported(); @@ -212,7 +235,13 @@ public NullabilityInfo Create(PropertyInfo propertyInfo) if (setter != null) { - CheckNullabilityAttributes(nullability, setter.GetParametersAsSpan()[^1].GetCustomAttributesData()); +#if NETCOREAPP + ParameterInfo parameter = setter.GetParametersAsSpan()[^1]; +#else + ParameterInfo[] parameters = setter.GetParameters(); + ParameterInfo parameter = parameters[parameters.Length - 1]; +#endif + CheckNullabilityAttributes(nullability, parameter.GetCustomAttributesData()); } else { @@ -243,7 +272,14 @@ private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method) /// public NullabilityInfo Create(EventInfo eventInfo) { +#if NETCOREAPP ArgumentNullException.ThrowIfNull(eventInfo); +#else + if (eventInfo is null) + { + throw new ArgumentNullException(nameof(eventInfo)); + } +#endif EnsureIsSupported(); @@ -260,7 +296,14 @@ public NullabilityInfo Create(EventInfo eventInfo) /// public NullabilityInfo Create(FieldInfo fieldInfo) { +#if NETCOREAPP ArgumentNullException.ThrowIfNull(fieldInfo); +#else + if (fieldInfo is null) + { + throw new ArgumentNullException(nameof(fieldInfo)); + } +#endif EnsureIsSupported(); @@ -456,7 +499,12 @@ private static Type GetPropertyMetaType(PropertyInfo property) return method.ReturnType; } - return property.GetSetMethod(true)!.GetParametersAsSpan()[0].ParameterType; +#if NETCOREAPP + ReadOnlySpan parameters = property.GetSetMethod(true)!.GetParametersAsSpan(); +#else + ParameterInfo[] parameters = property.GetSetMethod(true)!.GetParameters(); +#endif + return parameters[0].ParameterType; } private void CheckGenericParameters(NullabilityInfo nullability, MemberInfo metaMember, Type metaType, Type? reflectedType) @@ -497,7 +545,11 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T Debug.Assert(genericParameter.IsGenericParameter); if (reflectedType is not null +#if NETCOREAPP && !genericParameter.IsGenericMethodParameter +#else + && !NetstandardHelpers.IsGenericMethodParameter(genericParameter) +#endif && TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType)) { return true; @@ -528,7 +580,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 NETCOREAPP + !genericParameter.IsGenericMethodParameter); +#else + !NetstandardHelpers.IsGenericMethodParameter(genericParameter)); +#endif Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context; if (genericParameter.DeclaringType == contextTypeDefinition) @@ -666,4 +723,45 @@ public bool ParseNullableState(int index, ref NullabilityState state) } } } + +#if !NETCOREAPP + internal static class NetstandardHelpers + { + [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) + { + if (member is null) + { + throw new ArgumentNullException(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(Type type) + => type.IsGenericParameter && type.DeclaringMethod is not null; + } +#endif } diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs index f1c76e7674f07a..472bf4005af064 100644 --- a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs @@ -70,6 +70,11 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults) /// public JsonKnownNamingPolicy DictionaryKeyPolicy { get; set; } + /// + /// Specifies the default value of when set. + /// + public bool IgnoreNullableAnnotations { get; set; } + /// /// Specifies the default value of when set. /// diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 12173a0e917de9..2e8c36cd604171 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -672,6 +672,15 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe writer.WriteLine($"properties[{i}].Order = {property.Order};"); } + if (property.DisallowNullWrites) + { + writer.WriteLine($"properties[{i}].DisallowNullWrites = true;"); + } + if (property.DisallowNullReads) + { + writer.WriteLine($"properties[{i}].DisallowNullReads = true;"); + } + writer.WriteLine(); } @@ -705,7 +714,8 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin 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)}}, + DisallowNullReads = {{FormatBoolLiteral(spec.DisallowNullReads)}}, }; """); @@ -1135,6 +1145,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti if (optionsSpec.DictionaryKeyPolicy is JsonKnownNamingPolicy dictionaryKeyPolicy) writer.WriteLine($"DictionaryKeyPolicy = {FormatNamingPolicy(dictionaryKeyPolicy)},"); + if (optionsSpec.IgnoreNullableAnnotations is bool ignoreNullableAnnotations) + writer.WriteLine($"IgnoreNullableAnnotations = {FormatBoolLiteral(ignoreNullableAnnotations)},"); + 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..f87c21ab3f7456 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? ignoreNullableAnnotations = 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.IgnoreNullableAnnotations): + ignoreNullableAnnotations = (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, + IgnoreNullableAnnotations = ignoreNullableAnnotations, 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 disallowNullWrites, + out bool disallowNullReads); if (hasJsonIncludeButIsInaccessible) { @@ -1184,6 +1192,8 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type) PropertyType = propertyTypeRef, DeclaringType = declaringType, ConverterType = converterType, + DisallowNullWrites = disallowNullWrites, + DisallowNullReads = disallowNullReads, }; } @@ -1301,7 +1311,9 @@ private void ProcessMember( out bool canUseGetter, out bool canUseSetter, out bool hasJsonIncludeButIsInaccessible, - out bool isSetterInitOnly) + out bool isSetterInitOnly, + out bool disallowNullWrites, + out bool disallowNullReads) { isAccessible = false; isReadOnly = false; @@ -1310,6 +1322,8 @@ private void ProcessMember( canUseSetter = false; hasJsonIncludeButIsInaccessible = false; isSetterInitOnly = false; + disallowNullWrites = false; + disallowNullReads = false; switch (memberInfo) { @@ -1358,6 +1372,12 @@ private void ProcessMember( { isReadOnly = true; } + + if (!propertyInfo.Type.IsValueType) + { + disallowNullWrites = propertyInfo.GetMethod != null && IsReturnValueNonNullable(propertyInfo, propertyInfo.NullableAnnotation); + disallowNullReads = propertyInfo.SetMethod != null && IsParameterNonNullable(propertyInfo, propertyInfo.NullableAnnotation); + } break; case IFieldSymbol fieldInfo: isReadOnly = fieldInfo.IsReadOnly; @@ -1380,6 +1400,12 @@ private void ProcessMember( { hasJsonIncludeButIsInaccessible = hasJsonInclude; } + + if (!fieldInfo.Type.IsValueType) + { + disallowNullWrites = IsReturnValueNonNullable(fieldInfo, fieldInfo.NullableAnnotation); + disallowNullReads = IsParameterNonNullable(fieldInfo, fieldInfo.NullableAnnotation); + } break; default: Debug.Fail("Method given an invalid symbol type."); @@ -1430,6 +1456,7 @@ private void ProcessMember( HasDefaultValue = parameterInfo.HasExplicitDefaultValue, DefaultValue = parameterInfo.HasExplicitDefaultValue ? parameterInfo.ExplicitDefaultValue : null, ParameterIndex = i, + DisallowNullReads = IsParameterNonNullable(parameterInfo, parameterInfo.NullableAnnotation), }; } } @@ -1593,6 +1620,30 @@ private static string DeterminePropertyNameFieldName(string effectiveJsonPropert return sb.ToString(); } + // TODO: is this the right place to place this? Look into the roslynextensions helper. + private static bool IsReturnValueNonNullable(ISymbol symbol, NullableAnnotation returnTypeAnnotation) + { + return + !HasCodeAnalysisAttribute(symbol, "MaybeNullAttribute") && + (returnTypeAnnotation is NullableAnnotation.NotAnnotated || + HasCodeAnalysisAttribute(symbol, "NotNullAttribute")); + } + + private static bool IsParameterNonNullable(ISymbol symbol, NullableAnnotation parameterAnnotation) + { + return + !HasCodeAnalysisAttribute(symbol, "AllowNullAttribute") && + (parameterAnnotation is NullableAnnotation.NotAnnotated || + HasCodeAnalysisAttribute(symbol, "DisallowNullAttribute")); + } + + private static bool HasCodeAnalysisAttribute(ISymbol symbol, string attributeName) + { + return symbol.GetAttributes().Any(attr => + attr.AttributeClass?.Name == attributeName && + attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis"); + } + private JsonPrimitiveTypeKind? GetPrimitiveTypeKind(ITypeSymbol type) { if (type.IsNumberType()) diff --git a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs index 68e32d01531569..85d6ff89225da6 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 DisallowNullReads { 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..6b3b8042074b57 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 disallows serializing null values based on its nullability annotations. + /// + public required bool DisallowNullWrites { get; init; } + + /// + /// Whether the property disallows deserializing null values based on its nullability annotations. + /// + public required bool DisallowNullReads{ 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..e866779cdb474b 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? IgnoreNullableAnnotations { 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..3fd4f73e8a7ec4 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 IgnoreNullableAnnotations { 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 { } } @@ -1097,6 +1098,7 @@ public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefau public bool WriteIndented { get { throw null; } set { } } public char IndentCharacter { get { throw null; } set { } } public int IndentSize { get { throw null; } set { } } + public bool IgnoreNullableAnnotations { get { throw null; } set { } } } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. Applications should use the generic JsonStringEnumConverter instead.")] public partial class JsonStringEnumConverter : System.Text.Json.Serialization.JsonConverterFactory @@ -1277,6 +1279,7 @@ public JsonParameterInfoValues() { } public string Name { get { throw null; } init { } } public System.Type ParameterType { get { throw null; } init { } } public int Position { get { throw null; } init { } } + public bool DisallowNullReads { get { throw null; } init { } } } public partial class JsonPolymorphismOptions { @@ -1303,6 +1306,8 @@ internal JsonPropertyInfo() { } public System.Type PropertyType { get { throw null; } } public System.Action? Set { get { throw null; } set { } } public System.Func? ShouldSerialize { get { throw null; } set { } } + public bool DisallowNullWrites { get { throw null; } set { } } + public bool DisallowNullReads { get { throw null; } set { } } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public sealed partial class JsonPropertyInfoValues diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index e343daa47cf5aa..c7ac6a8f8687f2 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -737,4 +737,10 @@ New line can be only "\n" or "\r\n". + + The nullable annotations of the property or field '{0}' on type '{1}' don't allow null values. + + + NullabilityInfoContext is not supported in the current application because 'System.Reflection.NullabilityInfoContext.IsSupported' is set to false. Set the MSBuild Property 'NullabilityInfoContextSupport' to true in order to enable it. + diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 7b49b89792cd74..2b57eb315e37b0 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -347,6 +347,8 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs index 3e1291c676d922..dc21123ffb822e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs @@ -10,5 +10,11 @@ internal static class AppContextSwitchHelper switchName: "System.Text.Json.Serialization.EnableSourceGenReflectionFallback", isEnabled: out bool value) ? value : false; + + public static bool IgnoreNullableAnnotationsDefault { get; } = + AppContext.TryGetSwitch( + switchName: "System.Text.Json.Serialization.IgnoreNullableAnnotationsDefault", + isEnabled: out bool value) + ? value : false; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index b00661bd568f45..821db61dfc14f8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -22,6 +22,11 @@ protected sealed override bool ReadAndCacheConstructorArgument(scoped ref ReadSt if (success && !(arg == null && jsonParameterInfo.IgnoreNullTokensOnRead)) { + if (arg == null && jsonParameterInfo.DisallowNullReads && !jsonParameterInfo.Options.IgnoreNullableAnnotations) + { + ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(jsonParameterInfo.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); + } + ((object[])state.Current.CtorArgumentState!.Arguments)[jsonParameterInfo.Position] = arg!; // if this is required property IgnoreNullTokensOnRead will always be false because we don't allow for both to be true diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs index af80e25fc02377..bfac75a182bfb5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs @@ -66,9 +66,20 @@ private static bool TryRead( bool success = info.EffectiveConverter.TryRead(ref reader, info.ParameterType, info.Options, ref state, out TArg? value, out _); - arg = value is null && jsonParameterInfo.IgnoreNullTokensOnRead - ? info.DefaultValue // Use default value specified on parameter, if any. - : value; + if (value is null) + { + if (info.IgnoreNullTokensOnRead) + { + // Use default value specified on parameter, if any. + value = info.DefaultValue; + } + else if (info.DisallowNullReads && !info.Options.IgnoreNullableAnnotations) + { + ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(info.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); + } + } + + arg = value; if (success) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index e171d8cdebe490..7c316ea19ddcc7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -507,6 +507,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) left.NewLine == right.NewLine && // Read through property due to lazy initialization of the backing field left._allowOutOfOrderMetadataProperties == right._allowOutOfOrderMetadataProperties && left._allowTrailingCommas == right._allowTrailingCommas && + left._ignoreNullableAnnotations == right._ignoreNullableAnnotations && left._ignoreNullValues == right._ignoreNullValues && left._ignoreReadOnlyProperties == right._ignoreReadOnlyProperties && left._ignoreReadonlyFields == right._ignoreReadonlyFields && @@ -565,6 +566,7 @@ public int GetHashCode(JsonSerializerOptions options) AddHashCode(ref hc, options.NewLine); // Read through property due to lazy initialization of the backing field AddHashCode(ref hc, options._allowOutOfOrderMetadataProperties); AddHashCode(ref hc, options._allowTrailingCommas); + AddHashCode(ref hc, options._ignoreNullableAnnotations); AddHashCode(ref hc, options._ignoreNullValues); AddHashCode(ref hc, options._ignoreReadOnlyProperties); AddHashCode(ref hc, options._ignoreReadonlyFields); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index e7123e3a4431e2..77f8d546fe8c49 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -67,7 +67,8 @@ public static JsonSerializerOptions Web private static JsonSerializerOptions? s_webOptions; - // For any new option added, adding it to the options copied in the copy constructor below must be considered. + // For any new option added, considered adding it to the options copied in the copy constructor below + // and consider updating the EqualtyComparer used for comparing CachingContexts. private IJsonTypeInfoResolver? _typeInfoResolver; private JsonNamingPolicy? _dictionaryKeyPolicy; private JsonNamingPolicy? _jsonPropertyNamingPolicy; @@ -85,6 +86,7 @@ public static JsonSerializerOptions Web private int _maxDepth; private bool _allowOutOfOrderMetadataProperties; private bool _allowTrailingCommas; + private bool _ignoreNullableAnnotations = AppContextSwitchHelper.IgnoreNullableAnnotationsDefault; private bool _ignoreNullValues; private bool _ignoreReadOnlyProperties; private bool _ignoreReadonlyFields; @@ -138,6 +140,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _maxDepth = options._maxDepth; _allowOutOfOrderMetadataProperties = options._allowOutOfOrderMetadataProperties; _allowTrailingCommas = options._allowTrailingCommas; + _ignoreNullableAnnotations = options._ignoreNullableAnnotations; _ignoreNullValues = options._ignoreNullValues; _ignoreReadOnlyProperties = options._ignoreReadOnlyProperties; _ignoreReadonlyFields = options._ignoreReadonlyFields; @@ -779,6 +782,22 @@ public string NewLine } } + /// + /// Gets or sets a value that indicates whether nullability annotations of nullable reference types are ignored during serialization and deserialization. + /// + /// + /// Thrown if this property is set after serialization or deserialization has occurred. + /// + public bool IgnoreNullableAnnotations + { + get => _ignoreNullableAnnotations; + set + { + VerifyMutable(); + _ignoreNullableAnnotations = value; + } + } + /// /// Returns true if options uses compatible built-in resolvers or a combination of compatible built-in resolvers. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index 3ce9c15709819a..b5c10b417b71cd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -313,7 +313,8 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo) ParameterType = reflectionInfo.ParameterType, Position = reflectionInfo.Position, HasDefaultValue = reflectionInfo.HasDefaultValue, - DefaultValue = reflectionInfo.GetDefaultValue() + DefaultValue = reflectionInfo.GetDefaultValue(), + DisallowNullReads = DetermineParameterNullabilityAllowance(reflectionInfo) }; jsonParameters[i] = jsonInfo; @@ -354,6 +355,7 @@ private static void PopulatePropertyInfo( DeterminePropertyPolicies(jsonPropertyInfo, memberInfo); DeterminePropertyName(jsonPropertyInfo, memberInfo); DeterminePropertyIsRequired(jsonPropertyInfo, memberInfo, shouldCheckForRequiredKeyword); + DeterminePropertyNullabilityAllowance(jsonPropertyInfo, memberInfo); if (ignoreCondition != JsonIgnoreCondition.Always) { @@ -467,5 +469,40 @@ internal static void DeterminePropertyAccessors(JsonPropertyInfo jsonPrope return MemberAccessor.CreateParameterlessConstructor(type, defaultCtor); } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static void DeterminePropertyNullabilityAllowance(JsonPropertyInfo propertyInfo, MemberInfo memberInfo) + { + if (propertyInfo.PropertyType.IsValueType) + { + return; + } + + NullabilityInfo nullability; + if (propertyInfo.MemberType is MemberTypes.Property) + { + nullability = new NullabilityInfoContext().Create((PropertyInfo)memberInfo); + } + else + { + Debug.Assert(propertyInfo.MemberType is MemberTypes.Field); + nullability = new NullabilityInfoContext().Create((FieldInfo)memberInfo); + } + + propertyInfo.DisallowNullReads = nullability.WriteState is NullabilityState.NotNull; + propertyInfo.DisallowNullWrites = nullability.ReadState is NullabilityState.NotNull; + } + + private static bool DetermineParameterNullabilityAllowance(ParameterInfo parameterInfo) + { + if (parameterInfo.ParameterType.IsValueType) + { + return false; + } + + NullabilityInfo nullability = new NullabilityInfoContext().Create(parameterInfo); + return nullability.WriteState is NullabilityState.NotNull; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index 5112d8b010940b..10cf7a336adae3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -35,6 +35,8 @@ internal abstract class JsonParameterInfo public JsonPropertyInfo MatchingProperty { get; } + public bool DisallowNullReads { get; } + public JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonPropertyInfo matchingProperty) { Debug.Assert(matchingProperty.IsConfigured); @@ -43,11 +45,13 @@ public JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonProper ShouldDeserialize = !matchingProperty.IsIgnored; Options = matchingProperty.Options; Position = parameterInfoValues.Position; + DisallowNullReads = parameterInfoValues.DisallowNullReads; ParameterType = matchingProperty.PropertyType; NameAsUtf8Bytes = matchingProperty.NameAsUtf8Bytes; IgnoreNullTokensOnRead = matchingProperty.IgnoreNullTokensOnRead; NumberHandling = matchingProperty.EffectiveNumberHandling; + //DisallowNullWrites = matchingProperty.DisallowNullWrites; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs index 636d1b99ef75aa..077912e8ede04e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs @@ -41,5 +41,11 @@ public sealed class JsonParameterInfoValues /// /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. public object? DefaultValue { get; init; } + + /// + /// Whether the parameter allows values. + /// + /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. + public bool DisallowNullReads { get; init; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 4a6f10aa440630..b277601463183f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -233,6 +233,16 @@ public JsonObjectCreationHandling? ObjectCreationHandling internal bool IsVirtual { get; set; } internal bool IsSourceGenerated { get; set; } + /// + /// Gets or sets a value indicating whether the allows values on deserialization. + /// + public bool DisallowNullReads { get; set; } + + /// + /// Gets or sets a value indicating whether the allows values on serialization. + /// + public bool DisallowNullWrites { get; set; } + /// /// Specifies whether the current property is a special extension data property. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index 01369330feb68f..0ad071a6144f65 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -198,6 +198,12 @@ value is not null && if (EffectiveConverter.HandleNullOnWrite) { + if (DisallowNullWrites && EffectiveConverter.IsInternalConverter && !Options.IgnoreNullableAnnotations) + { + Debug.Assert(!EffectiveConverter.IsValueType); + ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); + } + if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; @@ -213,6 +219,12 @@ value is not null && } else { + if (DisallowNullWrites && !Options.IgnoreNullableAnnotations) + { + Debug.Assert(!EffectiveConverter.IsValueType); + ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); + } + writer.WriteNullSection(EscapedNameSection); } @@ -275,6 +287,12 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta if (!IgnoreNullTokensOnRead) { + if (DisallowNullReads && !Options.IgnoreNullableAnnotations) + { + Debug.Assert(!EffectiveConverter.IsValueType); + ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); + } + T? value = default; Set!(obj, value!); } @@ -292,6 +310,14 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta { // Optimize for internal converters by avoiding the extra call to TryRead. T? fastValue = EffectiveConverter.Read(ref reader, PropertyType, Options); + + if (fastValue is null && DisallowNullReads && !Options.IgnoreNullableAnnotations) + { + Debug.Fail("We currently don't have an internal converter that returns null that could trigger this, if you hit this, please add a test case."); + Debug.Assert(!EffectiveConverter.IsValueType); + ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); + } + Set!(obj, fastValue!); } @@ -316,6 +342,12 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta // We cannot do reader.Skip early because converter decides if populating will happen or not if (CanDeserialize) { + if (value is null && DisallowNullReads && !Options.IgnoreNullableAnnotations) + { + Debug.Assert(!EffectiveConverter.IsValueType); + ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); + } + Set!(obj, value!); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index b46a44f10f3175..9e24d897cadaca 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -1165,6 +1165,8 @@ internal void ConfigureConstructorParameters() matchingEntry.DuplicateName); } + //if (matchingEntry.JsonPropertyInfo.DisallowNullReads != parameterInfo) + Debug.Assert(matchingEntry.JsonPropertyInfo != null); JsonPropertyInfo jsonPropertyInfo = matchingEntry.JsonPropertyInfo; JsonParameterInfo jsonParameterInfo = jsonPropertyInfo.CreateJsonParameterInfo(parameterInfo); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index d50f3f261fe06e..85bddceee8c252 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -63,6 +63,12 @@ public static void ThrowInvalidOperationException_DeserializeUnableToAssignNull( throw new InvalidOperationException(SR.Format(SR.DeserializeUnableToAssignNull, declaredType)); } + [DoesNotReturn] + public static void ThrowJsonException_NullabilityDoesNotAllowNull(string propertyName, Type propertyType) + { + throw new JsonException(SR.Format(SR.NullableAnnotationsDontAllowNull, propertyName, propertyType)) { AppendPathInformation = true }; + } + [DoesNotReturn] public static void ThrowInvalidOperationException_ObjectCreationHandlingPopulateNotSupportedByConverter(JsonPropertyInfo propertyInfo) { diff --git a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs new file mode 100644 index 00000000000000..b4680ed0a5306a --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs @@ -0,0 +1,480 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public abstract class NullableAnnotationsTests : SerializerTests + { + protected NullableAnnotationsTests(JsonSerializerWrapper serializerUnderTest) + : base(serializerUnderTest) { } + + [Fact] + public void IgnoreNullableAnnotationsIsDisabledByDefault() + => Assert.False(new JsonSerializerOptions().IgnoreNullableAnnotations); + + #region Read into Not Nullable + [Fact] + public async Task ReadNullIntoNotNullablePropertyThrows() + { + string json = """{"Property":null}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Contains("Property", ex.Message); + Assert.Contains(typeof(NotNullablePropertyClass).ToString(), ex.Message); + } + + [Fact] + public async Task ReadEmptyObjectIntoTypeWithNotNullableProperty() + { + string json = "{}"; + + NotNullablePropertyClass result = await Serializer.DeserializeWrapper(json); + Assert.Null(result.Property); + } + + [Fact] + public async Task ReadNullIntoNotNullablePropertyWithIgnoreNullableAnnotations() + { + string json = """{"Property":null}"""; + + JsonSerializerOptions options = new() { IgnoreNullableAnnotations = true }; + NotNullablePropertyClass result = await Serializer.DeserializeWrapper(json, options); + Assert.Null(result.Property); + } + + [Fact] + public async Task ReadNullIntoReadonlyPropertySkipped() + { + string json = """{"ReadonlyProperty":null}"""; + + NotNullableReadonlyPropertyClass result = await Serializer.DeserializeWrapper(json); + Assert.Null(result.ReadonlyProperty); + } + + [Fact] + public async Task ReadNullIntoNotNullableFieldThrows() + { + string json = """{"Field":null}"""; + + JsonSerializerOptions options = new() { IncludeFields = true }; + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, options)); + Assert.Contains("Field", ex.Message); + Assert.Contains(typeof(NotNullableFieldClass).ToString(), ex.Message); + } + + [Fact] + public async Task ReadNullIntoNotNullableSpecialTypeProperties() + { + string json = """ + { + "JsonDocument":null, + "MemoryByte":null, + "ReadOnlyMemoryByte":null, + "MemoryOfT":null, + "ReadOnlyMemoryOfT":null + } + """; + + NotNullableSpecialTypePropertiesClass result = await Serializer.DeserializeWrapper(json); + Assert.Equal(JsonValueKind.Null, result.JsonDocument?.RootElement.ValueKind); + Assert.Equal(default, result.MemoryByte); + Assert.Equal(default, result.ReadOnlyMemoryByte); + Assert.Equal(default, result.MemoryOfT); + Assert.Equal(default, result.ReadOnlyMemoryOfT); + } + + [Fact] + public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverter() + { + string json = """{"PropertyWithHandleNullConverter":null}"""; + + NotNullablePropertyWithHandleNullConverterClass result = await Serializer.DeserializeWrapper(json); + Assert.NotNull(result.PropertyWithHandleNullConverter); + } + + [Fact] + public async Task ReadNullIntoNotNullablePropertyWithConverterThrows() + { + string json = """{"PropertyWithConverter":null}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Contains("PropertyWithConverter", ex.Message); + Assert.Contains(typeof(NotNullablePropertyWithConverterClass).ToString(), ex.Message); + } + + [Fact] + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterThrows() + { + string json = """{"PropertyWithAlwaysNullConverter":"42"}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Contains("PropertyWithAlwaysNullConverter", ex.Message); + Assert.Contains(typeof(NotNullablePropertyWithAlwaysNullConverterClass).ToString(), ex.Message); + } + + [Fact] + public async Task ReadNullIntoNotNullablePropertyWithParameterizedCtorThrows() + { + string json = """{"CtorProperty":null}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Contains("CtorProperty", ex.Message); + Assert.Contains(typeof(NotNullablePropertyParameterizedCtorClass).ToString(), ex.Message); + } + + [Fact] + public async Task ReadEmptyObjectIntoTypeWithNotNullablePropertyWithParameterizedCtor() + { + string json = "{}"; + + NotNullablePropertyParameterizedCtorClass result = await Serializer.DeserializeWrapper(json); + Assert.Null(result.CtorProperty); + } + + [Fact] + public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverterWithParameterizedCtor() + { + string json = """{"CtorPropertyWithHandleNullConverter":null}"""; + + var result = await Serializer.DeserializeWrapper(json); + Assert.NotNull(result.CtorPropertyWithHandleNullConverter); + } + + [Fact] + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterWithParameterizedCtorThrows() + { + string json = """{"CtorPropertyWithAlwaysNullConverter":"42"}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Contains("CtorPropertyWithAlwaysNullConverter", ex.Message); + Assert.Contains(typeof(NotNullablePropertyWithAlwaysNullConverterParameterizedCtorClass).ToString(), ex.Message); + } + + [Fact] + public async Task ReadNullIntoNotNullablePropertyWithLargeParameterizedCtorThrows() + { + string json = """{"CtorProperty2":null}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Contains("CtorProperty2", ex.Message); + Assert.Contains(typeof(NotNullablePropertiesLargeParameterizedCtorClass).ToString(), ex.Message); + } + + [Fact] + public async Task ReadEmptyObjectIntoTypeWithNotNullablePropertyWithLargeParameterizedCtor() + { + string json = "{}"; + + NotNullablePropertiesLargeParameterizedCtorClass result = await Serializer.DeserializeWrapper(json); + Assert.Null(result.CtorProperty0); + Assert.Null(result.CtorProperty1); + Assert.Null(result.CtorProperty2); + Assert.Null(result.CtorProperty3); + Assert.Null(result.CtorProperty4); + } + + [Fact] + public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverterWithLargeParameterizedCtor() + { + string json = """{"LargeCtorPropertyWithHandleNullConverter":null}"""; + + var result = await Serializer.DeserializeWrapper(json); + Assert.NotNull(result.LargeCtorPropertyWithHandleNullConverter); + } + + [Fact] + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterWithLargeParameterizedCtorThrows() + { + string json = """{"LargeCtorPropertyWithAlwaysNullConverter":"42"}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Contains("LargeCtorPropertyWithAlwaysNullConverter", ex.Message); + Assert.Contains(typeof(NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass).ToString(), ex.Message); + } + + [Fact] + public async Task ReadNullIntoDisallowNullPropertyThrows() + { + string json = """{"Property":null}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Contains("Property", ex.Message); + Assert.Contains(typeof(DisallowNullPropertyClass).ToString(), ex.Message); + } + #endregion Read into Not Nullable + + #region Write from Not Nullable + [Fact] + public async Task WriteNullFromNotNullablePropertyThrows() + { + NotNullablePropertyClass obj = new() { Property = null! }; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(obj)); + Assert.Contains("Property", ex.Message); + Assert.Contains(typeof(NotNullablePropertyClass).ToString(), ex.Message); + } + + [Fact] + public async Task WriteNullFromNotNullableFieldThrows() + { + NotNullableFieldClass obj = new() { Field = null! }; + + JsonSerializerOptions options = new() { IncludeFields = true }; + Exception ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(obj, options)); + Assert.Contains("Field", ex.Message); + Assert.Contains(typeof(NotNullableFieldClass).ToString(), ex.Message); + } + + [Fact] + public async Task WriteNullFromNotNullableJsonDocumentPropertyThrows() + { + NotNullableSpecialTypePropertiesClass obj = new() { JsonDocument = null! }; + + // Unlike Deserialize, JsonDocument should throw on Serialize because it would write null. + Exception ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(obj)); + Assert.Contains("JsonDocument", ex.Message); + Assert.Contains(typeof(NotNullableSpecialTypePropertiesClass).ToString(), ex.Message); + } + + //[Fact] + + #endregion Write from Not Nullable + + #region Read into Nullable + [Fact] + public async Task ReadNullIntoNullableProperty() + { + string json = """{"Property":null}"""; + + NullablePropertyClass result = await Serializer.DeserializeWrapper(json); + Assert.Null(result.Property); + } + + [Fact] + public async Task ReadEmptyObjectIntoTypeWithNullableProperty() + { + string json = "{}"; + + NullablePropertyClass result = await Serializer.DeserializeWrapper(json); + Assert.Null(result.Property); + } + + public async Task ReadNullIntoNullableField() + { + string json = """{"Field":null}"""; + + NullableFieldClass result = await Serializer.DeserializeWrapper(json); + Assert.Null(result.Field); + } + #endregion + + // Need to use **public** types; otherwise, nullability would be trimmed out. + #region Not Nullable classes + public class NotNullablePropertyClass + { +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + public string Property { get; set; } +#pragma warning restore CS8618 + } + + public class NotNullableReadonlyPropertyClass + { +#pragma warning disable CS8618 + public string ReadonlyProperty { get; } +#pragma warning restore CS8618 + } + + public class NotNullableFieldClass + { +#pragma warning disable CS8618 + public string Field; +#pragma warning restore CS8618 + } + + public class NotNullableSpecialTypePropertiesClass + { + // types with internal converter that handles null. +#pragma warning disable CS8618 + public JsonDocument JsonDocument { get; set; } +#pragma warning restore CS8618 + public Memory MemoryByte { get; set; } + public ReadOnlyMemory ReadOnlyMemoryByte { get; set; } + public Memory MemoryOfT { get; set; } + public ReadOnlyMemory ReadOnlyMemoryOfT { get; set; } + } + + public class NotNullablePropertyWithHandleNullConverterClass + { +#pragma warning disable CS8618 + [JsonConverter(typeof(MyHandleNullConverter))] + public MyClass PropertyWithHandleNullConverter { get; set; } +#pragma warning restore CS8618 + } + + public class NotNullablePropertyWithAlwaysNullConverterClass + { +#pragma warning disable CS8618 + [JsonConverter(typeof(MyAlwaysNullConverter))] + public MyClass PropertyWithAlwaysNullConverter { get; set; } +#pragma warning restore CS8618 + } + + public class NotNullablePropertyWithConverterClass + { +#pragma warning disable CS8618 + [JsonConverter(typeof(MyConverter))] + public MyClass PropertyWithConverter { get; set; } +#pragma warning restore CS8618 + } + + public class MyClass { } + + public class MyHandleNullConverter : MyConverter + { + public override bool HandleNull => true; + } + + public class MyConverter : JsonConverter + { + public override MyClass Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new MyClass(); + } + + public override void Write(Utf8JsonWriter writer, MyClass value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + } + + public class MyAlwaysNullConverter : JsonConverter + { + public override bool HandleNull => true; + + public override MyClass Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return null!; + } + + public override void Write(Utf8JsonWriter writer, MyClass value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + } + + public class NotNullablePropertyParameterizedCtorClass + { + public string CtorProperty { get; } + + [JsonConstructor] + public NotNullablePropertyParameterizedCtorClass(string ctorProperty) => CtorProperty = ctorProperty; + } + + public class NotNullablePropertyWithHandleNullConverterParameterizedCtorClass + { + [JsonConverter(typeof(MyHandleNullConverter))] + public MyClass CtorPropertyWithHandleNullConverter { get; } + + [JsonConstructor] + public NotNullablePropertyWithHandleNullConverterParameterizedCtorClass(MyClass ctorPropertyWithHandleNullConverter) => CtorPropertyWithHandleNullConverter = ctorPropertyWithHandleNullConverter; + } + + public class NotNullablePropertyWithAlwaysNullConverterParameterizedCtorClass + { + [JsonConverter(typeof(MyAlwaysNullConverter))] + public MyClass CtorPropertyWithAlwaysNullConverter { get; } + + [JsonConstructor] + public NotNullablePropertyWithAlwaysNullConverterParameterizedCtorClass(MyClass ctorPropertyWithAlwaysNullConverter) => CtorPropertyWithAlwaysNullConverter = ctorPropertyWithAlwaysNullConverter; + } + + public class NotNullablePropertiesLargeParameterizedCtorClass + { + public string CtorProperty0 { get; } + public string CtorProperty1 { get; } + public string CtorProperty2 { get; } + public string CtorProperty3 { get; } + public string CtorProperty4 { get; } + + [JsonConstructor] + public NotNullablePropertiesLargeParameterizedCtorClass(string ctorProperty0, string ctorProperty1, string ctorProperty2, string ctorProperty3, string ctorProperty4) + { + CtorProperty0 = ctorProperty0; + CtorProperty1 = ctorProperty1; + CtorProperty2 = ctorProperty2; + CtorProperty3 = ctorProperty3; + CtorProperty4 = ctorProperty4; + } + } + + public class NotNullablePropertyWithHandleNullConverterLargeParameterizedCtorClass + { + public string CtorProperty0 { get; } + public string CtorProperty1 { get; } + [JsonConverter(typeof(MyHandleNullConverter))] + public MyClass LargeCtorPropertyWithHandleNullConverter { get; } + public string CtorProperty3 { get; } + public string CtorProperty4 { get; } + + [JsonConstructor] + public NotNullablePropertyWithHandleNullConverterLargeParameterizedCtorClass( + string ctorProperty0, string ctorProperty1, MyClass largeCtorPropertyWithHandleNullConverter, string ctorProperty3, string ctorProperty4) + { + CtorProperty0 = ctorProperty0; + CtorProperty1 = ctorProperty1; + LargeCtorPropertyWithHandleNullConverter = largeCtorPropertyWithHandleNullConverter; + CtorProperty3 = ctorProperty3; + CtorProperty4 = ctorProperty4; + } + } + + public class NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass + { + public string CtorProperty0 { get; } + public string CtorProperty1 { get; } + [JsonConverter(typeof(MyAlwaysNullConverter))] + public MyClass LargeCtorPropertyWithAlwaysNullConverter { get; } + public string CtorProperty3 { get; } + public string CtorProperty4 { get; } + + [JsonConstructor] + public NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass( + string ctorProperty0, string ctorProperty1, MyClass largeCtorPropertyWithAlwaysNullConverter, string ctorProperty3, string ctorProperty4) + { + CtorProperty0 = ctorProperty0; + CtorProperty1 = ctorProperty1; + LargeCtorPropertyWithAlwaysNullConverter = largeCtorPropertyWithAlwaysNullConverter; + CtorProperty3 = ctorProperty3; + CtorProperty4 = ctorProperty4; + } + } + #endregion Not Nullable classes + + #region [DisallowNull] classes + public class DisallowNullPropertyClass + { + [DisallowNull] + public string? Property { get; set; } + } + #endregion [DisallowNull] classes + + #region Nullable classes + public class NullablePropertyClass + { + public string? Property { get; set; } + } + + public class NullableFieldClass + { + public string? Field; + } + #endregion Nullable classes + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs new file mode 100644 index 00000000000000..73de089ec628a0 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//#nullable enable + +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Tests; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public sealed class NullableAnnotationsTests_Metadata_String : NullableAnnotationsTests_Metadata + { + public NullableAnnotationsTests_Metadata_String() + : base(new StringSerializerWrapper(NullableAnnotationsTestsContext_Metadata.Default)) { } + } + + public sealed class NullableAnnotationsTests_Metadata_AsyncStream : NullableAnnotationsTests_Metadata + { + public NullableAnnotationsTests_Metadata_AsyncStream() + : base(new AsyncStreamSerializerWrapper(NullableAnnotationsTestsContext_Metadata.Default)) { } + } + + public abstract partial class NullableAnnotationsTests_Metadata : NullableAnnotationsTests + { + protected NullableAnnotationsTests_Metadata(JsonSerializerWrapper serializer) + : base(serializer) { } + + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(NotNullablePropertyClass))] + [JsonSerializable(typeof(NotNullableReadonlyPropertyClass))] + [JsonSerializable(typeof(NotNullableFieldClass))] + [JsonSerializable(typeof(NotNullableSpecialTypePropertiesClass))] + [JsonSerializable(typeof(NotNullablePropertyWithHandleNullConverterClass))] + [JsonSerializable(typeof(NotNullablePropertyWithConverterClass))] + [JsonSerializable(typeof(NotNullablePropertyWithAlwaysNullConverterClass))] + [JsonSerializable(typeof(NotNullablePropertyParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertyWithHandleNullConverterParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertyWithAlwaysNullConverterParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertiesLargeParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertyWithHandleNullConverterLargeParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass))] + [JsonSerializable(typeof(DisallowNullPropertyClass))] + [JsonSerializable(typeof(NullablePropertyClass))] + [JsonSerializable(typeof(NullableFieldClass))] + internal sealed partial class NullableAnnotationsTestsContext_Metadata + : JsonSerializerContext { } + } + + public sealed class NullableAnnotationsTests_Default_String : NullableAnnotationsTests_Default + { + public NullableAnnotationsTests_Default_String() + : base(new StringSerializerWrapper(NullableAnnotationsTestsContext_Default.Default)) { } + } + + public sealed class NullableAnnotationsTests_Default_AsyncStream : NullableAnnotationsTests_Default + { + public NullableAnnotationsTests_Default_AsyncStream() + : base(new AsyncStreamSerializerWrapper(NullableAnnotationsTestsContext_Default.Default)) { } + } + + public abstract partial class NullableAnnotationsTests_Default : NullableAnnotationsTests + { + protected NullableAnnotationsTests_Default(JsonSerializerWrapper serializer) + : base(serializer) { } + + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default)] + [JsonSerializable(typeof(NotNullablePropertyClass))] + [JsonSerializable(typeof(NotNullableReadonlyPropertyClass))] + [JsonSerializable(typeof(NotNullableFieldClass))] + [JsonSerializable(typeof(NotNullableSpecialTypePropertiesClass))] + [JsonSerializable(typeof(NotNullablePropertyWithHandleNullConverterClass))] + [JsonSerializable(typeof(NotNullablePropertyWithConverterClass))] + [JsonSerializable(typeof(NotNullablePropertyWithAlwaysNullConverterClass))] + [JsonSerializable(typeof(NotNullablePropertyParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertyWithHandleNullConverterParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertyWithAlwaysNullConverterParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertiesLargeParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertyWithHandleNullConverterLargeParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass))] + [JsonSerializable(typeof(DisallowNullPropertyClass))] + [JsonSerializable(typeof(NullablePropertyClass))] + [JsonSerializable(typeof(NullableFieldClass))] + internal sealed partial class NullableAnnotationsTestsContext_Default + : JsonSerializerContext + { } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index b350fc936c7983..c42a454c2c57d8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -30,6 +30,8 @@ + @@ -84,6 +86,7 @@ + @@ -121,6 +124,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/NullableAnnotationsTests.cs new file mode 100644 index 00000000000000..f87f90d19a2a8e --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/NullableAnnotationsTests.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization.Tests +{ + public sealed partial class NullableAnnotationsTests_String : NullableAnnotationsTests + { + public NullableAnnotationsTests_String() : base(JsonSerializerWrapper.StringSerializer) { } + } + + public sealed partial class NullableAnnotationsTests_AsyncStreamWithSmallBuffer : NullableAnnotationsTests + { + public NullableAnnotationsTests_AsyncStreamWithSmallBuffer() : base(JsonSerializerWrapper.AsyncStreamSerializerWithSmallBuffer) { } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index b0c8e41f7179d0..164c3ca7e782d0 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -66,6 +66,7 @@ public static void SetOptionsFail() Assert.Equal(JsonCommentHandling.Disallow, options.ReadCommentHandling); Assert.Equal(JsonUnmappedMemberHandling.Skip, options.UnmappedMemberHandling); Assert.False(options.WriteIndented); + Assert.False(options.IgnoreNullableAnnotations); TestIListNonThrowingOperationsWhenImmutable(options.Converters, tc); TestIListNonThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -85,6 +86,7 @@ public static void SetOptionsFail() Assert.Throws(() => options.UnmappedMemberHandling = options.UnmappedMemberHandling); Assert.Throws(() => options.WriteIndented = options.WriteIndented); Assert.Throws(() => options.TypeInfoResolver = options.TypeInfoResolver); + Assert.Throws(() => options.IgnoreNullableAnnotations = options.IgnoreNullableAnnotations); TestIListThrowingOperationsWhenImmutable(options.Converters, tc); TestIListThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -915,6 +917,31 @@ public static void Options_JsonSerializerContext_Combine_FallbackToReflection() JsonTestHelper.AssertJsonEqual("""{"Value":null,"Thing":null}""", json); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void Options_IgnoreNullableAnnotationsDefault_Net8CompatibilitySwitch() + { + var options = new RemoteInvokeOptions + { + RuntimeConfigurationOptions = + { + ["System.Text.Json.Serialization.IgnoreNullableAnnotationsDefault"] = true, + } + }; + + RemoteExecutor.Invoke(static () => + { + var jsonOptions = new JsonSerializerOptions(); + Assert.True(jsonOptions.IgnoreNullableAnnotations); + + var value = new NullableAnnotationsTests.NotNullablePropertyClass(); + Assert.Null(value.Property); + string json = JsonSerializer.Serialize(value, jsonOptions); + + value = JsonSerializer.Deserialize(json, jsonOptions); + Assert.Null(value.Property); + }, options).Dispose(); + } + private static void GenericObjectOrJsonElementConverterTestHelper(string converterName, object objectValue, string stringValue) { var options = new JsonSerializerOptions(); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index 3dc7cc9e50d40f..f869b639d65739 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -67,6 +67,7 @@ + @@ -200,6 +201,7 @@ + From 650b53e3f56d154266d8264ce4a9530e3421f9e9 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 21 May 2024 15:37:44 +0100 Subject: [PATCH 02/19] Complete implementation and make all NullableAnnotations tests pass. --- .../src/System/Reflection/NullabilityInfo.cs | 12 +- .../Reflection/NullabilityInfoContext.cs | 64 +- .../gen/Helpers/RoslynExtensions.cs | 111 ++++ ...urceGenerator.Emitter.ExceptionMessages.cs | 3 + .../gen/JsonSourceGenerator.Emitter.cs | 105 ++- .../gen/JsonSourceGenerator.Parser.cs | 38 +- .../src/Resources/Strings.resx | 10 +- .../src/System.Text.Json.csproj | 1 - .../src/System/ReflectionExtensions.cs | 51 ++ .../Text/Json/Serialization/ArgumentState.cs | 4 - .../Json/Serialization/ConfigurationList.cs | 20 +- .../Object/ObjectDefaultConverter.cs | 2 + ...ParameterizedConstructorConverter.Large.cs | 11 +- ...ParameterizedConstructorConverter.Small.cs | 32 +- ...ctWithParameterizedConstructorConverter.cs | 73 +-- .../JsonSerializer.Read.HandlePropertyName.cs | 3 +- .../Serialization/JsonSerializerContext.cs | 1 + .../Serialization/JsonSerializerOptions.cs | 3 + .../DefaultJsonTypeInfoResolver.Helpers.cs | 122 +++- .../Metadata/JsonMetadataServices.Helpers.cs | 5 +- .../Metadata/JsonParameterInfo.cs | 57 +- .../Metadata/JsonParameterInfoOfT.cs | 1 - .../Metadata/JsonPropertyInfo.cs | 117 +++- .../Metadata/JsonPropertyInfoOfT.cs | 31 +- .../Metadata/JsonTypeInfo.Cache.cs | 196 +----- .../Serialization/Metadata/JsonTypeInfo.cs | 167 +++-- .../Serialization/Metadata/ParameterRef.cs | 22 - .../Text/Json/ThrowHelper.Serialization.cs | 20 +- .../tests/Common/NullableAnnotationsTests.cs | 605 ++++++++++++------ .../JsonSerializerWrapper.SourceGen.cs | 4 + .../Serialization/NullableAnnotationsTests.cs | 46 +- .../System.Text.Json.Tests.csproj | 2 + 32 files changed, 1149 insertions(+), 790 deletions(-) delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ParameterRef.cs 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 1737dd259e49ba..25b0aa629be6d9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs @@ -8,10 +8,10 @@ namespace System.Reflection /// /// A class that represents nullability info /// -#if !NETCOREAPP - internal -#else +#if NETCOREAPP public +#else + internal #endif sealed class NullabilityInfo { @@ -51,10 +51,10 @@ internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState /// /// An enum that represents nullability state /// -#if !NETCOREAPP - internal -#else +#if NETCOREAPP public +#else + internal #endif enum NullabilityState { 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 d547db4eb5f3d4..b0e16765516049 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs @@ -11,10 +11,10 @@ namespace System.Reflection /// Provides APIs for populating nullability information/context from reflection members: /// , , and . /// -#if !NETCOREAPP - internal -#else +#if NETCOREAPP public +#else + internal #endif sealed class NullabilityInfoContext { @@ -73,10 +73,7 @@ public NullabilityInfo Create(ParameterInfo parameterInfo) #if NETCOREAPP ArgumentNullException.ThrowIfNull(parameterInfo); #else - if (parameterInfo is null) - { - throw new ArgumentNullException(nameof(parameterInfo)); - } + NetstandardHelpers.ThrowIfNull(parameterInfo, nameof(parameterInfo)); #endif EnsureIsSupported(); @@ -127,11 +124,8 @@ private void CheckParameterMetadataType(ParameterInfo parameter, NullabilityInfo private static ParameterInfo? GetMetaParameter(MethodBase metaMethod, ParameterInfo parameter) { -#if NETCOREAPP ReadOnlySpan parameters = metaMethod.GetParametersAsSpan(); -#else - ReadOnlySpan parameters = metaMethod.GetParameters(); -#endif + for (int i = 0; i < parameters.Length; i++) { if (parameter.Position == i && @@ -209,10 +203,7 @@ public NullabilityInfo Create(PropertyInfo propertyInfo) #if NETCOREAPP ArgumentNullException.ThrowIfNull(propertyInfo); #else - if (propertyInfo is null) - { - throw new ArgumentNullException(nameof(propertyInfo)); - } + NetstandardHelpers.ThrowIfNull(propertyInfo, nameof(propertyInfo)); #endif EnsureIsSupported(); @@ -235,12 +226,8 @@ public NullabilityInfo Create(PropertyInfo propertyInfo) if (setter != null) { -#if NETCOREAPP - ParameterInfo parameter = setter.GetParametersAsSpan()[^1]; -#else - ParameterInfo[] parameters = setter.GetParameters(); + ReadOnlySpan parameters = setter.GetParametersAsSpan(); ParameterInfo parameter = parameters[parameters.Length - 1]; -#endif CheckNullabilityAttributes(nullability, parameter.GetCustomAttributesData()); } else @@ -275,10 +262,7 @@ public NullabilityInfo Create(EventInfo eventInfo) #if NETCOREAPP ArgumentNullException.ThrowIfNull(eventInfo); #else - if (eventInfo is null) - { - throw new ArgumentNullException(nameof(eventInfo)); - } + NetstandardHelpers.ThrowIfNull(eventInfo, nameof(eventInfo)); #endif EnsureIsSupported(); @@ -299,10 +283,7 @@ public NullabilityInfo Create(FieldInfo fieldInfo) #if NETCOREAPP ArgumentNullException.ThrowIfNull(fieldInfo); #else - if (fieldInfo is null) - { - throw new ArgumentNullException(nameof(fieldInfo)); - } + NetstandardHelpers.ThrowIfNull(fieldInfo, nameof(fieldInfo)); #endif EnsureIsSupported(); @@ -499,11 +480,7 @@ private static Type GetPropertyMetaType(PropertyInfo property) return method.ReturnType; } -#if NETCOREAPP ReadOnlySpan parameters = property.GetSetMethod(true)!.GetParametersAsSpan(); -#else - ParameterInfo[] parameters = property.GetSetMethod(true)!.GetParameters(); -#endif return parameters[0].ParameterType; } @@ -548,7 +525,7 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T #if NETCOREAPP && !genericParameter.IsGenericMethodParameter #else - && !NetstandardHelpers.IsGenericMethodParameter(genericParameter) + && !genericParameter.IsGenericMethodParameter() #endif && TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType)) { @@ -584,7 +561,7 @@ private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(Nullabili #if NETCOREAPP !genericParameter.IsGenericMethodParameter); #else - !NetstandardHelpers.IsGenericMethodParameter(genericParameter)); + !genericParameter.IsGenericMethodParameter()); #endif Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context; @@ -727,15 +704,21 @@ public bool ParseNullableState(int index, ref NullabilityState state) #if !NETCOREAPP 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) { - if (member is null) - { - throw new ArgumentNullException(nameof(member)); - } + ThrowIfNull(member, nameof(member)); const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; foreach (MemberInfo myMemberInfo in type.GetMembers(all)) @@ -760,8 +743,11 @@ private static bool HasSameMetadataDefinitionAs(this MemberInfo info, MemberInfo return true; } - public static bool IsGenericMethodParameter(Type type) + 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/gen/Helpers/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs index 3f3ecb506fd83d..0b56d5dd952bfb 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 instation. + // 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 instation. + // 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 IsNonNullable(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 2e8c36cd604171..f4fd4dffc39150 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"; @@ -674,11 +675,11 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe if (property.DisallowNullWrites) { - writer.WriteLine($"properties[{i}].DisallowNullWrites = true;"); + writer.WriteLine($"properties[{i}].DisallowNullWrites = !{OptionsLocalVariableName}.IgnoreNullableAnnotations;"); } if (property.DisallowNullReads) { - writer.WriteLine($"properties[{i}].DisallowNullReads = true;"); + writer.WriteLine($"properties[{i}].DisallowNullReads = !{OptionsLocalVariableName}.IgnoreNullableAnnotations;"); } writer.WriteLine(); @@ -691,24 +692,20 @@ 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}}), @@ -716,9 +713,13 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin HasDefaultValue = {{FormatBoolLiteral(spec.HasDefaultValue)}}, DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}}, DisallowNullReads = {{FormatBoolLiteral(spec.DisallowNullReads)}}, - }; - + }, """); + + if (++i < paramCount) + { + writer.WriteLine(); + } } foreach (PropertyInitializerGenerationSpec spec in propertyInitializers) @@ -729,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) @@ -770,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) { @@ -794,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. @@ -803,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('@')}"; @@ -817,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; } } @@ -849,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('}'); } @@ -988,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.DisallowNullWrites => SerializedValueCheckType.DisallowNull, + _ => SerializedValueCheckType.None, }; } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index f87c21ab3f7456..a2bd2a3383f755 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -1373,11 +1373,7 @@ private void ProcessMember( isReadOnly = true; } - if (!propertyInfo.Type.IsValueType) - { - disallowNullWrites = propertyInfo.GetMethod != null && IsReturnValueNonNullable(propertyInfo, propertyInfo.NullableAnnotation); - disallowNullReads = propertyInfo.SetMethod != null && IsParameterNonNullable(propertyInfo, propertyInfo.NullableAnnotation); - } + propertyInfo.ResolveNullabilityAnnotations(out disallowNullWrites, out disallowNullReads); break; case IFieldSymbol fieldInfo: isReadOnly = fieldInfo.IsReadOnly; @@ -1401,11 +1397,7 @@ private void ProcessMember( hasJsonIncludeButIsInaccessible = hasJsonInclude; } - if (!fieldInfo.Type.IsValueType) - { - disallowNullWrites = IsReturnValueNonNullable(fieldInfo, fieldInfo.NullableAnnotation); - disallowNullReads = IsParameterNonNullable(fieldInfo, fieldInfo.NullableAnnotation); - } + fieldInfo.ResolveNullabilityAnnotations(out disallowNullWrites, out disallowNullReads); break; default: Debug.Fail("Method given an invalid symbol type."); @@ -1456,7 +1448,7 @@ private void ProcessMember( HasDefaultValue = parameterInfo.HasExplicitDefaultValue, DefaultValue = parameterInfo.HasExplicitDefaultValue ? parameterInfo.ExplicitDefaultValue : null, ParameterIndex = i, - DisallowNullReads = IsParameterNonNullable(parameterInfo, parameterInfo.NullableAnnotation), + DisallowNullReads = parameterInfo.IsNonNullable(), }; } } @@ -1620,30 +1612,6 @@ private static string DeterminePropertyNameFieldName(string effectiveJsonPropert return sb.ToString(); } - // TODO: is this the right place to place this? Look into the roslynextensions helper. - private static bool IsReturnValueNonNullable(ISymbol symbol, NullableAnnotation returnTypeAnnotation) - { - return - !HasCodeAnalysisAttribute(symbol, "MaybeNullAttribute") && - (returnTypeAnnotation is NullableAnnotation.NotAnnotated || - HasCodeAnalysisAttribute(symbol, "NotNullAttribute")); - } - - private static bool IsParameterNonNullable(ISymbol symbol, NullableAnnotation parameterAnnotation) - { - return - !HasCodeAnalysisAttribute(symbol, "AllowNullAttribute") && - (parameterAnnotation is NullableAnnotation.NotAnnotated || - HasCodeAnalysisAttribute(symbol, "DisallowNullAttribute")); - } - - private static bool HasCodeAnalysisAttribute(ISymbol symbol, string attributeName) - { - return symbol.GetAttributes().Any(attr => - attr.AttributeClass?.Name == attributeName && - attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis"); - } - private JsonPrimitiveTypeKind? GetPrimitiveTypeKind(ITypeSymbol type) { if (type.IsNumberType()) diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index c7ac6a8f8687f2..cb3fda7f490928 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -737,8 +737,14 @@ New line can be only "\n" or "\r\n". - - The nullable annotations of the property or field '{0}' on type '{1}' don't allow null values. + + The property or field '{0}' on type '{1}' doesn't allow getting null values. Consider updating its nullability annotation. + + + The property or field '{0}' on type '{1}' doesn't allow setting null values. Consider updating its nullability annotation. + + + The constructor parameter '{0}' on type '{1}' doesn't allow null values. Consider updating its nullability annotation. NullabilityInfoContext is not supported in the current application because 'System.Reflection.NullabilityInfoContext.IsSupported' is set to false. Set the MSBuild Property 'NullabilityInfoContextSupport' to true in order to enable it. diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 2b57eb315e37b0..9cc961c2a3b7e3 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -276,7 +276,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - diff --git a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs index ec6b46db24e877..4767aa16155f86 100644 --- a/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs +++ b/src/libraries/System.Text.Json/src/System/ReflectionExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; @@ -20,6 +21,8 @@ internal static partial class ReflectionExtensions public static bool IsNullableOfT(this Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == s_nullableType; + public static bool IsNullableType(this Type type) => !type.IsValueType || IsNullableOfT(type); + /// /// Returns when the given type is assignable from including support /// when is by using the {T} generic parameter for . @@ -113,5 +116,53 @@ private static bool HasCustomAttributeWithName(this MemberInfo memberInfo, strin return result; #endif } + + public static ParameterInfo GetGenericParameterDefinition(this ParameterInfo parameter) + { + if (parameter.Member is { DeclaringType.IsConstructedGenericType: true } + or MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false }) + { + var genericMethod = (MethodBase)parameter.Member.GetGenericMemberDefinition()!; + return genericMethod.GetParameters()[parameter.Position]; + } + + return parameter; + } + + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", + Justification = "Looking up the generic member definition of the provided member.")] + public static MemberInfo GetGenericMemberDefinition(this MemberInfo member) + { + if (member is Type type) + { + return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; + } + + if (member.DeclaringType!.IsConstructedGenericType) + { + const BindingFlags AllMemberFlags = + BindingFlags.Static | BindingFlags.Instance | + BindingFlags.Public | BindingFlags.NonPublic; + + Type genericTypeDef = member.DeclaringType.GetGenericTypeDefinition(); + foreach (MemberInfo genericMember in genericTypeDef.GetMember(member.Name, AllMemberFlags)) + { + if (genericMember.MetadataToken == member.MetadataToken) + { + return genericMember; + } + } + + Debug.Fail("Unreachable code"); + throw new Exception(); + } + + if (member is MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false } method) + { + return method.GetGenericMethodDefinition(); + } + + return member; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ArgumentState.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ArgumentState.cs index 9ba29087b9cf4d..0e0b3f3761ac31 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ArgumentState.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ArgumentState.cs @@ -27,9 +27,5 @@ internal sealed class ArgumentState // Current constructor parameter value. public JsonParameterInfo? JsonParameterInfo; - - // For performance, we order the parameters by the first deserialize and PropertyIndex helps find the right slot quicker. - public int ParameterIndex; - public List? ParameterRefCache; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs index a4b1a1643d7f69..39593f39d297dd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs @@ -23,6 +23,7 @@ public ConfigurationList(IEnumerable? source = null) public abstract bool IsReadOnly { get; } protected abstract void OnCollectionModifying(); + protected virtual void OnCollectionModified() { } protected virtual void ValidateAddedValue(TItem item) { } public TItem this[int index] @@ -38,9 +39,10 @@ public TItem this[int index] ThrowHelper.ThrowArgumentNullException(nameof(value)); } - ValidateAddedValue(value); OnCollectionModifying(); + ValidateAddedValue(value); _list[index] = value; + OnCollectionModified(); } } @@ -53,15 +55,17 @@ public void Add(TItem item) ThrowHelper.ThrowArgumentNullException(nameof(item)); } - ValidateAddedValue(item); OnCollectionModifying(); + ValidateAddedValue(item); _list.Add(item); + OnCollectionModified(); } public void Clear() { OnCollectionModifying(); _list.Clear(); + OnCollectionModified(); } public bool Contains(TItem item) @@ -91,21 +95,29 @@ public void Insert(int index, TItem item) ThrowHelper.ThrowArgumentNullException(nameof(item)); } - ValidateAddedValue(item); OnCollectionModifying(); + ValidateAddedValue(item); _list.Insert(index, item); + OnCollectionModified(); } public bool Remove(TItem item) { OnCollectionModifying(); - return _list.Remove(item); + bool removed = _list.Remove(item); + if (removed) + { + OnCollectionModified(); + } + + return removed; } public void RemoveAt(int index) { OnCollectionModifying(); _list.RemoveAt(index); + OnCollectionModified(); } IEnumerator IEnumerable.GetEnumerator() diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index 880e7dccbb3f1b..bb47eeba0e20fc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -185,6 +185,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, unescapedPropertyName, ref state, options, + out byte[] _, out bool useExtensionProperty); state.Current.UseExtensionProperty = useExtensionProperty; @@ -296,6 +297,7 @@ internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTyp unescapedPropertyName, ref state, options, + out byte[] _, out bool useExtensionProperty); ReadPropertyValue(obj, ref state, ref reader, jsonPropertyInfo, useExtensionProperty); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index 821db61dfc14f8..fde5887f6f50cc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -22,9 +22,9 @@ protected sealed override bool ReadAndCacheConstructorArgument(scoped ref ReadSt if (success && !(arg == null && jsonParameterInfo.IgnoreNullTokensOnRead)) { - if (arg == null && jsonParameterInfo.DisallowNullReads && !jsonParameterInfo.Options.IgnoreNullableAnnotations) + if (arg == null && jsonParameterInfo.DisallowNullReads) { - ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(jsonParameterInfo.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); + ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(jsonParameterInfo.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); } ((object[])state.Current.CtorArgumentState!.Arguments)[jsonParameterInfo.Position] = arg!; @@ -58,12 +58,9 @@ protected sealed override void InitializeConstructorArgumentCaches(ref ReadStack Debug.Assert(typeInfo.ParameterCache != null); - List> cache = typeInfo.ParameterCache.List; - object?[] arguments = ArrayPool.Shared.Rent(cache.Count); - - for (int i = 0; i < typeInfo.ParameterCount; i++) + object?[] arguments = ArrayPool.Shared.Rent(typeInfo.ParameterCache.Count); + foreach (JsonParameterInfo parameterInfo in typeInfo.ParameterCache) { - JsonParameterInfo parameterInfo = cache[i].Value; arguments[parameterInfo.Position] = parameterInfo.DefaultValue; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs index bfac75a182bfb5..e77c4df53d9f9d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs @@ -66,26 +66,25 @@ private static bool TryRead( bool success = info.EffectiveConverter.TryRead(ref reader, info.ParameterType, info.Options, ref state, out TArg? value, out _); - if (value is null) + if (success) { - if (info.IgnoreNullTokensOnRead) - { - // Use default value specified on parameter, if any. - value = info.DefaultValue; - } - else if (info.DisallowNullReads && !info.Options.IgnoreNullableAnnotations) + if (value is null) { - ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(info.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); + if (info.IgnoreNullTokensOnRead) + { + // Use default value specified on parameter, if any. + value = info.DefaultValue; + } + else if (info.DisallowNullReads) + { + ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(info.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); + } } - } - - arg = value; - if (success) - { state.Current.MarkRequiredPropertyAsRead(jsonParameterInfo.MatchingProperty); } + arg = value; return success; } @@ -94,13 +93,12 @@ protected override void InitializeConstructorArgumentCaches(ref ReadStack state, JsonTypeInfo typeInfo = state.Current.JsonTypeInfo; Debug.Assert(typeInfo.CreateObjectWithArgs != null); + Debug.Assert(typeInfo.ParameterCache != null); var arguments = new Arguments(); - List> cache = typeInfo.ParameterCache!.List; - for (int i = 0; i < typeInfo.ParameterCount; i++) + foreach (JsonParameterInfo parameterInfo in typeInfo.ParameterCache) { - JsonParameterInfo parameterInfo = cache[i].Value; switch (parameterInfo.Position) { case 0: @@ -117,7 +115,7 @@ protected override void InitializeConstructorArgumentCaches(ref ReadStack state, break; default: Debug.Fail("More than 4 params: we should be in override for LargeObjectWithParameterizedConstructorConverter."); - throw new InvalidOperationException(); + break; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 1841815ce40cfa..3d6ca980e11c0b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -261,12 +261,6 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo state.Current.JsonTypeInfo.UpdateSortedPropertyCache(ref state.Current); } - // Check if we are trying to build the sorted parameter cache. - if (argumentState.ParameterRefCache != null) - { - state.Current.JsonTypeInfo.UpdateSortedParameterCache(ref state.Current); - } - return true; } @@ -308,7 +302,12 @@ private void ReadConstructorArguments(scoped ref ReadStack state, ref Utf8JsonRe continue; } - if (TryLookupConstructorParameter(unescapedPropertyName, ref state, ref reader, options, out JsonParameterInfo? jsonParameterInfo)) + if (TryLookupConstructorParameter( + unescapedPropertyName, + ref state, + options, + out JsonPropertyInfo jsonPropertyInfo, + out JsonParameterInfo? jsonParameterInfo)) { // Set the property value. reader.ReadWithVerify(); @@ -335,14 +334,6 @@ private void ReadConstructorArguments(scoped ref ReadStack state, ref Utf8JsonRe } else { - JsonPropertyInfo jsonPropertyInfo = JsonSerializer.LookupProperty( - obj: null, - unescapedPropertyName, - ref state, - options, - out _, - createExtensionProperty: false); - if (jsonPropertyInfo.CanDeserialize) { ArgumentState argumentState = state.Current.CtorArgumentState!; @@ -424,24 +415,12 @@ private bool ReadConstructorArgumentsWithContinuation(scoped ref ReadStack state if (TryLookupConstructorParameter( unescapedPropertyName, ref state, - ref reader, options, + out jsonPropertyInfo, out jsonParameterInfo)) { jsonPropertyInfo = null; } - else - { - jsonPropertyInfo = JsonSerializer.LookupProperty( - obj: null!, - unescapedPropertyName, - ref state, - options, - out bool useExtensionProperty, - createExtensionProperty: false); - - state.Current.UseExtensionProperty = useExtensionProperty; - } state.Current.PropertyState = StackFramePropertyState.Name; } @@ -609,31 +588,41 @@ private void BeginRead(scoped ref ReadStack state, JsonSerializerOptions options /// /// Lookup the constructor parameter given its name in the reader. /// - protected virtual bool TryLookupConstructorParameter( + protected static bool TryLookupConstructorParameter( scoped ReadOnlySpan unescapedPropertyName, scoped ref ReadStack state, - ref Utf8JsonReader reader, JsonSerializerOptions options, + out JsonPropertyInfo jsonPropertyInfo, [NotNullWhen(true)] out JsonParameterInfo? jsonParameterInfo) { - Debug.Assert(state.Current.JsonTypeInfo.Kind == JsonTypeInfoKind.Object); + Debug.Assert(state.Current.JsonTypeInfo.Kind is JsonTypeInfoKind.Object); + Debug.Assert(state.Current.CtorArgumentState != null); - jsonParameterInfo = state.Current.JsonTypeInfo.GetParameter( + jsonPropertyInfo = JsonSerializer.LookupProperty( + obj: null, unescapedPropertyName, - ref state.Current, - out byte[] utf8PropertyName); - - // Increment ConstructorParameterIndex so GetParameter() checks the next parameter first when called again. - state.Current.CtorArgumentState!.ParameterIndex++; + ref state, + options, + out byte[] utf8PropertyName, + out bool useExtensionProperty, + createExtensionProperty: false); // For case insensitive and missing property support of JsonPath, remember the value on the temporary stack. state.Current.JsonPropertyName = utf8PropertyName; - state.Current.CtorArgumentState.JsonParameterInfo = jsonParameterInfo; - - state.Current.NumberHandling = jsonParameterInfo?.NumberHandling; - - return jsonParameterInfo != null; + jsonParameterInfo = jsonPropertyInfo.ParameterInfo; + if (jsonParameterInfo != null) + { + state.Current.JsonPropertyInfo = null; + state.Current.CtorArgumentState!.JsonParameterInfo = jsonParameterInfo; + state.Current.NumberHandling = jsonParameterInfo.NumberHandling; + return true; + } + else + { + state.Current.UseExtensionProperty = useExtensionProperty; + return false; + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index e3a2bece9b3865..0a50aba7ad0ce0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -21,6 +21,7 @@ internal static JsonPropertyInfo LookupProperty( ReadOnlySpan unescapedPropertyName, ref ReadStack state, JsonSerializerOptions options, + out byte[] utf8PropertyName, out bool useExtensionProperty, bool createExtensionProperty = true) { @@ -38,7 +39,7 @@ internal static JsonPropertyInfo LookupProperty( JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.GetProperty( unescapedPropertyName, ref state.Current, - out byte[] utf8PropertyName); + out utf8PropertyName); // Increment PropertyIndex so GetProperty() checks the next property first when called again. state.Current.PropertyIndex++; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs index b3564a75701fd8..82fd5d349bec7a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs @@ -70,6 +70,7 @@ options.Encoder is null && // Ensure options values are consistent with expected defaults. options.DefaultIgnoreCondition == generatedSerializerOptions.DefaultIgnoreCondition && + options.IgnoreNullableAnnotations == generatedSerializerOptions.IgnoreNullableAnnotations && options.IgnoreReadOnlyFields == generatedSerializerOptions.IgnoreReadOnlyFields && options.IgnoreReadOnlyProperties == generatedSerializerOptions.IgnoreReadOnlyProperties && options.IncludeFields == generatedSerializerOptions.IncludeFields && diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 77f8d546fe8c49..50b6adea78d7c1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -1071,7 +1071,10 @@ protected override void ValidateAddedValue(IJsonTypeInfoResolver item) protected override void OnCollectionModifying() { _options.VerifyMutable(); + } + protected override void OnCollectionModified() + { // Collection modified by the user: replace the main // resolver with the resolver chain as our source of truth. _options._typeInfoResolver = this; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index b5c10b417b71cd..f41a6fb87def9e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -71,12 +71,14 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte if (typeInfo.Kind is JsonTypeInfoKind.Object) { - PopulateProperties(typeInfo); + NullabilityInfoContext? nullabilityCtx = options.IgnoreNullableAnnotations ? null : new(); if (converter.ConstructorIsParameterized) { - PopulateParameterInfoValues(typeInfo); + PopulateParameterInfoValues(typeInfo, nullabilityCtx); } + + PopulateProperties(typeInfo, nullabilityCtx); } // Plug in any converter configuration -- should be run last. @@ -87,7 +89,7 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void PopulateProperties(JsonTypeInfo typeInfo) + private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoContext? nullabilityCtx) { Debug.Assert(!typeInfo.IsReadOnly); Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object); @@ -111,6 +113,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo) AddMembersDeclaredBySuperType( typeInfo, currentType, + nullabilityCtx, constructorHasSetsRequiredMembersAttribute, ref state); } @@ -153,6 +156,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo) private static void AddMembersDeclaredBySuperType( JsonTypeInfo typeInfo, Type currentType, + NullabilityInfoContext? nullabilityCtx, bool constructorHasSetsRequiredMembersAttribute, ref JsonTypeInfo.PropertyHierarchyResolutionState state) { @@ -183,6 +187,7 @@ private static void AddMembersDeclaredBySuperType( typeInfo, typeToConvert: propertyInfo.PropertyType, memberInfo: propertyInfo, + nullabilityCtx, shouldCheckMembersForRequiredMemberAttribute, hasJsonIncludeAttribute, ref state); @@ -198,6 +203,7 @@ private static void AddMembersDeclaredBySuperType( typeInfo, typeToConvert: fieldInfo.FieldType, memberInfo: fieldInfo, + nullabilityCtx, shouldCheckMembersForRequiredMemberAttribute, hasJsonIncludeAtribute, ref state); @@ -211,11 +217,12 @@ private static void AddMember( JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, + NullabilityInfoContext? nullabilityCtx, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute, ref JsonTypeInfo.PropertyHierarchyResolutionState state) { - JsonPropertyInfo? jsonPropertyInfo = CreatePropertyInfo(typeInfo, typeToConvert, memberInfo, typeInfo.Options, shouldCheckForRequiredKeyword, hasJsonIncludeAttribute); + JsonPropertyInfo? jsonPropertyInfo = CreatePropertyInfo(typeInfo, typeToConvert, memberInfo, nullabilityCtx, typeInfo.Options, shouldCheckForRequiredKeyword, hasJsonIncludeAttribute); if (jsonPropertyInfo == null) { // ignored invalid property @@ -232,6 +239,7 @@ private static void AddMember( JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, + NullabilityInfoContext? nullabilityCtx, JsonSerializerOptions options, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute) @@ -259,7 +267,7 @@ private static void AddMember( } JsonPropertyInfo jsonPropertyInfo = typeInfo.CreatePropertyUsingReflection(typeToConvert, declaringType: memberInfo.DeclaringType); - PopulatePropertyInfo(jsonPropertyInfo, memberInfo, customConverter, ignoreCondition, shouldCheckForRequiredKeyword, hasJsonIncludeAttribute); + PopulatePropertyInfo(jsonPropertyInfo, memberInfo, customConverter, ignoreCondition, nullabilityCtx, shouldCheckForRequiredKeyword, hasJsonIncludeAttribute); return jsonPropertyInfo; } @@ -289,7 +297,9 @@ private static bool PropertyIsOverriddenAndIgnored(PropertyInfo propertyInfo, Di propertyInfo.PropertyType == ignoredMember.PropertyType; } - private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo) + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, NullabilityInfoContext? nullabilityCtx) { Debug.Assert(typeInfo.Converter.ConstructorInfo != null); ParameterInfo[] parameters = typeInfo.Converter.ConstructorInfo.GetParameters(); @@ -314,13 +324,16 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo) Position = reflectionInfo.Position, HasDefaultValue = reflectionInfo.HasDefaultValue, DefaultValue = reflectionInfo.GetDefaultValue(), - DisallowNullReads = DetermineParameterNullabilityAllowance(reflectionInfo) + DisallowNullReads = + nullabilityCtx != null && + reflectionInfo.ParameterType.IsNullableType() && + DetermineParameterNullability(reflectionInfo, nullabilityCtx) is NullabilityState.NotNull, }; jsonParameters[i] = jsonInfo; } - typeInfo.ParameterInfoValues = jsonParameters; + typeInfo.PopulateParameterInfoValues(jsonParameters); } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] @@ -330,6 +343,7 @@ private static void PopulatePropertyInfo( MemberInfo memberInfo, JsonConverter? customConverter, JsonIgnoreCondition? ignoreCondition, + NullabilityInfoContext? nullabilityCtx, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute) { @@ -355,7 +369,7 @@ private static void PopulatePropertyInfo( DeterminePropertyPolicies(jsonPropertyInfo, memberInfo); DeterminePropertyName(jsonPropertyInfo, memberInfo); DeterminePropertyIsRequired(jsonPropertyInfo, memberInfo, shouldCheckForRequiredKeyword); - DeterminePropertyNullabilityAllowance(jsonPropertyInfo, memberInfo); + DeterminePropertyNullability(jsonPropertyInfo, memberInfo, nullabilityCtx); if (ignoreCondition != JsonIgnoreCondition.Always) { @@ -472,37 +486,103 @@ internal static void DeterminePropertyAccessors(JsonPropertyInfo jsonPrope [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void DeterminePropertyNullabilityAllowance(JsonPropertyInfo propertyInfo, MemberInfo memberInfo) + private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, MemberInfo memberInfo, NullabilityInfoContext? nullabilityCtx) { - if (propertyInfo.PropertyType.IsValueType) + if (nullabilityCtx is null || !propertyInfo.PropertyTypeCanBeNull) { return; } - NullabilityInfo nullability; + NullabilityInfo nullabilityInfo; if (propertyInfo.MemberType is MemberTypes.Property) { - nullability = new NullabilityInfoContext().Create((PropertyInfo)memberInfo); + nullabilityInfo = nullabilityCtx.Create((PropertyInfo)memberInfo); } else { Debug.Assert(propertyInfo.MemberType is MemberTypes.Field); - nullability = new NullabilityInfoContext().Create((FieldInfo)memberInfo); + nullabilityInfo = nullabilityCtx.Create((FieldInfo)memberInfo); } - propertyInfo.DisallowNullReads = nullability.WriteState is NullabilityState.NotNull; - propertyInfo.DisallowNullWrites = nullability.ReadState is NullabilityState.NotNull; + propertyInfo.DisallowNullReads = nullabilityInfo.WriteState is NullabilityState.NotNull; + propertyInfo.DisallowNullWrites = nullabilityInfo.ReadState is NullabilityState.NotNull; } - private static bool DetermineParameterNullabilityAllowance(ParameterInfo parameterInfo) + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static NullabilityState DetermineParameterNullability(ParameterInfo parameterInfo, NullabilityInfoContext nullabilityCtx) { - if (parameterInfo.ParameterType.IsValueType) +#if NETCOREAPP && !NET9_0_OR_GREATER + // Workaround for https://github.com/dotnet/runtime/issues/92487 + // The fix has been incorporated into .NET 9 and later (and the polyfilled implementations in netfx). + if (parameterInfo.GetGenericParameterDefinition() is { ParameterType: { IsGenericParameter: true } typeParam }) { - return false; + // Step 1. Look for nullable annotations on the type parameter. + if (GetNullableFlags(typeParam) is byte[] flags) + { + return TranslateByte(flags[0]); + } + + // Step 2. Look for nullable annotations on the generic method declaration. + if (typeParam.DeclaringMethod != null && GetNullableContextFlag(typeParam.DeclaringMethod) is byte flag) + { + return TranslateByte(flag); + } + + // Step 3. Look for nullable annotations on the generic method declaration. + if (GetNullableContextFlag(typeParam.DeclaringType!) is byte flag2) + { + return TranslateByte(flag2); + } + + // Default to nullable. + return NullabilityState.Nullable; + + static byte[]? GetNullableFlags(MemberInfo member) + { + foreach (Attribute attr in member.GetCustomAttributes()) + { + Type attrType = attr.GetType(); + if (attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableAttribute") + { + return (byte[])attr.GetType().GetField("NullableFlags")?.GetValue(attr)!; + } + } + + return null; + } + + static byte? GetNullableContextFlag(MemberInfo member) + { + foreach (Attribute attr in member.GetCustomAttributes()) + { + Type attrType = attr.GetType(); + if (attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableContextAttribute") + { + return (byte?)attr?.GetType().GetField("Flag")?.GetValue(attr)!; + } + } + + return null; + } + + static NullabilityState TranslateByte(byte b) => + b switch + { + 1 => NullabilityState.NotNull, + 2 => NullabilityState.Nullable, + _ => NullabilityState.Unknown + }; } +#endif + + //if (parameterInfo.ParameterType.IsValueType) + //{ + // return false; + //} - NullabilityInfo nullability = new NullabilityInfoContext().Create(parameterInfo); - return nullability.WriteState is NullabilityState.NotNull; + NullabilityInfo nullability = nullabilityCtx.Create(parameterInfo); + return nullability.WriteState; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs index 5c1de5c199b1fe..7ff6048a8fa393 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs @@ -20,6 +20,7 @@ private static JsonTypeInfo CreateCore(JsonConverter converter, JsonSerial // Plug in any converter configuration -- should be run last. converter.ConfigureJsonTypeInfo(typeInfo, options); + typeInfo.IsCustomized = false; return typeInfo; } @@ -57,6 +58,7 @@ private static JsonTypeInfo CreateCore(JsonSerializerOptions options, Json // Plug in any converter configuration -- should be run last. converter.ConfigureJsonTypeInfo(typeInfo, options); + typeInfo.IsCustomized = false; return typeInfo; } @@ -94,6 +96,7 @@ private static JsonTypeInfo CreateCore( // Plug in any converter configuration -- should be run last. converter.ConfigureJsonTypeInfo(typeInfo, options); + typeInfo.IsCustomized = false; return typeInfo; } @@ -117,7 +120,7 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, Func internal abstract class JsonParameterInfo { - public JsonConverter EffectiveConverter => MatchingProperty.EffectiveConverter; - - // The default value of the parameter. This is `DefaultValue` of the `ParameterInfo`, if specified, or the `default` for the `ParameterType`. - public object? DefaultValue { get; private protected init; } - - public bool IgnoreNullTokensOnRead { get; } - - public JsonSerializerOptions Options { get; } - - // The name of the parameter as UTF-8 bytes. - public byte[] NameAsUtf8Bytes { get; } + internal JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonPropertyInfo matchingProperty) + { + Debug.Assert(matchingProperty.PropertyType == parameterInfoValues.ParameterType); - public JsonNumberHandling? NumberHandling { get; } + Position = parameterInfoValues.Position; + HasDefaultValue = parameterInfoValues.HasDefaultValue; + DisallowNullReads = parameterInfoValues.DisallowNullReads && !matchingProperty.Options.IgnoreNullableAnnotations; + MatchingProperty = matchingProperty; + } public int Position { get; } + public bool HasDefaultValue { get; } - public JsonTypeInfo JsonTypeInfo => MatchingProperty.JsonTypeInfo; - - public Type ParameterType { get; } - - public bool ShouldDeserialize { get; } - + // The default value of the parameter. This is `DefaultValue` of the `ParameterInfo`, if specified, or the `default` for the `ParameterType`. + public object? DefaultValue { get; private protected init; } public JsonPropertyInfo MatchingProperty { get; } + public bool DisallowNullReads { get; internal set; } - public bool DisallowNullReads { get; } - - public JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonPropertyInfo matchingProperty) - { - Debug.Assert(matchingProperty.IsConfigured); - - MatchingProperty = matchingProperty; - ShouldDeserialize = !matchingProperty.IsIgnored; - Options = matchingProperty.Options; - Position = parameterInfoValues.Position; - DisallowNullReads = parameterInfoValues.DisallowNullReads; + public Type DeclaringType => MatchingProperty.DeclaringType; + public Type ParameterType => MatchingProperty.PropertyType; + public JsonConverter EffectiveConverter => MatchingProperty.EffectiveConverter; + public bool IgnoreNullTokensOnRead => MatchingProperty.IgnoreNullTokensOnRead; + public JsonSerializerOptions Options => MatchingProperty.Options; - ParameterType = matchingProperty.PropertyType; - NameAsUtf8Bytes = matchingProperty.NameAsUtf8Bytes; - IgnoreNullTokensOnRead = matchingProperty.IgnoreNullTokensOnRead; - NumberHandling = matchingProperty.EffectiveNumberHandling; - //DisallowNullWrites = matchingProperty.DisallowNullWrites; - } + // The name of the parameter as UTF-8 bytes. + public byte[] NameAsUtf8Bytes => MatchingProperty.NameAsUtf8Bytes; + public JsonNumberHandling? NumberHandling => MatchingProperty.EffectiveNumberHandling; + public JsonTypeInfo JsonTypeInfo => MatchingProperty.JsonTypeInfo; + public bool ShouldDeserialize => !MatchingProperty.IsIgnored; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs index 5c1f57af9bf867..273f86eb316661 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoOfT.cs @@ -19,7 +19,6 @@ public JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonProper : base(parameterInfoValues, matchingPropertyInfo) { Debug.Assert(parameterInfoValues.ParameterType == typeof(T)); - Debug.Assert(matchingPropertyInfo.IsConfigured); MatchingProperty = matchingPropertyInfo; DefaultValue = parameterInfoValues.HasDefaultValue && parameterInfoValues.DefaultValue is not null diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index b277601463183f..5ec77a09ea3ab4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -17,7 +17,7 @@ public abstract class JsonPropertyInfo { internal static readonly JsonPropertyInfo s_missingProperty = GetPropertyPlaceholder(); - internal JsonTypeInfo? ParentTypeInfo { get; private set; } + internal JsonTypeInfo? DeclaringTypeInfo { get; private set; } /// /// Converter after applying CustomConverter (i.e. JsonConverterAttribute) @@ -234,14 +234,73 @@ public JsonObjectCreationHandling? ObjectCreationHandling internal bool IsSourceGenerated { get; set; } /// - /// Gets or sets a value indicating whether the allows values on deserialization. + /// Gets or sets a value indicating if the serializer should reject property values during deserialization. /// - public bool DisallowNullReads { get; set; } + /// + /// The instance has been locked for further modification. + /// + /// -or- + /// + /// The current is not a nullable type. + /// + /// + /// For contracts originating from or , + /// the value of this property will be mapped from nullable reference type annotations. + /// + public bool DisallowNullReads + { + get => _disallowNullReads; + set + { + VerifyMutable(); + + if (value && !PropertyTypeCanBeNull) + { + throw new InvalidOperationException("The property type is not nullable."); + } + + if (ParameterInfo != null) + { + ParameterInfo.DisallowNullReads = value; + } + + _disallowNullReads = value; + } + } + + private bool _disallowNullReads; /// - /// Gets or sets a value indicating whether the allows values on serialization. + /// Gets or sets a value indicating if the serializer should reject property values during serialization. /// - public bool DisallowNullWrites { get; set; } + /// + /// The instance has been locked for further modification. + /// + /// -or- + /// + /// The current is not a nullable type. + /// + /// + /// For contracts originating from or , + /// the value of this property will be mapped from nullable reference type annotations. + /// + public bool DisallowNullWrites + { + get => _disallowNullWrites; + set + { + VerifyMutable(); + + if (value && !PropertyTypeCanBeNull) + { + throw new InvalidOperationException("The property type is not nullable."); + } + + _disallowNullWrites = value; + } + } + + private bool _disallowNullWrites; /// /// Specifies whether the current property is a special extension data property. @@ -301,13 +360,15 @@ public bool IsRequired private bool _isRequired; + internal JsonParameterInfo? ParameterInfo { get; private set; } + internal JsonPropertyInfo(Type declaringType, Type propertyType, JsonTypeInfo? declaringTypeInfo, JsonSerializerOptions options) { Debug.Assert(declaringTypeInfo is null || declaringType.IsAssignableFrom(declaringTypeInfo.Type)); DeclaringType = declaringType; PropertyType = propertyType; - ParentTypeInfo = declaringTypeInfo; // null parentTypeInfo means it's not tied yet + DeclaringTypeInfo = declaringTypeInfo; // null declaringTypeInfo means it's not tied yet Options = options; } @@ -331,14 +392,14 @@ internal static JsonPropertyInfo GetPropertyPlaceholder() private protected void VerifyMutable() { - ParentTypeInfo?.VerifyMutable(); + DeclaringTypeInfo?.VerifyMutable(); } internal bool IsConfigured { get; private set; } internal void Configure() { - Debug.Assert(ParentTypeInfo != null); + Debug.Assert(DeclaringTypeInfo != null); Debug.Assert(!IsConfigured); if (IsIgnored) @@ -472,10 +533,10 @@ private void DetermineSerializationCapabilities() private void DetermineNumberHandlingForTypeInfo() { - Debug.Assert(ParentTypeInfo != null, "We should have ensured parent is assigned in JsonTypeInfo"); - Debug.Assert(!ParentTypeInfo.IsConfigured); + Debug.Assert(DeclaringTypeInfo != null, "We should have ensured parent is assigned in JsonTypeInfo"); + Debug.Assert(!DeclaringTypeInfo.IsConfigured); - JsonNumberHandling? declaringTypeNumberHandling = ParentTypeInfo.NumberHandling; + JsonNumberHandling? declaringTypeNumberHandling = DeclaringTypeInfo.NumberHandling; if (declaringTypeNumberHandling != null && declaringTypeNumberHandling != JsonNumberHandling.Strict && !EffectiveConverter.IsInternalConverter) { @@ -500,7 +561,7 @@ private void DetermineNumberHandlingForTypeInfo() private void DetermineNumberHandlingForProperty() { - Debug.Assert(ParentTypeInfo != null, "We should have ensured parent is assigned in JsonTypeInfo"); + Debug.Assert(DeclaringTypeInfo != null, "We should have ensured parent is assigned in JsonTypeInfo"); Debug.Assert(!IsConfigured, "Should not be called post-configuration."); Debug.Assert(_jsonTypeInfo != null, "Must have already been determined on configuration."); @@ -509,7 +570,7 @@ private void DetermineNumberHandlingForProperty() if (numberHandlingIsApplicable) { // Priority 1: Get handling from attribute on property/field, its parent class type or property type. - JsonNumberHandling? handling = NumberHandling ?? ParentTypeInfo.NumberHandling ?? _jsonTypeInfo.NumberHandling; + JsonNumberHandling? handling = NumberHandling ?? DeclaringTypeInfo.NumberHandling ?? _jsonTypeInfo.NumberHandling; // Priority 2: Get handling from JsonSerializerOptions instance. if (!handling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) @@ -528,7 +589,7 @@ private void DetermineNumberHandlingForProperty() private void DetermineEffectiveObjectCreationHandlingForProperty() { Debug.Assert(EffectiveConverter != null, "Must have calculated the effective converter."); - Debug.Assert(ParentTypeInfo != null, "We should have ensured parent is assigned in JsonTypeInfo"); + Debug.Assert(DeclaringTypeInfo != null, "We should have ensured parent is assigned in JsonTypeInfo"); Debug.Assert(!IsConfigured, "Should not be called post-configuration."); JsonObjectCreationHandling effectiveObjectCreationHandling = JsonObjectCreationHandling.Replace; @@ -537,8 +598,8 @@ private void DetermineEffectiveObjectCreationHandlingForProperty() // Consult type-level configuration, then global configuration. // Ignore global configuration if we're using a parameterized constructor. JsonObjectCreationHandling preferredCreationHandling = - ParentTypeInfo.PreferredPropertyObjectCreationHandling - ?? (ParentTypeInfo.DetermineUsesParameterizedConstructor() + DeclaringTypeInfo.PreferredPropertyObjectCreationHandling + ?? (DeclaringTypeInfo.DetermineUsesParameterizedConstructor() ? JsonObjectCreationHandling.Replace : Options.PreferredObjectCreationHandling); @@ -547,7 +608,7 @@ private void DetermineEffectiveObjectCreationHandlingForProperty() EffectiveConverter.CanPopulate && Get != null && (!PropertyType.IsValueType || Set != null) && - !ParentTypeInfo.SupportsPolymorphicDeserialization && + !DeclaringTypeInfo.SupportsPolymorphicDeserialization && !(Set == null && IgnoreReadOnlyMember); effectiveObjectCreationHandling = canPopulate ? JsonObjectCreationHandling.Populate : JsonObjectCreationHandling.Replace; @@ -586,7 +647,7 @@ private void DetermineEffectiveObjectCreationHandlingForProperty() if (effectiveObjectCreationHandling is JsonObjectCreationHandling.Populate) { - if (ParentTypeInfo.DetermineUsesParameterizedConstructor()) + if (DeclaringTypeInfo.DetermineUsesParameterizedConstructor()) { ThrowHelper.ThrowNotSupportedException_ObjectCreationHandlingPropertyDoesNotSupportParameterizedConstructors(); } @@ -601,6 +662,16 @@ private void DetermineEffectiveObjectCreationHandlingForProperty() EffectiveObjectCreationHandling = effectiveObjectCreationHandling; } + private void DetermineParameterInfo() + { + Debug.Assert(DeclaringTypeInfo?.IsConfigured is false); + ParameterInfo = DeclaringTypeInfo.CreateMatchingParameterInfo(this); + if (ParameterInfo != null) + { + _disallowNullReads = ParameterInfo.DisallowNullReads; + } + } + private bool NumberHandingIsApplicable() { if (EffectiveConverter.IsInternalConverterForNumberType) @@ -694,7 +765,7 @@ internal bool IgnoreReadOnlyMember /// /// True if the corresponding cref="JsonTypeInfo.PropertyInfoForTypeInfo"/> is this instance. /// - internal bool IsForTypeInfo { get; set; } + internal bool IsForTypeInfo { get; init; } // There are 3 copies of the property name: // 1) Name. The unescaped property name. @@ -852,14 +923,16 @@ internal bool ReadJsonExtensionDataValue(scoped ref ReadStack state, ref Utf8Jso internal void EnsureChildOf(JsonTypeInfo parent) { - if (ParentTypeInfo == null) + if (DeclaringTypeInfo is null) { - ParentTypeInfo = parent; + DeclaringTypeInfo = parent; } - else if (ParentTypeInfo != parent) + else if (DeclaringTypeInfo != parent) { ThrowHelper.ThrowInvalidOperationException_JsonPropertyInfoIsBoundToDifferentJsonTypeInfo(this); } + + DetermineParameterInfo(); } /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index 0ad071a6144f65..ad9e4a886c6760 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -196,14 +196,13 @@ value is not null && { Debug.Assert(PropertyTypeCanBeNull); - if (EffectiveConverter.HandleNullOnWrite) + if (DisallowNullWrites && !Options.IgnoreNullableAnnotations) { - if (DisallowNullWrites && EffectiveConverter.IsInternalConverter && !Options.IgnoreNullableAnnotations) - { - Debug.Assert(!EffectiveConverter.IsValueType); - ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); - } + ThrowHelper.ThrowJsonException_PropertyGetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); + } + if (EffectiveConverter.HandleNullOnWrite) + { if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; @@ -219,12 +218,6 @@ value is not null && } else { - if (DisallowNullWrites && !Options.IgnoreNullableAnnotations) - { - Debug.Assert(!EffectiveConverter.IsValueType); - ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); - } - writer.WriteNullSection(EscapedNameSection); } @@ -287,10 +280,9 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta if (!IgnoreNullTokensOnRead) { - if (DisallowNullReads && !Options.IgnoreNullableAnnotations) + if (DisallowNullReads) { - Debug.Assert(!EffectiveConverter.IsValueType); - ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); + ThrowHelper.ThrowJsonException_PropertySetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); } T? value = default; @@ -311,11 +303,11 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta // Optimize for internal converters by avoiding the extra call to TryRead. T? fastValue = EffectiveConverter.Read(ref reader, PropertyType, Options); - if (fastValue is null && DisallowNullReads && !Options.IgnoreNullableAnnotations) + if (fastValue is null && DisallowNullReads) { Debug.Fail("We currently don't have an internal converter that returns null that could trigger this, if you hit this, please add a test case."); Debug.Assert(!EffectiveConverter.IsValueType); - ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); + ThrowHelper.ThrowJsonException_PropertySetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); } Set!(obj, fastValue!); @@ -342,10 +334,9 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta // We cannot do reader.Skip early because converter decides if populating will happen or not if (CanDeserialize) { - if (value is null && DisallowNullReads && !Options.IgnoreNullableAnnotations) + if (value is null && DisallowNullReads) { - Debug.Assert(!EffectiveConverter.IsValueType); - ThrowHelper.ThrowJsonException_NullabilityDoesNotAllowNull(Name, state.Current.JsonTypeInfo.Type); + ThrowHelper.ThrowJsonException_PropertySetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); } Set!(obj, value!); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs index 5a901fbb80eae6..297b9f0cb2dde2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs @@ -22,9 +22,6 @@ public abstract partial class JsonTypeInfo // followed by a byte representing the length. private const int PropertyNameKeyLength = 7; - // The limit to how many constructor parameter names from the JSON are cached in _parameterRefsSorted before using _parameterCache. - private const int ParameterNameCountCacheThreshold = 32; - // The limit to how many property names from the JSON are cached in _propertyRefsSorted before using PropertyCache. private const int PropertyNameCountCacheThreshold = 64; @@ -34,7 +31,8 @@ public abstract partial class JsonTypeInfo // All of the serializable parameters on a POCO constructor keyed on parameter name. // Only parameters which bind to properties are cached. - internal JsonPropertyDictionary? ParameterCache { get; private set; } + internal List? ParameterCache { get; private set; } + internal bool UsesParameterizedConstructor { get @@ -47,10 +45,6 @@ internal bool UsesParameterizedConstructor // All of the serializable properties on a POCO (except the optional extension property) keyed on property name. internal JsonPropertyDictionary? PropertyCache { get; private set; } - // Fast cache of constructor parameters by first JSON ordering; may not contain all parameters. Accessed before ParameterCache. - // Use an array (instead of List) for highest performance. - private volatile ParameterRef[]? _parameterRefsSorted; - // Fast cache of properties by first JSON ordering; may not contain all properties. Accessed before PropertyCache. // Use an array (instead of List) for highest performance. private volatile PropertyRef[]? _propertyRefsSorted; @@ -243,140 +237,6 @@ internal JsonPropertyInfo GetProperty( return info; } - // AggressiveInlining used although a large method it is only called from one location and is on a hot path. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal JsonParameterInfo? GetParameter( - ReadOnlySpan propertyName, - ref ReadStackFrame frame, - out byte[] utf8PropertyName) - { - ParameterRef parameterRef; - - ulong key = GetKey(propertyName); - - // Keep a local copy of the cache in case it changes by another thread. - ParameterRef[]? localParameterRefsSorted = _parameterRefsSorted; - - // If there is an existing cache, then use it. - if (localParameterRefsSorted != null) - { - // Start with the current parameter index, and then go forwards\backwards. - int parameterIndex = frame.CtorArgumentState!.ParameterIndex; - - int count = localParameterRefsSorted.Length; - int iForward = Math.Min(parameterIndex, count); - int iBackward = iForward - 1; - - while (true) - { - if (iForward < count) - { - parameterRef = localParameterRefsSorted[iForward]; - if (IsParameterRefEqual(parameterRef, propertyName, key)) - { - utf8PropertyName = parameterRef.NameFromJson; - return parameterRef.Info; - } - - ++iForward; - - if (iBackward >= 0) - { - parameterRef = localParameterRefsSorted[iBackward]; - if (IsParameterRefEqual(parameterRef, propertyName, key)) - { - utf8PropertyName = parameterRef.NameFromJson; - return parameterRef.Info; - } - - --iBackward; - } - } - else if (iBackward >= 0) - { - parameterRef = localParameterRefsSorted[iBackward]; - if (IsParameterRefEqual(parameterRef, propertyName, key)) - { - utf8PropertyName = parameterRef.NameFromJson; - return parameterRef.Info; - } - - --iBackward; - } - else - { - // Property was not found. - break; - } - } - } - - // No cached item was found. Try the main dictionary which has all of the parameters. - Debug.Assert(ParameterCache != null); - - if (ParameterCache.TryGetValue(JsonHelpers.Utf8GetString(propertyName), out JsonParameterInfo? info)) - { - Debug.Assert(info != null); - - if (Options.PropertyNameCaseInsensitive) - { - if (propertyName.SequenceEqual(info.NameAsUtf8Bytes)) - { - Debug.Assert(key == GetKey(info.NameAsUtf8Bytes.AsSpan())); - - // Use the existing byte[] reference instead of creating another one. - utf8PropertyName = info.NameAsUtf8Bytes!; - } - else - { - // Make a copy of the original Span. - utf8PropertyName = propertyName.ToArray(); - } - } - else - { - Debug.Assert(key == GetKey(info.NameAsUtf8Bytes!.AsSpan())); - utf8PropertyName = info.NameAsUtf8Bytes!; - } - } - else - { - Debug.Assert(info == null); - - // Make a copy of the original Span. - utf8PropertyName = propertyName.ToArray(); - } - - // Check if we should add this to the cache. - // Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache. - int cacheCount = 0; - if (localParameterRefsSorted != null) - { - cacheCount = localParameterRefsSorted.Length; - } - - // Do a quick check for the stable (after warm-up) case. - if (cacheCount < ParameterNameCountCacheThreshold) - { - // Do a slower check for the warm-up case. - if (frame.CtorArgumentState!.ParameterRefCache != null) - { - cacheCount += frame.CtorArgumentState.ParameterRefCache.Count; - } - - // Check again to append the cache up to the threshold. - if (cacheCount < ParameterNameCountCacheThreshold) - { - frame.CtorArgumentState.ParameterRefCache ??= new List(); - - parameterRef = new ParameterRef(key, info!, utf8PropertyName); - frame.CtorArgumentState.ParameterRefCache.Add(parameterRef); - } - } - - return info; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan propertyName, ulong key) { @@ -393,22 +253,6 @@ private static bool IsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan< return false; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsParameterRefEqual(in ParameterRef parameterRef, ReadOnlySpan parameterName, ulong key) - { - if (key == parameterRef.Key) - { - // We compare the whole name, although we could skip the first 7 bytes (but it's not any faster) - if (parameterName.Length <= PropertyNameKeyLength || - parameterName.SequenceEqual(parameterRef.NameFromJson)) - { - return true; - } - } - - return false; - } - /// /// Get a key from the property name. /// The key consists of the first 7 bytes of the property name and then the length. @@ -502,41 +346,5 @@ internal void UpdateSortedPropertyCache(ref ReadStackFrame frame) frame.PropertyRefCache = null; } - - internal void UpdateSortedParameterCache(ref ReadStackFrame frame) - { - Debug.Assert(frame.CtorArgumentState!.ParameterRefCache != null); - - // frame.PropertyRefCache is only read\written by a single thread -- the thread performing - // the deserialization for a given object instance. - - List listToAppend = frame.CtorArgumentState.ParameterRefCache; - - // _parameterRefsSorted can be accessed by multiple threads, so replace the reference when - // appending to it. No lock() is necessary. - - if (_parameterRefsSorted != null) - { - List replacementList = new List(_parameterRefsSorted); - Debug.Assert(replacementList.Count <= ParameterNameCountCacheThreshold); - - // Verify replacementList will not become too large. - while (replacementList.Count + listToAppend.Count > ParameterNameCountCacheThreshold) - { - // This code path is rare; keep it simple by using RemoveAt() instead of RemoveRange() which requires calculating index\count. - listToAppend.RemoveAt(listToAppend.Count - 1); - } - - // Add the new items; duplicates are possible but that is tolerated during property lookup. - replacementList.AddRange(listToAppend); - _parameterRefsSorted = replacementList.ToArray(); - } - else - { - _parameterRefsSorted = listToAppend.ToArray(); - } - - frame.CtorArgumentState.ParameterRefCache = null; - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 9e24d897cadaca..a5d28f8040e81c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -789,18 +789,18 @@ private void DetermineIsCompatibleWithCurrentOptions() // Defines the core predicate that must be checked for every node in the type graph. bool IsCurrentNodeCompatible() { - if (Options.CanUseFastPathSerializationLogic) - { - // Simple case/backward compatibility: options uses a combination of compatible built-in converters. - return true; - } - if (IsCustomized) { // Return false if we have detected contract customization by the user. return false; } + if (Options.CanUseFastPathSerializationLogic) + { + // Simple case/backward compatibility: options uses a combination of compatible built-in converters. + return true; + } + return OriginatingResolver.IsCompatibleWithOptions(Options); } } @@ -986,7 +986,7 @@ public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name) return propertyInfo; } - internal JsonParameterInfoValues[]? ParameterInfoValues { get; set; } + private Dictionary? _parameterInfoValuesIndex; // Untyped, root-level serialization methods internal abstract void SerializeAsObject(Utf8JsonWriter writer, object? rootValue); @@ -1008,40 +1008,13 @@ internal ref struct PropertyHierarchyResolutionState(JsonSerializerOptions optio public bool IsPropertyOrderSpecified; } - private sealed class ParameterLookupKey + private readonly struct ParameterLookupKey(Type type, string name) : IEquatable { - public ParameterLookupKey(string name, Type type) - { - Name = name; - Type = type; - } - - public string Name { get; } - public Type Type { get; } - - public override int GetHashCode() - { - return StringComparer.OrdinalIgnoreCase.GetHashCode(Name); - } - - public override bool Equals([NotNullWhen(true)] object? obj) - { - Debug.Assert(obj is ParameterLookupKey); - - ParameterLookupKey other = (ParameterLookupKey)obj; - return Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); - } - } - - private sealed class ParameterLookupValue - { - public ParameterLookupValue(JsonPropertyInfo jsonPropertyInfo) - { - JsonPropertyInfo = jsonPropertyInfo; - } - - public string? DuplicateName { get; set; } - public JsonPropertyInfo JsonPropertyInfo { get; } + public Type Type { get; } = type; + public string Name { get; } = name; + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name); + public bool Equals(ParameterLookupKey other) => Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); + public override bool Equals([NotNullWhen(true)] object? obj) => obj is ParameterLookupKey key && Equals(key); } internal void ConfigureProperties() @@ -1059,7 +1032,7 @@ internal void ConfigureProperties() foreach (JsonPropertyInfo property in properties) { - Debug.Assert(property.ParentTypeInfo == this); + Debug.Assert(property.DeclaringTypeInfo == this); if (property.IsExtensionData) { @@ -1115,6 +1088,45 @@ internal void ConfigureProperties() : JsonUnmappedMemberHandling.Skip); } + internal void PopulateParameterInfoValues(JsonParameterInfoValues[] parameterInfoValues) + { + if (parameterInfoValues.Length == 0) + { + return; + } + + Dictionary parameterIndex = new(parameterInfoValues.Length); + foreach (JsonParameterInfoValues parameterInfoValue in parameterInfoValues) + { + ParameterLookupKey paramKey = new(parameterInfoValue.ParameterType, parameterInfoValue.Name); + parameterIndex.TryAdd(paramKey, parameterInfoValue); // Ignore conflicts since they are reported at serialization time. + } + + ParameterCount = parameterInfoValues.Length; + _parameterInfoValuesIndex = parameterIndex; + } + + internal JsonParameterInfo? CreateMatchingParameterInfo(JsonPropertyInfo propertyInfo) + { + Debug.Assert( + !Converter.ConstructorIsParameterized || _parameterInfoValuesIndex is not null, + "Metadata with parameterized constructors must have populated parameter info metadata."); + + if (_parameterInfoValuesIndex is not { } index) + { + return null; + } + + string propertyName = propertyInfo.MemberName ?? propertyInfo.Name; + ParameterLookupKey propKey = new(propertyInfo.PropertyType, propertyName); + if (index.TryGetValue(propKey, out JsonParameterInfoValues? matchingParameterInfoValues)) + { + return propertyInfo.CreateJsonParameterInfo(matchingParameterInfoValues); + } + + return null; + } + internal void ConfigureConstructorParameters() { Debug.Assert(Kind == JsonTypeInfoKind.Object); @@ -1122,68 +1134,41 @@ internal void ConfigureConstructorParameters() Debug.Assert(PropertyCache is not null); Debug.Assert(ParameterCache is null); - JsonParameterInfoValues[] jsonParameters = ParameterInfoValues ?? Array.Empty(); - var parameterCache = new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, jsonParameters.Length); - - // Cache the lookup from object property name to JsonPropertyInfo using a case-insensitive comparer. - // Case-insensitive is used to support both camel-cased parameter names and exact matches when C# - // record types or anonymous types are used. - // The property name key does not use [JsonPropertyName] or PropertyNamingPolicy since we only bind - // the parameter name to the object property name and do not use the JSON version of the name here. - var nameLookup = new Dictionary(PropertyCache.Count); + List parameterCache = new(ParameterCount); + Dictionary parameterIndex = new(ParameterCount); foreach (KeyValuePair kvp in PropertyCache.List) { - JsonPropertyInfo jsonProperty = kvp.Value; - string propertyName = jsonProperty.MemberName ?? jsonProperty.Name; - - ParameterLookupKey key = new(propertyName, jsonProperty.PropertyType); - ParameterLookupValue value = new(jsonProperty); - - if (!nameLookup.TryAdd(key, value)) + JsonPropertyInfo propertyInfo = kvp.Value; + JsonParameterInfo? parameterInfo = propertyInfo.ParameterInfo; + if (parameterInfo is null) { - // More than one property has the same case-insensitive name and Type. - // Remember so we can throw a nice exception if this property is used as a parameter name. - ParameterLookupValue existing = nameLookup[key]; - existing.DuplicateName = propertyName; + continue; } - } - - foreach (JsonParameterInfoValues parameterInfo in jsonParameters) - { - ParameterLookupKey paramToCheck = new(parameterInfo.Name, parameterInfo.ParameterType); - if (nameLookup.TryGetValue(paramToCheck, out ParameterLookupValue? matchingEntry)) + ParameterLookupKey paramKey = new(propertyInfo.PropertyType, propertyInfo.Name); + if (parameterIndex.TryGetValue(paramKey, out JsonParameterInfo? confictingParameterInfo)) { - if (matchingEntry.DuplicateName != null) - { - // Multiple object properties cannot bind to the same constructor parameter. - ThrowHelper.ThrowInvalidOperationException_MultiplePropertiesBindToConstructorParameters( - Type, - parameterInfo.Name!, - matchingEntry.JsonPropertyInfo.Name, - matchingEntry.DuplicateName); - } + // Multiple object properties cannot bind to the same constructor parameter. + ThrowHelper.ThrowInvalidOperationException_MultiplePropertiesBindToConstructorParameters( + Type, + propertyInfo.Name, + propertyInfo.Name, + confictingParameterInfo.MatchingProperty.Name); + } - //if (matchingEntry.JsonPropertyInfo.DisallowNullReads != parameterInfo) + parameterIndex.Add(paramKey, parameterInfo); + parameterCache.Add(parameterInfo); + } - Debug.Assert(matchingEntry.JsonPropertyInfo != null); - JsonPropertyInfo jsonPropertyInfo = matchingEntry.JsonPropertyInfo; - JsonParameterInfo jsonParameterInfo = jsonPropertyInfo.CreateJsonParameterInfo(parameterInfo); - parameterCache.Add(jsonPropertyInfo.Name, jsonParameterInfo); - } - // It is invalid for the extension data property to bind to a constructor argument. - else if (ExtensionDataProperty != null && - StringComparer.OrdinalIgnoreCase.Equals(paramToCheck.Name, ExtensionDataProperty.Name)) - { - Debug.Assert(ExtensionDataProperty.MemberName != null, "Custom property info cannot be data extension property"); - ThrowHelper.ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(ExtensionDataProperty.MemberName, ExtensionDataProperty); - } + if (ExtensionDataProperty is { ParameterInfo: not null }) + { + Debug.Assert(ExtensionDataProperty.MemberName != null, "Custom property info cannot be data extension property"); + ThrowHelper.ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(ExtensionDataProperty.MemberName, ExtensionDataProperty); } - ParameterCount = jsonParameters.Length; ParameterCache = parameterCache; - ParameterInfoValues = null; + _parameterInfoValuesIndex = null; } internal static void ValidateType(Type type) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ParameterRef.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ParameterRef.cs deleted file mode 100644 index 7e81bed6815ca3..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ParameterRef.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Text.Json.Serialization.Metadata -{ - internal readonly struct ParameterRef - { - public ParameterRef(ulong key, JsonParameterInfo info, byte[] nameFromJson) - { - Key = key; - Info = info; - NameFromJson = nameFromJson; - } - - public readonly ulong Key; - - public readonly JsonParameterInfo Info; - - // NameFromJson may be different than Info.NameAsUtf8Bytes when case insensitive is enabled. - public readonly byte[] NameFromJson; - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 85bddceee8c252..71eb40cce1bea9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -64,9 +64,21 @@ public static void ThrowInvalidOperationException_DeserializeUnableToAssignNull( } [DoesNotReturn] - public static void ThrowJsonException_NullabilityDoesNotAllowNull(string propertyName, Type propertyType) + public static void ThrowJsonException_PropertyGetterDisallowNull(string propertyName, Type declaringType) { - throw new JsonException(SR.Format(SR.NullableAnnotationsDontAllowNull, propertyName, propertyType)) { AppendPathInformation = true }; + throw new JsonException(SR.Format(SR.PropertyGetterDisallowNull, propertyName, declaringType)) { AppendPathInformation = true }; + } + + [DoesNotReturn] + public static void ThrowJsonException_PropertySetterDisallowNull(string propertyName, Type declaringType) + { + throw new JsonException(SR.Format(SR.PropertySetterDisallowNull, propertyName, declaringType)) { AppendPathInformation = true }; + } + + [DoesNotReturn] + public static void ThrowJsonException_ConstructorParameterDisallowNull(string parameterName, Type declaringType) + { + throw new JsonException(SR.Format(SR.ConstructorParameterDisallowNull, parameterName, declaringType)) { AppendPathInformation = true }; } [DoesNotReturn] @@ -764,8 +776,8 @@ public static void ThrowInvalidOperationException_MetadataReferenceOfTypeCannotB [DoesNotReturn] public static void ThrowInvalidOperationException_JsonPropertyInfoIsBoundToDifferentJsonTypeInfo(JsonPropertyInfo propertyInfo) { - Debug.Assert(propertyInfo.ParentTypeInfo != null, "We should not throw this exception when ParentTypeInfo is null"); - throw new InvalidOperationException(SR.Format(SR.JsonPropertyInfoBoundToDifferentParent, propertyInfo.Name, propertyInfo.ParentTypeInfo.Type.FullName)); + Debug.Assert(propertyInfo.DeclaringTypeInfo != null, "We should not throw this exception when ParentTypeInfo is null"); + throw new InvalidOperationException(SR.Format(SR.JsonPropertyInfoBoundToDifferentParent, propertyInfo.Name, propertyInfo.DeclaringTypeInfo.Type.FullName)); } [DoesNotReturn] diff --git a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs index b4680ed0a5306a..337216b4cc7a65 100644 --- a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs @@ -1,309 +1,388 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable enable - -using System.Diagnostics; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using System.Text.Json.Serialization.Metadata; using Xunit; +using System.Linq; + +#nullable enable annotations namespace System.Text.Json.Serialization.Tests { public abstract class NullableAnnotationsTests : SerializerTests { + private static readonly JsonSerializerOptions s_optionsWithIgnoredNullability = new JsonSerializerOptions { IgnoreNullValues = true }; + private static readonly JsonSerializerOptions s_optionsWithEnforcedNullability = new JsonSerializerOptions { IgnoreNullValues = false }; + protected NullableAnnotationsTests(JsonSerializerWrapper serializerUnderTest) : base(serializerUnderTest) { } - [Fact] - public void IgnoreNullableAnnotationsIsDisabledByDefault() - => Assert.False(new JsonSerializerOptions().IgnoreNullableAnnotations); - - #region Read into Not Nullable - [Fact] - public async Task ReadNullIntoNotNullablePropertyThrows() + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertyGetter))] + public async Task WriteNullFromNotNullablePropertyGetter_EnforcedNullability_ThrowsJsonException(Type type, string propertyName) { - string json = """{"Property":null}"""; + object value = Activator.CreateInstance(type)!; + + JsonException ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, type, s_optionsWithEnforcedNullability)); - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); - Assert.Contains("Property", ex.Message); - Assert.Contains(typeof(NotNullablePropertyClass).ToString(), ex.Message); + Assert.Contains(propertyName, ex.Message); + Assert.Contains(type.Name.Split('`')[0], ex.Message); } - [Fact] - public async Task ReadEmptyObjectIntoTypeWithNotNullableProperty() + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertyGetter))] + public async Task WriteNullFromNotNullablePropertyGetter_IgnoredNullability_Succeeds(Type type, string _) { - string json = "{}"; - - NotNullablePropertyClass result = await Serializer.DeserializeWrapper(json); - Assert.Null(result.Property); + object value = Activator.CreateInstance(type)!; + string json = await Serializer.SerializeWrapper(value, type, s_optionsWithIgnoredNullability); + Assert.NotNull(json); } - [Fact] - public async Task ReadNullIntoNotNullablePropertyWithIgnoreNullableAnnotations() + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertyGetter))] + public async Task WriteNullFromNotNullablePropertyGetter_EnforcedNullability_DisabledFlag_Succeeds(Type type, string propertyName) { - string json = """{"Property":null}"""; + object value = Activator.CreateInstance(type)!; + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type, s_optionsWithEnforcedNullability, mutable: true); + JsonPropertyInfo propertyInfo = typeInfo.Properties.FirstOrDefault(p => p.Name == propertyName); + + Assert.NotNull(propertyInfo); + Assert.True(propertyInfo.DisallowNullWrites); - JsonSerializerOptions options = new() { IgnoreNullableAnnotations = true }; - NotNullablePropertyClass result = await Serializer.DeserializeWrapper(json, options); - Assert.Null(result.Property); + propertyInfo.DisallowNullWrites = false; + Assert.False(propertyInfo.DisallowNullWrites); + + string json = await Serializer.SerializeWrapper(value, typeInfo); + Assert.NotNull(json); } - [Fact] - public async Task ReadNullIntoReadonlyPropertySkipped() + public static IEnumerable GetTypesWithNonNullablePropertyGetter() { - string json = """{"ReadonlyProperty":null}"""; + yield return Wrap(typeof(NotNullablePropertyClass), nameof(NotNullablePropertyClass.Property)); + yield return Wrap(typeof(NotNullableFieldClass), nameof(NotNullableFieldClass.Field)); + yield return Wrap(typeof(NotNullablePropertyWithConverterClass), nameof(NotNullablePropertyWithConverterClass.PropertyWithConverter)); + yield return Wrap(typeof(NotNullPropertyClass), nameof(NotNullPropertyClass.Property)); + yield return Wrap(typeof(NotNullPropertyClass), nameof(NotNullPropertyClass.Property)); + yield return Wrap(typeof(NotNullablePropertyParameterizedCtorClass), nameof(NotNullablePropertyParameterizedCtorClass.CtorProperty)); + yield return Wrap(typeof(NotNullablePropertiesLargeParameterizedCtorClass), nameof(NotNullablePropertiesLargeParameterizedCtorClass.CtorProperty2)); + yield return Wrap(typeof(NotNullableSpecialTypePropertiesClass), nameof(NotNullableSpecialTypePropertiesClass.JsonDocument)); + yield return Wrap(typeof(NullableObliviousConstructorParameter), nameof(NullableObliviousConstructorParameter.Property)); + yield return Wrap(typeof(NotNullGenericPropertyClass), nameof(NotNullGenericPropertyClass.Property)); - NotNullableReadonlyPropertyClass result = await Serializer.DeserializeWrapper(json); - Assert.Null(result.ReadonlyProperty); + static object[] Wrap(Type type, string propertyName) => [type, propertyName]; } - [Fact] - public async Task ReadNullIntoNotNullableFieldThrows() + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertyGetter))] + public async Task WriteNullFromNullablePropertyGetter_EnforcedNullability_Succeeds(Type type, string _) { - string json = """{"Field":null}"""; + object value = Activator.CreateInstance(type)!; + string json = await Serializer.SerializeWrapper(value, type, s_optionsWithEnforcedNullability); + Assert.NotNull(json); + } - JsonSerializerOptions options = new() { IncludeFields = true }; - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, options)); - Assert.Contains("Field", ex.Message); - Assert.Contains(typeof(NotNullableFieldClass).ToString(), ex.Message); + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertyGetter))] + public async Task WriteNullFromNullablePropertyGetter_IgnoredNullability_Succeeds(Type type, string _) + { + object value = Activator.CreateInstance(type)!; + string json = await Serializer.SerializeWrapper(value, type, s_optionsWithIgnoredNullability); + Assert.NotNull(json); } - [Fact] - public async Task ReadNullIntoNotNullableSpecialTypeProperties() + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertyGetter))] + public async Task WriteNullFromNullablePropertyGetter_EnforcedNullability_EnabledFlag_ThrowsJsonException(Type type, string propertyName) { - string json = """ - { - "JsonDocument":null, - "MemoryByte":null, - "ReadOnlyMemoryByte":null, - "MemoryOfT":null, - "ReadOnlyMemoryOfT":null - } - """; + object value = Activator.CreateInstance(type)!; + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type, s_optionsWithEnforcedNullability, mutable: true); + JsonPropertyInfo propertyInfo = typeInfo.Properties.FirstOrDefault(p => p.Name == propertyName); - NotNullableSpecialTypePropertiesClass result = await Serializer.DeserializeWrapper(json); - Assert.Equal(JsonValueKind.Null, result.JsonDocument?.RootElement.ValueKind); - Assert.Equal(default, result.MemoryByte); - Assert.Equal(default, result.ReadOnlyMemoryByte); - Assert.Equal(default, result.MemoryOfT); - Assert.Equal(default, result.ReadOnlyMemoryOfT); + Assert.NotNull(propertyInfo); + Assert.False(propertyInfo.DisallowNullWrites); + + propertyInfo.DisallowNullWrites = true; + Assert.True(propertyInfo.DisallowNullWrites); + + JsonException ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, typeInfo)); + + Assert.Contains(propertyName, ex.Message); + Assert.Contains(type.Name.Split('`')[0], ex.Message); } - [Fact] - public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverter() + public static IEnumerable GetTypesWithNullablePropertyGetter() { - string json = """{"PropertyWithHandleNullConverter":null}"""; + yield return Wrap(typeof(NullablePropertyClass), nameof(NullablePropertyClass.Property)); + yield return Wrap(typeof(NullableFieldClass), nameof(NullableFieldClass.Field)); + yield return Wrap(typeof(MaybeNullPropertyClass), nameof(MaybeNullPropertyClass.Property)); + yield return Wrap(typeof(MaybeNullPropertyClass), nameof(MaybeNullPropertyClass.Property)); + yield return Wrap(typeof(NullableObliviousPropertyClass), nameof(NullableObliviousPropertyClass.Property)); + yield return Wrap(typeof(GenericPropertyClass), nameof(GenericPropertyClass.Property)); + yield return Wrap(typeof(NullableGenericPropertyClass), nameof(NullableGenericPropertyClass.Property)); - NotNullablePropertyWithHandleNullConverterClass result = await Serializer.DeserializeWrapper(json); - Assert.NotNull(result.PropertyWithHandleNullConverter); + static object[] Wrap(Type type, string propertyName) => [type, propertyName]; } - [Fact] - public async Task ReadNullIntoNotNullablePropertyWithConverterThrows() + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertySetter))] + public async Task ReadNullIntoNotNullablePropertySetter_EnforcedNullability_ThrowsJsonException(Type type, string propertyName) { - string json = """{"PropertyWithConverter":null}"""; + string json = $$"""{"{{propertyName}}":null}"""; - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); - Assert.Contains("PropertyWithConverter", ex.Message); - Assert.Contains(typeof(NotNullablePropertyWithConverterClass).ToString(), ex.Message); + JsonException ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, type, s_optionsWithEnforcedNullability)); + + Assert.Contains(propertyName, ex.Message); + Assert.Contains(type.Name.Split('`')[0], ex.Message); } - [Fact] - public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterThrows() + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertySetter))] + public async Task ReadNullIntoNotNullablePropertySetter_IgnoredNullability_Succeeds(Type type, string propertyName) { - string json = """{"PropertyWithAlwaysNullConverter":"42"}"""; + string json = $$"""{"{{propertyName}}":null}"""; + object? result = await Serializer.DeserializeWrapper(json, type, s_optionsWithIgnoredNullability); + Assert.IsType(type, result); + } - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); - Assert.Contains("PropertyWithAlwaysNullConverter", ex.Message); - Assert.Contains(typeof(NotNullablePropertyWithAlwaysNullConverterClass).ToString(), ex.Message); + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertySetter))] + public async Task ReadEmptyObjectIntoNotNullablePropertySetter_Succeeds(Type type, string _) + { + object result = await Serializer.DeserializeWrapper("{}", type, s_optionsWithEnforcedNullability); + Assert.IsType(type, result); } - [Fact] - public async Task ReadNullIntoNotNullablePropertyWithParameterizedCtorThrows() + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertySetter))] + public async Task ReadNullIntoNotNullablePropertySetter_EnforcedNullability_DisabledFlag_Succeeds(Type type, string propertyName) { - string json = """{"CtorProperty":null}"""; + string json = $$"""{"{{propertyName}}":null}"""; + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type, s_optionsWithEnforcedNullability, mutable: true); + JsonPropertyInfo propertyInfo = typeInfo.Properties.FirstOrDefault(p => p.Name == propertyName); + + Assert.NotNull(propertyInfo); + Assert.True(propertyInfo.DisallowNullReads); - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); - Assert.Contains("CtorProperty", ex.Message); - Assert.Contains(typeof(NotNullablePropertyParameterizedCtorClass).ToString(), ex.Message); + propertyInfo.DisallowNullReads = false; + Assert.False(propertyInfo.DisallowNullReads); + + object? result = await Serializer.DeserializeWrapper(json, typeInfo); + Assert.IsType(type, result); } - [Fact] - public async Task ReadEmptyObjectIntoTypeWithNotNullablePropertyWithParameterizedCtor() + public static IEnumerable GetTypesWithNonNullablePropertySetter() { - string json = "{}"; + yield return Wrap(typeof(NotNullablePropertyClass), nameof(NotNullablePropertyClass.Property)); + yield return Wrap(typeof(NotNullableFieldClass), nameof(NotNullableFieldClass.Field)); + yield return Wrap(typeof(NotNullablePropertyWithConverterClass), nameof(NotNullablePropertyWithConverterClass.PropertyWithConverter)); + yield return Wrap(typeof(DisallowNullPropertyClass), nameof(DisallowNullPropertyClass.Property)); + yield return Wrap(typeof(DisallowNullPropertyClass), nameof(DisallowNullPropertyClass.Property)); + yield return Wrap(typeof(NotNullablePropertyParameterizedCtorClass), nameof(NotNullablePropertyParameterizedCtorClass.CtorProperty)); + yield return Wrap(typeof(NotNullablePropertiesLargeParameterizedCtorClass), nameof(NotNullablePropertiesLargeParameterizedCtorClass.CtorProperty2)); + yield return Wrap(typeof(NotNullGenericPropertyClass), nameof(NotNullGenericPropertyClass.Property)); + yield return Wrap(typeof(DisallowNullConstructorParameter), nameof(DisallowNullConstructorParameter.Property)); + yield return Wrap(typeof(DisallowNullConstructorParameter), nameof(DisallowNullConstructorParameter.Property)); + yield return Wrap(typeof(NotNullGenericConstructorParameter), nameof(NotNullGenericConstructorParameter.Property)); - NotNullablePropertyParameterizedCtorClass result = await Serializer.DeserializeWrapper(json); - Assert.Null(result.CtorProperty); + static object[] Wrap(Type type, string propertyName) => [type, propertyName]; } - [Fact] - public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverterWithParameterizedCtor() + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertySetter))] + public async Task ReadNullIntoNullablePropertySetter_EnforcedNullability_Succeeds(Type type, string propertyName) { - string json = """{"CtorPropertyWithHandleNullConverter":null}"""; + string json = $$"""{"{{propertyName}}":null}"""; + object? result = await Serializer.DeserializeWrapper(json, type, s_optionsWithEnforcedNullability); + Assert.IsType(type, result); + } - var result = await Serializer.DeserializeWrapper(json); - Assert.NotNull(result.CtorPropertyWithHandleNullConverter); + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertySetter))] + public async Task ReadNullIntoNullablePropertySetter_IgnoredNullability_Succeeds(Type type, string propertyName) + { + string json = $$"""{"{{propertyName}}":null}"""; + object? result = await Serializer.DeserializeWrapper(json, type, s_optionsWithIgnoredNullability); + Assert.IsType(type, result); } - [Fact] - public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterWithParameterizedCtorThrows() + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertySetter))] + public async Task ReadNullIntoNullablePropertySetter_EnforcedNullability_EnabledFlag_ThrowsJsonException(Type type, string propertyName) { - string json = """{"CtorPropertyWithAlwaysNullConverter":"42"}"""; + string json = $$"""{"{{propertyName}}":null}"""; + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type, s_optionsWithEnforcedNullability, mutable: true); + JsonPropertyInfo propertyInfo = typeInfo.Properties.FirstOrDefault(p => p.Name == propertyName); - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); - Assert.Contains("CtorPropertyWithAlwaysNullConverter", ex.Message); - Assert.Contains(typeof(NotNullablePropertyWithAlwaysNullConverterParameterizedCtorClass).ToString(), ex.Message); + Assert.NotNull(propertyInfo); + Assert.False(propertyInfo.DisallowNullReads); + + propertyInfo.DisallowNullReads = true; + Assert.True(propertyInfo.DisallowNullReads); + + JsonException ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, typeInfo)); + + Assert.Contains(propertyName, ex.Message); + Assert.Contains(type.Name.Split('`')[0], ex.Message); } - [Fact] - public async Task ReadNullIntoNotNullablePropertyWithLargeParameterizedCtorThrows() + public static IEnumerable GetTypesWithNullablePropertySetter() { - string json = """{"CtorProperty2":null}"""; + yield return Wrap(typeof(NullablePropertyClass), nameof(NullablePropertyClass.Property)); + yield return Wrap(typeof(NullableFieldClass), nameof(NullableFieldClass.Field)); + yield return Wrap(typeof(AllowNullPropertyClass), nameof(AllowNullPropertyClass.Property)); + yield return Wrap(typeof(AllowNullPropertyClass), nameof(AllowNullPropertyClass.Property)); + yield return Wrap(typeof(NullableObliviousPropertyClass), nameof(NullableObliviousPropertyClass.Property)); + yield return Wrap(typeof(NullableObliviousConstructorParameter), nameof(NullableObliviousConstructorParameter.Property)); + yield return Wrap(typeof(GenericPropertyClass), nameof(GenericPropertyClass.Property)); + yield return Wrap(typeof(NullableGenericPropertyClass), nameof(NullableGenericPropertyClass.Property)); + yield return Wrap(typeof(AllowNullConstructorParameter), nameof(AllowNullConstructorParameter.Property)); + yield return Wrap(typeof(AllowNullConstructorParameter), nameof(AllowNullConstructorParameter.Property)); + yield return Wrap(typeof(GenericConstructorParameter), nameof(GenericConstructorParameter.Property)); + yield return Wrap(typeof(NullableGenericConstructorParameter), nameof(NullableGenericConstructorParameter.Property)); - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); - Assert.Contains("CtorProperty2", ex.Message); - Assert.Contains(typeof(NotNullablePropertiesLargeParameterizedCtorClass).ToString(), ex.Message); + static object[] Wrap(Type type, string propertyName) => [type, propertyName]; } [Fact] - public async Task ReadEmptyObjectIntoTypeWithNotNullablePropertyWithLargeParameterizedCtor() + public async Task ReadNullIntoReadonlyProperty_Succeeds() { - string json = "{}"; - - NotNullablePropertiesLargeParameterizedCtorClass result = await Serializer.DeserializeWrapper(json); - Assert.Null(result.CtorProperty0); - Assert.Null(result.CtorProperty1); - Assert.Null(result.CtorProperty2); - Assert.Null(result.CtorProperty3); - Assert.Null(result.CtorProperty4); + string json = """{"ReadonlyProperty":null}"""; + NotNullableReadonlyPropertyClass result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.Null(result.ReadonlyProperty); } [Fact] - public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverterWithLargeParameterizedCtor() + public async Task ReadNullIntoNotNullableSpecialTypeProperties() { - string json = """{"LargeCtorPropertyWithHandleNullConverter":null}"""; + string json = """ + { + "JsonDocument":null, + "MemoryByte":null, + "ReadOnlyMemoryByte":null, + "MemoryOfT":null, + "ReadOnlyMemoryOfT":null + } + """; - var result = await Serializer.DeserializeWrapper(json); - Assert.NotNull(result.LargeCtorPropertyWithHandleNullConverter); + NotNullableSpecialTypePropertiesClass result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.Equal(JsonValueKind.Null, result.JsonDocument?.RootElement.ValueKind); + Assert.Equal(default, result.MemoryByte); + Assert.Equal(default, result.ReadOnlyMemoryByte); + Assert.Equal(default, result.MemoryOfT); + Assert.Equal(default, result.ReadOnlyMemoryOfT); } [Fact] - public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterWithLargeParameterizedCtorThrows() + public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverter() { - string json = """{"LargeCtorPropertyWithAlwaysNullConverter":"42"}"""; + string json = """{"PropertyWithHandleNullConverter":null}"""; - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); - Assert.Contains("LargeCtorPropertyWithAlwaysNullConverter", ex.Message); - Assert.Contains(typeof(NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass).ToString(), ex.Message); + NotNullablePropertyWithHandleNullConverterClass result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.NotNull(result.PropertyWithHandleNullConverter); } [Fact] - public async Task ReadNullIntoDisallowNullPropertyThrows() + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterThrows() { - string json = """{"Property":null}"""; + string json = """{"PropertyWithAlwaysNullConverter":"42"}"""; - Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); - Assert.Contains("Property", ex.Message); - Assert.Contains(typeof(DisallowNullPropertyClass).ToString(), ex.Message); + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability)); + Assert.Contains("PropertyWithAlwaysNullConverter", ex.Message); + Assert.Contains(nameof(NotNullablePropertyWithAlwaysNullConverterClass), ex.Message); } - #endregion Read into Not Nullable - #region Write from Not Nullable [Fact] - public async Task WriteNullFromNotNullablePropertyThrows() + public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverterWithParameterizedCtor() { - NotNullablePropertyClass obj = new() { Property = null! }; + string json = """{"CtorPropertyWithHandleNullConverter":null}"""; - Exception ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(obj)); - Assert.Contains("Property", ex.Message); - Assert.Contains(typeof(NotNullablePropertyClass).ToString(), ex.Message); + var result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.NotNull(result.CtorPropertyWithHandleNullConverter); } [Fact] - public async Task WriteNullFromNotNullableFieldThrows() + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterWithParameterizedCtorThrows() { - NotNullableFieldClass obj = new() { Field = null! }; + string json = """{"CtorPropertyWithAlwaysNullConverter":"42"}"""; - JsonSerializerOptions options = new() { IncludeFields = true }; - Exception ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(obj, options)); - Assert.Contains("Field", ex.Message); - Assert.Contains(typeof(NotNullableFieldClass).ToString(), ex.Message); + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability)); + Assert.Contains("CtorPropertyWithAlwaysNullConverter", ex.Message); + Assert.Contains(nameof(NotNullablePropertyWithAlwaysNullConverterParameterizedCtorClass), ex.Message); } [Fact] - public async Task WriteNullFromNotNullableJsonDocumentPropertyThrows() + public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverterWithLargeParameterizedCtor() { - NotNullableSpecialTypePropertiesClass obj = new() { JsonDocument = null! }; + string json = """{"LargeCtorPropertyWithHandleNullConverter":null}"""; - // Unlike Deserialize, JsonDocument should throw on Serialize because it would write null. - Exception ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(obj)); - Assert.Contains("JsonDocument", ex.Message); - Assert.Contains(typeof(NotNullableSpecialTypePropertiesClass).ToString(), ex.Message); + var result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.NotNull(result.LargeCtorPropertyWithHandleNullConverter); } - //[Fact] - - #endregion Write from Not Nullable - - #region Read into Nullable [Fact] - public async Task ReadNullIntoNullableProperty() + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterWithLargeParameterizedCtorThrows() { - string json = """{"Property":null}"""; + string json = """{"LargeCtorPropertyWithAlwaysNullConverter":"42"}"""; - NullablePropertyClass result = await Serializer.DeserializeWrapper(json); - Assert.Null(result.Property); + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability)); + Assert.Contains("LargeCtorPropertyWithAlwaysNullConverter", ex.Message); + Assert.Contains(nameof(NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass), ex.Message); } [Fact] - public async Task ReadEmptyObjectIntoTypeWithNullableProperty() + public async Task WriteNotNullPropertiesWithNullIgnoreConditions_Succeeds() { - string json = "{}"; - - NullablePropertyClass result = await Serializer.DeserializeWrapper(json); - Assert.Null(result.Property); + // JsonIgnoreCondition.WhenWritingNull/Default takes precedence over DisallowNullWrites. + var value = new NotNullablePropertyWithIgnoreConditions { WhenWritingNull = null!, WhenWritingDefault = null! }; + string json = await Serializer.SerializeWrapper(value, s_optionsWithEnforcedNullability); + Assert.Equal("{}", json); } - public async Task ReadNullIntoNullableField() + public class NotNullablePropertyClass { - string json = """{"Field":null}"""; + public string Property { get; set; } + } - NullableFieldClass result = await Serializer.DeserializeWrapper(json); - Assert.Null(result.Field); + public class NullableObliviousPropertyClass + { +#nullable disable annotations + public string Property { get; set; } +#nullable restore annotations } - #endregion - // Need to use **public** types; otherwise, nullability would be trimmed out. - #region Not Nullable classes - public class NotNullablePropertyClass + public class NullableObliviousConstructorParameter { -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. public string Property { get; set; } -#pragma warning restore CS8618 + + public NullableObliviousConstructorParameter() { } + + [JsonConstructor] +#nullable disable annotations + public NullableObliviousConstructorParameter(string property) +#nullable restore annotations + { + Property = property; + } } public class NotNullableReadonlyPropertyClass { -#pragma warning disable CS8618 public string ReadonlyProperty { get; } -#pragma warning restore CS8618 } public class NotNullableFieldClass { -#pragma warning disable CS8618 + [JsonInclude] public string Field; -#pragma warning restore CS8618 } public class NotNullableSpecialTypePropertiesClass { // types with internal converter that handles null. -#pragma warning disable CS8618 public JsonDocument JsonDocument { get; set; } -#pragma warning restore CS8618 public Memory MemoryByte { get; set; } public ReadOnlyMemory ReadOnlyMemoryByte { get; set; } public Memory MemoryOfT { get; set; } @@ -312,26 +391,20 @@ public class NotNullableSpecialTypePropertiesClass public class NotNullablePropertyWithHandleNullConverterClass { -#pragma warning disable CS8618 [JsonConverter(typeof(MyHandleNullConverter))] public MyClass PropertyWithHandleNullConverter { get; set; } -#pragma warning restore CS8618 } public class NotNullablePropertyWithAlwaysNullConverterClass { -#pragma warning disable CS8618 [JsonConverter(typeof(MyAlwaysNullConverter))] public MyClass PropertyWithAlwaysNullConverter { get; set; } -#pragma warning restore CS8618 } public class NotNullablePropertyWithConverterClass { -#pragma warning disable CS8618 [JsonConverter(typeof(MyConverter))] public MyClass PropertyWithConverter { get; set; } -#pragma warning restore CS8618 } public class MyClass { } @@ -373,6 +446,8 @@ public class NotNullablePropertyParameterizedCtorClass { public string CtorProperty { get; } + public NotNullablePropertyParameterizedCtorClass() { } + [JsonConstructor] public NotNullablePropertyParameterizedCtorClass(string ctorProperty) => CtorProperty = ctorProperty; } @@ -382,6 +457,8 @@ public class NotNullablePropertyWithHandleNullConverterParameterizedCtorClass [JsonConverter(typeof(MyHandleNullConverter))] public MyClass CtorPropertyWithHandleNullConverter { get; } + public NotNullablePropertyWithHandleNullConverterParameterizedCtorClass() { } + [JsonConstructor] public NotNullablePropertyWithHandleNullConverterParameterizedCtorClass(MyClass ctorPropertyWithHandleNullConverter) => CtorPropertyWithHandleNullConverter = ctorPropertyWithHandleNullConverter; } @@ -403,6 +480,15 @@ public class NotNullablePropertiesLargeParameterizedCtorClass public string CtorProperty3 { get; } public string CtorProperty4 { get; } + public NotNullablePropertiesLargeParameterizedCtorClass() + { + CtorProperty0 = "str"; + CtorProperty1 = "str"; + // CtorProperty2 intentionally left uninitialized. + CtorProperty3 = "str"; + CtorProperty4 = "str"; + } + [JsonConstructor] public NotNullablePropertiesLargeParameterizedCtorClass(string ctorProperty0, string ctorProperty1, string ctorProperty2, string ctorProperty3, string ctorProperty4) { @@ -455,17 +541,170 @@ public NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass( CtorProperty4 = ctorProperty4; } } - #endregion Not Nullable classes - #region [DisallowNull] classes + public class NotNullPropertyClass + { + [NotNull] + public string? Property { get; set; } + } + + public class MaybeNullPropertyClass + { + [MaybeNull] + public string Property { get; set; } + } + + public class AllowNullPropertyClass + { + [AllowNull] + public string Property { get; set; } + } + public class DisallowNullPropertyClass { [DisallowNull] public string? Property { get; set; } } - #endregion [DisallowNull] classes - #region Nullable classes + public class AllowNullConstructorParameter + { + public string? Property { get; set; } + + public AllowNullConstructorParameter() { } + + [JsonConstructor] + public AllowNullConstructorParameter([AllowNull] string property) + { + Property = property; + } + } + + public class DisallowNullConstructorParameter + { + public string Property { get; set; } + + public DisallowNullConstructorParameter() { } + + [JsonConstructor] + public DisallowNullConstructorParameter([DisallowNull] string? property) + { + Property = property; + } + } + + public class NotNullPropertyClass + { + [NotNull] + public T? Property { get; set; } + } + + public class MaybeNullPropertyClass + { + [MaybeNull] + public T Property { get; set; } + } + + public class AllowNullPropertyClass + { + [AllowNull] + public T Property { get; set; } + } + + public class DisallowNullPropertyClass + { + [DisallowNull] + public T? Property { get; set; } + } + + public class AllowNullConstructorParameter + { + public T? Property { get; set; } + + public AllowNullConstructorParameter() { } + + [JsonConstructor] + public AllowNullConstructorParameter([AllowNull] T property) + { + Property = property; + } + } + + public class DisallowNullConstructorParameter + { + public T Property { get; set; } + + public DisallowNullConstructorParameter() { } + + [JsonConstructor] + public DisallowNullConstructorParameter([DisallowNull] T? property) + { + Property = property; + } + } + + public class GenericPropertyClass + { + public T Property { get; set; } + } + + public class NullableGenericPropertyClass + { + public T? Property { get; set; } + } + + public class NotNullGenericPropertyClass where T : notnull + { + public T Property { get; set; } + } + + public class GenericConstructorParameter + { + public T Property { get; set; } + + public GenericConstructorParameter() { } + + [JsonConstructor] + public GenericConstructorParameter(T property) + { + Property = property; + } + } + + public class NullableGenericConstructorParameter + { + public T? Property { get; set; } + + public NullableGenericConstructorParameter() { } + + [JsonConstructor] + public NullableGenericConstructorParameter(T? property) + { + Property = property; + } + } + + public class NotNullGenericConstructorParameter where T : notnull + { + public T Property { get; set; } + + public NotNullGenericConstructorParameter() { } + + [JsonConstructor] + public NotNullGenericConstructorParameter(T property) + { + Property = property; + } + } + + public class NotNullablePropertyWithIgnoreConditions + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string WhenWritingNull { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string WhenWritingDefault { get; set; } + } + public class NullablePropertyClass { public string? Property { get; set; } @@ -473,8 +712,8 @@ public class NullablePropertyClass public class NullableFieldClass { + [JsonInclude] public string? Field; } - #endregion Nullable classes } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSerializerWrapper.SourceGen.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSerializerWrapper.SourceGen.cs index 6a8188330d4189..23aa7621fdde87 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSerializerWrapper.SourceGen.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSerializerWrapper.SourceGen.cs @@ -50,6 +50,8 @@ public override Task DeserializeWrapper(string json, JsonTypeInfo jsonTy public override Task DeserializeWrapper(string json, Type type, JsonSerializerContext context) => Task.FromResult(JsonSerializer.Deserialize(json, type, context)); + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions? options = null, bool mutable = false) => base.GetTypeInfo(type, GetOptions(options), mutable); + private JsonSerializerOptions GetOptions(JsonSerializerOptions? options = null) { if (options is null) @@ -118,6 +120,8 @@ public override Task SerializeWrapper(Stream stream, object value, JsonTypeInfo public override Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerContext context) => JsonSerializer.SerializeAsync(stream, value, inputType, context); + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions? options = null, bool mutable = false) => base.GetTypeInfo(type, GetOptions(options), mutable); + private JsonSerializerOptions GetOptions(JsonSerializerOptions? options = null) { if (options is null) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs index 73de089ec628a0..07b8cefd11f226 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -//#nullable enable - using System.Text.Json.Serialization; using System.Text.Json.Serialization.Tests; @@ -25,7 +23,7 @@ public abstract partial class NullableAnnotationsTests_Metadata : NullableAnnota protected NullableAnnotationsTests_Metadata(JsonSerializerWrapper serializer) : base(serializer) { } - [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata, IgnoreNullableAnnotations = false)] [JsonSerializable(typeof(NotNullablePropertyClass))] [JsonSerializable(typeof(NotNullableReadonlyPropertyClass))] [JsonSerializable(typeof(NotNullableFieldClass))] @@ -39,9 +37,29 @@ protected NullableAnnotationsTests_Metadata(JsonSerializerWrapper serializer) [JsonSerializable(typeof(NotNullablePropertiesLargeParameterizedCtorClass))] [JsonSerializable(typeof(NotNullablePropertyWithHandleNullConverterLargeParameterizedCtorClass))] [JsonSerializable(typeof(NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullPropertyClass))] + [JsonSerializable(typeof(MaybeNullPropertyClass))] + [JsonSerializable(typeof(AllowNullPropertyClass))] [JsonSerializable(typeof(DisallowNullPropertyClass))] + [JsonSerializable(typeof(AllowNullConstructorParameter))] + [JsonSerializable(typeof(DisallowNullConstructorParameter))] + [JsonSerializable(typeof(NotNullPropertyClass))] + [JsonSerializable(typeof(MaybeNullPropertyClass))] + [JsonSerializable(typeof(AllowNullPropertyClass))] + [JsonSerializable(typeof(DisallowNullPropertyClass))] + [JsonSerializable(typeof(AllowNullConstructorParameter))] + [JsonSerializable(typeof(DisallowNullConstructorParameter))] [JsonSerializable(typeof(NullablePropertyClass))] [JsonSerializable(typeof(NullableFieldClass))] + [JsonSerializable(typeof(NullableObliviousPropertyClass))] + [JsonSerializable(typeof(NullableObliviousConstructorParameter))] + [JsonSerializable(typeof(GenericPropertyClass))] + [JsonSerializable(typeof(NullableGenericPropertyClass))] + [JsonSerializable(typeof(NotNullGenericPropertyClass))] + [JsonSerializable(typeof(GenericConstructorParameter))] + [JsonSerializable(typeof(NullableGenericConstructorParameter))] + [JsonSerializable(typeof(NotNullGenericConstructorParameter))] + [JsonSerializable(typeof(NotNullablePropertyWithIgnoreConditions))] internal sealed partial class NullableAnnotationsTestsContext_Metadata : JsonSerializerContext { } } @@ -63,7 +81,7 @@ public abstract partial class NullableAnnotationsTests_Default : NullableAnnotat protected NullableAnnotationsTests_Default(JsonSerializerWrapper serializer) : base(serializer) { } - [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default)] + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default, IgnoreNullableAnnotations = false)] [JsonSerializable(typeof(NotNullablePropertyClass))] [JsonSerializable(typeof(NotNullableReadonlyPropertyClass))] [JsonSerializable(typeof(NotNullableFieldClass))] @@ -77,9 +95,29 @@ protected NullableAnnotationsTests_Default(JsonSerializerWrapper serializer) [JsonSerializable(typeof(NotNullablePropertiesLargeParameterizedCtorClass))] [JsonSerializable(typeof(NotNullablePropertyWithHandleNullConverterLargeParameterizedCtorClass))] [JsonSerializable(typeof(NotNullablePropertyWithAlwaysNullConverterLargeParameterizedCtorClass))] + [JsonSerializable(typeof(NotNullPropertyClass))] + [JsonSerializable(typeof(MaybeNullPropertyClass))] + [JsonSerializable(typeof(AllowNullPropertyClass))] [JsonSerializable(typeof(DisallowNullPropertyClass))] + [JsonSerializable(typeof(AllowNullConstructorParameter))] + [JsonSerializable(typeof(DisallowNullConstructorParameter))] + [JsonSerializable(typeof(NotNullPropertyClass))] + [JsonSerializable(typeof(MaybeNullPropertyClass))] + [JsonSerializable(typeof(AllowNullPropertyClass))] + [JsonSerializable(typeof(DisallowNullPropertyClass))] + [JsonSerializable(typeof(AllowNullConstructorParameter))] + [JsonSerializable(typeof(DisallowNullConstructorParameter))] [JsonSerializable(typeof(NullablePropertyClass))] [JsonSerializable(typeof(NullableFieldClass))] + [JsonSerializable(typeof(NullableObliviousPropertyClass))] + [JsonSerializable(typeof(NullableObliviousConstructorParameter))] + [JsonSerializable(typeof(GenericPropertyClass))] + [JsonSerializable(typeof(NullableGenericPropertyClass))] + [JsonSerializable(typeof(NotNullGenericPropertyClass))] + [JsonSerializable(typeof(GenericConstructorParameter))] + [JsonSerializable(typeof(NullableGenericConstructorParameter))] + [JsonSerializable(typeof(NotNullGenericConstructorParameter))] + [JsonSerializable(typeof(NotNullablePropertyWithIgnoreConditions))] internal sealed partial class NullableAnnotationsTestsContext_Default : JsonSerializerContext { } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index f869b639d65739..bbddf00081650a 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -19,6 +19,8 @@ true + + $(Features.Replace('nullablePublicOnly', '') true From 12cb92e0c985b740292f06f09c3ce63697786b1e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 21 May 2024 15:38:06 +0100 Subject: [PATCH 03/19] Update annotations for all failing unit tests. --- ...CollectionTests.Dictionary.NonStringKey.cs | 2 +- .../CollectionTests.Dictionary.cs | 40 +++--- .../ConstructorTests.Exceptions.cs | 10 +- .../tests/Common/ExtensionDataTests.cs | 2 +- .../JsonCreationHandlingTests.Object.cs | 10 +- .../tests/Common/JsonSerializerWrapper.cs | 12 +- .../tests/Common/NodeInteropTests.cs | 2 +- .../tests/Common/PropertyNameTests.cs | 2 +- ...pertyVisibilityTests.NonPublicAccessors.cs | 6 +- .../tests/Common/PropertyVisibilityTests.cs | 48 ++++---- .../ReferenceHandlerTests.IgnoreCycles.cs | 16 +-- .../ReferenceHandlerTests.Serialize.cs | 14 +-- .../ReferenceHandlerTests.cs | 8 +- .../tests/Common/RequiredKeywordTests.cs | 18 +-- .../Common/SampleTestData.OrderPayload.cs | 30 ++--- .../TestClasses/TestClasses.Constructor.cs | 62 +++++----- .../TestClasses.NonGenericCollections.cs | 14 +-- .../TestClasses/TestClasses.Polymorphic.cs | 64 +++++----- .../TestClasses.SimpleTestClass.cs | 116 +++++++++--------- ...estClasses.SimpleTestClassWithNullables.cs | 40 +++--- .../TestClasses.SimpleTestClassWithObject.cs | 100 +++++++-------- .../TestClasses.SimpleTestStruct.cs | 54 ++++---- .../TestClasses.SimpleTestStructWithFields.cs | 2 +- .../TestClasses.ValueTypedMember.cs | 6 +- .../tests/Common/TestClasses/TestClasses.cs | 42 +++---- .../RealWorldContextTests.cs | 6 +- .../TestClasses.cs | 4 +- .../JsonNode/JsonObjectTests.cs | 2 +- .../JsonNode/JsonValueTests.cs | 2 +- .../CustomObjectConverterTests.cs | 8 +- .../Serialization/Array.ReadTests.cs | 7 +- .../Serialization/CacheTests.cs | 1 + .../Serialization/ContinuationTests.cs | 10 +- .../CustomConverterTests.DerivedTypes.cs | 12 +- .../CustomConverterTests.HandleNull.cs | 2 +- .../CustomConverterTests.Object.cs | 4 +- .../Serialization/CyclicTests.cs | 14 +-- .../Serialization/ExceptionTests.cs | 10 +- .../MetadataTests/DefaultJsonPropertyInfo.cs | 6 +- ...nTypeInfoResolverTests.JsonPropertyInfo.cs | 8 +- .../DefaultJsonTypeInfoResolverTests.cs | 4 +- .../MetadataTests/MetadataTests.cs | 10 +- .../Serialization/OptionsTests.cs | 1 + .../PolymorphicTests.CustomTypeHierarchies.cs | 5 +- 44 files changed, 421 insertions(+), 415 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs index 4d61a6f8b3091d..d81977e28912fb 100644 --- a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs +++ b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs @@ -380,7 +380,7 @@ private class ClassWithDictionary private class UnsupportedDictionaryWrapper { - public Dictionary Dictionary { get; set; } + public Dictionary? Dictionary { get; set; } } public class FixedNamingPolicy : JsonNamingPolicy diff --git a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.cs b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.cs index 27b67db47aa12c..96ae1b0803c89c 100644 --- a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.cs +++ b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Dictionary.cs @@ -1117,69 +1117,69 @@ public class ClassWithIgnoredDictionary1 { public Dictionary Parsed1 { get; set; } public Dictionary Parsed2 { get; set; } - public Dictionary Skipped3 { get; } + public Dictionary? Skipped3 { get; } } public class ClassWithIgnoredDictionary2 { public IDictionary Parsed1 { get; set; } - public IDictionary Skipped2 { get; } + public IDictionary? Skipped2 { get; } public IDictionary Parsed3 { get; set; } } public class ClassWithIgnoredDictionary3 { public Dictionary Parsed1 { get; set; } - public Dictionary Skipped2 { get; } - public Dictionary Skipped3 { get; } + public Dictionary? Skipped2 { get; } + public Dictionary? Skipped3 { get; } } public class ClassWithIgnoredDictionary4 { - public Dictionary Skipped1 { get; } + public Dictionary? Skipped1 { get; } public Dictionary Parsed2 { get; set; } public Dictionary Parsed3 { get; set; } } public class ClassWithIgnoredDictionary5 { - public Dictionary Skipped1 { get; } + public Dictionary? Skipped1 { get; } public Dictionary Parsed2 { get; set; } - public Dictionary Skipped3 { get; } + public Dictionary? Skipped3 { get; } } public class ClassWithIgnoredDictionary6 { - public Dictionary Skipped1 { get; } - public Dictionary Skipped2 { get; } + public Dictionary? Skipped1 { get; } + public Dictionary? Skipped2 { get; } public Dictionary Parsed3 { get; set; } } public class ClassWithIgnoredDictionary7 { - public Dictionary Skipped1 { get; } - public Dictionary Skipped2 { get; } - public Dictionary Skipped3 { get; } + public Dictionary? Skipped1 { get; } + public Dictionary? Skipped2 { get; } + public Dictionary? Skipped3 { get; } } public class ClassWithIgnoredIDictionary { public IDictionary Parsed1 { get; set; } - public IDictionary Skipped2 { get; } + public IDictionary? Skipped2 { get; } public IDictionary Parsed3 { get; set; } } public class ClassWithIgnoreAttributeDictionary { public Dictionary Parsed1 { get; set; } - [JsonIgnore] public Dictionary Skipped2 { get; set; } // Note this has a setter. + [JsonIgnore] public Dictionary? Skipped2 { get; set; } // Note this has a setter. public Dictionary Parsed3 { get; set; } } public class ClassWithIgnoredImmutableDictionary { public ImmutableDictionary Parsed1 { get; set; } - public ImmutableDictionary Skipped2 { get; } + public ImmutableDictionary? Skipped2 { get; } public ImmutableDictionary Parsed3 { get; set; } } @@ -1614,25 +1614,25 @@ public class AllSingleUpperProperties_Child public class ClassWithDictionaryOfString_ChildWithDictionaryOfString { public string Test { get; set; } - public Dictionary Dict { get; set; } + public Dictionary? Dict { get; set; } public ClassWithDictionaryOfString Child { get; set; } } public class ClassWithDictionaryOfString { - public string Test { get; set; } - public Dictionary Dict { get; set; } + public string? Test { get; set; } + public Dictionary? Dict { get; set; } } public class ClassWithDictionaryAndProperty_DictionaryLast { public string Test { get; set; } - public Dictionary Dict { get; set; } + public Dictionary? Dict { get; set; } } public class ClassWithDictionaryAndProperty_DictionaryFirst { - public Dictionary Dict { get; set; } + public Dictionary? Dict { get; set; } public string Test { get; set; } } diff --git a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.Exceptions.cs b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.Exceptions.cs index 08fb8dee598998..d9cbf6f69c2447 100644 --- a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.Exceptions.cs +++ b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.Exceptions.cs @@ -175,9 +175,9 @@ public async Task PathForChildPropertyFails() public class RootClass { - public ChildClass Child { get; } + public ChildClass? Child { get; } - public RootClass(ChildClass child) + public RootClass(ChildClass? child) { Child = child; } @@ -186,9 +186,9 @@ public RootClass(ChildClass child) public class ChildClass { public int MyInt { get; set; } - public int[] MyIntArray { get; set; } - public Dictionary MyDictionary { get; set; } - public ChildClass[] Children { get; set; } + public int[]? MyIntArray { get; set; } + public Dictionary? MyDictionary { get; set; } + public ChildClass[]? Children { get; set; } } private const string PathForChildListFails_Json = @"{""Child"":{""MyIntArray"":[1, bad]}"; diff --git a/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs index 5801c31e41085e..ab1960772ecc3a 100644 --- a/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs @@ -1111,7 +1111,7 @@ public class ClassWithMultipleDictionaries [JsonExtensionData] public Dictionary MyOverflow { get; set; } - public Dictionary ActualDictionary { get; set; } + public Dictionary? ActualDictionary { get; set; } } [Fact] diff --git a/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs b/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs index e25adffef53fa2..5f50962441b3fc 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonCreationHandlingTests.Object.cs @@ -137,7 +137,7 @@ internal class ClassWithReadOnlyProperty_SimpleClass internal class ClassWithWritableProperty_SimpleClass { [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] - public SimpleClass Property { get; set; } = new() + public SimpleClass? Property { get; set; } = new() { StringValue = "InitialValue", IntValue = 43, @@ -297,7 +297,7 @@ internal class ClassWithReadOnlyProperty_SimpleClassWithSmallParametrizedCtor internal class ClassWithWritableProperty_SimpleClassWithSmallParametrizedCtor { [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] - public SimpleClassWithSmallParametrizedCtor Property { get; set; } = new("InitialValue", 43); + public SimpleClassWithSmallParametrizedCtor? Property { get; set; } = new("InitialValue", 43); } internal class SimpleClassWithSmallParametrizedCtor @@ -422,7 +422,7 @@ internal class ClassWithReadOnlyProperty_SimpleClassWithLargeParametrizedCtor internal class ClassWithWritableProperty_SimpleClassWithLargeParametrizedCtor { [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] - public SimpleClassWithLargeParametrizedCtor Property { get; set; } = new("InitialValue1", 43, "InitialValue2", 44, "InitialValue3", 45, "InitialValue4"); + public SimpleClassWithLargeParametrizedCtor? Property { get; set; } = new("InitialValue1", 43, "InitialValue2", 44, "InitialValue3", 45, "InitialValue4"); } internal class SimpleClassWithLargeParametrizedCtor @@ -475,7 +475,7 @@ public async Task CreationHandlingSetWithAttribute_CanPopulateAndSerialize_Class public class ClassWithProperty_BaseClassWithPolymorphismOnSerializationOnly { [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] - public BaseClassWithPolymorphismOnSerializationOnly Property { get; set; } = + public BaseClassWithPolymorphismOnSerializationOnly? Property { get; set; } = new DerivedClass_DerivingFrom_BaseClassWithPolymorphismOnSerializationOnly() { BaseClassProp = "base", @@ -822,7 +822,7 @@ public class ClassWithRecursiveRequiredProperty public int Value { get; set; } [JsonRequired] - public ClassWithRecursiveRequiredProperty Next { get; set; } + public ClassWithRecursiveRequiredProperty? Next { get; set; } } [Theory] diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapper.cs b/src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapper.cs index 27c75415222110..cf0dc76b243966 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapper.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapper.cs @@ -46,15 +46,15 @@ public abstract partial class JsonSerializerWrapper public abstract Task DeserializeWrapper(string json, Type type, JsonSerializerContext context); - public JsonTypeInfo GetTypeInfo(Type type, bool mutable = false) + public virtual JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions? options = null, bool mutable = false) { - JsonSerializerOptions defaultOptions = DefaultOptions; - // return a fresh mutable instance or the cached readonly metadata - return mutable ? defaultOptions.TypeInfoResolver.GetTypeInfo(type, defaultOptions) : defaultOptions.GetTypeInfo(type); + options ??= DefaultOptions; + options.MakeReadOnly(populateMissingResolver: true); + return mutable ? options.TypeInfoResolver.GetTypeInfo(type, options) : options.GetTypeInfo(type); } - public JsonTypeInfo GetTypeInfo(bool mutable = false) - => (JsonTypeInfo)GetTypeInfo(typeof(T), mutable); + public JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options = null,bool mutable = false) + => (JsonTypeInfo)GetTypeInfo(typeof(T), options, mutable); public JsonSerializerOptions GetDefaultOptionsWithMetadataModifier(Action modifier) { diff --git a/src/libraries/System.Text.Json/tests/Common/NodeInteropTests.cs b/src/libraries/System.Text.Json/tests/Common/NodeInteropTests.cs index 15a199295999bf..e6d251542614f9 100644 --- a/src/libraries/System.Text.Json/tests/Common/NodeInteropTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NodeInteropTests.cs @@ -30,7 +30,7 @@ public async Task CompareResultsAgainstSerializer() public class Poco { - public string MyString { get; set; } + public string? MyString { get; set; } public JsonNode Node { get; set; } public JsonArray Array { get; set; } public JsonValue Value { get; set; } diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs index 9e6066fc308caa..df1755fd393d2d 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyNameTests.cs @@ -403,7 +403,7 @@ public class OverridePropertyNameDesignTime_TestClass public int myInt { get; set; } [JsonPropertyName("BlahObject")] - public object myObject { get; set; } + public object? myObject { get; set; } } public class DuplicatePropertyNameDesignTime_TestClass diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs index 7a54c2f596851b..f0e7cf55665597 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.NonPublicAccessors.cs @@ -37,7 +37,7 @@ public async Task NonPublic_AccessorsNotSupported_WithoutAttribute() public class MyClass_WithNonPublicAccessors { public int MyInt { get; private set; } - public string MyString { get; internal set; } + public string? MyString { get; internal set; } public float MyFloat { private get; set; } public Uri MyUri { internal get; set; } @@ -73,11 +73,11 @@ public class MyClass_WithNonPublicAccessors_WithPropertyAttributes [JsonInclude] public int MyInt { get; private set; } [JsonInclude] - public string MyString { get; internal set; } + public string? MyString { get; internal set; } [JsonInclude] public float MyFloat { private get; set; } [JsonInclude] - public Uri MyUri { internal get; set; } + public Uri? MyUri { internal get; set; } // For test validation. internal float GetMyFloat => MyFloat; diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs index f957db15a96b36..010b6a580ed20d 100644 --- a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs @@ -950,7 +950,7 @@ public class DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverri public class DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName : Class_With_VirtualProperty { [JsonPropertyName("MyProp")] - public string MyString { get; set; } + public string? MyString { get; set; } [JsonIgnore] public override bool MyProp { get; set; } @@ -970,7 +970,7 @@ public class DerivedClass_With_Ignored_NewProperty : Class_With_Property public class DerivedClass_With_NewProperty_And_ConflictingPropertyName : Class_With_Property { [JsonPropertyName("MyProp")] - public string MyString { get; set; } + public string? MyString { get; set; } [JsonIgnore] public new bool MyProp { get; set; } @@ -985,7 +985,7 @@ public class DerivedClass_With_Ignored_NewProperty_Of_DifferentType : Class_With public class DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName : Class_With_Property { [JsonPropertyName("MyProp")] - public string MyString { get; set; } + public string? MyString { get; set; } [JsonIgnore] public new int MyProp { get; set; } @@ -1022,13 +1022,13 @@ public class DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType : C public class FurtherDerivedClass_With_ConflictingPropertyName : DerivedClass_WithIgnoredOverride { [JsonPropertyName("MyProp")] - public string MyString { get; set; } + public string? MyString { get; set; } } public class DerivedClass_WithConflictingPropertyName : Class_With_VirtualProperty { [JsonPropertyName("MyProp")] - public string MyString { get; set; } + public string? MyString { get; set; } } public class FurtherDerivedClass_With_IgnoredOverride : DerivedClass_WithConflictingPropertyName @@ -1852,7 +1852,7 @@ public class ClassWithClassProperty_IgnoreConditionWhenWritingDefault { public int Int1 { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MyString { get; set; } = "DefaultString"; + public string? MyString { get; set; } = "DefaultString"; public int Int2 { get; set; } } @@ -1863,7 +1863,7 @@ public class ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor public string MyString { get; set; } = "DefaultString"; public int Int2 { get; set; } - public ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor(string myString) + public ClassWithClassProperty_IgnoreConditionWhenWritingDefault_Ctor(string? myString) { if (myString != null) { @@ -1997,7 +1997,7 @@ public class ClassWithStructProperty_IgnoreConditionNever { public int Int1 { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string MyString { get; set; } + public string? MyString { get; set; } public int Int2 { get; set; } } @@ -2005,10 +2005,10 @@ public class ClassWithStructProperty_IgnoreConditionNever_Ctor { public int Int1 { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string MyString { get; } + public string? MyString { get; } public int Int2 { get; set; } - public ClassWithStructProperty_IgnoreConditionNever_Ctor(string myString) + public ClassWithStructProperty_IgnoreConditionNever_Ctor(string? myString) { MyString = myString; } @@ -2057,10 +2057,10 @@ public async Task ClassWithComplexObjectsUsingIgnoreWhenWritingDefaultAttribute( public class ClassUsingIgnoreWhenWritingDefaultAttribute { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public SimpleTestClass Class { get; set; } + public SimpleTestClass? Class { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public Dictionary Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; + public Dictionary? Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; } [Fact] @@ -2094,7 +2094,7 @@ public class ClassUsingIgnoreNeverAttribute public SimpleTestClass Class { get; set; } = new SimpleTestClass { MyInt16 = 18 }; [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public Dictionary Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; + public Dictionary? Dictionary { get; set; } = new Dictionary { ["Key"] = "Value" }; } [Fact] @@ -2175,7 +2175,7 @@ public class ClassWithReadOnlyStringProperty public class ClassWithReadOnlyStringProperty_IgnoreNever { [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string MyString { get; } + public string? MyString { get; } public ClassWithReadOnlyStringProperty_IgnoreNever(string myString) => MyString = myString; } @@ -2198,7 +2198,7 @@ public class ClassWithReadOnlyStringField public class ClassWithReadOnlyStringField_IgnoreNever { [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string MyString { get; } + public string? MyString { get; } public ClassWithReadOnlyStringField_IgnoreNever(string myString) => MyString = myString; } @@ -2249,7 +2249,7 @@ public async Task IgnoreCondition_WhenWritingDefault_Globally_Works() public class ClassWithProps { - public string MyString { get; set; } + public string? MyString { get; set; } public int MyInt { get; set; } public Point_2D_Struct MyPoint { get; set; } } @@ -2264,7 +2264,7 @@ public async Task IgnoreCondition_WhenWritingDefault_PerProperty_Works() public class ClassWithPropsAndIgnoreAttributes { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public string MyString { get; set; } + public string? MyString { get; set; } public int MyInt { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Point_2D_Struct MyPoint { get; set; } @@ -2306,7 +2306,7 @@ public async Task IgnoreCondition_WhenWritingDefault_DoesNotApplyToDeserializati public class ClassWithInitializedProps { - public string MyString { get; set; } = "Default"; + public string? MyString { get; set; } = "Default"; public int MyInt { get; set; } = -1; public Point_2D_Struct MyPoint { get; set; } = new Point_2D_Struct(-1, -1); } @@ -2517,7 +2517,7 @@ public class ClassWithThingsToIgnore { public string MyString1_IgnoredWhenWritingNull { get; set; } - public string MyString2_IgnoredWhenWritingNull; + public string? MyString2_IgnoredWhenWritingNull; public int MyInt1; @@ -2527,7 +2527,7 @@ public class ClassWithThingsToIgnore public bool? MyNullableBool2_IgnoredWhenWritingNull; - public PointClass MyPointClass1_IgnoredWhenWritingNull; + public PointClass? MyPointClass1_IgnoredWhenWritingNull; public PointClass MyPointClass2_IgnoredWhenWritingNull { get; set; } @@ -2586,7 +2586,7 @@ public class ClassWithThingsToIgnore_PerProperty public string MyString1_IgnoredWhenWritingNull { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string MyString2_IgnoredWhenWritingNull; + public string? MyString2_IgnoredWhenWritingNull; [JsonInclude] public int MyInt1; @@ -2600,7 +2600,7 @@ public class ClassWithThingsToIgnore_PerProperty public bool? MyNullableBool2_IgnoredWhenWritingNull; [JsonInclude, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public PointClass MyPointClass1_IgnoredWhenWritingNull; + public PointClass? MyPointClass1_IgnoredWhenWritingNull; [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public PointClass MyPointClass2_IgnoredWhenWritingNull { get; set; } @@ -2828,7 +2828,7 @@ public async Task JsonIgnoreCondition_WhenWritingDefault_OnBoxedPrimitive() public class MyClassWithValueTypeInterfaceProperty { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public IInterface MyProp { get; set; } + public IInterface? MyProp { get; set; } public interface IInterface { } public struct MyStruct : IInterface { } @@ -3039,7 +3039,7 @@ public class ClassWithIgnoredCallbacks public class ClassWithCallbacks { - public Func Func { get; set; } + public Func? Func { get; set; } public Action Action { get; set; } = (val) => Console.WriteLine(); } diff --git a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs index c7b07271cc66a8..0affb96da401e4 100644 --- a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs +++ b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs @@ -497,12 +497,12 @@ private object GetNextProperty(Type type, object obj) public class NodeWithObjectProperty { - public object Next { get; set; } + public object? Next { get; set; } } public class NodeWithNodeProperty { - public NodeWithNodeProperty Next { get; set; } + public NodeWithNodeProperty? Next { get; set; } } public class ClassWithGenericProperty @@ -518,22 +518,22 @@ public class TreeNode public interface IValueNodeWithObjectProperty { - public object Next { get; set; } + public object? Next { get; set; } } public struct ValueNodeWithObjectProperty : IValueNodeWithObjectProperty { - public object Next { get; set; } + public object? Next { get; set; } } public interface IValueNodeWithIValueNodeProperty { - public IValueNodeWithIValueNodeProperty Next { get; set; } + public IValueNodeWithIValueNodeProperty? Next { get; set; } } public struct ValueNodeWithIValueNodeProperty : IValueNodeWithIValueNodeProperty { - public IValueNodeWithIValueNodeProperty Next { get; set; } + public IValueNodeWithIValueNodeProperty? Next { get; set; } } public class EmptyClass { } @@ -559,8 +559,8 @@ public class RecursiveList : List { } public class Person { public string Name { get; set; } - public object DayOfBirth { get; set; } - public Person Parent { get; set; } + public object? DayOfBirth { get; set; } + public Person? Parent { get; set; } } class PersonConverter : JsonConverter diff --git a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Serialize.cs b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Serialize.cs index 6fde273fed85b5..dd166409de2699 100644 --- a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Serialize.cs +++ b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Serialize.cs @@ -16,13 +16,13 @@ public abstract partial class ReferenceHandlerTests : SerializerTests public class Employee { - public string Name { get; set; } - public Employee Manager { get; set; } - public Employee Manager2 { get; set; } - public List Subordinates { get; set; } - public List Subordinates2 { get; set; } - public Dictionary Contacts { get; set; } - public Dictionary Contacts2 { get; set; } + public string? Name { get; set; } + public Employee? Manager { get; set; } + public Employee? Manager2 { get; set; } + public List? Subordinates { get; set; } + public List? Subordinates2 { get; set; } + public Dictionary? Contacts { get; set; } + public Dictionary? Contacts2 { get; set; } //Properties with default value to verify they get overwritten when deserializing into them. public List SubordinatesString { get; set; } = new List { "Bob" }; diff --git a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.cs b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.cs index 34e2e159b3a6dc..d0db5d9c585256 100644 --- a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.cs @@ -818,14 +818,14 @@ public async Task BoxedStructReferencePreservation_SiblingPrimitiveValues() public class ClassWithObjectProperty { - public ClassWithObjectProperty Child { get; set; } - public object Sibling { get; set; } + public ClassWithObjectProperty? Child { get; set; } + public object? Sibling { get; set; } } public class ClassWithListOfObjectProperty { - public ClassWithListOfObjectProperty Child { get; set; } - public List ListOfObjects { get; set; } + public ClassWithListOfObjectProperty? Child { get; set; } + public List? ListOfObjects { get; set; } } public interface IBoxedStructWithObjectProperty diff --git a/src/libraries/System.Text.Json/tests/Common/RequiredKeywordTests.cs b/src/libraries/System.Text.Json/tests/Common/RequiredKeywordTests.cs index 6e028929d5c0bc..22c50becfa020c 100644 --- a/src/libraries/System.Text.Json/tests/Common/RequiredKeywordTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/RequiredKeywordTests.cs @@ -74,7 +74,7 @@ public class PersonWithRequiredMembers { public required string FirstName { get; set; } public string MiddleName { get; set; } = ""; - public required string LastName { get; set; } + public required string? LastName { get; set; } } [Theory] @@ -227,11 +227,11 @@ public class PersonWithRequiredMembersAndSmallParametrizedCtor { public required string FirstName { get; set; } public string MiddleName { get; set; } = ""; - public required string LastName { get; set; } - public required string Info1 { get; set; } + public required string? LastName { get; set; } + public required string? Info1 { get; set; } public required string Info2 { get; set; } - public PersonWithRequiredMembersAndSmallParametrizedCtor(string firstName, string lastName) + public PersonWithRequiredMembersAndSmallParametrizedCtor(string firstName, string? lastName) { FirstName = firstName; LastName = lastName; @@ -337,17 +337,17 @@ public async Task ClassWithRequiredKeywordAndLargeParametrizedCtorFailsDeseriali public class PersonWithRequiredMembersAndLargeParametrizedCtor { // Using suffix for names so that checking if required property is missing can be done with simple string.Contains without false positives - public required string AProp { get; set; } + public required string? AProp { get; set; } public required string BProp { get; set; } public required string CProp { get; set; } public required string DProp { get; set; } - public required string EProp { get; set; } + public required string? EProp { get; set; } public required string FProp { get; set; } public required string GProp { get; set; } - public required string HProp { get; set; } - public required string IProp { get; set; } + public required string? HProp { get; set; } + public required string? IProp { get; set; } - public PersonWithRequiredMembersAndLargeParametrizedCtor(string aprop, string bprop, string cprop, string dprop, string eprop, string fprop, string gprop) + public PersonWithRequiredMembersAndLargeParametrizedCtor(string? aprop, string bprop, string cprop, string dprop, string? eprop, string fprop, string gprop) { AProp = aprop; BProp = bprop; diff --git a/src/libraries/System.Text.Json/tests/Common/SampleTestData.OrderPayload.cs b/src/libraries/System.Text.Json/tests/Common/SampleTestData.OrderPayload.cs index 452389d765942c..afb42e9172d5db 100644 --- a/src/libraries/System.Text.Json/tests/Common/SampleTestData.OrderPayload.cs +++ b/src/libraries/System.Text.Json/tests/Common/SampleTestData.OrderPayload.cs @@ -24,8 +24,8 @@ public partial class Order public DateTime Confirmed { get; set; } public DateTime ShippingDate { get; set; } public DateTime EstimatedDelivery { get; set; } - public IEnumerable RelatedOrder { get; set; } - public User ReviewedBy { get; set; } + public IEnumerable? RelatedOrder { get; set; } + public User? ReviewedBy { get; set; } } public class Product @@ -64,8 +64,8 @@ public class Product public DateTime Created { get; set; } public DateTime Updated { get; set; } public bool IsActive { get; set; } - public IEnumerable SimilarProducts { get; set; } - public IEnumerable RelatedProducts { get; set; } + public IEnumerable? SimilarProducts { get; set; } + public IEnumerable? RelatedProducts { get; set; } } public class Review @@ -85,7 +85,7 @@ public class Comment public long Id { get; set; } public long OrderNumber { get; set; } public User Customer { get; set; } - public User Employee { get; set; } + public User? Employee { get; set; } public IEnumerable Responses { get; set; } public string Title { get; set; } public string Message { get; set; } @@ -94,7 +94,7 @@ public class Comment public class ShippingInfo { public long OrderNumber { get; set; } - public User Employee { get; set; } + public User? Employee { get; set; } public string CarrierId { get; set; } public string ShippingType { get; set; } public DateTime EstimatedDelivery { get; set; } @@ -107,7 +107,7 @@ public class ShippingInfo public class Price { - public Product Product { get; set; } + public Product? Product { get; set; } public bool AllowDiscount { get; set; } public decimal OriginalPrice { get; set; } public decimal RecommendedPrice { get; set; } @@ -117,19 +117,19 @@ public class Price public class PreviewImage { - public string Id { get; set; } - public string Filter { get; set; } - public string Size { get; set; } + public string? Id { get; set; } + public string? Filter { get; set; } + public string? Size { get; set; } public int Width { get; set; } public int Height { get; set; } } public class FeaturedImage { - public string Id { get; set; } + public string? Id { get; set; } public int Width { get; set; } public int Height { get; set; } - public string PhotoId { get; set; } + public string? PhotoId { get; set; } } public class Image @@ -141,15 +141,15 @@ public class Image public class User { - public BasicPerson PersonalInfo { get; set; } + public BasicPerson? PersonalInfo { get; set; } public string UserId { get; set; } public string Name { get; set; } public string Username { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } public string ImageId { get; set; } - public string TwitterId { get; set; } - public string FacebookId { get; set; } + public string? TwitterId { get; set; } + public string? FacebookId { get; set; } public int SubscriptionType { get; set; } public bool IsNew { get; set; } public bool IsEmployee { get; set; } diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs index c2bf9e06a9a335..6508ec201e63af 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs @@ -195,11 +195,11 @@ public struct Point_2D_Struct_WithMultipleAttributes_OneNonPublic public class SinglePublicParameterizedCtor { public int MyInt { get; private set; } - public string MyString { get; private set; } + public string? MyString { get; private set; } public SinglePublicParameterizedCtor() { } - public SinglePublicParameterizedCtor(int myInt, string myString) + public SinglePublicParameterizedCtor(int myInt, string? myString) { MyInt = myInt; MyString = myString; @@ -209,7 +209,7 @@ public SinglePublicParameterizedCtor(int myInt, string myString) public class SingleParameterlessCtor_MultiplePublicParameterizedCtor { public int MyInt { get; private set; } - public string MyString { get; private set; } + public string? MyString { get; private set; } public SingleParameterlessCtor_MultiplePublicParameterizedCtor() { } @@ -218,7 +218,7 @@ public SingleParameterlessCtor_MultiplePublicParameterizedCtor(int myInt) MyInt = myInt; } - public SingleParameterlessCtor_MultiplePublicParameterizedCtor(int myInt, string myString) + public SingleParameterlessCtor_MultiplePublicParameterizedCtor(int myInt, string? myString) { MyInt = myInt; MyString = myString; @@ -228,7 +228,7 @@ public SingleParameterlessCtor_MultiplePublicParameterizedCtor(int myInt, string public struct SingleParameterlessCtor_MultiplePublicParameterizedCtor_Struct { public int MyInt { get; private set; } - public string MyString { get; private set; } + public string? MyString { get; private set; } public SingleParameterlessCtor_MultiplePublicParameterizedCtor_Struct(int myInt) { @@ -236,7 +236,7 @@ public SingleParameterlessCtor_MultiplePublicParameterizedCtor_Struct(int myInt) MyString = null; } - public SingleParameterlessCtor_MultiplePublicParameterizedCtor_Struct(int myInt, string myString) + public SingleParameterlessCtor_MultiplePublicParameterizedCtor_Struct(int myInt, string? myString) { MyInt = myInt; MyString = myString; @@ -330,7 +330,7 @@ public MultiplePublicParameterizedCtor(int myInt, string myString) public struct MultiplePublicParameterizedCtor_Struct { public int MyInt { get; private set; } - public string MyString { get; private set; } + public string? MyString { get; private set; } public MultiplePublicParameterizedCtor_Struct(int myInt) { @@ -338,7 +338,7 @@ public MultiplePublicParameterizedCtor_Struct(int myInt) MyString = null; } - public MultiplePublicParameterizedCtor_Struct(int myInt, string myString) + public MultiplePublicParameterizedCtor_Struct(int myInt, string? myString) { MyInt = myInt; MyString = myString; @@ -348,7 +348,7 @@ public MultiplePublicParameterizedCtor_Struct(int myInt, string myString) public class MultiplePublicParameterizedCtor_WithAttribute { public int MyInt { get; private set; } - public string MyString { get; private set; } + public string? MyString { get; private set; } [JsonConstructor] public MultiplePublicParameterizedCtor_WithAttribute(int myInt) @@ -356,7 +356,7 @@ public MultiplePublicParameterizedCtor_WithAttribute(int myInt) MyInt = myInt; } - public MultiplePublicParameterizedCtor_WithAttribute(int myInt, string myString) + public MultiplePublicParameterizedCtor_WithAttribute(int myInt, string? myString) { MyInt = myInt; MyString = myString; @@ -366,7 +366,7 @@ public MultiplePublicParameterizedCtor_WithAttribute(int myInt, string myString) public struct MultiplePublicParameterizedCtor_WithAttribute_Struct { public int MyInt { get; private set; } - public string MyString { get; private set; } + public string? MyString { get; private set; } public MultiplePublicParameterizedCtor_WithAttribute_Struct(int myInt) { @@ -375,7 +375,7 @@ public MultiplePublicParameterizedCtor_WithAttribute_Struct(int myInt) } [JsonConstructor] - public MultiplePublicParameterizedCtor_WithAttribute_Struct(int myInt, string myString) + public MultiplePublicParameterizedCtor_WithAttribute_Struct(int myInt, string? myString) { MyInt = myInt; MyString = myString; @@ -385,7 +385,7 @@ public MultiplePublicParameterizedCtor_WithAttribute_Struct(int myInt, string my public class ParameterlessCtor_MultiplePublicParameterizedCtor_WithAttribute { public int MyInt { get; private set; } - public string MyString { get; private set; } + public string? MyString { get; private set; } public ParameterlessCtor_MultiplePublicParameterizedCtor_WithAttribute() { } @@ -395,7 +395,7 @@ public ParameterlessCtor_MultiplePublicParameterizedCtor_WithAttribute(int myInt MyInt = myInt; } - public ParameterlessCtor_MultiplePublicParameterizedCtor_WithAttribute(int myInt, string myString) + public ParameterlessCtor_MultiplePublicParameterizedCtor_WithAttribute(int myInt, string? myString) { MyInt = myInt; MyString = myString; @@ -571,10 +571,10 @@ public ClassWrapper_For_Int_String(int @int, string @string) // Parameter names public class ClassWrapper_For_Int_Point_3D_String { public int MyInt { get; } - + public Point_3D_Struct MyPoint3DStruct { get; } - public string MyString { get; } + public string? MyString { get; } public ClassWrapper_For_Int_Point_3D_String(Point_3D_Struct myPoint3DStruct) { @@ -584,7 +584,7 @@ public ClassWrapper_For_Int_Point_3D_String(Point_3D_Struct myPoint3DStruct) } [JsonConstructor] - public ClassWrapper_For_Int_Point_3D_String(int myInt, Point_3D_Struct myPoint3DStruct, string myString) + public ClassWrapper_For_Int_Point_3D_String(int myInt, Point_3D_Struct myPoint3DStruct, string? myString) { MyInt = myInt; MyPoint3DStruct = myPoint3DStruct; @@ -1739,14 +1739,14 @@ public SimpleClassWithParameterizedCtor_Derived_GenericIDictionary_ObjectExt(int public class Parameterized_IndexViewModel_Immutable : ITestClass { - public List ActiveOrUpcomingEvents { get; } - public CampaignSummaryViewModel FeaturedCampaign { get; } + public List? ActiveOrUpcomingEvents { get; } + public CampaignSummaryViewModel? FeaturedCampaign { get; } public bool IsNewAccount { get; } public bool HasFeaturedCampaign => FeaturedCampaign != null; public Parameterized_IndexViewModel_Immutable( - List activeOrUpcomingEvents, - CampaignSummaryViewModel featuredCampaign, + List? activeOrUpcomingEvents, + CampaignSummaryViewModel? featuredCampaign, bool isNewAccount) { ActiveOrUpcomingEvents = activeOrUpcomingEvents; @@ -1838,7 +1838,7 @@ public Tuple< ObjWCtorMixedParams, ObjWCtorMixedParams, ObjWCtorMixedParams, - ObjWCtorMixedParams> MyTuple { get; } + ObjWCtorMixedParams>? MyTuple { get; } public Parameterized_Class_With_ComplexTuple( Tuple< @@ -1848,7 +1848,7 @@ public Parameterized_Class_With_ComplexTuple( ObjWCtorMixedParams, ObjWCtorMixedParams, ObjWCtorMixedParams, - ObjWCtorMixedParams> myTuple) => MyTuple = myTuple; + ObjWCtorMixedParams>? myTuple) => MyTuple = myTuple; private const string s_inner_json = @" { @@ -2235,13 +2235,13 @@ public class Point_With_Array : ITestClass public int X { get; } public int Y { get; } - public int[] Arr { get; } + public int[]? Arr { get; } public static readonly string s_json = @"{""X"":1,""Y"":2,""Arr"":[1,2]}"; public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json); - public Point_With_Array(int x, int y, int[] arr) + public Point_With_Array(int x, int y, int[]? arr) { X = x; Y = y; @@ -2264,13 +2264,13 @@ public class Point_With_Dictionary : ITestClass public int X { get; } public int Y { get; } - public Dictionary Dict { get; } + public Dictionary? Dict { get; } public static readonly string s_json = @"{""X"":1,""Y"":2,""Dict"":{""1"":1,""2"":2}}"; public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json); - public Point_With_Dictionary(int x, int y, Dictionary dict) + public Point_With_Dictionary(int x, int y, Dictionary? dict) { X = x; Y = y; @@ -2293,13 +2293,13 @@ public class Point_With_Object : ITestClass public int X { get; } public int Y { get; } - public Point_With_Array Obj { get; } + public Point_With_Array? Obj { get; } public static readonly string s_json = @$"{{""X"":1,""Y"":2,""Obj"":{Point_With_Array.s_json}}}"; public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json); - public Point_With_Object(int x, int y, Point_With_Array obj) + public Point_With_Object(int x, int y, Point_With_Array? obj) { X = x; Y = y; @@ -2405,11 +2405,11 @@ public string FormattedDate public class ClassWithNestedClass { - public ClassWithNestedClass MyClass { get; } + public ClassWithNestedClass? MyClass { get; } public Point_2D_Struct_WithAttribute MyPoint { get; } - public ClassWithNestedClass(ClassWithNestedClass myClass, Point_2D_Struct_WithAttribute myPoint) + public ClassWithNestedClass(ClassWithNestedClass? myClass, Point_2D_Struct_WithAttribute myPoint) { MyClass = myClass; MyPoint = myPoint; diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.NonGenericCollections.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.NonGenericCollections.cs index d66e42935c1d2a..35582ad3deb97d 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.NonGenericCollections.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.NonGenericCollections.cs @@ -9,13 +9,13 @@ namespace System.Text.Json.Serialization.Tests { public class SimpleTestClassWithNonGenericCollectionWrappers : ITestClass { - public WrapperForIList MyIListWrapper { get; set; } - public WrapperForIDictionary MyIDictionaryWrapper { get; set; } - public HashtableWrapper MyHashtableWrapper { get; set; } - public ArrayListWrapper MyArrayListWrapper { get; set; } - public SortedListWrapper MySortedListWrapper { get; set; } - public StackWrapper MyStackWrapper { get; set; } - public QueueWrapper MyQueueWrapper { get; set; } + public WrapperForIList? MyIListWrapper { get; set; } + public WrapperForIDictionary? MyIDictionaryWrapper { get; set; } + public HashtableWrapper? MyHashtableWrapper { get; set; } + public ArrayListWrapper? MyArrayListWrapper { get; set; } + public SortedListWrapper? MySortedListWrapper { get; set; } + public StackWrapper? MyStackWrapper { get; set; } + public QueueWrapper? MyQueueWrapper { get; set; } public static readonly string s_json = @"{" + diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Polymorphic.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Polymorphic.cs index c0837698c12678..f4f2394d12b09e 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Polymorphic.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Polymorphic.cs @@ -10,7 +10,7 @@ namespace System.Text.Json.Serialization.Tests public abstract class Person : ITestClass { public string Name { get; set; } - public Address Address { get; set; } + public Address? Address { get; set; } public Person() { @@ -40,7 +40,7 @@ public void VerifyNonVirtual() public class Address : ITestClass { - public string City { get; set; } + public string? City { get; set; } public virtual void Initialize() { @@ -124,36 +124,36 @@ public override void Verify() public class ObjectWithObjectProperties { - public object /*Address*/ Address { get; set; } - public object /*List*/ List { get; set; } - public object /*string[]*/ Array { get; set; } - public object /*IEnumerable of strings*/ IEnumerable { get; set; } - public object /*IList of strings */ IList { get; set; } - public object /*ICollection of strings */ ICollection { get; set; } - public object /*IEnumerable*/ IEnumerableT { get; set; } - public object /*IList*/ IListT { get; set; } - public object /*ICollection*/ ICollectionT { get; set; } - public object /*IReadOnlyCollection*/ IReadOnlyCollectionT { get; set; } - public object /*IReadOnlyList*/ IReadOnlyListT { get; set; } - public object /*ISet*/ ISetT { get; set; } - public object /*Stack*/ StackT { get; set; } - public object /*Queue*/ QueueT { get; set; } - public object /*HashSet*/ HashSetT { get; set; } - public object /*LinkedList*/ LinkedListT { get; set; } - public object /*SortedSet*/ SortedSetT { get; set; } - public object /*ImmutableArray*/ ImmutableArrayT { get; set; } - public object /*IImmutableList*/ IImmutableListT { get; set; } - public object /*IImmutableStack*/ IImmutableStackT { get; set; } - public object /*IImmutableQueue*/ IImmutableQueueT { get; set; } - public object /*IImmutableSet*/ IImmutableSetT { get; set; } - public object /*ImmutableHashSet*/ ImmutableHashSetT { get; set; } - public object /*ImmutableList*/ ImmutableListT { get; set; } - public object /*ImmutableStack*/ ImmutableStackT { get; set; } - public object /*ImmutableQueue*/ ImmutableQueueT { get; set; } - public object /*ImmutableSortedSet*/ ImmutableSortedSetT { get; set; } - public object /*int?*/ NullableInt { get; set; } - public object /*object*/ Object { get; set; } - public object /*int?[]*/ NullableIntArray { get; set; } + public object? /*Address*/ Address { get; set; } + public object? /*List*/ List { get; set; } + public object? /*string[]*/ Array { get; set; } + public object? /*IEnumerable of strings*/ IEnumerable { get; set; } + public object? /*IList of strings */ IList { get; set; } + public object? /*ICollection of strings */ ICollection { get; set; } + public object? /*IEnumerable*/ IEnumerableT { get; set; } + public object? /*IList*/ IListT { get; set; } + public object? /*ICollection*/ ICollectionT { get; set; } + public object? /*IReadOnlyCollection*/ IReadOnlyCollectionT { get; set; } + public object? /*IReadOnlyList*/ IReadOnlyListT { get; set; } + public object? /*ISet*/ ISetT { get; set; } + public object? /*Stack*/ StackT { get; set; } + public object? /*Queue*/ QueueT { get; set; } + public object? /*HashSet*/ HashSetT { get; set; } + public object? /*LinkedList*/ LinkedListT { get; set; } + public object? /*SortedSet*/ SortedSetT { get; set; } + public object? /*ImmutableArray*/ ImmutableArrayT { get; set; } + public object? /*IImmutableList*/ IImmutableListT { get; set; } + public object? /*IImmutableStack*/ IImmutableStackT { get; set; } + public object? /*IImmutableQueue*/ IImmutableQueueT { get; set; } + public object? /*IImmutableSet*/ IImmutableSetT { get; set; } + public object? /*ImmutableHashSet*/ ImmutableHashSetT { get; set; } + public object? /*ImmutableList*/ ImmutableListT { get; set; } + public object? /*ImmutableStack*/ ImmutableStackT { get; set; } + public object? /*ImmutableQueue*/ ImmutableQueueT { get; set; } + public object? /*ImmutableSortedSet*/ ImmutableSortedSetT { get; set; } + public object? /*int?*/ NullableInt { get; set; } + public object? /*object*/ Object { get; set; } + public object? /*int?[]*/ NullableIntArray { get; set; } public ObjectWithObjectProperties() { diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClass.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClass.cs index 418c102a6ff295..d888eee83dc274 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClass.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClass.cs @@ -20,7 +20,7 @@ public class SimpleTestClass : ITestClass public byte MyByte { get; set; } public sbyte MySByte { get; set; } public char MyChar { get; set; } - public string MyString { get; set; } + public string? MyString { get; set; } public decimal MyDecimal { get; set; } public bool MyBooleanTrue { get; set; } public bool MyBooleanFalse { get; set; } @@ -29,7 +29,7 @@ public class SimpleTestClass : ITestClass public DateTime MyDateTime { get; set; } public DateTimeOffset MyDateTimeOffset { get; set; } public Guid MyGuid { get; set; } - public Uri MyUri { get; set; } + public Uri? MyUri { get; set; } public SampleEnumSByte MySByteEnum { get; set; } public SampleEnumByte MyByteEnum { get; set; } public SampleEnum MyEnum { get; set; } @@ -41,63 +41,63 @@ public class SimpleTestClass : ITestClass public SampleEnumUInt64 MyUInt64Enum { get; set; } public SimpleStruct MySimpleStruct { get; set; } public SimpleTestStruct MySimpleTestStruct { get; set; } - public short[] MyInt16Array { get; set; } - public int[] MyInt32Array { get; set; } - public long[] MyInt64Array { get; set; } - public ushort[] MyUInt16Array { get; set; } - public uint[] MyUInt32Array { get; set; } - public ulong[] MyUInt64Array { get; set; } - public byte[] MyByteArray { get; set; } - public sbyte[] MySByteArray { get; set; } - public char[] MyCharArray { get; set; } - public string[] MyStringArray { get; set; } - public decimal[] MyDecimalArray { get; set; } - public bool[] MyBooleanTrueArray { get; set; } - public bool[] MyBooleanFalseArray { get; set; } - public float[] MySingleArray { get; set; } - public double[] MyDoubleArray { get; set; } - public DateTime[] MyDateTimeArray { get; set; } - public DateTimeOffset[] MyDateTimeOffsetArray { get; set; } - public Guid[] MyGuidArray { get; set; } - public Uri[] MyUriArray { get; set; } - public SampleEnum[] MyEnumArray { get; set; } - public int[][] MyInt16TwoDimensionArray { get; set; } - public List> MyInt16TwoDimensionList { get; set; } - public int[][][] MyInt16ThreeDimensionArray { get; set; } - public List>> MyInt16ThreeDimensionList { get; set; } - public List MyStringList { get; set; } - public IEnumerable MyStringIEnumerable { get; set; } - public IList MyStringIList { get; set; } - public ICollection MyStringICollection { get; set; } - public IEnumerable MyStringIEnumerableT { get; set; } - public IList MyStringIListT { get; set; } - public ICollection MyStringICollectionT { get; set; } - public IReadOnlyCollection MyStringIReadOnlyCollectionT { get; set; } - public IReadOnlyList MyStringIReadOnlyListT { get; set; } - public ISet MyStringISetT { get; set; } + public short[]? MyInt16Array { get; set; } + public int[]? MyInt32Array { get; set; } + public long[]? MyInt64Array { get; set; } + public ushort[]? MyUInt16Array { get; set; } + public uint[]? MyUInt32Array { get; set; } + public ulong[]? MyUInt64Array { get; set; } + public byte[]? MyByteArray { get; set; } + public sbyte[]? MySByteArray { get; set; } + public char[]? MyCharArray { get; set; } + public string[]? MyStringArray { get; set; } + public decimal[]? MyDecimalArray { get; set; } + public bool[]? MyBooleanTrueArray { get; set; } + public bool[]? MyBooleanFalseArray { get; set; } + public float[]? MySingleArray { get; set; } + public double[]? MyDoubleArray { get; set; } + public DateTime[]? MyDateTimeArray { get; set; } + public DateTimeOffset[]? MyDateTimeOffsetArray { get; set; } + public Guid[]? MyGuidArray { get; set; } + public Uri[]? MyUriArray { get; set; } + public SampleEnum[]? MyEnumArray { get; set; } + public int[][]? MyInt16TwoDimensionArray { get; set; } + public List>? MyInt16TwoDimensionList { get; set; } + public int[][][]? MyInt16ThreeDimensionArray { get; set; } + public List>>? MyInt16ThreeDimensionList { get; set; } + public List? MyStringList { get; set; } + public IEnumerable? MyStringIEnumerable { get; set; } + public IList? MyStringIList { get; set; } + public ICollection? MyStringICollection { get; set; } + public IEnumerable? MyStringIEnumerableT { get; set; } + public IList? MyStringIListT { get; set; } + public ICollection? MyStringICollectionT { get; set; } + public IReadOnlyCollection? MyStringIReadOnlyCollectionT { get; set; } + public IReadOnlyList? MyStringIReadOnlyListT { get; set; } + public ISet? MyStringISetT { get; set; } public KeyValuePair MyStringToStringKeyValuePair { get; set; } - public IDictionary MyStringToStringIDict { get; set; } - public Dictionary MyStringToStringGenericDict { get; set; } - public IDictionary MyStringToStringGenericIDict { get; set; } - public IReadOnlyDictionary MyStringToStringGenericIReadOnlyDict { get; set; } - public ImmutableDictionary MyStringToStringImmutableDict { get; set; } - public IImmutableDictionary MyStringToStringIImmutableDict { get; set; } - public ImmutableSortedDictionary MyStringToStringImmutableSortedDict { get; set; } - public Stack MyStringStackT { get; set; } - public Queue MyStringQueueT { get; set; } - public HashSet MyStringHashSetT { get; set; } - public LinkedList MyStringLinkedListT { get; set; } - public SortedSet MyStringSortedSetT { get; set; } - public IImmutableList MyStringIImmutableListT { get; set; } - public IImmutableStack MyStringIImmutableStackT { get; set; } - public IImmutableQueue MyStringIImmutableQueueT { get; set; } - public IImmutableSet MyStringIImmutableSetT { get; set; } - public ImmutableHashSet MyStringImmutableHashSetT { get; set; } - public ImmutableList MyStringImmutableListT { get; set; } - public ImmutableStack MyStringImmutableStackT { get; set; } - public ImmutableQueue MyStringImmutablQueueT { get; set; } - public ImmutableSortedSet MyStringImmutableSortedSetT { get; set; } - public List MyListOfNullString { get; set; } + public IDictionary? MyStringToStringIDict { get; set; } + public Dictionary? MyStringToStringGenericDict { get; set; } + public IDictionary? MyStringToStringGenericIDict { get; set; } + public IReadOnlyDictionary? MyStringToStringGenericIReadOnlyDict { get; set; } + public ImmutableDictionary? MyStringToStringImmutableDict { get; set; } + public IImmutableDictionary? MyStringToStringIImmutableDict { get; set; } + public ImmutableSortedDictionary? MyStringToStringImmutableSortedDict { get; set; } + public Stack? MyStringStackT { get; set; } + public Queue? MyStringQueueT { get; set; } + public HashSet? MyStringHashSetT { get; set; } + public LinkedList? MyStringLinkedListT { get; set; } + public SortedSet? MyStringSortedSetT { get; set; } + public IImmutableList? MyStringIImmutableListT { get; set; } + public IImmutableStack? MyStringIImmutableStackT { get; set; } + public IImmutableQueue? MyStringIImmutableQueueT { get; set; } + public IImmutableSet? MyStringIImmutableSetT { get; set; } + public ImmutableHashSet? MyStringImmutableHashSetT { get; set; } + public ImmutableList? MyStringImmutableListT { get; set; } + public ImmutableStack? MyStringImmutableStackT { get; set; } + public ImmutableQueue? MyStringImmutablQueueT { get; set; } + public ImmutableSortedSet? MyStringImmutableSortedSetT { get; set; } + public List? MyListOfNullString { get; set; } public static readonly string s_json = $"{{{s_partialJsonProperties},{s_partialJsonArrays}}}"; public static readonly string s_json_flipped = $"{{{s_partialJsonArrays},{s_partialJsonProperties}}}"; diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithNullables.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithNullables.cs index 4aa6e36b5f821b..f3f2a35f1747ee 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithNullables.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithNullables.cs @@ -26,26 +26,26 @@ public abstract class SimpleBaseClassWithNullables public DateTimeOffset? MyDateTimeOffset { get; set; } public Guid? MyGuid { get; set; } public SampleEnum? MyEnum { get; set; } - public short?[] MyInt16Array { get; set; } - public int?[] MyInt32Array { get; set; } - public long?[] MyInt64Array { get; set; } - public ushort?[] MyUInt16Array { get; set; } - public uint?[] MyUInt32Array { get; set; } - public ulong?[] MyUInt64Array { get; set; } - public byte?[] MyByteArray { get; set; } - public sbyte?[] MySByteArray { get; set; } - public char?[] MyCharArray { get; set; } - public decimal?[] MyDecimalArray { get; set; } - public bool?[] MyBooleanTrueArray { get; set; } - public bool?[] MyBooleanFalseArray { get; set; } - public float?[] MySingleArray { get; set; } - public double?[] MyDoubleArray { get; set; } - public DateTime?[] MyDateTimeArray { get; set; } - public DateTimeOffset?[] MyDateTimeOffsetArray { get; set; } - public Guid?[] MyGuidArray { get; set; } - public SampleEnum?[] MyEnumArray { get; set; } - public Dictionary MyStringToStringDict { get; set; } - public List MyListOfNullInt { get; set; } + public short?[]? MyInt16Array { get; set; } + public int?[]? MyInt32Array { get; set; } + public long?[]? MyInt64Array { get; set; } + public ushort?[]? MyUInt16Array { get; set; } + public uint?[]? MyUInt32Array { get; set; } + public ulong?[]? MyUInt64Array { get; set; } + public byte?[]? MyByteArray { get; set; } + public sbyte?[]? MySByteArray { get; set; } + public char?[]? MyCharArray { get; set; } + public decimal?[]? MyDecimalArray { get; set; } + public bool?[]? MyBooleanTrueArray { get; set; } + public bool?[]? MyBooleanFalseArray { get; set; } + public float?[]? MySingleArray { get; set; } + public double?[]? MyDoubleArray { get; set; } + public DateTime?[]? MyDateTimeArray { get; set; } + public DateTimeOffset?[]? MyDateTimeOffsetArray { get; set; } + public Guid?[]? MyGuidArray { get; set; } + public SampleEnum?[]? MyEnumArray { get; set; } + public Dictionary? MyStringToStringDict { get; set; } + public List? MyListOfNullInt { get; set; } } public class SimpleTestClassWithNulls : SimpleBaseClassWithNullables, ITestClass diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithObject.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithObject.cs index 4235d4334d2f75..465d26dd1ab31c 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithObject.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestClassWithObject.cs @@ -11,56 +11,56 @@ namespace System.Text.Json.Serialization.Tests { public class SimpleTestClassWithObject : SimpleTestClassWithSimpleObject { - public object MyInt16Array { get; set; } - public object MyInt32Array { get; set; } - public object MyInt64Array { get; set; } - public object MyUInt16Array { get; set; } - public object MyUInt32Array { get; set; } - public object MyUInt64Array { get; set; } - public object MyByteArray { get; set; } - public object MySByteArray { get; set; } - public object MyCharArray { get; set; } - public object MyStringArray { get; set; } - public object MyDecimalArray { get; set; } - public object MyBooleanTrueArray { get; set; } - public object MyBooleanFalseArray { get; set; } - public object MySingleArray { get; set; } - public object MyDoubleArray { get; set; } - public object MyDateTimeArray { get; set; } - public object MyEnumArray { get; set; } - public object MyStringList { get; set; } - public object MyStringIEnumerable { get; set; } - public object MyStringIList { get; set; } - public object MyStringICollection { get; set; } - public object MyStringIEnumerableT { get; set; } - public object MyStringIListT { get; set; } - public object MyStringICollectionT { get; set; } - public object MyStringIReadOnlyCollectionT { get; set; } - public object MyStringIReadOnlyListT { get; set; } - public object MyStringISetT { get; set; } - public object MyStringToStringKeyValuePair { get; set; } - public object MyStringToStringIDict { get; set; } - public object MyStringToStringGenericDict { get; set; } - public object MyStringToStringGenericIDict { get; set; } - public object MyStringToStringGenericIReadOnlyDict { get; set; } - public object MyStringToStringImmutableDict { get; set; } - public object MyStringToStringIImmutableDict { get; set; } - public object MyStringToStringImmutableSortedDict { get; set; } - public object MyStringStackT { get; set; } - public object MyStringQueueT { get; set; } - public object MyStringHashSetT { get; set; } - public object MyStringLinkedListT { get; set; } - public object MyStringSortedSetT { get; set; } - public object MyStringIImmutableListT { get; set; } - public object MyStringIImmutableStackT { get; set; } - public object MyStringIImmutableQueueT { get; set; } - public object MyStringIImmutableSetT { get; set; } - public object MyStringImmutableHashSetT { get; set; } - public object MyStringImmutableArray { get; set; } - public object MyStringImmutableListT { get; set; } - public object MyStringImmutableStackT { get; set; } - public object MyStringImmutablQueueT { get; set; } - public object MyStringImmutableSortedSetT { get; set; } + public object? MyInt16Array { get; set; } + public object? MyInt32Array { get; set; } + public object? MyInt64Array { get; set; } + public object? MyUInt16Array { get; set; } + public object? MyUInt32Array { get; set; } + public object? MyUInt64Array { get; set; } + public object? MyByteArray { get; set; } + public object? MySByteArray { get; set; } + public object? MyCharArray { get; set; } + public object? MyStringArray { get; set; } + public object? MyDecimalArray { get; set; } + public object? MyBooleanTrueArray { get; set; } + public object? MyBooleanFalseArray { get; set; } + public object? MySingleArray { get; set; } + public object? MyDoubleArray { get; set; } + public object? MyDateTimeArray { get; set; } + public object? MyEnumArray { get; set; } + public object? MyStringList { get; set; } + public object? MyStringIEnumerable { get; set; } + public object? MyStringIList { get; set; } + public object? MyStringICollection { get; set; } + public object? MyStringIEnumerableT { get; set; } + public object? MyStringIListT { get; set; } + public object? MyStringICollectionT { get; set; } + public object? MyStringIReadOnlyCollectionT { get; set; } + public object? MyStringIReadOnlyListT { get; set; } + public object? MyStringISetT { get; set; } + public object? MyStringToStringKeyValuePair { get; set; } + public object? MyStringToStringIDict { get; set; } + public object? MyStringToStringGenericDict { get; set; } + public object? MyStringToStringGenericIDict { get; set; } + public object? MyStringToStringGenericIReadOnlyDict { get; set; } + public object? MyStringToStringImmutableDict { get; set; } + public object? MyStringToStringIImmutableDict { get; set; } + public object? MyStringToStringImmutableSortedDict { get; set; } + public object? MyStringStackT { get; set; } + public object? MyStringQueueT { get; set; } + public object? MyStringHashSetT { get; set; } + public object? MyStringLinkedListT { get; set; } + public object? MyStringSortedSetT { get; set; } + public object? MyStringIImmutableListT { get; set; } + public object? MyStringIImmutableStackT { get; set; } + public object? MyStringIImmutableQueueT { get; set; } + public object? MyStringIImmutableSetT { get; set; } + public object? MyStringImmutableHashSetT { get; set; } + public object? MyStringImmutableArray { get; set; } + public object? MyStringImmutableListT { get; set; } + public object? MyStringImmutableStackT { get; set; } + public object? MyStringImmutablQueueT { get; set; } + public object? MyStringImmutableSortedSetT { get; set; } public static new readonly string s_json = @"{" + diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStruct.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStruct.cs index 066deef42fb8a0..297be8331c1972 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStruct.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStruct.cs @@ -18,7 +18,7 @@ public struct SimpleTestStruct : ITestClass public byte MyByte { get; set; } public sbyte MySByte { get; set; } public char MyChar { get; set; } - public string MyString { get; set; } + public string? MyString { get; set; } public decimal MyDecimal { get; set; } public bool MyBooleanTrue { get; set; } public bool MyBooleanFalse { get; set; } @@ -30,32 +30,32 @@ public struct SimpleTestStruct : ITestClass public SampleEnumInt64 MyInt64Enum { get; set; } public SampleEnumUInt64 MyUInt64Enum { get; set; } public SimpleStruct MySimpleStruct { get; set; } - public SimpleTestClass MySimpleTestClass { get; set; } - public short[] MyInt16Array { get; set; } - public int[] MyInt32Array { get; set; } - public long[] MyInt64Array { get; set; } - public ushort[] MyUInt16Array { get; set; } - public uint[] MyUInt32Array { get; set; } - public ulong[] MyUInt64Array { get; set; } - public byte[] MyByteArray { get; set; } - public sbyte[] MySByteArray { get; set; } - public char[] MyCharArray { get; set; } - public string[] MyStringArray { get; set; } - public decimal[] MyDecimalArray { get; set; } - public bool[] MyBooleanTrueArray { get; set; } - public bool[] MyBooleanFalseArray { get; set; } - public float[] MySingleArray { get; set; } - public double[] MyDoubleArray { get; set; } - public DateTime[] MyDateTimeArray { get; set; } - public DateTimeOffset[] MyDateTimeOffsetArray { get; set; } - public SampleEnum[] MyEnumArray { get; set; } - public List MyStringList { get; set; } - public IEnumerable MyStringIEnumerableT { get; set; } - public IList MyStringIListT { get; set; } - public ICollection MyStringICollectionT { get; set; } - public IReadOnlyCollection MyStringIReadOnlyCollectionT { get; set; } - public IReadOnlyList MyStringIReadOnlyListT { get; set; } - public ISet MyStringISetT { get; set; } + public SimpleTestClass? MySimpleTestClass { get; set; } + public short[]? MyInt16Array { get; set; } + public int[]? MyInt32Array { get; set; } + public long[]? MyInt64Array { get; set; } + public ushort[]? MyUInt16Array { get; set; } + public uint[]? MyUInt32Array { get; set; } + public ulong[]? MyUInt64Array { get; set; } + public byte[]? MyByteArray { get; set; } + public sbyte[]? MySByteArray { get; set; } + public char[]? MyCharArray { get; set; } + public string[]? MyStringArray { get; set; } + public decimal[]? MyDecimalArray { get; set; } + public bool[]? MyBooleanTrueArray { get; set; } + public bool[]? MyBooleanFalseArray { get; set; } + public float[]? MySingleArray { get; set; } + public double[]? MyDoubleArray { get; set; } + public DateTime[]? MyDateTimeArray { get; set; } + public DateTimeOffset[]? MyDateTimeOffsetArray { get; set; } + public SampleEnum[]? MyEnumArray { get; set; } + public List? MyStringList { get; set; } + public IEnumerable? MyStringIEnumerableT { get; set; } + public IList? MyStringIListT { get; set; } + public ICollection? MyStringICollectionT { get; set; } + public IReadOnlyCollection? MyStringIReadOnlyCollectionT { get; set; } + public IReadOnlyList? MyStringIReadOnlyListT { get; set; } + public ISet? MyStringISetT { get; set; } public static readonly string s_json = $"{{{s_partialJsonProperties},{s_partialJsonArrays}}}"; public static readonly string s_json_flipped = $"{{{s_partialJsonArrays},{s_partialJsonProperties}}}"; diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStructWithFields.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStructWithFields.cs index 9adedf55f44c38..abb1e6f0ce607b 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStructWithFields.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.SimpleTestStructWithFields.cs @@ -30,7 +30,7 @@ public struct SimpleTestStructWithFields : ITestClass public SampleEnumInt64 MyInt64Enum; public SampleEnumUInt64 MyUInt64Enum; public SimpleStruct MySimpleStruct; - public SimpleTestClass MySimpleTestClass; + public SimpleTestClass? MySimpleTestClass; public short[] MyInt16Array; public int[] MyInt32Array; public long[] MyInt64Array; diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ValueTypedMember.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ValueTypedMember.cs index 31e51649cf3a03..eb39b9e243d7f6 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ValueTypedMember.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.ValueTypedMember.cs @@ -11,7 +11,7 @@ public class TestClassWithValueTypedMember : ITestClass public ValueTypedMember MyValueTypedField; - public RefTypedMember MyRefTypedProperty { get; set; } + public RefTypedMember? MyRefTypedProperty { get; set; } public RefTypedMember MyRefTypedField; @@ -38,9 +38,9 @@ public class TestClassWithNullableValueTypedMember : ITestClass public ValueTypedMember? MyValueTypedField; - public RefTypedMember MyRefTypedProperty { get; set; } + public RefTypedMember? MyRefTypedProperty { get; set; } - public RefTypedMember MyRefTypedField; + public RefTypedMember? MyRefTypedField; public void Initialize() { diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs index 92c12b1dfc44c9..3f830ddf476835 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs @@ -129,7 +129,7 @@ public void Verify() public class TestClassWithNull { - public string MyString { get; set; } + public string? MyString { get; set; } public static readonly string s_json = @"{" + @"""MyString"" : null" + @@ -145,31 +145,31 @@ public void Verify() public class TestClassWithInitializedProperties { - public string MyString { get; set; } = "Hello"; + public string? MyString { get; set; } = "Hello"; public int? MyInt { get; set; } = 1; public DateTime? MyDateTime { get; set; } = new DateTime(1995, 4, 16); - public int[] MyIntArray { get; set; } = new int[] { 1 }; - public List MyIntList { get; set; } = new List { 1 }; - public List MyNullableIntList { get; set; } = new List { 1 }; - public List MyObjectList { get; set; } = new List { 1 }; - public List> MyListList { get; set; } = new List> { new List { 1 } }; - public List> MyDictionaryList { get; set; } = new List> { + public int[]? MyIntArray { get; set; } = new int[] { 1 }; + public List? MyIntList { get; set; } = new List { 1 }; + public List? MyNullableIntList { get; set; } = new List { 1 }; + public List? MyObjectList { get; set; } = new List { 1 }; + public List>? MyListList { get; set; } = new List> { new List { 1 } }; + public List>? MyDictionaryList { get; set; } = new List> { new Dictionary { ["key"] = "value" } }; - public Dictionary MyStringDictionary { get; set; } = new Dictionary { ["key"] = "value" }; - public Dictionary MyNullableDateTimeDictionary { get; set; } = new Dictionary { ["key"] = new DateTime(1995, 04, 16) }; - public Dictionary MyObjectDictionary { get; set; } = new Dictionary { ["key"] = "value" }; - public Dictionary> MyStringDictionaryDictionary { get; set; } = new Dictionary> + public Dictionary? MyStringDictionary { get; set; } = new Dictionary { ["key"] = "value" }; + public Dictionary? MyNullableDateTimeDictionary { get; set; } = new Dictionary { ["key"] = new DateTime(1995, 04, 16) }; + public Dictionary? MyObjectDictionary { get; set; } = new Dictionary { ["key"] = "value" }; + public Dictionary>? MyStringDictionaryDictionary { get; set; } = new Dictionary> { ["key"] = new Dictionary { ["key"] = "value" } }; - public Dictionary> MyListDictionary { get; set; } = new Dictionary> { + public Dictionary>? MyListDictionary { get; set; } = new Dictionary> { ["key"] = new List { "value" } }; - public Dictionary> MyObjectDictionaryDictionary { get; set; } = new Dictionary> + public Dictionary>? MyObjectDictionaryDictionary { get; set; } = new Dictionary> { ["key"] = new Dictionary { @@ -698,7 +698,7 @@ public TestClassWithInitializedArray() public class SimpleClassWithDictionary { public int MyInt { get; set; } - public Dictionary MyDictionary { get; set; } + public Dictionary? MyDictionary { get; set; } } public class OuterClassHavingPropertiesDefinedAfterClassWithDictionary { @@ -1732,9 +1732,9 @@ public class BasicJsonAddress public class BasicCompany : ITestClass { - public List sites { get; set; } - public BasicJsonAddress mainSite { get; set; } - public string name { get; set; } + public List? sites { get; set; } + public BasicJsonAddress? mainSite { get; set; } + public string? name { get; set; } public static readonly byte[] s_data = Encoding.UTF8.GetBytes( "{\n" + @@ -1812,7 +1812,7 @@ public class ClassWithUnicodeProperty public class ClassWithExtensionProperty { - public SimpleTestClass MyNestedClass { get; set; } + public SimpleTestClass? MyNestedClass { get; set; } public int MyInt { get; set; } [JsonExtensionData] @@ -1821,7 +1821,7 @@ public class ClassWithExtensionProperty public class ClassWithExtensionField { - public SimpleTestClass MyNestedClass { get; set; } + public SimpleTestClass? MyNestedClass { get; set; } public int MyInt { get; set; } [JsonInclude] @@ -2294,7 +2294,7 @@ public void Dispose() public class ClassWithRecursiveCollectionTypes { public ClassWithRecursiveCollectionTypes? Nested { get; set; } - public List List { get; set; } + public List? List { get; set; } public IReadOnlyDictionary? Dictionary { get; set; } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs index 152b716d2aaf16..c058cf81a3d164 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs @@ -933,9 +933,9 @@ public class ClassWithDateOnlyAndTimeOnlyValues public class ClassWithNullableProperties { - public Uri Uri { get; set; } - public int[] Array { get; set; } - public MyPoco Poco { get; set; } + public Uri? Uri { get; set; } + public int[]? Array { get; set; } + public MyPoco? Poco { get; set; } public Uri? NullableUri { get; set; } public int[]? NullableArray { get; set; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs index 64a4f13818f068..7056034bbab9ff 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs @@ -87,7 +87,7 @@ public class WeatherForecastWithPOCOs public DateTimeOffset Date { get; set; } public int TemperatureCelsius { get; set; } public string Summary { get; set; } - public string SummaryField; + public string? SummaryField; public List DatesAvailable { get; set; } public Dictionary TemperatureRanges { get; set; } public string[] SummaryWords { get; set; } @@ -115,7 +115,7 @@ public class EmptyPoco public class MyType { - public MyType Type; + public MyType? Type; } public class MyType2 diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs index 9e505ea65057b6..8ae552714bca99 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs @@ -1126,7 +1126,7 @@ private class SimpleClass public string Name { get; set; } - public SimpleClass NestedObject { get; set; } + public SimpleClass? NestedObject { get; set; } } [Fact] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs index 26cbc93555ef7e..ad0bddbd3fd1c5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs @@ -428,7 +428,7 @@ public static void DeepEquals_EscapedString() private class Student { public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/NewtonsoftTests/CustomObjectConverterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/NewtonsoftTests/CustomObjectConverterTests.cs index 887910ac5a32cd..987b6922f742d1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/NewtonsoftTests/CustomObjectConverterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/NewtonsoftTests/CustomObjectConverterTests.cs @@ -192,14 +192,14 @@ internal class NullInterfaceTestClass public virtual string Company { get; set; } public virtual Range DecimalRange { get; set; } public virtual Range IntRange { get; set; } - public virtual Range NullDecimalRange { get; set; } + public virtual Range? NullDecimalRange { get; set; } } internal class MyClass { - public string Value { get; set; } + public string? Value { get; set; } - public IThing Thing { get; set; } + public IThing? Thing { get; set; } } internal interface IThing @@ -215,6 +215,6 @@ internal class MyThing : IThing internal class ByteArrayClass { public byte[] ByteArray { get; set; } - public byte[] NullByteArray { get; set; } + public byte[]? NullByteArray { get; set; } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Array.ReadTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Array.ReadTests.cs index 115e91c909db01..d06465de159395 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Array.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Array.ReadTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Xunit; @@ -44,7 +45,7 @@ public static void ReadNullByteArray() public class PocoWithByteArrayProperty { - public byte[] Value { get; set; } + public byte[]? Value { get; set; } } [Fact] @@ -510,18 +511,21 @@ public class ClassWithNonNullEnumerableGetters private ImmutableArray _immutableArray = default; private ImmutableList _immutableList = null; + [AllowNull] public string[] Array { get => _array ?? new string[] { "-1" }; set { _array = value; } } + [AllowNull] public List List { get => _list ?? new List { "-1" }; set { _list = value; } } + [AllowNull] public StringListWrapper ListWrapper { get => _listWrapper ?? new StringListWrapper { "-1" }; @@ -534,6 +538,7 @@ public ImmutableArray MyImmutableArray set { _immutableArray = value; } } + [AllowNull] public ImmutableList MyImmutableList { get => _immutableList ?? ImmutableList.CreateRange(new List { "-1" }); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs index 64d96934ed7804..23f5c311d04c38 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs @@ -373,6 +373,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou yield return (GetProp(nameof(JsonSerializerOptions.ReadCommentHandling)), JsonCommentHandling.Skip); yield return (GetProp(nameof(JsonSerializerOptions.UnknownTypeHandling)), JsonUnknownTypeHandling.JsonNode); yield return (GetProp(nameof(JsonSerializerOptions.WriteIndented)), true); + yield return (GetProp(nameof(JsonSerializerOptions.IgnoreNullableAnnotations)), !JsonSerializerOptions.Default.IgnoreNullableAnnotations); yield return (GetProp(nameof(JsonSerializerOptions.IndentCharacter)), '\t'); yield return (GetProp(nameof(JsonSerializerOptions.IndentSize)), 1); yield return (GetProp(nameof(JsonSerializerOptions.ReferenceHandler)), ReferenceHandler.Preserve); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ContinuationTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ContinuationTests.cs index b08e8b758e910c..6186a221a9f3ee 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ContinuationTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ContinuationTests.cs @@ -291,7 +291,7 @@ private interface INestedObject private class TestClass : ITestObject where TNested : INestedObject { public string A { get; set; } - public string B { get; set; } + public string? B { get; set; } public int C { get; set; } public int? D { get; set; } public float E { get; set; } @@ -330,7 +330,7 @@ void ITestObject.Verify() private class TestValueType : ITestObject where TNested : INestedObject { public string A { get; set; } - public string B { get; set; } + public string? B { get; set; } public int C { get; set; } public int? D { get; set; } public float E { get; set; } @@ -368,7 +368,7 @@ void ITestObject.Verify() private class NestedClass : INestedObject { - public string A { get; set; } + public string? A { get; set; } public int B { get; set; } void INestedObject.Initialize() @@ -386,7 +386,7 @@ void INestedObject.Verify() private struct NestedValueType : INestedObject { - public string A { get; set; } + public string? A { get; set; } public int B { get; set; } void INestedObject.Initialize() @@ -404,7 +404,7 @@ void INestedObject.Verify() private class NestedClassWithParamCtor : NestedClass { - public NestedClassWithParamCtor(string a) + public NestedClassWithParamCtor(string? a) => A = a; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs index c3decdf1c99f41..b6b8b8f463ee61 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs @@ -131,20 +131,20 @@ public class UnsupportedDictionaryWrapper : Dictionary { } public class DerivedTypesWrapper { - public ListWrapper ListWrapper { get; set; } - public List List { get; set; } - public DictionaryWrapper DictionaryWrapper { get; set; } - public Dictionary Dictionary { get; set; } + public ListWrapper? ListWrapper { get; set; } + public List? List { get; set; } + public DictionaryWrapper? DictionaryWrapper { get; set; } + public Dictionary? Dictionary { get; set; } } public class UnsupportedDerivedTypesWrapper_Dictionary { - public UnsupportedDictionaryWrapper DictionaryWrapper { get; set; } + public UnsupportedDictionaryWrapper? DictionaryWrapper { get; set; } } public class UnsupportedDerivedTypesWrapper_IEnumerable { - public StringIEnumerableWrapper IEnumerableWrapper { get; set; } + public StringIEnumerableWrapper? IEnumerableWrapper { get; set; } } public class ListWrapperConverter : JsonConverter diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.HandleNull.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.HandleNull.cs index 191a8283f94058..e95f970e89d601 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.HandleNull.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.HandleNull.cs @@ -532,7 +532,7 @@ public static void SetterCalledWhenConverterReturnsNull() private class ClassWithInitializedUri { - public Uri MyUri { get; set; } = new Uri("https://microsoft.com"); + public Uri? MyUri { get; set; } = new Uri("https://microsoft.com"); } public class UriToNullConverter : JsonConverter diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs index 7e3f8247023f7c..e4c2b1b1c02517 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.Object.cs @@ -256,9 +256,9 @@ private class TestClassWithFieldsHavingCustomConverter public string Name { get; set; } public Customer Customer { get; set; } public DerivedCustomer DerivedCustomer { get; set; } - public DerivedCustomer NullDerivedCustomer { get; set; } + public DerivedCustomer? NullDerivedCustomer { get; set; } public int IntValue { get; set; } - public string Message { get; set; } + public string? Message { get; set; } } /// diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CyclicTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CyclicTests.cs index 20ff0837cbea29..93dacf41c0a8d1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CyclicTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CyclicTests.cs @@ -121,14 +121,14 @@ public TestClassWithCycle(string name) Name = name; } - public TestClassWithCycle Parent { get; set; } + public TestClassWithCycle? Parent { get; set; } public List Children { get; set; } = new List(); public string Name { get; set; } } public class TestClassWithArrayOfElementsOfTheSameClass { - public TestClassWithArrayOfElementsOfTheSameClass[] Array { get; set; } + public TestClassWithArrayOfElementsOfTheSameClass[]? Array { get; set; } } public class CycleRoot @@ -141,16 +141,16 @@ public class Child1 public IList Child2IList { get; set; } = new List(); public List Child2List { get; set; } = new List(); public Dictionary Child2Dictionary { get; set; } = new Dictionary(); - public Child2 Child2 { get; set; } + public Child2? Child2 { get; set; } } public class Child2 { // Add properties that cause a cycle (Root->Child1->Child2->Child1) - public Child1 Child1 { get; set; } - public IList Child1IList { get; set; } - public IList Child1List { get; set; } - public Dictionary Child1Dictionary { get; set; } + public Child1? Child1 { get; set; } + public IList? Child1IList { get; set; } + public IList? Child1List { get; set; } + public Dictionary? Child1Dictionary { get; set; } } [Fact] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExceptionTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExceptionTests.cs index e439fdb7543edd..4e4c90d3d19628 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExceptionTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/ExceptionTests.cs @@ -427,7 +427,7 @@ private class ClassWithInvalidArray private class ClassWithInvalidDictionary { - public Dictionary UnsupportedDictionary { get; set; } + public Dictionary? UnsupportedDictionary { get; set; } } [Fact] @@ -573,22 +573,22 @@ public StructWithBadCtor(SerializationInfo info, StreamingContext ctx) { } public class ClassWithBadCtor_WithProps { - public SerializationInfo Info { get; set; } + public SerializationInfo? Info { get; set; } public StreamingContext Ctx { get; set; } - public ClassWithBadCtor_WithProps(SerializationInfo info, StreamingContext ctx) => + public ClassWithBadCtor_WithProps(SerializationInfo? info, StreamingContext ctx) => (Info, Ctx) = (info, ctx); } public struct StructWithBadCtor_WithProps { - public SerializationInfo Info { get; set; } + public SerializationInfo? Info { get; set; } public StreamingContext Ctx { get; set; } [JsonConstructor] - public StructWithBadCtor_WithProps(SerializationInfo info, StreamingContext ctx) => + public StructWithBadCtor_WithProps(SerializationInfo? info, StreamingContext ctx) => (Info, Ctx) = (info, ctx); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonPropertyInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonPropertyInfo.cs index 7aa6e62830d33f..40da610826f2ac 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonPropertyInfo.cs @@ -462,15 +462,15 @@ public void StructWithRequiredPropertiesDoesWorkCorrectlyWithJsonRequiredCustomA public class ClassWithRequiredCustomAttributes { [JsonPropertyOrder(0)] - public string NonRequired { get; set; } + public string? NonRequired { get; set; } [JsonPropertyOrder(1)] [JsonRequired] - public string RequiredA { get; set; } + public string? RequiredA { get; set; } [JsonPropertyOrder(2)] [JsonRequired] - public string RequiredB { get; set; } + public string? RequiredB { get; set; } } public class ClassWithRequiredCustomAttributeAndDataExtensionProperty diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs index 417c0b3b03c9d1..eacb292bfc20fa 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonPropertyInfo.cs @@ -1100,18 +1100,18 @@ static void TestJsonIgnoreConditionDelegate(JsonIgnoreCondition defaultIgnoreCon private class TestClassWithEveryPossibleJsonIgnore { [JsonIgnore(Condition = JsonIgnoreCondition.Always)] - public string AlwaysProperty { get; set; } + public string? AlwaysProperty { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int WhenWritingDefaultProperty { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string WhenWritingNullProperty { get; set; } + public string? WhenWritingNullProperty { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string NeverProperty { get; set; } + public string? NeverProperty { get; set; } - public string Property { get; set; } + public string? Property { get; set; } } private class TestClassWithProperty diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.cs index b34f0d1e42e366..3c4dbc003969f7 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.cs @@ -196,7 +196,7 @@ private static void InvokeGeneric(Type type, string methodName, params object[] private class SomeClass { - public object ObjProp { get; set; } + public object? ObjProp { get; set; } public int IntProp { get; set; } } @@ -227,7 +227,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions private class SomeRecursiveClass { public int IntProp { get; set; } - public SomeRecursiveClass RecursiveProperty { get; set; } + public SomeRecursiveClass? RecursiveProperty { get; set; } } [JsonDerivedType(typeof(DerivedClass))] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.cs index 7d8c2238a085d0..b1339b85cb7469 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/MetadataTests.cs @@ -66,11 +66,11 @@ internal class WeatherForecastWithPOCOs { public DateTimeOffset Date { get; set; } public int TemperatureCelsius { get; set; } - public string Summary { get; set; } - public string SummaryField; - public List DatesAvailable { get; set; } - public Dictionary TemperatureRanges { get; set; } - public string[] SummaryWords { get; set; } + public string? Summary { get; set; } + public string? SummaryField; + public List? DatesAvailable { get; set; } + public Dictionary? TemperatureRanges { get; set; } + public string[]? SummaryWords { get; set; } } public class HighLowTemps diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index 164c3ca7e782d0..7e7f72c45dc243 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -917,6 +917,7 @@ public static void Options_JsonSerializerContext_Combine_FallbackToReflection() JsonTestHelper.AssertJsonEqual("""{"Value":null,"Thing":null}""", json); } + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public static void Options_IgnoreNullableAnnotationsDefault_Net8CompatibilitySwitch() { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs index 09063a41d9908b..0a98fe51dd47ea 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs @@ -1039,7 +1039,6 @@ public async Task NestedPolymorphicClassesIncreaseReadAndWriteStackWhenNeeded() Info = "1" } } - } } }; @@ -1088,7 +1087,7 @@ class TestNodeList : TestNode { public string Info { get; set; } - public IEnumerable List { get; set; } + public IEnumerable? List { get; set; } public override void AssertEqualTo(TestNode other) { @@ -1119,7 +1118,7 @@ public override void AssertEqualTo(TestNode other) class TestLeaf : TestNode { - public string Test { get; set; } + public string? Test { get; set; } public override void AssertEqualTo(TestNode other) { From c22a5676815c65c58221dc40d8e2444f3ae5d458 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 21 May 2024 15:53:23 +0100 Subject: [PATCH 04/19] Update src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs --- src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs index 0b56d5dd952bfb..d0e6be047f3c53 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs @@ -285,7 +285,7 @@ public static void ResolveNullabilityAnnotations(this IFieldSymbol field, out bo { // 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. + // 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; From 6be27d929cf6b0f7bae8d5338fba30a0f249e80e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 21 May 2024 15:53:55 +0100 Subject: [PATCH 05/19] Update src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs --- src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs index d0e6be047f3c53..3df19b883e37ed 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs @@ -304,7 +304,7 @@ public static void ResolveNullabilityAnnotations(this IPropertySymbol property, { // 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. + // 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; From 0be6dfec7d79336e946fe46feaa11f6cbfbed5f0 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 21 May 2024 16:22:29 +0100 Subject: [PATCH 06/19] Address feedback --- .../src/System/Reflection/NullabilityInfo.cs | 4 ++-- .../System/Reflection/NullabilityInfoContext.cs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) 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 25b0aa629be6d9..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,7 @@ namespace System.Reflection /// /// A class that represents nullability info /// -#if NETCOREAPP +#if NET public #else internal @@ -51,7 +51,7 @@ internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState /// /// An enum that represents nullability state /// -#if NETCOREAPP +#if NET public #else internal 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 b0e16765516049..15e928590698e3 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,7 @@ namespace System.Reflection /// Provides APIs for populating nullability information/context from reflection members: /// , , and . /// -#if NETCOREAPP +#if NET public #else internal @@ -70,7 +70,7 @@ private enum NotAnnotatedStatus /// public NullabilityInfo Create(ParameterInfo parameterInfo) { -#if NETCOREAPP +#if NET ArgumentNullException.ThrowIfNull(parameterInfo); #else NetstandardHelpers.ThrowIfNull(parameterInfo, nameof(parameterInfo)); @@ -200,7 +200,7 @@ private static void CheckNullabilityAttributes(NullabilityInfo nullability, ILis /// public NullabilityInfo Create(PropertyInfo propertyInfo) { -#if NETCOREAPP +#if NET ArgumentNullException.ThrowIfNull(propertyInfo); #else NetstandardHelpers.ThrowIfNull(propertyInfo, nameof(propertyInfo)); @@ -259,7 +259,7 @@ private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method) /// public NullabilityInfo Create(EventInfo eventInfo) { -#if NETCOREAPP +#if NET ArgumentNullException.ThrowIfNull(eventInfo); #else NetstandardHelpers.ThrowIfNull(eventInfo, nameof(eventInfo)); @@ -280,7 +280,7 @@ public NullabilityInfo Create(EventInfo eventInfo) /// public NullabilityInfo Create(FieldInfo fieldInfo) { -#if NETCOREAPP +#if NET ArgumentNullException.ThrowIfNull(fieldInfo); #else NetstandardHelpers.ThrowIfNull(fieldInfo, nameof(fieldInfo)); @@ -522,7 +522,7 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T Debug.Assert(genericParameter.IsGenericParameter); if (reflectedType is not null -#if NETCOREAPP +#if NET && !genericParameter.IsGenericMethodParameter #else && !genericParameter.IsGenericMethodParameter() @@ -558,7 +558,7 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(NullabilityInfo nullability, Type genericParameter, Type context, Type reflectedType) { Debug.Assert(genericParameter.IsGenericParameter && -#if NETCOREAPP +#if NET !genericParameter.IsGenericMethodParameter); #else !genericParameter.IsGenericMethodParameter()); @@ -701,7 +701,7 @@ public bool ParseNullableState(int index, ref NullabilityState state) } } -#if !NETCOREAPP +#if !NET internal static class NetstandardHelpers { public static void ThrowIfNull(object? argument, string paramName) From 1b0d954b2840c6642f8746bb6e840cf4810909ac Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 21 May 2024 20:55:26 +0100 Subject: [PATCH 07/19] Update to latest approved API and semantics. --- .../JsonSourceGenerationOptionsAttribute.cs | 10 ++-- .../gen/Helpers/RoslynExtensions.cs | 4 +- .../gen/JsonSourceGenerator.Emitter.cs | 16 +++--- .../gen/JsonSourceGenerator.Parser.cs | 26 ++++----- .../gen/Model/ParameterGenerationSpec.cs | 2 +- .../gen/Model/PropertyGenerationSpec.cs | 8 +-- .../gen/Model/SourceGenerationOptionsSpec.cs | 2 +- .../System.Text.Json/ref/System.Text.Json.cs | 10 ++-- .../Text/Json/AppContextSwitchHelper.cs | 4 +- ...ParameterizedConstructorConverter.Large.cs | 2 +- ...ParameterizedConstructorConverter.Small.cs | 2 +- .../Serialization/JsonSerializerContext.cs | 2 +- .../JsonSerializerOptions.Caching.cs | 4 +- .../Serialization/JsonSerializerOptions.cs | 17 ++++-- .../DefaultJsonTypeInfoResolver.Helpers.cs | 27 +++++---- .../Metadata/JsonParameterInfo.cs | 4 +- .../Metadata/JsonParameterInfoValues.cs | 2 +- .../Metadata/JsonPropertyInfo.cs | 56 +++++++++++-------- .../Metadata/JsonPropertyInfoOfT.cs | 8 +-- .../tests/Common/NullableAnnotationsTests.cs | 30 +++++----- .../Serialization/NullableAnnotationsTests.cs | 4 +- .../Serialization/CacheTests.cs | 2 +- .../Serialization/OptionsTests.cs | 49 ++++++++++------ 23 files changed, 160 insertions(+), 131 deletions(-) diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs index 472bf4005af064..8fd1e3353dc4f8 100644 --- a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs @@ -70,11 +70,6 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults) /// public JsonKnownNamingPolicy DictionaryKeyPolicy { get; set; } - /// - /// Specifies the default value of when set. - /// - public bool IgnoreNullableAnnotations { get; set; } - /// /// Specifies the default value of when set. /// @@ -120,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 3df19b883e37ed..5512321b0e2f04 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs @@ -317,7 +317,7 @@ public static void ResolveNullabilityAnnotations(this IPropertySymbol property, } } - public static bool IsNonNullable(this IParameterSymbol parameter) + public static bool IsNullable(this IParameterSymbol parameter) { if (parameter.Type.IsNullableType()) { @@ -326,7 +326,7 @@ public static bool IsNonNullable(this IParameterSymbol parameter) // 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 !IsInputTypeNonNullable(parameter, parameter.Type); } return false; diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index f4fd4dffc39150..dc9abcada6a798 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -673,13 +673,13 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe writer.WriteLine($"properties[{i}].Order = {property.Order};"); } - if (property.DisallowNullWrites) + if (property.IsGetterNonNullableAnnotation) { - writer.WriteLine($"properties[{i}].DisallowNullWrites = !{OptionsLocalVariableName}.IgnoreNullableAnnotations;"); + writer.WriteLine($"properties[{i}].IsGetNullable = false;"); } - if (property.DisallowNullReads) + if (property.IsSetterNonNullableAnnotation) { - writer.WriteLine($"properties[{i}].DisallowNullReads = !{OptionsLocalVariableName}.IgnoreNullableAnnotations;"); + writer.WriteLine($"properties[{i}].IsSetNullable = false;"); } writer.WriteLine(); @@ -712,7 +712,7 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin Position = {{spec.ParameterIndex}}, HasDefaultValue = {{FormatBoolLiteral(spec.HasDefaultValue)}}, DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}}, - DisallowNullReads = {{FormatBoolLiteral(spec.DisallowNullReads)}}, + IsNullable = {{FormatBoolLiteral(spec.IsNullable)}}, }, """); @@ -1037,7 +1037,7 @@ private static SerializedValueCheckType GetCheckType(ContextGenerationSpec conte { JsonIgnoreCondition.WhenWritingNull => propertySpec.PropertyType.CanBeNull ? SerializedValueCheckType.IgnoreWhenNull : SerializedValueCheckType.None, JsonIgnoreCondition.WhenWritingDefault => propertySpec.PropertyType.CanBeNull ? SerializedValueCheckType.IgnoreWhenNull : SerializedValueCheckType.IgnoreWhenDefault, - _ when propertySpec.DisallowNullWrites => SerializedValueCheckType.DisallowNull, + _ when propertySpec.IsGetterNonNullableAnnotation && contextSpec.GeneratedOptionsSpec?.RespectNullableAnnotations is true => SerializedValueCheckType.DisallowNull, _ => SerializedValueCheckType.None, }; } @@ -1182,8 +1182,8 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti if (optionsSpec.DictionaryKeyPolicy is JsonKnownNamingPolicy dictionaryKeyPolicy) writer.WriteLine($"DictionaryKeyPolicy = {FormatNamingPolicy(dictionaryKeyPolicy)},"); - if (optionsSpec.IgnoreNullableAnnotations is bool ignoreNullableAnnotations) - writer.WriteLine($"IgnoreNullableAnnotations = {FormatBoolLiteral(ignoreNullableAnnotations)},"); + if (optionsSpec.RespectNullableAnnotations is bool ignoreNullableAnnotations) + writer.WriteLine($"RespectNullableAnnotations = {FormatBoolLiteral(ignoreNullableAnnotations)},"); 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 a2bd2a3383f755..c317e340862689 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -330,7 +330,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN dictionaryKeyPolicy = (JsonKnownNamingPolicy)namedArg.Value.Value!; break; - case nameof(JsonSourceGenerationOptionsAttribute.IgnoreNullableAnnotations): + case nameof(JsonSourceGenerationOptionsAttribute.RespectNullableAnnotations): ignoreNullableAnnotations = (bool)namedArg.Value.Value!; break; @@ -417,7 +417,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN Converters = converters?.ToImmutableEquatableArray(), DefaultIgnoreCondition = defaultIgnoreCondition, DictionaryKeyPolicy = dictionaryKeyPolicy, - IgnoreNullableAnnotations = ignoreNullableAnnotations, + RespectNullableAnnotations = ignoreNullableAnnotations, IgnoreReadOnlyFields = ignoreReadOnlyFields, IgnoreReadOnlyProperties = ignoreReadOnlyProperties, IncludeFields = includeFields, @@ -1127,8 +1127,8 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type) out bool canUseSetter, out bool hasJsonIncludeButIsInaccessible, out bool setterIsInitOnly, - out bool disallowNullWrites, - out bool disallowNullReads); + out bool isGetterNonNullable, + out bool isSetterNonNullable); if (hasJsonIncludeButIsInaccessible) { @@ -1192,8 +1192,8 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type) PropertyType = propertyTypeRef, DeclaringType = declaringType, ConverterType = converterType, - DisallowNullWrites = disallowNullWrites, - DisallowNullReads = disallowNullReads, + IsGetterNonNullableAnnotation = isGetterNonNullable, + IsSetterNonNullableAnnotation = isSetterNonNullable, }; } @@ -1312,8 +1312,8 @@ private void ProcessMember( out bool canUseSetter, out bool hasJsonIncludeButIsInaccessible, out bool isSetterInitOnly, - out bool disallowNullWrites, - out bool disallowNullReads) + out bool isGetterNonNullable, + out bool isSetterNonNullable) { isAccessible = false; isReadOnly = false; @@ -1322,8 +1322,8 @@ private void ProcessMember( canUseSetter = false; hasJsonIncludeButIsInaccessible = false; isSetterInitOnly = false; - disallowNullWrites = false; - disallowNullReads = false; + isGetterNonNullable = false; + isSetterNonNullable = false; switch (memberInfo) { @@ -1373,7 +1373,7 @@ private void ProcessMember( isReadOnly = true; } - propertyInfo.ResolveNullabilityAnnotations(out disallowNullWrites, out disallowNullReads); + propertyInfo.ResolveNullabilityAnnotations(out isGetterNonNullable, out isSetterNonNullable); break; case IFieldSymbol fieldInfo: isReadOnly = fieldInfo.IsReadOnly; @@ -1397,7 +1397,7 @@ private void ProcessMember( hasJsonIncludeButIsInaccessible = hasJsonInclude; } - fieldInfo.ResolveNullabilityAnnotations(out disallowNullWrites, out disallowNullReads); + fieldInfo.ResolveNullabilityAnnotations(out isGetterNonNullable, out isSetterNonNullable); break; default: Debug.Fail("Method given an invalid symbol type."); @@ -1448,7 +1448,7 @@ private void ProcessMember( HasDefaultValue = parameterInfo.HasExplicitDefaultValue, DefaultValue = parameterInfo.HasExplicitDefaultValue ? parameterInfo.ExplicitDefaultValue : null, ParameterIndex = i, - DisallowNullReads = parameterInfo.IsNonNullable(), + 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 85d6ff89225da6..2dea67e2159ccb 100644 --- a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs @@ -32,6 +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 DisallowNullReads { 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 6b3b8042074b57..a003a9d74d48c2 100644 --- a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs @@ -98,14 +98,14 @@ public sealed record PropertyGenerationSpec public required bool CanUseSetter { get; init; } /// - /// Whether the property disallows serializing null values based on its nullability annotations. + /// Whether the property getter returns a nullable type with a non-nullable annotation. /// - public required bool DisallowNullWrites { get; init; } + public required bool IsGetterNonNullableAnnotation { get; init; } /// - /// Whether the property disallows deserializing null values based on its nullability annotations. + /// Whether the property setter accepts a nullable type with a non-nullable annotation. /// - public required bool DisallowNullReads{ get; init; } + 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 e866779cdb474b..9b7ecc8ea6920a 100644 --- a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs @@ -28,7 +28,7 @@ public sealed record SourceGenerationOptionsSpec public required JsonKnownNamingPolicy? DictionaryKeyPolicy { get; init; } - public required bool? IgnoreNullableAnnotations { get; init; } + public required bool? RespectNullableAnnotations { get; init; } public required bool? IgnoreReadOnlyFields { 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 3fd4f73e8a7ec4..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,7 +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 IgnoreNullableAnnotations { 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 { } } @@ -1092,13 +1092,13 @@ 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 { } } public bool WriteIndented { get { throw null; } set { } } public char IndentCharacter { get { throw null; } set { } } public int IndentSize { get { throw null; } set { } } - public bool IgnoreNullableAnnotations { get { throw null; } set { } } } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. Applications should use the generic JsonStringEnumConverter instead.")] public partial class JsonStringEnumConverter : System.Text.Json.Serialization.JsonConverterFactory @@ -1276,10 +1276,10 @@ 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 { } } - public bool DisallowNullReads { get { throw null; } init { } } } public partial class JsonPolymorphismOptions { @@ -1297,7 +1297,9 @@ internal JsonPropertyInfo() { } public System.Text.Json.Serialization.JsonConverter? CustomConverter { get { throw null; } set { } } public System.Func? Get { get { throw null; } set { } } public bool IsExtensionData { get { throw null; } set { } } + public bool IsGetNullable { get { throw null; } set { } } public bool IsRequired { get { throw null; } set { } } + public bool IsSetNullable { get { throw null; } set { } } public string Name { get { throw null; } set { } } public System.Text.Json.Serialization.JsonNumberHandling? NumberHandling { get { throw null; } set { } } public System.Text.Json.Serialization.JsonObjectCreationHandling? ObjectCreationHandling { get { throw null; } set { } } @@ -1306,8 +1308,6 @@ internal JsonPropertyInfo() { } public System.Type PropertyType { get { throw null; } } public System.Action? Set { get { throw null; } set { } } public System.Func? ShouldSerialize { get { throw null; } set { } } - public bool DisallowNullWrites { get { throw null; } set { } } - public bool DisallowNullReads { get { throw null; } set { } } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public sealed partial class JsonPropertyInfoValues diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs index dc21123ffb822e..f2333a19c315f6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs @@ -11,9 +11,9 @@ internal static class AppContextSwitchHelper isEnabled: out bool value) ? value : false; - public static bool IgnoreNullableAnnotationsDefault { get; } = + public static bool RespectNullableAnnotationsDefault { get; } = AppContext.TryGetSwitch( - switchName: "System.Text.Json.Serialization.IgnoreNullableAnnotationsDefault", + switchName: "System.Text.Json.Serialization.RespectNullableAnnotationsDefault", isEnabled: out bool value) ? value : false; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index fde5887f6f50cc..5452390981fa4c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -22,7 +22,7 @@ protected sealed override bool ReadAndCacheConstructorArgument(scoped ref ReadSt if (success && !(arg == null && jsonParameterInfo.IgnoreNullTokensOnRead)) { - if (arg == null && jsonParameterInfo.DisallowNullReads) + if (arg == null && !jsonParameterInfo.IsNullable && jsonParameterInfo.Options.RespectNullableAnnotations) { ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(jsonParameterInfo.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs index e77c4df53d9f9d..3942d4fa631a78 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs @@ -75,7 +75,7 @@ private static bool TryRead( // Use default value specified on parameter, if any. value = info.DefaultValue; } - else if (info.DisallowNullReads) + else if (!info.IsNullable && info.Options.RespectNullableAnnotations) { ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(info.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs index 82fd5d349bec7a..89be1b06f6ed62 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs @@ -70,7 +70,7 @@ options.Encoder is null && // Ensure options values are consistent with expected defaults. options.DefaultIgnoreCondition == generatedSerializerOptions.DefaultIgnoreCondition && - options.IgnoreNullableAnnotations == generatedSerializerOptions.IgnoreNullableAnnotations && + options.RespectNullableAnnotations == generatedSerializerOptions.RespectNullableAnnotations && options.IgnoreReadOnlyFields == generatedSerializerOptions.IgnoreReadOnlyFields && options.IgnoreReadOnlyProperties == generatedSerializerOptions.IgnoreReadOnlyProperties && options.IncludeFields == generatedSerializerOptions.IncludeFields && diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index 7c316ea19ddcc7..f8add81e50d9d4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -507,7 +507,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) left.NewLine == right.NewLine && // Read through property due to lazy initialization of the backing field left._allowOutOfOrderMetadataProperties == right._allowOutOfOrderMetadataProperties && left._allowTrailingCommas == right._allowTrailingCommas && - left._ignoreNullableAnnotations == right._ignoreNullableAnnotations && + left._respectNullableAnnotations == right._respectNullableAnnotations && left._ignoreNullValues == right._ignoreNullValues && left._ignoreReadOnlyProperties == right._ignoreReadOnlyProperties && left._ignoreReadonlyFields == right._ignoreReadonlyFields && @@ -566,7 +566,7 @@ public int GetHashCode(JsonSerializerOptions options) AddHashCode(ref hc, options.NewLine); // Read through property due to lazy initialization of the backing field AddHashCode(ref hc, options._allowOutOfOrderMetadataProperties); AddHashCode(ref hc, options._allowTrailingCommas); - AddHashCode(ref hc, options._ignoreNullableAnnotations); + AddHashCode(ref hc, options._respectNullableAnnotations); AddHashCode(ref hc, options._ignoreNullValues); AddHashCode(ref hc, options._ignoreReadOnlyProperties); AddHashCode(ref hc, options._ignoreReadonlyFields); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 50b6adea78d7c1..749c896b4cb3b4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -86,7 +86,7 @@ public static JsonSerializerOptions Web private int _maxDepth; private bool _allowOutOfOrderMetadataProperties; private bool _allowTrailingCommas; - private bool _ignoreNullableAnnotations = AppContextSwitchHelper.IgnoreNullableAnnotationsDefault; + private bool _respectNullableAnnotations = AppContextSwitchHelper.RespectNullableAnnotationsDefault; private bool _ignoreNullValues; private bool _ignoreReadOnlyProperties; private bool _ignoreReadonlyFields; @@ -140,7 +140,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _maxDepth = options._maxDepth; _allowOutOfOrderMetadataProperties = options._allowOutOfOrderMetadataProperties; _allowTrailingCommas = options._allowTrailingCommas; - _ignoreNullableAnnotations = options._ignoreNullableAnnotations; + _respectNullableAnnotations = options._respectNullableAnnotations; _ignoreNullValues = options._ignoreNullValues; _ignoreReadOnlyProperties = options._ignoreReadOnlyProperties; _ignoreReadonlyFields = options._ignoreReadonlyFields; @@ -783,18 +783,23 @@ public string NewLine } /// - /// Gets or sets a value that indicates whether nullability annotations of nullable reference types are ignored during serialization and deserialization. + /// Gets or sets a value that indicates whether nullability annotations should be respected during serialization and deserialization. /// /// /// Thrown if this property is set after serialization or deserialization has occurred. /// - public bool IgnoreNullableAnnotations + /// + /// Due to restrictions in how nullable reference types are represented at run time, + /// this setting only governs nullability annotations of non-generic properties and fields. + /// It cannot be used to enforce nullability annotations of root-level types or generic parameters. + /// + public bool RespectNullableAnnotations { - get => _ignoreNullableAnnotations; + get => _respectNullableAnnotations; set { VerifyMutable(); - _ignoreNullableAnnotations = value; + _respectNullableAnnotations = value; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index f41a6fb87def9e..69df68510b4da5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -71,7 +71,7 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte if (typeInfo.Kind is JsonTypeInfoKind.Object) { - NullabilityInfoContext? nullabilityCtx = options.IgnoreNullableAnnotations ? null : new(); + NullabilityInfoContext nullabilityCtx = new(); if (converter.ConstructorIsParameterized) { @@ -89,7 +89,7 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoContext? nullabilityCtx) + private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoContext nullabilityCtx) { Debug.Assert(!typeInfo.IsReadOnly); Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object); @@ -156,7 +156,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoCon private static void AddMembersDeclaredBySuperType( JsonTypeInfo typeInfo, Type currentType, - NullabilityInfoContext? nullabilityCtx, + NullabilityInfoContext nullabilityCtx, bool constructorHasSetsRequiredMembersAttribute, ref JsonTypeInfo.PropertyHierarchyResolutionState state) { @@ -217,7 +217,7 @@ private static void AddMember( JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, - NullabilityInfoContext? nullabilityCtx, + NullabilityInfoContext nullabilityCtx, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute, ref JsonTypeInfo.PropertyHierarchyResolutionState state) @@ -239,7 +239,7 @@ private static void AddMember( JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, - NullabilityInfoContext? nullabilityCtx, + NullabilityInfoContext nullabilityCtx, JsonSerializerOptions options, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute) @@ -299,7 +299,7 @@ private static bool PropertyIsOverriddenAndIgnored(PropertyInfo propertyInfo, Di [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, NullabilityInfoContext? nullabilityCtx) + private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, NullabilityInfoContext nullabilityCtx) { Debug.Assert(typeInfo.Converter.ConstructorInfo != null); ParameterInfo[] parameters = typeInfo.Converter.ConstructorInfo.GetParameters(); @@ -324,10 +324,9 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, Nullabili Position = reflectionInfo.Position, HasDefaultValue = reflectionInfo.HasDefaultValue, DefaultValue = reflectionInfo.GetDefaultValue(), - DisallowNullReads = - nullabilityCtx != null && + IsNullable = reflectionInfo.ParameterType.IsNullableType() && - DetermineParameterNullability(reflectionInfo, nullabilityCtx) is NullabilityState.NotNull, + DetermineParameterNullability(reflectionInfo, nullabilityCtx) is not NullabilityState.NotNull, }; jsonParameters[i] = jsonInfo; @@ -343,7 +342,7 @@ private static void PopulatePropertyInfo( MemberInfo memberInfo, JsonConverter? customConverter, JsonIgnoreCondition? ignoreCondition, - NullabilityInfoContext? nullabilityCtx, + NullabilityInfoContext nullabilityCtx, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute) { @@ -486,9 +485,9 @@ internal static void DeterminePropertyAccessors(JsonPropertyInfo jsonPrope [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, MemberInfo memberInfo, NullabilityInfoContext? nullabilityCtx) + private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, MemberInfo memberInfo, NullabilityInfoContext nullabilityCtx) { - if (nullabilityCtx is null || !propertyInfo.PropertyTypeCanBeNull) + if (!propertyInfo.PropertyTypeCanBeNull) { return; } @@ -504,8 +503,8 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, nullabilityInfo = nullabilityCtx.Create((FieldInfo)memberInfo); } - propertyInfo.DisallowNullReads = nullabilityInfo.WriteState is NullabilityState.NotNull; - propertyInfo.DisallowNullWrites = nullabilityInfo.ReadState is NullabilityState.NotNull; + propertyInfo.IsGetNullable = nullabilityInfo.ReadState is not NullabilityState.NotNull; + propertyInfo.IsSetNullable = nullabilityInfo.WriteState is not NullabilityState.NotNull; } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index ccd90baa7e1ab9..8535cd3fc9c224 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -17,7 +17,7 @@ internal JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonProp Position = parameterInfoValues.Position; HasDefaultValue = parameterInfoValues.HasDefaultValue; - DisallowNullReads = parameterInfoValues.DisallowNullReads && !matchingProperty.Options.IgnoreNullableAnnotations; + IsNullable = parameterInfoValues.IsNullable; MatchingProperty = matchingProperty; } @@ -27,7 +27,7 @@ internal JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonProp // The default value of the parameter. This is `DefaultValue` of the `ParameterInfo`, if specified, or the `default` for the `ParameterType`. public object? DefaultValue { get; private protected init; } public JsonPropertyInfo MatchingProperty { get; } - public bool DisallowNullReads { get; internal set; } + public bool IsNullable { get; internal set; } public Type DeclaringType => MatchingProperty.DeclaringType; public Type ParameterType => MatchingProperty.PropertyType; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs index 077912e8ede04e..24ad156c6a60dd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs @@ -46,6 +46,6 @@ public sealed class JsonParameterInfoValues /// Whether the parameter allows values. /// /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. - public bool DisallowNullReads { get; init; } + public bool IsNullable { get; init; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 5ec77a09ea3ab4..9dc3b0e7d85406 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -234,22 +234,26 @@ public JsonObjectCreationHandling? ObjectCreationHandling internal bool IsSourceGenerated { get; set; } /// - /// Gets or sets a value indicating if the serializer should reject property values during deserialization. + /// Gets or sets a value indicating whether the return type of the getter is annotated as nullable. /// /// /// The instance has been locked for further modification. /// /// -or- /// - /// The current is not a nullable type. + /// The current is not a reference type or . /// /// - /// For contracts originating from or , - /// the value of this property will be mapped from nullable reference type annotations. + /// Contracts originating from or , + /// derive the value of this property from nullable reference type annotations, including annotations + /// from attributes such as or . + /// + /// This property has no effect on serialization unless the + /// has been enabled, in which case the serializer will reject any values returned by the getter. /// - public bool DisallowNullReads + public bool IsGetNullable { - get => _disallowNullReads; + get => _isGetNullable; set { VerifyMutable(); @@ -259,34 +263,31 @@ public bool DisallowNullReads throw new InvalidOperationException("The property type is not nullable."); } - if (ParameterInfo != null) - { - ParameterInfo.DisallowNullReads = value; - } - - _disallowNullReads = value; + _isGetNullable = value; } } - private bool _disallowNullReads; + private bool _isGetNullable; /// - /// Gets or sets a value indicating if the serializer should reject property values during serialization. + /// Gets or sets a value indicating whether the input type of the setter is annotated as nullable. /// /// /// The instance has been locked for further modification. /// /// -or- /// - /// The current is not a nullable type. + /// The current is not a reference type or . /// - /// - /// For contracts originating from or , - /// the value of this property will be mapped from nullable reference type annotations. - /// - public bool DisallowNullWrites + /// Contracts originating from or , + /// derive the value of this property from nullable reference type annotations, including annotations + /// from attributes such as or . + /// + /// This property has no effect on deserialization unless the + /// has been enabled, in which case the serializer will reject any deserialization results. + public bool IsSetNullable { - get => _disallowNullWrites; + get => _isSetNullable; set { VerifyMutable(); @@ -296,11 +297,16 @@ public bool DisallowNullWrites throw new InvalidOperationException("The property type is not nullable."); } - _disallowNullWrites = value; + if (ParameterInfo != null) + { + ParameterInfo.IsNullable = value; + } + + _isSetNullable = value; } } - private bool _disallowNullWrites; + private bool _isSetNullable; /// /// Specifies whether the current property is a special extension data property. @@ -370,6 +376,8 @@ internal JsonPropertyInfo(Type declaringType, Type propertyType, JsonTypeInfo? d PropertyType = propertyType; DeclaringTypeInfo = declaringTypeInfo; // null declaringTypeInfo means it's not tied yet Options = options; + + _isGetNullable = _isSetNullable = PropertyTypeCanBeNull; } internal static JsonPropertyInfo GetPropertyPlaceholder() @@ -668,7 +676,7 @@ private void DetermineParameterInfo() ParameterInfo = DeclaringTypeInfo.CreateMatchingParameterInfo(this); if (ParameterInfo != null) { - _disallowNullReads = ParameterInfo.DisallowNullReads; + _isSetNullable = ParameterInfo.IsNullable; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index ad9e4a886c6760..929c6789d2c02a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -196,7 +196,7 @@ value is not null && { Debug.Assert(PropertyTypeCanBeNull); - if (DisallowNullWrites && !Options.IgnoreNullableAnnotations) + if (!IsGetNullable && Options.RespectNullableAnnotations) { ThrowHelper.ThrowJsonException_PropertyGetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); } @@ -280,7 +280,7 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta if (!IgnoreNullTokensOnRead) { - if (DisallowNullReads) + if (!IsSetNullable && Options.RespectNullableAnnotations) { ThrowHelper.ThrowJsonException_PropertySetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); } @@ -303,7 +303,7 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta // Optimize for internal converters by avoiding the extra call to TryRead. T? fastValue = EffectiveConverter.Read(ref reader, PropertyType, Options); - if (fastValue is null && DisallowNullReads) + if (fastValue is null && !IsSetNullable && Options.RespectNullableAnnotations) { Debug.Fail("We currently don't have an internal converter that returns null that could trigger this, if you hit this, please add a test case."); Debug.Assert(!EffectiveConverter.IsValueType); @@ -334,7 +334,7 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta // We cannot do reader.Skip early because converter decides if populating will happen or not if (CanDeserialize) { - if (value is null && DisallowNullReads) + if (value is null && !IsSetNullable && Options.RespectNullableAnnotations) { ThrowHelper.ThrowJsonException_PropertySetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); } diff --git a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs index 337216b4cc7a65..392aaea5d2ef76 100644 --- a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs @@ -14,8 +14,8 @@ namespace System.Text.Json.Serialization.Tests { public abstract class NullableAnnotationsTests : SerializerTests { - private static readonly JsonSerializerOptions s_optionsWithIgnoredNullability = new JsonSerializerOptions { IgnoreNullValues = true }; - private static readonly JsonSerializerOptions s_optionsWithEnforcedNullability = new JsonSerializerOptions { IgnoreNullValues = false }; + private static readonly JsonSerializerOptions s_optionsWithIgnoredNullability = new JsonSerializerOptions { RespectNullableAnnotations = false }; + private static readonly JsonSerializerOptions s_optionsWithEnforcedNullability = new JsonSerializerOptions { RespectNullableAnnotations = true }; protected NullableAnnotationsTests(JsonSerializerWrapper serializerUnderTest) : base(serializerUnderTest) { } @@ -50,10 +50,10 @@ public async Task WriteNullFromNotNullablePropertyGetter_EnforcedNullability_Dis JsonPropertyInfo propertyInfo = typeInfo.Properties.FirstOrDefault(p => p.Name == propertyName); Assert.NotNull(propertyInfo); - Assert.True(propertyInfo.DisallowNullWrites); + Assert.False(propertyInfo.IsGetNullable); - propertyInfo.DisallowNullWrites = false; - Assert.False(propertyInfo.DisallowNullWrites); + propertyInfo.IsGetNullable = true; + Assert.True(propertyInfo.IsGetNullable); string json = await Serializer.SerializeWrapper(value, typeInfo); Assert.NotNull(json); @@ -102,10 +102,10 @@ public async Task WriteNullFromNullablePropertyGetter_EnforcedNullability_Enable JsonPropertyInfo propertyInfo = typeInfo.Properties.FirstOrDefault(p => p.Name == propertyName); Assert.NotNull(propertyInfo); - Assert.False(propertyInfo.DisallowNullWrites); + Assert.True(propertyInfo.IsGetNullable); - propertyInfo.DisallowNullWrites = true; - Assert.True(propertyInfo.DisallowNullWrites); + propertyInfo.IsGetNullable = false; + Assert.False(propertyInfo.IsGetNullable); JsonException ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, typeInfo)); @@ -164,10 +164,10 @@ public async Task ReadNullIntoNotNullablePropertySetter_EnforcedNullability_Disa JsonPropertyInfo propertyInfo = typeInfo.Properties.FirstOrDefault(p => p.Name == propertyName); Assert.NotNull(propertyInfo); - Assert.True(propertyInfo.DisallowNullReads); + Assert.False(propertyInfo.IsSetNullable); - propertyInfo.DisallowNullReads = false; - Assert.False(propertyInfo.DisallowNullReads); + propertyInfo.IsSetNullable = true; + Assert.True(propertyInfo.IsSetNullable); object? result = await Serializer.DeserializeWrapper(json, typeInfo); Assert.IsType(type, result); @@ -217,10 +217,10 @@ public async Task ReadNullIntoNullablePropertySetter_EnforcedNullability_Enabled JsonPropertyInfo propertyInfo = typeInfo.Properties.FirstOrDefault(p => p.Name == propertyName); Assert.NotNull(propertyInfo); - Assert.False(propertyInfo.DisallowNullReads); + Assert.True(propertyInfo.IsSetNullable); - propertyInfo.DisallowNullReads = true; - Assert.True(propertyInfo.DisallowNullReads); + propertyInfo.IsSetNullable = false; + Assert.False(propertyInfo.IsSetNullable); JsonException ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, typeInfo)); @@ -335,7 +335,7 @@ public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverter [Fact] public async Task WriteNotNullPropertiesWithNullIgnoreConditions_Succeeds() { - // JsonIgnoreCondition.WhenWritingNull/Default takes precedence over DisallowNullWrites. + // JsonIgnoreCondition.WhenWritingNull/Default takes precedence over nullability enforcement. var value = new NotNullablePropertyWithIgnoreConditions { WhenWritingNull = null!, WhenWritingDefault = null! }; string json = await Serializer.SerializeWrapper(value, s_optionsWithEnforcedNullability); Assert.Equal("{}", json); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs index 07b8cefd11f226..53ff0af6064b30 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs @@ -23,7 +23,7 @@ public abstract partial class NullableAnnotationsTests_Metadata : NullableAnnota protected NullableAnnotationsTests_Metadata(JsonSerializerWrapper serializer) : base(serializer) { } - [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata, IgnoreNullableAnnotations = false)] + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata, RespectNullableAnnotations = false)] [JsonSerializable(typeof(NotNullablePropertyClass))] [JsonSerializable(typeof(NotNullableReadonlyPropertyClass))] [JsonSerializable(typeof(NotNullableFieldClass))] @@ -81,7 +81,7 @@ public abstract partial class NullableAnnotationsTests_Default : NullableAnnotat protected NullableAnnotationsTests_Default(JsonSerializerWrapper serializer) : base(serializer) { } - [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default, IgnoreNullableAnnotations = false)] + [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Default, RespectNullableAnnotations = false)] [JsonSerializable(typeof(NotNullablePropertyClass))] [JsonSerializable(typeof(NotNullableReadonlyPropertyClass))] [JsonSerializable(typeof(NotNullableFieldClass))] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs index 23f5c311d04c38..739be45931b9e9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs @@ -373,7 +373,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou yield return (GetProp(nameof(JsonSerializerOptions.ReadCommentHandling)), JsonCommentHandling.Skip); yield return (GetProp(nameof(JsonSerializerOptions.UnknownTypeHandling)), JsonUnknownTypeHandling.JsonNode); yield return (GetProp(nameof(JsonSerializerOptions.WriteIndented)), true); - yield return (GetProp(nameof(JsonSerializerOptions.IgnoreNullableAnnotations)), !JsonSerializerOptions.Default.IgnoreNullableAnnotations); + yield return (GetProp(nameof(JsonSerializerOptions.RespectNullableAnnotations)), !JsonSerializerOptions.Default.RespectNullableAnnotations); yield return (GetProp(nameof(JsonSerializerOptions.IndentCharacter)), '\t'); yield return (GetProp(nameof(JsonSerializerOptions.IndentSize)), 1); yield return (GetProp(nameof(JsonSerializerOptions.ReferenceHandler)), ReferenceHandler.Preserve); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index 7e7f72c45dc243..3ada5243181d7c 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -66,7 +66,7 @@ public static void SetOptionsFail() Assert.Equal(JsonCommentHandling.Disallow, options.ReadCommentHandling); Assert.Equal(JsonUnmappedMemberHandling.Skip, options.UnmappedMemberHandling); Assert.False(options.WriteIndented); - Assert.False(options.IgnoreNullableAnnotations); + Assert.False(options.RespectNullableAnnotations); TestIListNonThrowingOperationsWhenImmutable(options.Converters, tc); TestIListNonThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -86,7 +86,7 @@ public static void SetOptionsFail() Assert.Throws(() => options.UnmappedMemberHandling = options.UnmappedMemberHandling); Assert.Throws(() => options.WriteIndented = options.WriteIndented); Assert.Throws(() => options.TypeInfoResolver = options.TypeInfoResolver); - Assert.Throws(() => options.IgnoreNullableAnnotations = options.IgnoreNullableAnnotations); + Assert.Throws(() => options.RespectNullableAnnotations = options.RespectNullableAnnotations); TestIListThrowingOperationsWhenImmutable(options.Converters, tc); TestIListThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -918,29 +918,46 @@ public static void Options_JsonSerializerContext_Combine_FallbackToReflection() } [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public static void Options_IgnoreNullableAnnotationsDefault_Net8CompatibilitySwitch() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(null)] + [InlineData(false)] + [InlineData(true)] + public static void Options_RespectNullableAnnotationsDefault_FeatureSwitch(bool? state) { - var options = new RemoteInvokeOptions + var options = new RemoteInvokeOptions(); + if (state.HasValue) { - RuntimeConfigurationOptions = - { - ["System.Text.Json.Serialization.IgnoreNullableAnnotationsDefault"] = true, - } - }; + options.RuntimeConfigurationOptions["System.Text.Json.Serialization.RespectNullableAnnotationsDefault"] = state.Value; + } - RemoteExecutor.Invoke(static () => + string arg = state ?? false ? "true" : "false"; + RemoteExecutor.Invoke(static arg => { + bool shouldRespectNullableAnnotations = bool.Parse(arg); + var jsonOptions = new JsonSerializerOptions(); - Assert.True(jsonOptions.IgnoreNullableAnnotations); + Assert.Equal(shouldRespectNullableAnnotations, jsonOptions.RespectNullableAnnotations); + Assert.Equal(shouldRespectNullableAnnotations, JsonSerializerOptions.Default.RespectNullableAnnotations); var value = new NullableAnnotationsTests.NotNullablePropertyClass(); - Assert.Null(value.Property); - string json = JsonSerializer.Serialize(value, jsonOptions); + string expectedJson = """{"Property":null}"""; - value = JsonSerializer.Deserialize(json, jsonOptions); Assert.Null(value.Property); - }, options).Dispose(); + + if (shouldRespectNullableAnnotations) + { + Assert.Throws(() => JsonSerializer.Serialize(value)); + Assert.Throws(() => JsonSerializer.Deserialize(expectedJson)); + } + else + { + string json = JsonSerializer.Serialize(value, jsonOptions); + Assert.Equal(expectedJson, json); + value = JsonSerializer.Deserialize(json, jsonOptions); + Assert.Null(value.Property); + } + + }, arg, options).Dispose(); } private static void GenericObjectOrJsonElementConverterTestHelper(string converterName, object objectValue, string stringValue) From 3c74fb80b5f923afafd3ac7b8646451117d318c6 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 00:45:59 +0300 Subject: [PATCH 08/19] Update src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Cantú --- .../src/System/Reflection/NullabilityInfoContext.cs | 1 - 1 file changed, 1 deletion(-) 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 15e928590698e3..cde33ef8aaad5f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs @@ -125,7 +125,6 @@ private void CheckParameterMetadataType(ParameterInfo parameter, NullabilityInfo private static ParameterInfo? GetMetaParameter(MethodBase metaMethod, ParameterInfo parameter) { ReadOnlySpan parameters = metaMethod.GetParametersAsSpan(); - for (int i = 0; i < parameters.Length; i++) { if (parameter.Position == i && From 3656b29e769027ea653606485306a9f1c26e8b33 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 00:46:13 +0300 Subject: [PATCH 09/19] Update src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Cantú --- .../src/System/Reflection/NullabilityInfoContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 cde33ef8aaad5f..5c56ef25197591 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs @@ -479,8 +479,7 @@ private static Type GetPropertyMetaType(PropertyInfo property) return method.ReturnType; } - ReadOnlySpan parameters = property.GetSetMethod(true)!.GetParametersAsSpan(); - return parameters[0].ParameterType; + return property.GetSetMethod(true)!.GetParametersAsSpan()[0].ParameterType; } private void CheckGenericParameters(NullabilityInfo nullability, MemberInfo metaMember, Type metaType, Type? reflectedType) From dcd54a9aa2de57e6eb73e7b02276e32b8bb6e860 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 09:25:46 +0100 Subject: [PATCH 10/19] Update src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Cantú --- .../System.Text.Json/gen/JsonSourceGenerator.Emitter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index dc9abcada6a798..febf787afeef8a 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1182,8 +1182,8 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti if (optionsSpec.DictionaryKeyPolicy is JsonKnownNamingPolicy dictionaryKeyPolicy) writer.WriteLine($"DictionaryKeyPolicy = {FormatNamingPolicy(dictionaryKeyPolicy)},"); - if (optionsSpec.RespectNullableAnnotations is bool ignoreNullableAnnotations) - writer.WriteLine($"RespectNullableAnnotations = {FormatBoolLiteral(ignoreNullableAnnotations)},"); + if (optionsSpec.RespectNullableAnnotations is bool respectNullableAnnotations) + writer.WriteLine($"RespectNullableAnnotations = {FormatBoolLiteral(respectNullableAnnotations)},"); if (optionsSpec.IgnoreReadOnlyFields is bool ignoreReadOnlyFields) writer.WriteLine($"IgnoreReadOnlyFields = {FormatBoolLiteral(ignoreReadOnlyFields)},"); From 080a3533badd1bea235743e19721e8f75f171952 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 09:31:55 +0100 Subject: [PATCH 11/19] Rename more ignoreNullableAnnotations stragglers. --- .../System.Text.Json/System.Text.Json.sln | 56 +++++++++++-------- .../gen/JsonSourceGenerator.Parser.cs | 6 +- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index 11d7c5d4944b0f..19051c30f62a80 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34902.84 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" @@ -45,7 +49,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "ref\Sys EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn3.11", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn3.11.csproj", "{5C0CE30B-DD4A-4F7A-87C0-5243F0C86885}" EndProject @@ -81,11 +85,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E49881A9-09F EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{F254F143-4704-4432-9995-20D87FA8BF8D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{8F44175A-3081-4CA8-90E7-9EE6754EACAA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8F44175A-3081-4CA8-90E7-9EE6754EACAA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{08A1F4D2-E4DA-4CD9-9107-89941EFEB79C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{08A1F4D2-E4DA-4CD9-9107-89941EFEB79C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E9E19E81-4AE0-412D-98EA-3B14694D3CA3}" EndProject @@ -249,47 +253,51 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {102945CA-3736-4B2C-8E68-242A0B247F2B} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {5C0CE30B-DD4A-4F7A-87C0-5243F0C86885} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {FCA21178-0411-45D6-B597-B7BE145CEE33} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {256A4653-4287-44B3-BDEF-67FC1522ED2F} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {F6A18EB5-A8CC-4A39-9E85-5FA226019C3D} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} {73D5739C-E382-4E22-A7D3-B82705C58C74} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {282400DF-F3D8-4419-90F1-1E2F2D8B760C} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {6E9E4359-44F8-45AA-AEC5-D0F9FFBB13D6} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {09F77672-101E-4495-9D88-29376919C121} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {BE27618A-2916-4269-9AD5-6BC5EDC32B30} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {C2C7BA09-F9EE-4E43-8EE4-871CC000342C} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {4774F56D-16A8-4ABB-8C73-5F57609F1773} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {B6D364E7-E5DB-4CF4-B87F-3CEDA3FF7478} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {FAB4FFF2-964D-45FF-89CC-8BB9CE618ED1} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {C56337BB-8CBC-4EE5-AB4D-8BB0A922813E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {6E9E4359-44F8-45AA-AEC5-D0F9FFBB13D6} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {B6D364E7-E5DB-4CF4-B87F-3CEDA3FF7478} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {715327DF-D6D6-4043-AC78-FA58F0C0E9E3} = {F254F143-4704-4432-9995-20D87FA8BF8D} {B815304D-502E-402C-ACE1-878DB4985CCC} = {F254F143-4704-4432-9995-20D87FA8BF8D} {E4B72517-C694-486A-950E-6AB03C651FDC} = {F254F143-4704-4432-9995-20D87FA8BF8D} + {FAB4FFF2-964D-45FF-89CC-8BB9CE618ED1} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {C56337BB-8CBC-4EE5-AB4D-8BB0A922813E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {04AEB008-EE4F-44DE-A361-2DBD2D0FD6A4} = {F254F143-4704-4432-9995-20D87FA8BF8D} {6485EED4-C313-4551-9865-8ADCED603629} = {F254F143-4704-4432-9995-20D87FA8BF8D} {143AFE8A-3490-444C-A82D-6A375EB59F01} = {F254F143-4704-4432-9995-20D87FA8BF8D} + {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {5C0CE30B-DD4A-4F7A-87C0-5243F0C86885} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {FCA21178-0411-45D6-B597-B7BE145CEE33} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {256A4653-4287-44B3-BDEF-67FC1522ED2F} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {F6A18EB5-A8CC-4A39-9E85-5FA226019C3D} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} {B32C4C93-6C85-4E88-B966-5A9F5CAD54B1} = {F254F143-4704-4432-9995-20D87FA8BF8D} {03D5F977-D2DA-4586-A662-C99B0E0EEC20} = {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} {A3FCA745-7F4D-4FB8-85A5-422AC92B2704} = {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} - {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {D0277072-F248-47EF-A417-D2612987D899} = {8F44175A-3081-4CA8-90E7-9EE6754EACAA} {A86642C4-E388-4871-B9FB-A5FCB8BF5CB0} = {8F44175A-3081-4CA8-90E7-9EE6754EACAA} - {8F44175A-3081-4CA8-90E7-9EE6754EACAA} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {8E1C465B-1DA9-4C2A-A6B5-082C55EFB95E} = {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} + {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} + {8F44175A-3081-4CA8-90E7-9EE6754EACAA} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5868B757-D821-41FC-952E-2113A0519506} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a3fca745-7f4d-4fb8-85a5-422ac92b2704}*SharedItemsImports = 5 + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a86642c4-e388-4871-b9fb-a5fcb8bf5cb0}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index c317e340862689..66d40939ca4233 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -268,7 +268,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN int? defaultBufferSize = null; JsonIgnoreCondition? defaultIgnoreCondition = null; JsonKnownNamingPolicy? dictionaryKeyPolicy = null; - bool? ignoreNullableAnnotations = null; + bool? respectNullableAnnotations = null; bool? ignoreReadOnlyFields = null; bool? ignoreReadOnlyProperties = null; bool? includeFields = null; @@ -331,7 +331,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN break; case nameof(JsonSourceGenerationOptionsAttribute.RespectNullableAnnotations): - ignoreNullableAnnotations = (bool)namedArg.Value.Value!; + respectNullableAnnotations = (bool)namedArg.Value.Value!; break; case nameof(JsonSourceGenerationOptionsAttribute.IgnoreReadOnlyFields): @@ -417,7 +417,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN Converters = converters?.ToImmutableEquatableArray(), DefaultIgnoreCondition = defaultIgnoreCondition, DictionaryKeyPolicy = dictionaryKeyPolicy, - RespectNullableAnnotations = ignoreNullableAnnotations, + RespectNullableAnnotations = respectNullableAnnotations, IgnoreReadOnlyFields = ignoreReadOnlyFields, IgnoreReadOnlyProperties = ignoreReadOnlyProperties, IncludeFields = includeFields, From 56015c7c3b524472f0482abd88a1ed69c33d6c06 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 09:34:17 +0100 Subject: [PATCH 12/19] Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Cantú --- .../src/System/Text/Json/Serialization/JsonSerializerOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 749c896b4cb3b4..ad948fc599cf4a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -67,7 +67,7 @@ public static JsonSerializerOptions Web private static JsonSerializerOptions? s_webOptions; - // For any new option added, considered adding it to the options copied in the copy constructor below + // For any new option added, consider adding it to the options copied in the copy constructor below // and consider updating the EqualtyComparer used for comparing CachingContexts. private IJsonTypeInfoResolver? _typeInfoResolver; private JsonNamingPolicy? _dictionaryKeyPolicy; From f82556cdf75c88667ec53f816b173c2d372d29d5 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 09:52:51 +0100 Subject: [PATCH 13/19] Remove commented out code and address feedback. --- .../Metadata/DefaultJsonTypeInfoResolver.Helpers.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index 69df68510b4da5..276ee0d9c09c7d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -511,7 +511,7 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] private static NullabilityState DetermineParameterNullability(ParameterInfo parameterInfo, NullabilityInfoContext nullabilityCtx) { -#if NETCOREAPP && !NET9_0_OR_GREATER +#if NET && !NET9_0_OR_GREATER // Workaround for https://github.com/dotnet/runtime/issues/92487 // The fix has been incorporated into .NET 9 and later (and the polyfilled implementations in netfx). if (parameterInfo.GetGenericParameterDefinition() is { ParameterType: { IsGenericParameter: true } typeParam }) @@ -528,7 +528,7 @@ private static NullabilityState DetermineParameterNullability(ParameterInfo para return TranslateByte(flag); } - // Step 3. Look for nullable annotations on the generic method declaration. + // Step 3. Look for nullable annotations on the generic type declaration. if (GetNullableContextFlag(typeParam.DeclaringType!) is byte flag2) { return TranslateByte(flag2); @@ -574,12 +574,6 @@ static NullabilityState TranslateByte(byte b) => }; } #endif - - //if (parameterInfo.ParameterType.IsValueType) - //{ - // return false; - //} - NullabilityInfo nullability = nullabilityCtx.Create(parameterInfo); return nullability.WriteState; } From 22dee5c505930611ae84a3d120d0f1ed39a4b365 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 09:54:25 +0100 Subject: [PATCH 14/19] Update src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Cantú --- .../System.Text.Json.SourceGeneration.Tests.targets | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index c42a454c2c57d8..fdbc936afc1ac1 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -30,8 +30,6 @@ - From 65305305b5e3c621a8867964ea5db3ea3f60e607 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 10:08:31 +0100 Subject: [PATCH 15/19] Ensure the original parameter name flows exception messages. --- .../Text/Json/Serialization/Metadata/JsonParameterInfo.cs | 6 ++++-- .../System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs | 2 +- .../src/System/Text/Json/Serialization/ReadStack.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index 8535cd3fc9c224..3f898a21b9a3a5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -16,12 +16,14 @@ internal JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonProp Debug.Assert(matchingProperty.PropertyType == parameterInfoValues.ParameterType); Position = parameterInfoValues.Position; + Name = parameterInfoValues.Name; HasDefaultValue = parameterInfoValues.HasDefaultValue; IsNullable = parameterInfoValues.IsNullable; MatchingProperty = matchingProperty; } public int Position { get; } + public string Name { get; } public bool HasDefaultValue { get; } // The default value of the parameter. This is `DefaultValue` of the `ParameterInfo`, if specified, or the `default` for the `ParameterType`. @@ -35,8 +37,8 @@ internal JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonProp public bool IgnoreNullTokensOnRead => MatchingProperty.IgnoreNullTokensOnRead; public JsonSerializerOptions Options => MatchingProperty.Options; - // The name of the parameter as UTF-8 bytes. - public byte[] NameAsUtf8Bytes => MatchingProperty.NameAsUtf8Bytes; + // The effective name of the parameter as UTF-8 bytes. + public byte[] JsonNameAsUtf8Bytes => MatchingProperty.NameAsUtf8Bytes; public JsonNumberHandling? NumberHandling => MatchingProperty.EffectiveNumberHandling; public JsonTypeInfo JsonTypeInfo => MatchingProperty.JsonTypeInfo; public bool ShouldDeserialize => !MatchingProperty.IsIgnored; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index a5d28f8040e81c..d2caa81e27cb99 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -1152,7 +1152,7 @@ internal void ConfigureConstructorParameters() // Multiple object properties cannot bind to the same constructor parameter. ThrowHelper.ThrowInvalidOperationException_MultiplePropertiesBindToConstructorParameters( Type, - propertyInfo.Name, + parameterInfo.Name, propertyInfo.Name, confictingParameterInfo.MatchingProperty.Name); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index 3b9791a041e09b..79670aeab7b174 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -355,7 +355,7 @@ static void AppendPropertyName(StringBuilder sb, string? propertyName) { // Attempt to get the JSON property name from the JsonPropertyInfo or JsonParameterInfo. utf8PropertyName = frame.JsonPropertyInfo?.NameAsUtf8Bytes ?? - frame.CtorArgumentState?.JsonParameterInfo?.NameAsUtf8Bytes; + frame.CtorArgumentState?.JsonParameterInfo?.JsonNameAsUtf8Bytes; } } From 89e6cfd574404f1ca28f7e81c435a992c9b0984d Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 15:53:19 +0100 Subject: [PATCH 16/19] Extract exceptions to a throw helper in the new properties. --- .../src/Resources/Strings.resx | 3 + .../Metadata/JsonPropertyInfo.cs | 4 +- .../Text/Json/ThrowHelper.Serialization.cs | 6 ++ ...tJsonTypeInfoResolverTests.JsonTypeInfo.cs | 56 +++++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index cb3fda7f490928..773044acee2337 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -360,6 +360,9 @@ The extension data property '{0}.{1}' is invalid. It must implement 'IDictionary<string, JsonElement>' or 'IDictionary<string, object>', or be 'JsonObject'. + + The property type '{0}' does not support null values and therefore cannot be marked as nullable. + The type '{0}' cannot have more than one member that has the attribute '{1}'. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 9dc3b0e7d85406..cce3b7de8939f6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -260,7 +260,7 @@ public bool IsGetNullable if (value && !PropertyTypeCanBeNull) { - throw new InvalidOperationException("The property type is not nullable."); + ThrowHelper.ThrowInvalidOperationException_PropertyTypeNotNullable(this); } _isGetNullable = value; @@ -294,7 +294,7 @@ public bool IsSetNullable if (value && !PropertyTypeCanBeNull) { - throw new InvalidOperationException("The property type is not nullable."); + ThrowHelper.ThrowInvalidOperationException_PropertyTypeNotNullable(this); } if (ParameterInfo != null) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 71eb40cce1bea9..b30a58b717e666 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -546,6 +546,12 @@ public static void ThrowInvalidOperationException_SerializationDataExtensionProp throw new InvalidOperationException(SR.Format(SR.SerializationDataExtensionPropertyInvalid, jsonPropertyInfo.PropertyType, jsonPropertyInfo.MemberName)); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_PropertyTypeNotNullable(JsonPropertyInfo jsonPropertyInfo) + { + throw new InvalidOperationException(SR.Format(SR.PropertyTypeNotNullable, jsonPropertyInfo.PropertyType)); + } + [DoesNotReturn] public static void ThrowInvalidOperationException_NodeJsonObjectCustomConverterNotAllowedOnExtensionProperty() { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs index bc6e3a28ff78f7..55e1ac60fcc3c0 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Numerics; using System.Reflection; using System.Text.Json.Serialization.Metadata; using System.Text.Json.Tests; @@ -1501,5 +1502,60 @@ public static void OriginatingResolver_GetterReturnsTheSetValue(Type type) typeInfo.OriginatingResolver = JsonSerializerOptions.Default.TypeInfoResolver; Assert.Same(JsonSerializerOptions.Default.TypeInfoResolver, typeInfo.OriginatingResolver); } + + [Theory] + [InlineData(typeof(bool?))] + [InlineData(typeof(int?))] + [InlineData(typeof(Guid?))] + [InlineData(typeof(BigInteger?))] + [InlineData(typeof(string))] + [InlineData(typeof(Poco))] + public static void JsonPropertyInfo_IsNullable_PropertyTypeSupportsNull_SupportsAllConfigurations(Type propertyType) + { + JsonTypeInfo typeInfo = JsonTypeInfo.CreateJsonTypeInfo(typeof(Poco), new()); // The declaring type shouldn't matter. + JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(propertyType, "SomePropertyName"); + + Assert.True(propertyInfo.IsGetNullable); + Assert.True(propertyInfo.IsSetNullable); + + propertyInfo.IsGetNullable = false; + propertyInfo.IsSetNullable = false; + + Assert.False(propertyInfo.IsGetNullable); + Assert.False(propertyInfo.IsSetNullable); + + propertyInfo.IsGetNullable = true; + propertyInfo.IsSetNullable = true; + + Assert.True(propertyInfo.IsGetNullable); + Assert.True(propertyInfo.IsSetNullable); + } + + [Theory] + [InlineData(typeof(bool))] + [InlineData(typeof(int))] + [InlineData(typeof(Guid))] + [InlineData(typeof(BigInteger))] + public static void JsonPropertyInfo_IsNullable_PropertyTypeNotNull_CannotBeMadeNullable(Type propertyType) + { + JsonTypeInfo typeInfo = JsonTypeInfo.CreateJsonTypeInfo(typeof(Poco), new()); // The declaring type shouldn't matter. + JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(propertyType, "SomePropertyName"); + + Assert.False(propertyInfo.IsGetNullable); + Assert.False(propertyInfo.IsSetNullable); + + Assert.Throws(() => propertyInfo.IsGetNullable = true); + Assert.Throws(() => propertyInfo.IsSetNullable = true); + + Assert.False(propertyInfo.IsGetNullable); + Assert.False(propertyInfo.IsSetNullable); + + // Setting to false is a no-op. + propertyInfo.IsGetNullable = false; + propertyInfo.IsSetNullable = false; + + Assert.False(propertyInfo.IsGetNullable); + Assert.False(propertyInfo.IsSetNullable); + } } } From 1e4388f47b83f56cbc2c2a726d73b3213a7164a9 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 16:19:50 +0100 Subject: [PATCH 17/19] Extend test coverage to Nullable properties. --- .../Metadata/JsonPropertyInfoOfT.cs | 2 - .../tests/Common/NullableAnnotationsTests.cs | 54 ++++++++++++++++++- .../Serialization/NullableAnnotationsTests.cs | 10 ++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index 929c6789d2c02a..ff38badacdc59d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -305,8 +305,6 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta if (fastValue is null && !IsSetNullable && Options.RespectNullableAnnotations) { - Debug.Fail("We currently don't have an internal converter that returns null that could trigger this, if you hit this, please add a test case."); - Debug.Assert(!EffectiveConverter.IsValueType); ThrowHelper.ThrowJsonException_PropertySetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); } diff --git a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs index 392aaea5d2ef76..6ad6cad83c6258 100644 --- a/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; +using System.Linq; using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; using Xunit; -using System.Linq; #nullable enable annotations @@ -65,6 +65,7 @@ public static IEnumerable GetTypesWithNonNullablePropertyGetter() yield return Wrap(typeof(NotNullableFieldClass), nameof(NotNullableFieldClass.Field)); yield return Wrap(typeof(NotNullablePropertyWithConverterClass), nameof(NotNullablePropertyWithConverterClass.PropertyWithConverter)); yield return Wrap(typeof(NotNullPropertyClass), nameof(NotNullPropertyClass.Property)); + yield return Wrap(typeof(NotNullStructPropertyClass), nameof(NotNullStructPropertyClass.Property)); yield return Wrap(typeof(NotNullPropertyClass), nameof(NotNullPropertyClass.Property)); yield return Wrap(typeof(NotNullablePropertyParameterizedCtorClass), nameof(NotNullablePropertyParameterizedCtorClass.CtorProperty)); yield return Wrap(typeof(NotNullablePropertiesLargeParameterizedCtorClass), nameof(NotNullablePropertiesLargeParameterizedCtorClass.CtorProperty2)); @@ -117,6 +118,8 @@ public static IEnumerable GetTypesWithNullablePropertyGetter() { yield return Wrap(typeof(NullablePropertyClass), nameof(NullablePropertyClass.Property)); yield return Wrap(typeof(NullableFieldClass), nameof(NullableFieldClass.Field)); + yield return Wrap(typeof(NullStructPropertyClass), nameof(NullStructPropertyClass.Property)); + yield return Wrap(typeof(NullStructConstructorParameterClass), nameof(NullStructConstructorParameterClass.Property)); yield return Wrap(typeof(MaybeNullPropertyClass), nameof(MaybeNullPropertyClass.Property)); yield return Wrap(typeof(MaybeNullPropertyClass), nameof(MaybeNullPropertyClass.Property)); yield return Wrap(typeof(NullableObliviousPropertyClass), nameof(NullableObliviousPropertyClass.Property)); @@ -179,6 +182,8 @@ public static IEnumerable GetTypesWithNonNullablePropertySetter() yield return Wrap(typeof(NotNullableFieldClass), nameof(NotNullableFieldClass.Field)); yield return Wrap(typeof(NotNullablePropertyWithConverterClass), nameof(NotNullablePropertyWithConverterClass.PropertyWithConverter)); yield return Wrap(typeof(DisallowNullPropertyClass), nameof(DisallowNullPropertyClass.Property)); + yield return Wrap(typeof(DisallowNullStructPropertyClass), nameof(DisallowNullStructPropertyClass.Property)); + yield return Wrap(typeof(DisallowNullStructConstructorParameter), nameof(DisallowNullStructConstructorParameter.Property)); yield return Wrap(typeof(DisallowNullPropertyClass), nameof(DisallowNullPropertyClass.Property)); yield return Wrap(typeof(NotNullablePropertyParameterizedCtorClass), nameof(NotNullablePropertyParameterizedCtorClass.CtorProperty)); yield return Wrap(typeof(NotNullablePropertiesLargeParameterizedCtorClass), nameof(NotNullablePropertiesLargeParameterizedCtorClass.CtorProperty2)); @@ -232,6 +237,8 @@ public static IEnumerable GetTypesWithNullablePropertySetter() { yield return Wrap(typeof(NullablePropertyClass), nameof(NullablePropertyClass.Property)); yield return Wrap(typeof(NullableFieldClass), nameof(NullableFieldClass.Field)); + yield return Wrap(typeof(NullStructPropertyClass), nameof(NullStructPropertyClass.Property)); + yield return Wrap(typeof(NullStructConstructorParameterClass), nameof(NullStructConstructorParameterClass.Property)); yield return Wrap(typeof(AllowNullPropertyClass), nameof(AllowNullPropertyClass.Property)); yield return Wrap(typeof(AllowNullPropertyClass), nameof(AllowNullPropertyClass.Property)); yield return Wrap(typeof(NullableObliviousPropertyClass), nameof(NullableObliviousPropertyClass.Property)); @@ -592,6 +599,49 @@ public DisallowNullConstructorParameter([DisallowNull] string? property) } } + public class NullStructPropertyClass + { + public int? Property { get; set; } + } + + public class NullStructConstructorParameterClass + { + public int? Property { get; set; } + + public NullStructConstructorParameterClass() { } + + [JsonConstructor] + public NullStructConstructorParameterClass(int? property) + { + Property = property; + } + } + + public class NotNullStructPropertyClass + { + [NotNull] + public int? Property { get; set; } + } + + public class DisallowNullStructPropertyClass + { + [DisallowNull] + public int? Property { get; set; } + } + + public class DisallowNullStructConstructorParameter + { + public int? Property { get; set; } + + public DisallowNullStructConstructorParameter() { } + + [JsonConstructor] + public DisallowNullStructConstructorParameter([DisallowNull] int? property) + { + Property = property; + } + } + public class NotNullPropertyClass { [NotNull] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs index 53ff0af6064b30..33fdc55b8a1d37 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs @@ -43,6 +43,11 @@ protected NullableAnnotationsTests_Metadata(JsonSerializerWrapper serializer) [JsonSerializable(typeof(DisallowNullPropertyClass))] [JsonSerializable(typeof(AllowNullConstructorParameter))] [JsonSerializable(typeof(DisallowNullConstructorParameter))] + [JsonSerializable(typeof(NullStructPropertyClass))] + [JsonSerializable(typeof(NullStructConstructorParameterClass))] + [JsonSerializable(typeof(NotNullStructPropertyClass))] + [JsonSerializable(typeof(DisallowNullStructPropertyClass))] + [JsonSerializable(typeof(DisallowNullStructConstructorParameter))] [JsonSerializable(typeof(NotNullPropertyClass))] [JsonSerializable(typeof(MaybeNullPropertyClass))] [JsonSerializable(typeof(AllowNullPropertyClass))] @@ -101,6 +106,11 @@ protected NullableAnnotationsTests_Default(JsonSerializerWrapper serializer) [JsonSerializable(typeof(DisallowNullPropertyClass))] [JsonSerializable(typeof(AllowNullConstructorParameter))] [JsonSerializable(typeof(DisallowNullConstructorParameter))] + [JsonSerializable(typeof(NullStructPropertyClass))] + [JsonSerializable(typeof(NullStructConstructorParameterClass))] + [JsonSerializable(typeof(NotNullStructPropertyClass))] + [JsonSerializable(typeof(DisallowNullStructPropertyClass))] + [JsonSerializable(typeof(DisallowNullStructConstructorParameter))] [JsonSerializable(typeof(NotNullPropertyClass))] [JsonSerializable(typeof(MaybeNullPropertyClass))] [JsonSerializable(typeof(AllowNullPropertyClass))] From e2a37918175bf9685a8606df9ecbd0b5b8a071e4 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 16:25:21 +0100 Subject: [PATCH 18/19] Revert sln changes --- .../System.Text.Json/System.Text.Json.sln | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index 19051c30f62a80..11d7c5d4944b0f 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -1,8 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.34902.84 -MinimumVisualStudioVersion = 10.0.40219.1 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{102945CA-3736-4B2C-8E68-242A0B247F2B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{73D5739C-E382-4E22-A7D3-B82705C58C74}" @@ -49,7 +45,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "ref\Sys EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json", "src\System.Text.Json.csproj", "{1285FF43-F491-4BE0-B92C-37DA689CBD4B}" EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn3.11", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn3.11.csproj", "{5C0CE30B-DD4A-4F7A-87C0-5243F0C86885}" EndProject @@ -85,11 +81,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E49881A9-09F EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{F254F143-4704-4432-9995-20D87FA8BF8D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8F44175A-3081-4CA8-90E7-9EE6754EACAA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{8F44175A-3081-4CA8-90E7-9EE6754EACAA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{08A1F4D2-E4DA-4CD9-9107-89941EFEB79C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{08A1F4D2-E4DA-4CD9-9107-89941EFEB79C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{E9E19E81-4AE0-412D-98EA-3B14694D3CA3}" EndProject @@ -253,51 +249,47 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {102945CA-3736-4B2C-8E68-242A0B247F2B} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {5C0CE30B-DD4A-4F7A-87C0-5243F0C86885} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {FCA21178-0411-45D6-B597-B7BE145CEE33} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {256A4653-4287-44B3-BDEF-67FC1522ED2F} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {F6A18EB5-A8CC-4A39-9E85-5FA226019C3D} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} + {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} {73D5739C-E382-4E22-A7D3-B82705C58C74} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {282400DF-F3D8-4419-90F1-1E2F2D8B760C} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {6E9E4359-44F8-45AA-AEC5-D0F9FFBB13D6} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {09F77672-101E-4495-9D88-29376919C121} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {BE27618A-2916-4269-9AD5-6BC5EDC32B30} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {C2C7BA09-F9EE-4E43-8EE4-871CC000342C} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {4774F56D-16A8-4ABB-8C73-5F57609F1773} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {B6D364E7-E5DB-4CF4-B87F-3CEDA3FF7478} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {715327DF-D6D6-4043-AC78-FA58F0C0E9E3} = {F254F143-4704-4432-9995-20D87FA8BF8D} - {B815304D-502E-402C-ACE1-878DB4985CCC} = {F254F143-4704-4432-9995-20D87FA8BF8D} - {E4B72517-C694-486A-950E-6AB03C651FDC} = {F254F143-4704-4432-9995-20D87FA8BF8D} {FAB4FFF2-964D-45FF-89CC-8BB9CE618ED1} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} {C56337BB-8CBC-4EE5-AB4D-8BB0A922813E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} + {E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {6E9E4359-44F8-45AA-AEC5-D0F9FFBB13D6} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {1C8262DB-7355-40A8-A2EC-4EED7363134A} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} {9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} + {715327DF-D6D6-4043-AC78-FA58F0C0E9E3} = {F254F143-4704-4432-9995-20D87FA8BF8D} + {B815304D-502E-402C-ACE1-878DB4985CCC} = {F254F143-4704-4432-9995-20D87FA8BF8D} + {E4B72517-C694-486A-950E-6AB03C651FDC} = {F254F143-4704-4432-9995-20D87FA8BF8D} {04AEB008-EE4F-44DE-A361-2DBD2D0FD6A4} = {F254F143-4704-4432-9995-20D87FA8BF8D} {6485EED4-C313-4551-9865-8ADCED603629} = {F254F143-4704-4432-9995-20D87FA8BF8D} {143AFE8A-3490-444C-A82D-6A375EB59F01} = {F254F143-4704-4432-9995-20D87FA8BF8D} - {7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {0371C5D8-D5F5-4747-9810-D91D71D8C0E4} - {1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {E49881A9-09F6-442F-9E1D-6D87F5F837F1} - {5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {5C0CE30B-DD4A-4F7A-87C0-5243F0C86885} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {FCA21178-0411-45D6-B597-B7BE145CEE33} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {33599A6C-F340-4E1B-9B4D-CB8946C22140} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {256A4653-4287-44B3-BDEF-67FC1522ED2F} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {F6A18EB5-A8CC-4A39-9E85-5FA226019C3D} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} - {A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {E07C6980-EB71-4D19-A80A-7BEB80B635B1} {B32C4C93-6C85-4E88-B966-5A9F5CAD54B1} = {F254F143-4704-4432-9995-20D87FA8BF8D} {03D5F977-D2DA-4586-A662-C99B0E0EEC20} = {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} {A3FCA745-7F4D-4FB8-85A5-422AC92B2704} = {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} + {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {D0277072-F248-47EF-A417-D2612987D899} = {8F44175A-3081-4CA8-90E7-9EE6754EACAA} {A86642C4-E388-4871-B9FB-A5FCB8BF5CB0} = {8F44175A-3081-4CA8-90E7-9EE6754EACAA} - {8E1C465B-1DA9-4C2A-A6B5-082C55EFB95E} = {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} - {10E5D5FA-09FE-4E7A-A8E0-377BC228ACCB} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} {8F44175A-3081-4CA8-90E7-9EE6754EACAA} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} + {8E1C465B-1DA9-4C2A-A6B5-082C55EFB95E} = {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} {08A1F4D2-E4DA-4CD9-9107-89941EFEB79C} = {E9E19E81-4AE0-412D-98EA-3B14694D3CA3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5868B757-D821-41FC-952E-2113A0519506} EndGlobalSection - GlobalSection(SharedMSBuildProjectFiles) = preSolution - ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a3fca745-7f4d-4fb8-85a5-422ac92b2704}*SharedItemsImports = 5 - ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a86642c4-e388-4871-b9fb-a5fcb8bf5cb0}*SharedItemsImports = 5 - EndGlobalSection EndGlobal From 4d63f59c4bd26d0b90f6b4068f46fd4e7b39e56e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 22 May 2024 17:26:13 +0100 Subject: [PATCH 19/19] Add second-pass review improvements. --- ...ctWithParameterizedConstructorConverter.Large.cs | 2 +- ...ctWithParameterizedConstructorConverter.Small.cs | 2 +- .../Json/Serialization/JsonSerializerOptions.cs | 5 +++++ .../Metadata/DefaultJsonTypeInfoResolver.Helpers.cs | 7 +++++-- .../Metadata/JsonMetadataServices.Helpers.cs | 6 ++++-- .../Json/Serialization/Metadata/JsonPropertyInfo.cs | 13 +++++++++++-- .../Json/Serialization/Metadata/JsonTypeInfo.cs | 5 ++--- 7 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index 5452390981fa4c..f435febc7f393a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -24,7 +24,7 @@ protected sealed override bool ReadAndCacheConstructorArgument(scoped ref ReadSt { if (arg == null && !jsonParameterInfo.IsNullable && jsonParameterInfo.Options.RespectNullableAnnotations) { - ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(jsonParameterInfo.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); + ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(jsonParameterInfo.Name, state.Current.JsonTypeInfo.Type); } ((object[])state.Current.CtorArgumentState!.Arguments)[jsonParameterInfo.Position] = arg!; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs index 3942d4fa631a78..0179dc68310fc0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs @@ -77,7 +77,7 @@ private static bool TryRead( } else if (!info.IsNullable && info.Options.RespectNullableAnnotations) { - ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(info.MatchingProperty.Name, state.Current.JsonTypeInfo.Type); + ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(info.Name, state.Current.JsonTypeInfo.Type); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index ad948fc599cf4a..acee88788c3a70 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -789,6 +789,11 @@ public string NewLine /// Thrown if this property is set after serialization or deserialization has occurred. /// /// + /// Nullability annotations are resolved from the properties, fields and constructor parameters + /// that are used by the serializer. This includes annotations stemming from attributes such as + /// , , + /// and . + /// /// Due to restrictions in how nullable reference types are represented at run time, /// this setting only governs nullability annotations of non-generic properties and fields. /// It cannot be used to enforce nullability annotations of root-level types or generic parameters. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs index 276ee0d9c09c7d..a6b71ea8220773 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs @@ -75,6 +75,8 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte if (converter.ConstructorIsParameterized) { + // NB parameter metadata must be populated *before* property metadata + // so that properties can be linked to their associated parameters. PopulateParameterInfoValues(typeInfo, nullabilityCtx); } @@ -511,9 +513,10 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] private static NullabilityState DetermineParameterNullability(ParameterInfo parameterInfo, NullabilityInfoContext nullabilityCtx) { -#if NET && !NET9_0_OR_GREATER +#if NET8_0 // Workaround for https://github.com/dotnet/runtime/issues/92487 - // The fix has been incorporated into .NET 9 and later (and the polyfilled implementations in netfx). + // The fix has been incorporated into .NET 9 (and the polyfilled implementations in netfx). + // Should be removed once .NET 8 support is dropped. if (parameterInfo.GetGenericParameterDefinition() is { ParameterType: { IsGenericParameter: true } typeParam }) { // Step 1. Look for nullable annotations on the type parameter. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs index 7ff6048a8fa393..b9cf70297f1ab1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs @@ -33,6 +33,8 @@ private static JsonTypeInfo CreateCore(JsonSerializerOptions options, Json var typeInfo = new JsonTypeInfo(converter, options); if (objectInfo.ObjectWithParameterizedConstructorCreator != null) { + // NB parameter metadata must be populated *before* property metadata + // so that properties can be linked to their associated parameters. typeInfo.CreateObjectWithArgs = objectInfo.ObjectWithParameterizedConstructorCreator; PopulateParameterInfoValues(typeInfo, objectInfo.ConstructorParameterMetadataInitializer); } @@ -118,9 +120,9 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, Func or . /// /// This property has no effect on serialization unless the - /// has been enabled, in which case the serializer will reject any values returned by the getter. + /// property has been enabled, in which case the serializer will reject any values returned by the getter. /// public bool IsGetNullable { @@ -279,12 +279,17 @@ public bool IsGetNullable /// /// The current is not a reference type or . /// + /// /// Contracts originating from or , /// derive the value of this property from nullable reference type annotations, including annotations /// from attributes such as or . /// /// This property has no effect on deserialization unless the - /// has been enabled, in which case the serializer will reject any deserialization results. + /// property has been enabled, in which case the serializer will reject any deserialization results. + /// + /// If the property has been associated with a deserialization constructor parameter, + /// this setting reflected the nullability annotation of the parameter and not the property setter. + /// public bool IsSetNullable { get => _isSetNullable; @@ -299,6 +304,7 @@ public bool IsSetNullable if (ParameterInfo != null) { + // Ensure the new setting is reflected in the associated parameter. ParameterInfo.IsNullable = value; } @@ -676,6 +682,9 @@ private void DetermineParameterInfo() ParameterInfo = DeclaringTypeInfo.CreateMatchingParameterInfo(this); if (ParameterInfo != null) { + // Given that we have associated a constructor parameter to this property, + // deserialization is no longer governed by the property setter. + // Ensure nullability configuration is copied over from the parameter to the property. _isSetNullable = ParameterInfo.IsNullable; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index d2caa81e27cb99..71d47aab99441f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -1147,17 +1147,16 @@ internal void ConfigureConstructorParameters() } ParameterLookupKey paramKey = new(propertyInfo.PropertyType, propertyInfo.Name); - if (parameterIndex.TryGetValue(paramKey, out JsonParameterInfo? confictingParameterInfo)) + if (!parameterIndex.TryAdd(paramKey, parameterInfo)) { // Multiple object properties cannot bind to the same constructor parameter. ThrowHelper.ThrowInvalidOperationException_MultiplePropertiesBindToConstructorParameters( Type, parameterInfo.Name, propertyInfo.Name, - confictingParameterInfo.MatchingProperty.Name); + parameterIndex[paramKey].MatchingProperty.Name); } - parameterIndex.Add(paramKey, parameterInfo); parameterCache.Add(parameterInfo); }