diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs index 2da593d7628634..3560d8a1190c2d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfo.cs @@ -8,7 +8,12 @@ namespace System.Reflection /// /// A class that represents nullability info /// - public sealed class NullabilityInfo +#if NET + public +#else + internal +#endif + sealed class NullabilityInfo { internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState writeState, NullabilityInfo? elementType, NullabilityInfo[] typeArguments) @@ -46,7 +51,12 @@ internal NullabilityInfo(Type type, NullabilityState readState, NullabilityState /// /// An enum that represents nullability state /// - public enum NullabilityState +#if NET + public +#else + internal +#endif + enum NullabilityState { /// /// Nullability context not enabled (oblivious) diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs index 9411bad737716e..5c56ef25197591 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/NullabilityInfoContext.cs @@ -11,7 +11,12 @@ namespace System.Reflection /// Provides APIs for populating nullability information/context from reflection members: /// , , and . /// - public sealed class NullabilityInfoContext +#if NET + public +#else + internal +#endif + sealed class NullabilityInfoContext { private const string CompilerServicesNameSpace = "System.Runtime.CompilerServices"; private readonly Dictionary _publicOnlyModules = new(); @@ -65,7 +70,11 @@ private enum NotAnnotatedStatus /// public NullabilityInfo Create(ParameterInfo parameterInfo) { +#if NET ArgumentNullException.ThrowIfNull(parameterInfo); +#else + NetstandardHelpers.ThrowIfNull(parameterInfo, nameof(parameterInfo)); +#endif EnsureIsSupported(); @@ -190,7 +199,11 @@ private static void CheckNullabilityAttributes(NullabilityInfo nullability, ILis /// public NullabilityInfo Create(PropertyInfo propertyInfo) { +#if NET ArgumentNullException.ThrowIfNull(propertyInfo); +#else + NetstandardHelpers.ThrowIfNull(propertyInfo, nameof(propertyInfo)); +#endif EnsureIsSupported(); @@ -212,7 +225,9 @@ public NullabilityInfo Create(PropertyInfo propertyInfo) if (setter != null) { - CheckNullabilityAttributes(nullability, setter.GetParametersAsSpan()[^1].GetCustomAttributesData()); + ReadOnlySpan parameters = setter.GetParametersAsSpan(); + ParameterInfo parameter = parameters[parameters.Length - 1]; + CheckNullabilityAttributes(nullability, parameter.GetCustomAttributesData()); } else { @@ -243,7 +258,11 @@ private bool IsPrivateOrInternalMethodAndAnnotationDisabled(MethodBase method) /// public NullabilityInfo Create(EventInfo eventInfo) { +#if NET ArgumentNullException.ThrowIfNull(eventInfo); +#else + NetstandardHelpers.ThrowIfNull(eventInfo, nameof(eventInfo)); +#endif EnsureIsSupported(); @@ -260,7 +279,11 @@ public NullabilityInfo Create(EventInfo eventInfo) /// public NullabilityInfo Create(FieldInfo fieldInfo) { +#if NET ArgumentNullException.ThrowIfNull(fieldInfo); +#else + NetstandardHelpers.ThrowIfNull(fieldInfo, nameof(fieldInfo)); +#endif EnsureIsSupported(); @@ -497,7 +520,11 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T Debug.Assert(genericParameter.IsGenericParameter); if (reflectedType is not null +#if NET && !genericParameter.IsGenericMethodParameter +#else + && !genericParameter.IsGenericMethodParameter() +#endif && TryUpdateGenericTypeParameterNullabilityFromReflectedType(nullability, genericParameter, reflectedType, reflectedType)) { return true; @@ -528,7 +555,12 @@ private bool TryUpdateGenericParameterNullability(NullabilityInfo nullability, T private bool TryUpdateGenericTypeParameterNullabilityFromReflectedType(NullabilityInfo nullability, Type genericParameter, Type context, Type reflectedType) { - Debug.Assert(genericParameter.IsGenericParameter && !genericParameter.IsGenericMethodParameter); + Debug.Assert(genericParameter.IsGenericParameter && +#if NET + !genericParameter.IsGenericMethodParameter); +#else + !genericParameter.IsGenericMethodParameter()); +#endif Type contextTypeDefinition = context.IsGenericType && !context.IsGenericTypeDefinition ? context.GetGenericTypeDefinition() : context; if (genericParameter.DeclaringType == contextTypeDefinition) @@ -666,4 +698,54 @@ public bool ParseNullableState(int index, ref NullabilityState state) } } } + +#if !NET + internal static class NetstandardHelpers + { + public static void ThrowIfNull(object? argument, string paramName) + { + if (argument is null) + { + Throw(paramName); + static void Throw(string paramName) => throw new ArgumentNullException(paramName); + } + } + + [Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", + Justification = "This is finding the MemberInfo with the same MetadataToken as specified MemberInfo. If the specified MemberInfo " + + "exists and wasn't trimmed, then the current Type's MemberInfo couldn't have been trimmed.")] + public static MemberInfo GetMemberWithSameMetadataDefinitionAs(this Type type, MemberInfo member) + { + ThrowIfNull(member, nameof(member)); + + const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; + foreach (MemberInfo myMemberInfo in type.GetMembers(all)) + { + if (myMemberInfo.HasSameMetadataDefinitionAs(member)) + { + return myMemberInfo; + } + } + + throw new MissingMemberException(type.FullName, member.Name); + } + + private static bool HasSameMetadataDefinitionAs(this MemberInfo info, MemberInfo other) + { + if (info.MetadataToken != other.MetadataToken) + return false; + + if (!info.Module.Equals(other.Module)) + return false; + + return true; + } + + public static bool IsGenericMethodParameter(this Type type) + => type.IsGenericParameter && type.DeclaringMethod is not null; + + public static ReadOnlySpan GetParametersAsSpan(this MethodBase metaMethod) + => metaMethod.GetParameters(); + } +#endif } diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs index f1c76e7674f07a..8fd1e3353dc4f8 100644 --- a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs @@ -115,6 +115,11 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults) /// public JsonCommentHandling ReadCommentHandling { get; set; } + /// + /// Specifies the default value of when set. + /// + public bool RespectNullableAnnotations { get; set; } + /// /// Specifies the default value of when set. /// diff --git a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs index 3f3ecb506fd83d..5512321b0e2f04 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/RoslynExtensions.cs @@ -50,6 +50,12 @@ public static ITypeSymbol EraseCompileTimeMetadata(this Compilation compilation, type = type.WithNullableAnnotation(NullableAnnotation.None); } + if (type is IArrayTypeSymbol arrayType) + { + ITypeSymbol elementType = compilation.EraseCompileTimeMetadata(arrayType.ElementType); + return compilation.CreateArrayTypeSymbol(elementType, arrayType.Rank); + } + if (type is INamedTypeSymbol namedType) { if (namedType.IsTupleType) @@ -189,6 +195,9 @@ SpecialType.System_Byte or SpecialType.System_UInt16 or SpecialType.System_UInt3 SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal; } + public static bool IsNullableType(this ITypeSymbol type) + => !type.IsValueType || type.OriginalDefinition.SpecialType is SpecialType.System_Nullable_T; + public static bool IsNullableValueType(this ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? elementType) { if (type.IsValueType && type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T }) @@ -269,5 +278,107 @@ public static string GetTypeKindKeyword(this TypeDeclarationSyntax typeDeclarati return null; } } + + public static void ResolveNullabilityAnnotations(this IFieldSymbol field, out bool isGetterNonNullable, out bool isSetterNonNullable) + { + if (field.Type.IsNullableType()) + { + // Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters, + // (e.g. the same metadata is being used for both KeyValuePair and KeyValuePair), + // we derive nullability annotations from the original definition of the field and not its instantiation. + // This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader. + field = field.OriginalDefinition; + + isGetterNonNullable = IsOutputTypeNonNullable(field, field.Type); + isSetterNonNullable = IsInputTypeNonNullable(field, field.Type); + } + else + { + isGetterNonNullable = isSetterNonNullable = false; + } + } + + public static void ResolveNullabilityAnnotations(this IPropertySymbol property, out bool isGetterNonNullable, out bool isSetterNonNullable) + { + if (property.Type.IsNullableType()) + { + // Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters, + // (e.g. the same metadata is being used for both KeyValuePair and KeyValuePair), + // we derive nullability annotations from the original definition of the field and not its instantiation. + // This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader. + property = property.OriginalDefinition; + + isGetterNonNullable = property.GetMethod != null && IsOutputTypeNonNullable(property, property.Type); + isSetterNonNullable = property.SetMethod != null && IsInputTypeNonNullable(property, property.Type); + } + else + { + isGetterNonNullable = isSetterNonNullable = false; + } + } + + public static bool IsNullable(this IParameterSymbol parameter) + { + if (parameter.Type.IsNullableType()) + { + // Because System.Text.Json cannot distinguish between nullable and non-nullable type parameters, + // (e.g. the same metadata is being used for both KeyValuePair and KeyValuePair), + // we derive nullability annotations from the original definition of the field and not instation. + // This preserves compatibility with the capabilities of the reflection-based NullabilityInfo reader. + parameter = parameter.OriginalDefinition; + return !IsInputTypeNonNullable(parameter, parameter.Type); + } + + return false; + } + + private static bool IsOutputTypeNonNullable(this ISymbol symbol, ITypeSymbol returnType) + { + if (symbol.HasCodeAnalysisAttribute("MaybeNullAttribute")) + { + return false; + } + + if (symbol.HasCodeAnalysisAttribute("NotNullAttribute")) + { + return true; + } + + if (returnType is ITypeParameterSymbol { HasNotNullConstraint: false }) + { + return false; + } + + return returnType.NullableAnnotation is NullableAnnotation.NotAnnotated; + } + + private static bool IsInputTypeNonNullable(this ISymbol symbol, ITypeSymbol inputType) + { + Debug.Assert(inputType.IsNullableType()); + + if (symbol.HasCodeAnalysisAttribute("AllowNullAttribute")) + { + return false; + } + + if (symbol.HasCodeAnalysisAttribute("DisallowNullAttribute")) + { + return true; + } + + if (inputType is ITypeParameterSymbol { HasNotNullConstraint: false }) + { + return false; + } + + return inputType.NullableAnnotation is NullableAnnotation.NotAnnotated; + } + + private static bool HasCodeAnalysisAttribute(this ISymbol symbol, string attributeName) + { + return symbol.GetAttributes().Any(attr => + attr.AttributeClass?.Name == attributeName && + attr.AttributeClass.ContainingNamespace.ToDisplayString() == "System.Diagnostics.CodeAnalysis"); + } } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs index 06d15bd3b030b9..99a07d9e40d68d 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs @@ -30,6 +30,9 @@ private static class ExceptionMessages public const string InvalidSerializablePropertyConfiguration = "Invalid serializable-property configuration specified for type '{0}'. For more information, see 'JsonSourceGenerationMode.Serialization'."; + + public const string PropertyGetterDisallowNull = + "The property or field '{0}' on type '{1}' doesn't allow getting null values. Consider updating its nullability annotation."; }; } } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 12173a0e917de9..febf787afeef8a 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -41,6 +41,7 @@ private sealed partial class Emitter // global::fully.qualified.name for referenced types private const string InvalidOperationExceptionTypeRef = "global::System.InvalidOperationException"; + private const string JsonExceptionTypeRef = "global::System.Text.Json.JsonException"; private const string TypeTypeRef = "global::System.Type"; private const string UnsafeTypeRef = "global::System.Runtime.CompilerServices.Unsafe"; private const string EqualityComparerTypeRef = "global::System.Collections.Generic.EqualityComparer"; @@ -672,6 +673,15 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe writer.WriteLine($"properties[{i}].Order = {property.Order};"); } + if (property.IsGetterNonNullableAnnotation) + { + writer.WriteLine($"properties[{i}].IsGetNullable = false;"); + } + if (property.IsSetterNonNullableAnnotation) + { + writer.WriteLine($"properties[{i}].IsSetNullable = false;"); + } + writer.WriteLine(); } @@ -682,33 +692,34 @@ private void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMe private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, string ctorParamMetadataInitMethodName, TypeGenerationSpec typeGenerationSpec) { - const string parametersVarName = "parameters"; - ImmutableEquatableArray parameters = typeGenerationSpec.CtorParamGenSpecs; ImmutableEquatableArray propertyInitializers = typeGenerationSpec.PropertyInitializerSpecs; int paramCount = parameters.Count + propertyInitializers.Count(propInit => !propInit.MatchesConstructorParameter); Debug.Assert(paramCount > 0); - writer.WriteLine($"private static {JsonParameterInfoValuesTypeRef}[] {ctorParamMetadataInitMethodName}()"); + writer.WriteLine($"private static {JsonParameterInfoValuesTypeRef}[] {ctorParamMetadataInitMethodName}() => new {JsonParameterInfoValuesTypeRef}[]"); writer.WriteLine('{'); writer.Indentation++; - writer.WriteLine($"var {parametersVarName} = new {JsonParameterInfoValuesTypeRef}[{paramCount}];"); - writer.WriteLine(); - + int i = 0; foreach (ParameterGenerationSpec spec in parameters) { writer.WriteLine($$""" - {{parametersVarName}}[{{spec.ParameterIndex}}] = new() + new() { Name = {{FormatStringLiteral(spec.Name)}}, ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}), Position = {{spec.ParameterIndex}}, HasDefaultValue = {{FormatBoolLiteral(spec.HasDefaultValue)}}, - DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}} - }; - + DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}}, + IsNullable = {{FormatBoolLiteral(spec.IsNullable)}}, + }, """); + + if (++i < paramCount) + { + writer.WriteLine(); + } } foreach (PropertyInitializerGenerationSpec spec in propertyInitializers) @@ -719,20 +730,22 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin } writer.WriteLine($$""" - {{parametersVarName}}[{{spec.ParameterIndex}}] = new() + new() { Name = {{FormatStringLiteral(spec.Name)}}, ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}), Position = {{spec.ParameterIndex}}, - }; - + }, """); - } - writer.WriteLine($"return {parametersVarName};"); + if (++i < paramCount) + { + writer.WriteLine(); + } + } writer.Indentation--; - writer.WriteLine('}'); + writer.WriteLine("};"); } private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGenerationSpec contextSpec, string serializeMethodName, TypeGenerationSpec typeGenSpec) @@ -760,6 +773,8 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio writer.WriteLine($"{WriterVarName}.WriteStartObject();"); writer.WriteLine(); + bool generateDisallowNullThrowHelper = false; + // Provide generation logic for each prop. foreach (int i in typeGenSpec.FastPathPropertyIndices) { @@ -784,7 +799,7 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio Debug.Assert(!_propertyNames.TryGetValue(effectiveJsonPropertyName, out string? existingName) || existingName == propertyNameFieldName); _propertyNames.TryAdd(effectiveJsonPropertyName, propertyNameFieldName); - DefaultCheckType defaultCheckType = GetDefaultCheckType(contextSpec, propertyGenSpec); + SerializedValueCheckType defaultCheckType = GetCheckType(contextSpec, propertyGenSpec); // For properties whose declared type differs from that of the serialized type // perform an explicit cast -- this is to account for hidden properties or diamond ambiguity. @@ -793,7 +808,7 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio : ValueVarName; string propValueExpr; - if (defaultCheckType != DefaultCheckType.None) + if (defaultCheckType != SerializedValueCheckType.None) { // Use temporary variable to evaluate property value only once string localVariableName = $"__value_{propertyGenSpec.NameSpecifiedInSourceCode.TrimStart('@')}"; @@ -807,25 +822,44 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio switch (defaultCheckType) { - case DefaultCheckType.Null: + case SerializedValueCheckType.IgnoreWhenNull: writer.WriteLine($"if ({propValueExpr} != null)"); writer.WriteLine('{'); writer.Indentation++; + + GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr); + + writer.Indentation--; + writer.WriteLine('}'); break; - case DefaultCheckType.Default: + case SerializedValueCheckType.IgnoreWhenDefault: writer.WriteLine($"if (!{EqualityComparerTypeRef}<{propertyGenSpec.PropertyType.FullyQualifiedName}>.Default.Equals(default, {propValueExpr}))"); writer.WriteLine('{'); writer.Indentation++; + + GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr); + + writer.Indentation--; + writer.WriteLine('}'); break; - } - GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr); + case SerializedValueCheckType.DisallowNull: + writer.WriteLine($$""" + if ({{propValueExpr}} is null) + { + ThrowPropertyNullException({{FormatStringLiteral(propertyGenSpec.EffectiveJsonPropertyName)}}); + } - if (defaultCheckType != DefaultCheckType.None) - { - writer.Indentation--; - writer.WriteLine('}'); + """); + + GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr); + generateDisallowNullThrowHelper = true; + break; + + default: + GenerateSerializePropertyStatement(writer, propertyTypeSpec, propertyNameFieldName, propValueExpr); + break; } } @@ -839,6 +873,17 @@ private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGeneratio writer.WriteLine($"((global::{JsonConstants.IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();"); } + if (generateDisallowNullThrowHelper) + { + writer.WriteLine(); + writer.WriteLine($$""" + static void ThrowPropertyNullException(string propertyName) + { + throw new {{JsonExceptionTypeRef}}(string.Format("{{ExceptionMessages.PropertyGetterDisallowNull}}", propertyName, {{FormatStringLiteral(typeGenSpec.TypeRef.Name)}})); + } + """); + } + writer.Indentation--; writer.WriteLine('}'); } @@ -978,20 +1023,22 @@ private static void GenerateSerializePropertyStatement(SourceWriter writer, Type } } - private enum DefaultCheckType + private enum SerializedValueCheckType { None, - Null, - Default, + IgnoreWhenNull, + IgnoreWhenDefault, + DisallowNull, } - private static DefaultCheckType GetDefaultCheckType(ContextGenerationSpec contextSpec, PropertyGenerationSpec propertySpec) + private static SerializedValueCheckType GetCheckType(ContextGenerationSpec contextSpec, PropertyGenerationSpec propertySpec) { return (propertySpec.DefaultIgnoreCondition ?? contextSpec.GeneratedOptionsSpec?.DefaultIgnoreCondition) switch { - JsonIgnoreCondition.WhenWritingNull => propertySpec.PropertyType.CanBeNull ? DefaultCheckType.Null : DefaultCheckType.None, - JsonIgnoreCondition.WhenWritingDefault => propertySpec.PropertyType.CanBeNull ? DefaultCheckType.Null : DefaultCheckType.Default, - _ => DefaultCheckType.None, + JsonIgnoreCondition.WhenWritingNull => propertySpec.PropertyType.CanBeNull ? SerializedValueCheckType.IgnoreWhenNull : SerializedValueCheckType.None, + JsonIgnoreCondition.WhenWritingDefault => propertySpec.PropertyType.CanBeNull ? SerializedValueCheckType.IgnoreWhenNull : SerializedValueCheckType.IgnoreWhenDefault, + _ when propertySpec.IsGetterNonNullableAnnotation && contextSpec.GeneratedOptionsSpec?.RespectNullableAnnotations is true => SerializedValueCheckType.DisallowNull, + _ => SerializedValueCheckType.None, }; } @@ -1135,6 +1182,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti if (optionsSpec.DictionaryKeyPolicy is JsonKnownNamingPolicy dictionaryKeyPolicy) writer.WriteLine($"DictionaryKeyPolicy = {FormatNamingPolicy(dictionaryKeyPolicy)},"); + if (optionsSpec.RespectNullableAnnotations is bool respectNullableAnnotations) + writer.WriteLine($"RespectNullableAnnotations = {FormatBoolLiteral(respectNullableAnnotations)},"); + if (optionsSpec.IgnoreReadOnlyFields is bool ignoreReadOnlyFields) writer.WriteLine($"IgnoreReadOnlyFields = {FormatBoolLiteral(ignoreReadOnlyFields)},"); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 5d94f860cd785f..66d40939ca4233 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -268,6 +268,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN int? defaultBufferSize = null; JsonIgnoreCondition? defaultIgnoreCondition = null; JsonKnownNamingPolicy? dictionaryKeyPolicy = null; + bool? respectNullableAnnotations = null; bool? ignoreReadOnlyFields = null; bool? ignoreReadOnlyProperties = null; bool? includeFields = null; @@ -329,6 +330,10 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN dictionaryKeyPolicy = (JsonKnownNamingPolicy)namedArg.Value.Value!; break; + case nameof(JsonSourceGenerationOptionsAttribute.RespectNullableAnnotations): + respectNullableAnnotations = (bool)namedArg.Value.Value!; + break; + case nameof(JsonSourceGenerationOptionsAttribute.IgnoreReadOnlyFields): ignoreReadOnlyFields = (bool)namedArg.Value.Value!; break; @@ -412,6 +417,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN Converters = converters?.ToImmutableEquatableArray(), DefaultIgnoreCondition = defaultIgnoreCondition, DictionaryKeyPolicy = dictionaryKeyPolicy, + RespectNullableAnnotations = respectNullableAnnotations, IgnoreReadOnlyFields = ignoreReadOnlyFields, IgnoreReadOnlyProperties = ignoreReadOnlyProperties, IncludeFields = includeFields, @@ -1120,7 +1126,9 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type) out bool canUseGetter, out bool canUseSetter, out bool hasJsonIncludeButIsInaccessible, - out bool setterIsInitOnly); + out bool setterIsInitOnly, + out bool isGetterNonNullable, + out bool isSetterNonNullable); if (hasJsonIncludeButIsInaccessible) { @@ -1184,6 +1192,8 @@ private bool IsValidDataExtensionPropertyType(ITypeSymbol type) PropertyType = propertyTypeRef, DeclaringType = declaringType, ConverterType = converterType, + IsGetterNonNullableAnnotation = isGetterNonNullable, + IsSetterNonNullableAnnotation = isSetterNonNullable, }; } @@ -1301,7 +1311,9 @@ private void ProcessMember( out bool canUseGetter, out bool canUseSetter, out bool hasJsonIncludeButIsInaccessible, - out bool isSetterInitOnly) + out bool isSetterInitOnly, + out bool isGetterNonNullable, + out bool isSetterNonNullable) { isAccessible = false; isReadOnly = false; @@ -1310,6 +1322,8 @@ private void ProcessMember( canUseSetter = false; hasJsonIncludeButIsInaccessible = false; isSetterInitOnly = false; + isGetterNonNullable = false; + isSetterNonNullable = false; switch (memberInfo) { @@ -1358,6 +1372,8 @@ private void ProcessMember( { isReadOnly = true; } + + propertyInfo.ResolveNullabilityAnnotations(out isGetterNonNullable, out isSetterNonNullable); break; case IFieldSymbol fieldInfo: isReadOnly = fieldInfo.IsReadOnly; @@ -1380,6 +1396,8 @@ private void ProcessMember( { hasJsonIncludeButIsInaccessible = hasJsonInclude; } + + fieldInfo.ResolveNullabilityAnnotations(out isGetterNonNullable, out isSetterNonNullable); break; default: Debug.Fail("Method given an invalid symbol type."); @@ -1430,6 +1448,7 @@ private void ProcessMember( HasDefaultValue = parameterInfo.HasExplicitDefaultValue, DefaultValue = parameterInfo.HasExplicitDefaultValue ? parameterInfo.ExplicitDefaultValue : null, ParameterIndex = i, + IsNullable = parameterInfo.IsNullable(), }; } } diff --git a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs index 68e32d01531569..2dea67e2159ccb 100644 --- a/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/ParameterGenerationSpec.cs @@ -32,5 +32,6 @@ public sealed record ParameterGenerationSpec // so it always satisfies the structural equality requirement for the record. public required object? DefaultValue { get; init; } public required int ParameterIndex { get; init; } + public required bool IsNullable { get; init; } } } diff --git a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs index 214c32b4d19e21..a003a9d74d48c2 100644 --- a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs @@ -97,6 +97,16 @@ public sealed record PropertyGenerationSpec /// public required bool CanUseSetter { get; init; } + /// + /// Whether the property getter returns a nullable type with a non-nullable annotation. + /// + public required bool IsGetterNonNullableAnnotation { get; init; } + + /// + /// Whether the property setter accepts a nullable type with a non-nullable annotation. + /// + public required bool IsSetterNonNullableAnnotation { get; init; } + /// /// The for the property. /// diff --git a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs index e16bebb6ceb49e..9b7ecc8ea6920a 100644 --- a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs @@ -28,6 +28,8 @@ public sealed record SourceGenerationOptionsSpec public required JsonKnownNamingPolicy? DictionaryKeyPolicy { get; init; } + public required bool? RespectNullableAnnotations { get; init; } + public required bool? IgnoreReadOnlyFields { get; init; } public required bool? IgnoreReadOnlyProperties { get; init; } diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 3fd6af13262bf1..f50564ba081fc8 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -401,6 +401,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } } + public bool RespectNullableAnnotations { get { throw null; } set { } } public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver? TypeInfoResolver { get { throw null; } set { } } public System.Collections.Generic.IList TypeInfoResolverChain { get { throw null; } } public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } @@ -1091,6 +1092,7 @@ public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefau public bool PropertyNameCaseInsensitive { get { throw null; } set { } } public System.Text.Json.Serialization.JsonKnownNamingPolicy PropertyNamingPolicy { get { throw null; } set { } } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } + public bool RespectNullableAnnotations { get { throw null; } set { } } public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } set { } } public bool UseStringEnumConverter { get { throw null; } set { } } @@ -1274,6 +1276,7 @@ public sealed partial class JsonParameterInfoValues public JsonParameterInfoValues() { } public object? DefaultValue { get { throw null; } init { } } public bool HasDefaultValue { get { throw null; } init { } } + public bool IsNullable { get { throw null; } init { } } public string Name { get { throw null; } init { } } public System.Type ParameterType { get { throw null; } init { } } public int Position { get { throw null; } init { } } @@ -1294,7 +1297,9 @@ internal JsonPropertyInfo() { } public System.Text.Json.Serialization.JsonConverter? CustomConverter { get { throw null; } set { } } public System.Func? 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 { } } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index e343daa47cf5aa..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}'. @@ -737,4 +740,16 @@ New line can be only "\n" or "\r\n". + + 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 7b49b89792cd74..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 - @@ -347,6 +346,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/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/AppContextSwitchHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs index 3e1291c676d922..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 @@ -10,5 +10,11 @@ internal static class AppContextSwitchHelper switchName: "System.Text.Json.Serialization.EnableSourceGenReflectionFallback", isEnabled: out bool value) ? value : false; + + public static bool RespectNullableAnnotationsDefault { get; } = + AppContext.TryGetSwitch( + 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/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 b00661bd568f45..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 @@ -22,6 +22,11 @@ protected sealed override bool ReadAndCacheConstructorArgument(scoped ref ReadSt if (success && !(arg == null && jsonParameterInfo.IgnoreNullTokensOnRead)) { + if (arg == null && !jsonParameterInfo.IsNullable && jsonParameterInfo.Options.RespectNullableAnnotations) + { + ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(jsonParameterInfo.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 @@ -53,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 af80e25fc02377..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 @@ -66,15 +66,25 @@ 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 (success) { + if (value is null) + { + if (info.IgnoreNullTokensOnRead) + { + // Use default value specified on parameter, if any. + value = info.DefaultValue; + } + else if (!info.IsNullable && info.Options.RespectNullableAnnotations) + { + ThrowHelper.ThrowJsonException_ConstructorParameterDisallowNull(info.Name, state.Current.JsonTypeInfo.Type); + } + } + state.Current.MarkRequiredPropertyAsRead(jsonParameterInfo.MatchingProperty); } + arg = value; return success; } @@ -83,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: @@ -106,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..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,6 +70,7 @@ options.Encoder is null && // Ensure options values are consistent with expected defaults. options.DefaultIgnoreCondition == generatedSerializerOptions.DefaultIgnoreCondition && + 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 e171d8cdebe490..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,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._respectNullableAnnotations == right._respectNullableAnnotations && 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._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 e7123e3a4431e2..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 @@ -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, 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; private JsonNamingPolicy? _jsonPropertyNamingPolicy; @@ -85,6 +86,7 @@ public static JsonSerializerOptions Web private int _maxDepth; private bool _allowOutOfOrderMetadataProperties; private bool _allowTrailingCommas; + private bool _respectNullableAnnotations = AppContextSwitchHelper.RespectNullableAnnotationsDefault; 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; + _respectNullableAnnotations = options._respectNullableAnnotations; _ignoreNullValues = options._ignoreNullValues; _ignoreReadOnlyProperties = options._ignoreReadOnlyProperties; _ignoreReadonlyFields = options._ignoreReadonlyFields; @@ -779,6 +782,32 @@ public string NewLine } } + /// + /// 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. + /// + /// + /// 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. + /// + public bool RespectNullableAnnotations + { + get => _respectNullableAnnotations; + set + { + VerifyMutable(); + _respectNullableAnnotations = value; + } + } + /// /// Returns true if options uses compatible built-in resolvers or a combination of compatible built-in resolvers. /// @@ -1052,7 +1081,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 3ce9c15709819a..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 @@ -71,12 +71,16 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte if (typeInfo.Kind is JsonTypeInfoKind.Object) { - PopulateProperties(typeInfo); + NullabilityInfoContext nullabilityCtx = new(); if (converter.ConstructorIsParameterized) { - PopulateParameterInfoValues(typeInfo); + // NB parameter metadata must be populated *before* property metadata + // so that properties can be linked to their associated parameters. + PopulateParameterInfoValues(typeInfo, nullabilityCtx); } + + PopulateProperties(typeInfo, nullabilityCtx); } // Plug in any converter configuration -- should be run last. @@ -87,7 +91,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 +115,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo) AddMembersDeclaredBySuperType( typeInfo, currentType, + nullabilityCtx, constructorHasSetsRequiredMembersAttribute, ref state); } @@ -153,6 +158,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 +189,7 @@ private static void AddMembersDeclaredBySuperType( typeInfo, typeToConvert: propertyInfo.PropertyType, memberInfo: propertyInfo, + nullabilityCtx, shouldCheckMembersForRequiredMemberAttribute, hasJsonIncludeAttribute, ref state); @@ -198,6 +205,7 @@ private static void AddMembersDeclaredBySuperType( typeInfo, typeToConvert: fieldInfo.FieldType, memberInfo: fieldInfo, + nullabilityCtx, shouldCheckMembersForRequiredMemberAttribute, hasJsonIncludeAtribute, ref state); @@ -211,11 +219,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 +241,7 @@ private static void AddMember( JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, + NullabilityInfoContext nullabilityCtx, JsonSerializerOptions options, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute) @@ -259,7 +269,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 +299,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(); @@ -313,13 +325,16 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo) ParameterType = reflectionInfo.ParameterType, Position = reflectionInfo.Position, HasDefaultValue = reflectionInfo.HasDefaultValue, - DefaultValue = reflectionInfo.GetDefaultValue() + DefaultValue = reflectionInfo.GetDefaultValue(), + IsNullable = + reflectionInfo.ParameterType.IsNullableType() && + DetermineParameterNullability(reflectionInfo, nullabilityCtx) is not NullabilityState.NotNull, }; jsonParameters[i] = jsonInfo; } - typeInfo.ParameterInfoValues = jsonParameters; + typeInfo.PopulateParameterInfoValues(jsonParameters); } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] @@ -329,6 +344,7 @@ private static void PopulatePropertyInfo( MemberInfo memberInfo, JsonConverter? customConverter, JsonIgnoreCondition? ignoreCondition, + NullabilityInfoContext nullabilityCtx, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute) { @@ -354,6 +370,7 @@ private static void PopulatePropertyInfo( DeterminePropertyPolicies(jsonPropertyInfo, memberInfo); DeterminePropertyName(jsonPropertyInfo, memberInfo); DeterminePropertyIsRequired(jsonPropertyInfo, memberInfo, shouldCheckForRequiredKeyword); + DeterminePropertyNullability(jsonPropertyInfo, memberInfo, nullabilityCtx); if (ignoreCondition != JsonIgnoreCondition.Always) { @@ -467,5 +484,101 @@ internal static void DeterminePropertyAccessors(JsonPropertyInfo jsonPrope return MemberAccessor.CreateParameterlessConstructor(type, defaultCtor); } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, MemberInfo memberInfo, NullabilityInfoContext nullabilityCtx) + { + if (!propertyInfo.PropertyTypeCanBeNull) + { + return; + } + + NullabilityInfo nullabilityInfo; + if (propertyInfo.MemberType is MemberTypes.Property) + { + nullabilityInfo = nullabilityCtx.Create((PropertyInfo)memberInfo); + } + else + { + Debug.Assert(propertyInfo.MemberType is MemberTypes.Field); + nullabilityInfo = nullabilityCtx.Create((FieldInfo)memberInfo); + } + + propertyInfo.IsGetNullable = nullabilityInfo.ReadState is not NullabilityState.NotNull; + propertyInfo.IsSetNullable = nullabilityInfo.WriteState is not NullabilityState.NotNull; + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + private static NullabilityState DetermineParameterNullability(ParameterInfo parameterInfo, NullabilityInfoContext nullabilityCtx) + { +#if NET8_0 + // Workaround for https://github.com/dotnet/runtime/issues/92487 + // 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. + 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 type 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 + 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..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 @@ -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; } @@ -32,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); } @@ -57,6 +60,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 +98,7 @@ private static JsonTypeInfo CreateCore( // Plug in any converter configuration -- should be run last. converter.ConfigureJsonTypeInfo(typeInfo, options); + typeInfo.IsCustomized = false; return typeInfo; } @@ -115,9 +120,9 @@ 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; + Name = parameterInfoValues.Name; + HasDefaultValue = parameterInfoValues.HasDefaultValue; + IsNullable = parameterInfoValues.IsNullable; + MatchingProperty = matchingProperty; + } public int Position { get; } + public string Name { 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 IsNullable { get; internal set; } - public JsonParameterInfo(JsonParameterInfoValues parameterInfoValues, JsonPropertyInfo matchingProperty) - { - Debug.Assert(matchingProperty.IsConfigured); - - MatchingProperty = matchingProperty; - ShouldDeserialize = !matchingProperty.IsIgnored; - Options = matchingProperty.Options; - Position = parameterInfoValues.Position; + 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; - } + // 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/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/JsonParameterInfoValues.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfoValues.cs index 636d1b99ef75aa..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 @@ -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 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 4a6f10aa440630..696a6dee1e7d55 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) @@ -233,6 +233,87 @@ public JsonObjectCreationHandling? ObjectCreationHandling internal bool IsVirtual { get; set; } internal bool IsSourceGenerated { get; set; } + /// + /// 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 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 serialization unless the + /// property has been enabled, in which case the serializer will reject any values returned by the getter. + /// + public bool IsGetNullable + { + get => _isGetNullable; + set + { + VerifyMutable(); + + if (value && !PropertyTypeCanBeNull) + { + ThrowHelper.ThrowInvalidOperationException_PropertyTypeNotNullable(this); + } + + _isGetNullable = value; + } + } + + private bool _isGetNullable; + + /// + /// 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 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 + /// 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; + set + { + VerifyMutable(); + + if (value && !PropertyTypeCanBeNull) + { + ThrowHelper.ThrowInvalidOperationException_PropertyTypeNotNullable(this); + } + + if (ParameterInfo != null) + { + // Ensure the new setting is reflected in the associated parameter. + ParameterInfo.IsNullable = value; + } + + _isSetNullable = value; + } + } + + private bool _isSetNullable; + /// /// Specifies whether the current property is a special extension data property. /// @@ -291,14 +372,18 @@ 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; + + _isGetNullable = _isSetNullable = PropertyTypeCanBeNull; } internal static JsonPropertyInfo GetPropertyPlaceholder() @@ -321,14 +406,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) @@ -462,10 +547,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) { @@ -490,7 +575,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."); @@ -499,7 +584,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) @@ -518,7 +603,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; @@ -527,8 +612,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); @@ -537,7 +622,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; @@ -576,7 +661,7 @@ private void DetermineEffectiveObjectCreationHandlingForProperty() if (effectiveObjectCreationHandling is JsonObjectCreationHandling.Populate) { - if (ParentTypeInfo.DetermineUsesParameterizedConstructor()) + if (DeclaringTypeInfo.DetermineUsesParameterizedConstructor()) { ThrowHelper.ThrowNotSupportedException_ObjectCreationHandlingPropertyDoesNotSupportParameterizedConstructors(); } @@ -591,6 +676,19 @@ private void DetermineEffectiveObjectCreationHandlingForProperty() EffectiveObjectCreationHandling = effectiveObjectCreationHandling; } + private void DetermineParameterInfo() + { + Debug.Assert(DeclaringTypeInfo?.IsConfigured is false); + 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; + } + } + private bool NumberHandingIsApplicable() { if (EffectiveConverter.IsInternalConverterForNumberType) @@ -684,7 +782,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. @@ -842,14 +940,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 01369330feb68f..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 @@ -196,6 +196,11 @@ value is not null && { Debug.Assert(PropertyTypeCanBeNull); + if (!IsGetNullable && Options.RespectNullableAnnotations) + { + ThrowHelper.ThrowJsonException_PropertyGetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); + } + if (EffectiveConverter.HandleNullOnWrite) { if (state.Current.PropertyState < StackFramePropertyState.Name) @@ -275,6 +280,11 @@ internal override bool ReadJsonAndSetMember(object obj, scoped ref ReadStack sta if (!IgnoreNullTokensOnRead) { + if (!IsSetNullable && Options.RespectNullableAnnotations) + { + ThrowHelper.ThrowJsonException_PropertySetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); + } + T? value = default; Set!(obj, value!); } @@ -292,6 +302,12 @@ 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 && !IsSetNullable && Options.RespectNullableAnnotations) + { + ThrowHelper.ThrowJsonException_PropertySetterDisallowNull(Name, state.Current.JsonTypeInfo.Type); + } + Set!(obj, fastValue!); } @@ -316,6 +332,11 @@ 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 && !IsSetNullable && Options.RespectNullableAnnotations) + { + 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 b46a44f10f3175..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 @@ -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,66 +1134,40 @@ 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); + JsonPropertyInfo propertyInfo = kvp.Value; + JsonParameterInfo? parameterInfo = propertyInfo.ParameterInfo; + if (parameterInfo is null) + { + continue; + } - if (!nameLookup.TryAdd(key, value)) + ParameterLookupKey paramKey = new(propertyInfo.PropertyType, propertyInfo.Name); + if (!parameterIndex.TryAdd(paramKey, parameterInfo)) { - // 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; + // Multiple object properties cannot bind to the same constructor parameter. + ThrowHelper.ThrowInvalidOperationException_MultiplePropertiesBindToConstructorParameters( + Type, + parameterInfo.Name, + propertyInfo.Name, + parameterIndex[paramKey].MatchingProperty.Name); } + + parameterCache.Add(parameterInfo); } - foreach (JsonParameterInfoValues parameterInfo in jsonParameters) + if (ExtensionDataProperty is { ParameterInfo: not null }) { - ParameterLookupKey paramToCheck = new(parameterInfo.Name, parameterInfo.ParameterType); - - if (nameLookup.TryGetValue(paramToCheck, out ParameterLookupValue? matchingEntry)) - { - 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); - } - - 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); - } + 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/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; } } 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..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 @@ -63,6 +63,24 @@ public static void ThrowInvalidOperationException_DeserializeUnableToAssignNull( throw new InvalidOperationException(SR.Format(SR.DeserializeUnableToAssignNull, declaredType)); } + [DoesNotReturn] + public static void ThrowJsonException_PropertyGetterDisallowNull(string propertyName, Type declaringType) + { + 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] public static void ThrowInvalidOperationException_ObjectCreationHandlingPopulateNotSupportedByConverter(JsonPropertyInfo propertyInfo) { @@ -528,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() { @@ -758,8 +782,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/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/NullableAnnotationsTests.cs b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs new file mode 100644 index 00000000000000..6ad6cad83c6258 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/NullableAnnotationsTests.cs @@ -0,0 +1,769 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; +using Xunit; + +#nullable enable annotations + +namespace System.Text.Json.Serialization.Tests +{ + public abstract class NullableAnnotationsTests : SerializerTests + { + 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) { } + + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertyGetter))] + public async Task WriteNullFromNotNullablePropertyGetter_EnforcedNullability_ThrowsJsonException(Type type, string propertyName) + { + object value = Activator.CreateInstance(type)!; + + JsonException ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, type, s_optionsWithEnforcedNullability)); + + Assert.Contains(propertyName, ex.Message); + Assert.Contains(type.Name.Split('`')[0], ex.Message); + } + + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertyGetter))] + public async Task WriteNullFromNotNullablePropertyGetter_IgnoredNullability_Succeeds(Type type, string _) + { + object value = Activator.CreateInstance(type)!; + string json = await Serializer.SerializeWrapper(value, type, s_optionsWithIgnoredNullability); + Assert.NotNull(json); + } + + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertyGetter))] + public async Task WriteNullFromNotNullablePropertyGetter_EnforcedNullability_DisabledFlag_Succeeds(Type type, string propertyName) + { + 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.False(propertyInfo.IsGetNullable); + + propertyInfo.IsGetNullable = true; + Assert.True(propertyInfo.IsGetNullable); + + string json = await Serializer.SerializeWrapper(value, typeInfo); + Assert.NotNull(json); + } + + public static IEnumerable GetTypesWithNonNullablePropertyGetter() + { + 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(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)); + yield return Wrap(typeof(NotNullableSpecialTypePropertiesClass), nameof(NotNullableSpecialTypePropertiesClass.JsonDocument)); + yield return Wrap(typeof(NullableObliviousConstructorParameter), nameof(NullableObliviousConstructorParameter.Property)); + yield return Wrap(typeof(NotNullGenericPropertyClass), nameof(NotNullGenericPropertyClass.Property)); + + static object[] Wrap(Type type, string propertyName) => [type, propertyName]; + } + + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertyGetter))] + public async Task WriteNullFromNullablePropertyGetter_EnforcedNullability_Succeeds(Type type, string _) + { + object value = Activator.CreateInstance(type)!; + string json = await Serializer.SerializeWrapper(value, type, s_optionsWithEnforcedNullability); + Assert.NotNull(json); + } + + [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); + } + + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertyGetter))] + public async Task WriteNullFromNullablePropertyGetter_EnforcedNullability_EnabledFlag_ThrowsJsonException(Type type, string propertyName) + { + 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.IsGetNullable); + + propertyInfo.IsGetNullable = false; + Assert.False(propertyInfo.IsGetNullable); + + JsonException ex = await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, typeInfo)); + + Assert.Contains(propertyName, ex.Message); + Assert.Contains(type.Name.Split('`')[0], ex.Message); + } + + 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)); + yield return Wrap(typeof(GenericPropertyClass), nameof(GenericPropertyClass.Property)); + yield return Wrap(typeof(NullableGenericPropertyClass), nameof(NullableGenericPropertyClass.Property)); + + static object[] Wrap(Type type, string propertyName) => [type, propertyName]; + } + + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertySetter))] + public async Task ReadNullIntoNotNullablePropertySetter_EnforcedNullability_ThrowsJsonException(Type type, string propertyName) + { + string json = $$"""{"{{propertyName}}":null}"""; + + JsonException ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, type, s_optionsWithEnforcedNullability)); + + Assert.Contains(propertyName, ex.Message); + Assert.Contains(type.Name.Split('`')[0], ex.Message); + } + + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertySetter))] + public async Task ReadNullIntoNotNullablePropertySetter_IgnoredNullability_Succeeds(Type type, string propertyName) + { + string json = $$"""{"{{propertyName}}":null}"""; + object? result = await Serializer.DeserializeWrapper(json, type, s_optionsWithIgnoredNullability); + Assert.IsType(type, result); + } + + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertySetter))] + public async Task ReadEmptyObjectIntoNotNullablePropertySetter_Succeeds(Type type, string _) + { + object result = await Serializer.DeserializeWrapper("{}", type, s_optionsWithEnforcedNullability); + Assert.IsType(type, result); + } + + [Theory] + [MemberData(nameof(GetTypesWithNonNullablePropertySetter))] + public async Task ReadNullIntoNotNullablePropertySetter_EnforcedNullability_DisabledFlag_Succeeds(Type type, string propertyName) + { + 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.False(propertyInfo.IsSetNullable); + + propertyInfo.IsSetNullable = true; + Assert.True(propertyInfo.IsSetNullable); + + object? result = await Serializer.DeserializeWrapper(json, typeInfo); + Assert.IsType(type, result); + } + + public static IEnumerable GetTypesWithNonNullablePropertySetter() + { + 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(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)); + 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)); + + static object[] Wrap(Type type, string propertyName) => [type, propertyName]; + } + + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertySetter))] + public async Task ReadNullIntoNullablePropertySetter_EnforcedNullability_Succeeds(Type type, string propertyName) + { + string json = $$"""{"{{propertyName}}":null}"""; + object? result = await Serializer.DeserializeWrapper(json, type, s_optionsWithEnforcedNullability); + Assert.IsType(type, result); + } + + [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); + } + + [Theory] + [MemberData(nameof(GetTypesWithNullablePropertySetter))] + public async Task ReadNullIntoNullablePropertySetter_EnforcedNullability_EnabledFlag_ThrowsJsonException(Type type, string propertyName) + { + 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.IsSetNullable); + + propertyInfo.IsSetNullable = false; + Assert.False(propertyInfo.IsSetNullable); + + JsonException ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, typeInfo)); + + Assert.Contains(propertyName, ex.Message); + Assert.Contains(type.Name.Split('`')[0], ex.Message); + } + + 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)); + 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)); + + static object[] Wrap(Type type, string propertyName) => [type, propertyName]; + } + + [Fact] + public async Task ReadNullIntoReadonlyProperty_Succeeds() + { + string json = """{"ReadonlyProperty":null}"""; + NotNullableReadonlyPropertyClass result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.Null(result.ReadonlyProperty); + } + + [Fact] + public async Task ReadNullIntoNotNullableSpecialTypeProperties() + { + string json = """ + { + "JsonDocument":null, + "MemoryByte":null, + "ReadOnlyMemoryByte":null, + "MemoryOfT":null, + "ReadOnlyMemoryOfT":null + } + """; + + 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 ReadNullIntoNotNullablePropertyWithHandleNullConverter() + { + string json = """{"PropertyWithHandleNullConverter":null}"""; + + NotNullablePropertyWithHandleNullConverterClass result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.NotNull(result.PropertyWithHandleNullConverter); + } + + [Fact] + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterThrows() + { + string json = """{"PropertyWithAlwaysNullConverter":"42"}"""; + + Exception ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability)); + Assert.Contains("PropertyWithAlwaysNullConverter", ex.Message); + Assert.Contains(nameof(NotNullablePropertyWithAlwaysNullConverterClass), ex.Message); + } + + [Fact] + public async Task ReadNullIntoNotNullablePropertyWithHandleNullConverterWithParameterizedCtor() + { + string json = """{"CtorPropertyWithHandleNullConverter":null}"""; + + var result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.NotNull(result.CtorPropertyWithHandleNullConverter); + } + + [Fact] + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterWithParameterizedCtorThrows() + { + string json = """{"CtorPropertyWithAlwaysNullConverter":"42"}"""; + + 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 ReadNullIntoNotNullablePropertyWithHandleNullConverterWithLargeParameterizedCtor() + { + string json = """{"LargeCtorPropertyWithHandleNullConverter":null}"""; + + var result = await Serializer.DeserializeWrapper(json, s_optionsWithEnforcedNullability); + Assert.NotNull(result.LargeCtorPropertyWithHandleNullConverter); + } + + [Fact] + public async Task ReadNotNullValueIntoNotNullablePropertyWithAlwaysNullConverterWithLargeParameterizedCtorThrows() + { + string json = """{"LargeCtorPropertyWithAlwaysNullConverter":"42"}"""; + + 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 WriteNotNullPropertiesWithNullIgnoreConditions_Succeeds() + { + // 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); + } + + public class NotNullablePropertyClass + { + public string Property { get; set; } + } + + public class NullableObliviousPropertyClass + { +#nullable disable annotations + public string Property { get; set; } +#nullable restore annotations + } + + public class NullableObliviousConstructorParameter + { + public string Property { get; set; } + + public NullableObliviousConstructorParameter() { } + + [JsonConstructor] +#nullable disable annotations + public NullableObliviousConstructorParameter(string property) +#nullable restore annotations + { + Property = property; + } + } + + public class NotNullableReadonlyPropertyClass + { + public string ReadonlyProperty { get; } + } + + public class NotNullableFieldClass + { + [JsonInclude] + public string Field; + } + + public class NotNullableSpecialTypePropertiesClass + { + // types with internal converter that handles null. + public JsonDocument JsonDocument { get; set; } + public Memory MemoryByte { get; set; } + public ReadOnlyMemory ReadOnlyMemoryByte { get; set; } + public Memory MemoryOfT { get; set; } + public ReadOnlyMemory ReadOnlyMemoryOfT { get; set; } + } + + public class NotNullablePropertyWithHandleNullConverterClass + { + [JsonConverter(typeof(MyHandleNullConverter))] + public MyClass PropertyWithHandleNullConverter { get; set; } + } + + public class NotNullablePropertyWithAlwaysNullConverterClass + { + [JsonConverter(typeof(MyAlwaysNullConverter))] + public MyClass PropertyWithAlwaysNullConverter { get; set; } + } + + public class NotNullablePropertyWithConverterClass + { + [JsonConverter(typeof(MyConverter))] + public MyClass PropertyWithConverter { get; set; } + } + + 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; } + + public NotNullablePropertyParameterizedCtorClass() { } + + [JsonConstructor] + public NotNullablePropertyParameterizedCtorClass(string ctorProperty) => CtorProperty = ctorProperty; + } + + public class NotNullablePropertyWithHandleNullConverterParameterizedCtorClass + { + [JsonConverter(typeof(MyHandleNullConverter))] + public MyClass CtorPropertyWithHandleNullConverter { get; } + + public NotNullablePropertyWithHandleNullConverterParameterizedCtorClass() { } + + [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; } + + 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) + { + 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; + } + } + + 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; } + } + + 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 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] + 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; } + } + + public class NullableFieldClass + { + [JsonInclude] + public string? Field; + } + } +} 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/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 new file mode 100644 index 00000000000000..33fdc55b8a1d37 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NullableAnnotationsTests.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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, RespectNullableAnnotations = false)] + [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(NotNullPropertyClass))] + [JsonSerializable(typeof(MaybeNullPropertyClass))] + [JsonSerializable(typeof(AllowNullPropertyClass))] + [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))] + [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 { } + } + + 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, RespectNullableAnnotations = false)] + [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(NotNullPropertyClass))] + [JsonSerializable(typeof(MaybeNullPropertyClass))] + [JsonSerializable(typeof(AllowNullPropertyClass))] + [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))] + [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.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..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 @@ -84,6 +84,7 @@ + @@ -121,6 +122,7 @@ + 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..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,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.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/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.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); + } } } 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/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..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,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.RespectNullableAnnotations); 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.RespectNullableAnnotations = options.RespectNullableAnnotations); TestIListThrowingOperationsWhenImmutable(options.Converters, tc); TestIListThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -915,6 +917,49 @@ public static void Options_JsonSerializerContext_Combine_FallbackToReflection() JsonTestHelper.AssertJsonEqual("""{"Value":null,"Thing":null}""", json); } + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(null)] + [InlineData(false)] + [InlineData(true)] + public static void Options_RespectNullableAnnotationsDefault_FeatureSwitch(bool? state) + { + var options = new RemoteInvokeOptions(); + if (state.HasValue) + { + options.RuntimeConfigurationOptions["System.Text.Json.Serialization.RespectNullableAnnotationsDefault"] = state.Value; + } + + string arg = state ?? false ? "true" : "false"; + RemoteExecutor.Invoke(static arg => + { + bool shouldRespectNullableAnnotations = bool.Parse(arg); + + var jsonOptions = new JsonSerializerOptions(); + Assert.Equal(shouldRespectNullableAnnotations, jsonOptions.RespectNullableAnnotations); + Assert.Equal(shouldRespectNullableAnnotations, JsonSerializerOptions.Default.RespectNullableAnnotations); + + var value = new NullableAnnotationsTests.NotNullablePropertyClass(); + string expectedJson = """{"Property":null}"""; + + Assert.Null(value.Property); + + 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) { var options = new JsonSerializerOptions(); 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) { 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..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 @@ -67,6 +69,7 @@ + @@ -200,6 +203,7 @@ +