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 7b5d7a1dfe77f..c4c74cf1a43a2 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -489,6 +489,8 @@ public abstract partial class JsonConverter : System.Text.Json.Serialization. { protected internal JsonConverter() { } public override bool CanConvert(System.Type typeToConvert) { throw null; } + public virtual bool HandleNull { get { throw null; } } + [return: System.Diagnostics.CodeAnalysis.MaybeNull] public abstract T Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options); public abstract void Write(System.Text.Json.Utf8JsonWriter writer, T value, System.Text.Json.JsonSerializerOptions options); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs index 3548737459413..a657398ca2d3f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs @@ -104,7 +104,7 @@ internal sealed override bool OnTryRead( // Read the value and add. reader.ReadWithVerify(); TValue element = elementConverter.Read(ref reader, typeof(TValue), options); - Add(element, options, ref state); + Add(element!, options, ref state); } } else @@ -129,7 +129,7 @@ internal sealed override bool OnTryRead( // Get the value from the converter and add it. elementConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element); - Add(element, options, ref state); + Add(element!, options, ref state); } } } @@ -247,7 +247,7 @@ internal sealed override bool OnTryRead( return false; } - Add(element, options, ref state); + Add(element!, options, ref state); state.Current.EndElement(); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfStringTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfStringTValueConverter.cs index 79aa533d14664..14ecb4526999c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfStringTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfStringTValueConverter.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Diagnostics; namespace System.Text.Json.Serialization.Converters { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs index dad477364d6d7..6803e9d58454f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs @@ -67,7 +67,7 @@ internal override bool OnTryRead( // Obtain the CLR value from the JSON and apply to the object. TElement element = elementConverter.Read(ref reader, elementConverter.TypeToConvert, options); - Add(element, ref state); + Add(element!, ref state); } } else @@ -83,7 +83,7 @@ internal override bool OnTryRead( // Get the value from the converter and add it. elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement element); - Add(element, ref state); + Add(element!, ref state); } } } @@ -184,7 +184,7 @@ internal override bool OnTryRead( return false; } - Add(element, ref state); + Add(element!, ref state); // No need to set PropertyState to TryRead since we're done with this element now. state.Current.EndElement(); 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 391a5710204b8..1e98384d117c6 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 @@ -33,28 +33,28 @@ protected override bool ReadAndCacheConstructorArgument(ref ReadStack state, ref success = ((JsonParameterInfo)jsonParameterInfo).ReadJsonTyped(ref state, ref reader, out TArg0 arg0); if (success) { - arguments.Arg0 = arg0; + arguments.Arg0 = arg0!; } break; case 1: success = ((JsonParameterInfo)jsonParameterInfo).ReadJsonTyped(ref state, ref reader, out TArg1 arg1); if (success) { - arguments.Arg1 = arg1; + arguments.Arg1 = arg1!; } break; case 2: success = ((JsonParameterInfo)jsonParameterInfo).ReadJsonTyped(ref state, ref reader, out TArg2 arg2); if (success) { - arguments.Arg2 = arg2; + arguments.Arg2 = arg2!; } break; case 3: success = ((JsonParameterInfo)jsonParameterInfo).ReadJsonTyped(ref state, ref reader, out TArg3 arg3); if (success) { - arguments.Arg3 = arg3; + arguments.Arg3 = arg3!; } break; default: diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs index fc5035deeb78e..f74a7de0cdbe1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs @@ -99,7 +99,7 @@ internal override bool OnTryRead( ThrowHelper.ThrowJsonException(); } - value = new KeyValuePair(k, v); + value = new KeyValuePair(k!, v!); return true; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs index 000fcd7d000aa..1fb68dfcdac30 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs @@ -17,6 +17,8 @@ public NullableConverter(JsonConverter converter) public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + // We do not check _converter.HandleNull, as the underlying struct cannot be null. + // A custom converter for some type T? can handle null. if (reader.TokenType == JsonTokenType.Null) { return null; @@ -30,6 +32,8 @@ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOption { if (!value.HasValue) { + // We do not check _converter.HandleNull, as the underlying struct cannot be null. + // A custom converter for some type T? can handle null. writer.WriteNullValue(); } else diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index 9b7a706678c36..65045d4c84f8e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -40,12 +40,6 @@ internal JsonConverter() { } internal abstract Type? ElementType { get; } - /// - /// Cached value of ShouldHandleNullValue. It is cached since the converter should never - /// change the value depending on state and because it may contain non-trival logic. - /// - internal bool HandleNullValue { get; set; } - /// /// Cached value of TypeToConvert.IsValueType, which is an expensive call. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs index 1785b669b2552..c49fd0d7947db 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; + namespace System.Text.Json.Serialization { public partial class JsonConverter @@ -14,6 +16,7 @@ public partial class JsonConverter return ReadCore(ref reader, options, ref state); } + [return: MaybeNull] internal T ReadCore( ref Utf8JsonReader reader, JsonSerializerOptions options, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 158094fd661eb..058e7dc395bd8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -22,7 +22,8 @@ protected internal JsonConverter() // In the future, this will be check for !IsSealed (and excluding value types). CanBePolymorphic = TypeToConvert == typeof(object); IsValueType = TypeToConvert.IsValueType; - HandleNullValue = ShouldHandleNullValue; + HandleNull = IsValueType; + CanBeNull = !IsValueType || Nullable.GetUnderlyingType(TypeToConvert) != null; IsInternalConverter = GetType().Assembly == typeof(JsonConverter).Assembly; CanUseDirectReadOrWrite = !CanBePolymorphic && IsInternalConverter && ClassType == ClassType.Value; } @@ -54,10 +55,19 @@ internal override sealed JsonParameterInfo CreateJsonParameterInfo() internal override Type? ElementType => null; - // Allow a converter that can't be null to return a null value representation, such as JsonElement or Nullable<>. - // In other cases, this will likely cause an JsonException in the converter. - // Do not call this directly; it is cached in HandleNullValue. - internal virtual bool ShouldHandleNullValue => IsValueType; + /// + /// Indicates whether should be passed to the converter on serialization, + /// and whether should be passed on deserialization. + /// + /// + /// The default value is for converters for value types, and for converters for reference types. + /// + public virtual bool HandleNull { get; } + + /// + /// Can be assigned to ? + /// + internal bool CanBeNull { get; } /// /// Is the converter built-in. @@ -79,7 +89,7 @@ internal virtual bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerO } // Provide a default implementation for value converters. - internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value) + internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNull] out T value) { value = Read(ref reader, typeToConvert, options); return true; @@ -95,9 +105,10 @@ internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, J /// The being converted. /// The being used. /// The value that was converted. + [return: MaybeNull] public abstract T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options); - internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T value) + internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNull] out T value) { if (ClassType == ClassType.Value) { @@ -105,9 +116,14 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali Debug.Assert(!state.IsContinuation); // For perf and converter simplicity, handle null here instead of forwarding to the converter. - if (reader.TokenType == JsonTokenType.Null && !HandleNullValue) + if (reader.TokenType == JsonTokenType.Null && !HandleNull) { - value = default!; + if (!CanBeNull) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + value = default; return true; } @@ -147,10 +163,15 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali // For performance, only perform validation on internal converters on debug builds. if (IsInternalConverter) { - if (reader.TokenType == JsonTokenType.Null && !HandleNullValue && !wasContinuation) + if (reader.TokenType == JsonTokenType.Null && !HandleNull && !wasContinuation) { + if (!CanBeNull) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + // For perf and converter simplicity, handle null here instead of forwarding to the converter. - value = default!; + value = default; success = true; } else @@ -164,9 +185,14 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali if (!wasContinuation) { // For perf and converter simplicity, handle null here instead of forwarding to the converter. - if (reader.TokenType == JsonTokenType.Null && !HandleNullValue) + if (reader.TokenType == JsonTokenType.Null && !HandleNull) { - value = default!; + if (!CanBeNull) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + value = default; state.Pop(true); return true; } @@ -213,7 +239,20 @@ internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions opt { if (value == null) { - writer.WriteNullValue(); + if (!HandleNull) + { + writer.WriteNullValue(); + } + else + { + Debug.Assert(ClassType == ClassType.Value); + Debug.Assert(!state.IsContinuation); + + int originalPropertyDepth = writer.CurrentDepth; + Write(writer, value, options); + VerifyWrite(originalPropertyDepth, writer); + } + return true; } @@ -236,15 +275,12 @@ internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions opt } } } - else + else if (value == null && !HandleNull) { - // We do not pass null values to converters unless HandleNullValue is true. Null values for properties were + // We do not pass null values to converters unless HandleNull is true. Null values for properties were // already handled in GetMemberAndWriteJson() so we don't need to check for IgnoreNullValues here. - if (value == null && !HandleNullValue) - { - writer.WriteNullValue(); - return true; - } + writer.WriteNullValue(); + return true; } if (ClassType == ClassType.Value) @@ -255,7 +291,6 @@ internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions opt Write(writer, value, options); VerifyWrite(originalPropertyDepth, writer); - return true; } @@ -299,48 +334,30 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth); } - bool success; JsonDictionaryConverter dictionaryConverter = (JsonDictionaryConverter)this; - if (ClassType == ClassType.Value) - { - Debug.Assert(!state.IsContinuation); - - int originalPropertyDepth = writer.CurrentDepth; + bool isContinuation = state.IsContinuation; + bool success; - // Ignore the naming policy for extension data. - state.Current.IgnoreDictionaryKeyPolicy = true; + state.Push(); - success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); - if (success) - { - VerifyWrite(originalPropertyDepth, writer); - } - } - else + if (!isContinuation) { - bool isContinuation = state.IsContinuation; - - state.Push(); - - if (!isContinuation) - { - Debug.Assert(state.Current.OriginalDepth == 0); - state.Current.OriginalDepth = writer.CurrentDepth; - } - - // Ignore the naming policy for extension data. - state.Current.IgnoreDictionaryKeyPolicy = true; + Debug.Assert(state.Current.OriginalDepth == 0); + state.Current.OriginalDepth = writer.CurrentDepth; + } - success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); - if (success) - { - VerifyWrite(state.Current.OriginalDepth, writer); - } + // Ignore the naming policy for extension data. + state.Current.IgnoreDictionaryKeyPolicy = true; - state.Pop(success); + success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); + if (success) + { + VerifyWrite(state.Current.OriginalDepth, writer); } + state.Pop(success); + return success; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs index 52a68effdc3aa..a03982f4e40d8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonParameterInfoOfT.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json.Serialization; @@ -53,9 +54,14 @@ public override bool ReadJson(ref ReadStack state, ref Utf8JsonReader reader, ou bool success; bool isNullToken = reader.TokenType == JsonTokenType.Null; - if (isNullToken && !_converter.HandleNullValue && !state.IsContinuation) + if (isNullToken && !_converter.HandleNull && !state.IsContinuation) { - // Don't have to check for IgnoreNullValue option here because we set the default value (likely null) regardless + if (!_converter.CanBeNull) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(_converter.TypeToConvert); + } + + // Don't have to check for IgnoreNullValue option here because we set the default value regardless. value = DefaultValue; return true; } @@ -77,13 +83,19 @@ public override bool ReadJson(ref ReadStack state, ref Utf8JsonReader reader, ou return success; } - public bool ReadJsonTyped(ref ReadStack state, ref Utf8JsonReader reader, out T value) + public bool ReadJsonTyped(ref ReadStack state, ref Utf8JsonReader reader, [MaybeNull] out T value) { bool success; bool isNullToken = reader.TokenType == JsonTokenType.Null; - if (isNullToken && !_converter.HandleNullValue && !state.IsContinuation) + if (isNullToken && !_converter.HandleNull && !state.IsContinuation) { + if (!_converter.CanBeNull) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(_converter.TypeToConvert); + } + + // Don't have to check for IgnoreNullValue option here because we set the default value regardless. value = TypedDefaultValue; return true; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs index 863590f3b5f2b..357a247185a92 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoOfT.cs @@ -102,7 +102,25 @@ public override bool GetMemberAndWriteJson(object obj, ref WriteStack state, Utf { if (!IgnoreNullValues) { - writer.WriteNull(EscapedName.Value); + if (!Converter.HandleNull) + { + writer.WriteNull(EscapedName.Value); + } + else + { + if (state.Current.PropertyState < StackFramePropertyState.Name) + { + state.Current.PropertyState = StackFramePropertyState.Name; + writer.WritePropertyName(EscapedName.Value); + } + + int originalDepth = writer.CurrentDepth; + Converter.Write(writer, value, Options); + if (originalDepth != writer.CurrentDepth) + { + ThrowHelper.ThrowJsonException_SerializationConverterWrite(Converter); + } + } } success = true; @@ -142,10 +160,15 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U { bool success; bool isNullToken = reader.TokenType == JsonTokenType.Null; - if (isNullToken && !Converter.HandleNullValue && !state.IsContinuation) + if (isNullToken && !Converter.HandleNull && !state.IsContinuation) { if (!IgnoreNullValues) { + if (!Converter.CanBeNull) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Converter.TypeToConvert); + } + T value = default; Set!(obj, value!); } @@ -161,7 +184,7 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U T fastvalue = Converter.Read(ref reader, RuntimePropertyType!, Options); if (!IgnoreNullValues || (!isNullToken && fastvalue != null)) { - Set!(obj, fastvalue); + Set!(obj, fastvalue!); } return true; @@ -173,7 +196,7 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U { if (!IgnoreNullValues || (!isNullToken && value != null)) { - Set!(obj, value); + Set!(obj, value!); } } } @@ -186,8 +209,13 @@ public override bool ReadJsonAsObject(ref ReadStack state, ref Utf8JsonReader re { bool success; bool isNullToken = reader.TokenType == JsonTokenType.Null; - if (isNullToken && !Converter.HandleNullValue && !state.IsContinuation) + if (isNullToken && !Converter.HandleNull && !state.IsContinuation) { + if (!Converter.CanBeNull) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Converter.TypeToConvert); + } + value = default(T)!; success = true; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs index e2aabd2ceacb9..77d588af38153 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonResumableConverterOfT.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; + namespace System.Text.Json.Serialization { /// @@ -11,7 +13,8 @@ namespace System.Text.Json.Serialization /// internal abstract class JsonResumableConverter : JsonConverter { - public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + [return: MaybeNull] + public sealed override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Bridge from resumable to value converters. if (options == null) @@ -25,7 +28,7 @@ public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, Jso return value; } - public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { // Bridge from resumable to value converters. if (options == null) @@ -37,5 +40,7 @@ public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializer state.Initialize(typeof(T), options, supportContinuation: false); TryWrite(writer, value, options, ref state); } + + public override bool HandleNull => false; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs index f8bf98def8199..bfdbd5993c65b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs @@ -3,12 +3,14 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace System.Text.Json { public static partial class JsonSerializer { + [return: MaybeNull] private static TValue ReadCore(ref Utf8JsonReader reader, Type returnType, JsonSerializerOptions options) { ReadStack state = default; @@ -17,6 +19,7 @@ private static TValue ReadCore(ref Utf8JsonReader reader, Type returnTyp return ReadCore(jsonConverter, ref reader, options, ref state); } + [return: MaybeNull] private static TValue ReadCore(JsonConverter jsonConverter, ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state) { if (jsonConverter is JsonConverter converter) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs index 5215d184204db..082c49645379e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs @@ -43,7 +43,7 @@ public static TValue Deserialize(string json, JsonSerializerOptions? opt throw new ArgumentNullException(nameof(json)); } - return Deserialize(json, typeof(TValue), options)!; + return Deserialize(json, typeof(TValue), options); } /// @@ -85,6 +85,7 @@ public static TValue Deserialize(string json, JsonSerializerOptions? opt return value; } + [return: MaybeNull] private static TValue Deserialize(string json, Type returnType, JsonSerializerOptions? options) { const long ArrayPoolMaxSizeBeforeUsingNormalAlloc = 1024 * 1024; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs index b7182ede223aa..666a6847094a3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs @@ -14,6 +14,7 @@ public static partial class JsonSerializer /// /// Internal version that allows re-entry with preserving ReadStack so that JsonPath works correctly. /// + [return: MaybeNull] internal static TValue Deserialize(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, string? propertyName = null) { if (options == null) @@ -162,6 +163,7 @@ private static void CheckSupportedOptions(JsonReaderOptions readerOptions, strin } } + [return: MaybeNull] private static TValue ReadValueCore(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { JsonReaderState readerState = reader.CurrentState; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs index 5332509163f1d..94af2f7adf5d0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; + namespace System.Text.Json.Serialization { // Used for value converters that need to re-enter the serializer since it will support JsonPath @@ -10,7 +12,8 @@ internal abstract class JsonValueConverter : JsonConverter { internal sealed override ClassType ClassType => ClassType.NewValue; - public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + [return: MaybeNull] + public sealed override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Bridge from resumable to value converters. if (options == null) @@ -24,7 +27,7 @@ public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, Jso return value; } - public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { // Bridge from resumable to value converters. if (options == null) diff --git a/src/libraries/System.Text.Json/tests/JsonTestHelper.cs b/src/libraries/System.Text.Json/tests/JsonTestHelper.cs index b83fcbe3f1a92..960e613502e5c 100644 --- a/src/libraries/System.Text.Json/tests/JsonTestHelper.cs +++ b/src/libraries/System.Text.Json/tests/JsonTestHelper.cs @@ -867,6 +867,9 @@ private static void AssertJsonEqual(JsonElement expected, JsonElement actual) case JsonValueKind.String: Assert.Equal(expected.GetString(), actual.GetString()); break; + case JsonValueKind.Number: + Assert.Equal(expected.GetRawText(), actual.GetRawText()); + break; default: throw new NotImplementedException(); } diff --git a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs index 2d72329f49480..d1cb10248613a 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs @@ -1115,6 +1115,14 @@ public static void ReadClassWithNullKeyValuePairValues() Assert.Null(obj.KvpWClassKvpVal.Value.Value); } + [Fact] + public static void Kvp_NullKeyIsFine() + { + KeyValuePair kvp = JsonSerializer.Deserialize>(@"{""Key"":null,""Value"":null}"); + Assert.Null(kvp.Key); + Assert.Null(kvp.Value); + } + [Fact] public static void ReadSimpleTestClass_GenericCollectionWrappers() { diff --git a/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.ParameterMatching.cs b/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.ParameterMatching.cs index f6979cf22c967..a69c9ad6b9a58 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.ParameterMatching.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ConstructorTests/ConstructorTests.ParameterMatching.cs @@ -339,19 +339,35 @@ public void PropertiesNotSet_WhenJSON_MapsToConstructorParameters() [Fact] public void IgnoreNullValues_DontSetNull_ToConstructorArguments_ThatCantBeNull() { - // Throw JsonException when null applied to types that can't be null. + // Throw JsonException when null applied to types that can't be null. Behavior should align with properties deserialized with setters. + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}")); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}")); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null}")); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null}")); + Assert.Throws(() => Serializer.Deserialize(@"{""Int"":null}")); + Assert.Throws(() => Serializer.Deserialize(@"{""Int"":null}")); + Assert.Throws(() => Serializer.Deserialize(@"{""ImmutableArray"":null}")); + Assert.Throws(() => Serializer.Deserialize(@"{""ImmutableArray"":null}")); // Throw even when IgnoreNullValues is true for symmetry with property deserialization, // until https://github.com/dotnet/runtime/issues/30795 is addressed. + var options = new JsonSerializerOptions { IgnoreNullValues = true }; Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""Int"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""ImmutableArray"":null}", options)); + Assert.Throws(() => Serializer.Deserialize(@"{""Point3DStruct"":null,""Int"":null,""ImmutableArray"":null}", options)); } [Fact] diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.HandleNull.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.HandleNull.cs new file mode 100644 index 0000000000000..19b359f92badd --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.HandleNull.cs @@ -0,0 +1,510 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Collections.Generic; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class CustomConverterTests + { + [Fact] + public static void ValueTypeConverter_NoOverride() + { + // Baseline + Assert.Throws(() => JsonSerializer.Deserialize("null")); + + // Per null handling default value for value types (true), converter handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new Int32NullConverter_SpecialCaseNull()); + + Assert.Equal(-1, JsonSerializer.Deserialize("null", options)); + } + + private class Int32NullConverter_SpecialCaseNull : JsonConverter + { + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return -1; + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + } + + [Fact] + public static void ValueTypeConverter_OptOut() + { + // Per null handling opt-out, serializer handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new Int32NullConverter_OptOut()); + + // Serializer throws JsonException if null is assigned to value that can't be null. + Assert.Throws(() => JsonSerializer.Deserialize("null", options)); + Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyInt"":null}", options)); + Assert.Throws(() => JsonSerializer.Deserialize>("[null]", options)); + Assert.Throws(() => JsonSerializer.Deserialize>(@"{""MyInt"":null}", options)); + } + + private class Int32NullConverter_OptOut : Int32NullConverter_SpecialCaseNull + { + public override bool HandleNull => false; + } + + private class ClassWithInt + { + public int MyInt { get; set; } + } + + [Fact] + public static void ValueTypeConverter_NullOptIn() + { + // Per null handling opt-in, converter handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new Int32NullConverter_NullOptIn()); + + Assert.Equal(-1, JsonSerializer.Deserialize("null", options)); + } + + private class Int32NullConverter_NullOptIn : Int32NullConverter_SpecialCaseNull + { + public override bool HandleNull => true; + } + + [Fact] + public static void ComplexValueTypeConverter_NoOverride() + { + // Baseline + Assert.Throws(() => JsonSerializer.Deserialize("null")); + + var options = new JsonSerializerOptions(); + options.Converters.Add(new PointStructConverter_SpecialCaseNull()); + + // Per null handling default value for value types (true), converter handles null. + var obj = JsonSerializer.Deserialize("null", options); + Assert.Equal(-1, obj.X); + Assert.Equal(-1, obj.Y); + } + + private class PointStructConverter_SpecialCaseNull : JsonConverter + { + public override Point_2D_Struct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return new Point_2D_Struct(-1, -1); + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, Point_2D_Struct value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + } + + [Fact] + public static void ComplexValueTypeConverter_OptOut() + { + // Per null handling opt-out, serializer handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new PointStructConverter_OptOut()); + + // Serializer throws JsonException if null is assigned to value that can't be null. + Assert.Throws(() => JsonSerializer.Deserialize("null", options)); + Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyPoint"":null}", options)); + Assert.Throws(() => JsonSerializer.Deserialize(@"{""MyPoint"":null}", options)); + Assert.Throws(() => JsonSerializer.Deserialize>("[null]", options)); + Assert.Throws(() => JsonSerializer.Deserialize>(@"{""MyPoint"":null}", options)); + } + + private class PointStructConverter_OptOut : PointStructConverter_SpecialCaseNull + { + public override bool HandleNull => false; + } + + private class ClassWithPoint + { + public Point_2D_Struct MyPoint { get; set; } + } + + private class ImmutableClassWithPoint + { + public Point_2D_Struct MyPoint { get; } + + public ImmutableClassWithPoint(Point_2D_Struct myPoint) => MyPoint = myPoint; + } + + [Fact] + public static void ComplexValueTypeConverter_NullOptIn() + { + // Baseline + Assert.Throws(() => JsonSerializer.Deserialize("null")); + + // Per null handling opt-in, converter handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new PointStructConverter_NullOptIn()); + + var obj = JsonSerializer.Deserialize("null", options); + Assert.Equal(-1, obj.X); + Assert.Equal(-1, obj.Y); + } + + private class PointStructConverter_NullOptIn : PointStructConverter_SpecialCaseNull + { + public override bool HandleNull => true; + } + + [Fact] + public static void NullableValueTypeConverter_NoOverride() + { + // Baseline + int? val = JsonSerializer.Deserialize("null"); + Assert.Null(val); + Assert.Equal("null", JsonSerializer.Serialize(val)); + + // Per null handling default value for value types (true), converter handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new NullableInt32NullConverter_SpecialCaseNull()); + + val = JsonSerializer.Deserialize("null", options); + Assert.Equal(-1, val); + + val = null; + Assert.Equal("-1", JsonSerializer.Serialize(val, options)); + } + + private class NullableInt32NullConverter_SpecialCaseNull : JsonConverter + { + public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return -1; + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options) + { + if (!value.HasValue) + { + writer.WriteNumberValue(-1); + return; + } + + throw new NotSupportedException(); + } + } + + [Fact] + public static void NullableValueTypeConverter_OptOut() + { + // Baseline + int? val = JsonSerializer.Deserialize("null"); + Assert.Null(val); + Assert.Equal("null", JsonSerializer.Serialize(val)); + + // Per null handling opt-out, serializer handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new NullableInt32NullConverter_NullOptOut()); + + val = JsonSerializer.Deserialize("null", options); + Assert.Null(val); + Assert.Equal("null", JsonSerializer.Serialize(val, options)); + } + + private class NullableInt32NullConverter_NullOptOut : NullableInt32NullConverter_SpecialCaseNull + { + public override bool HandleNull => false; + } + + [Fact] + public static void ReferenceTypeConverter_NoOverride() + { + // Baseline + Uri val = JsonSerializer.Deserialize("null"); + Assert.Null(val); + Assert.Equal("null", JsonSerializer.Serialize(val)); + + // Per null handling default value for reference types (false), serializer handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new UriNullConverter_SpecialCaseNull()); + + // Serializer sets default value. + val = JsonSerializer.Deserialize("null", options); + Assert.Null(val); + + // Serializer serializes null. + Assert.Equal("null", JsonSerializer.Serialize(val, options)); + } + + private class UriNullConverter_SpecialCaseNull : JsonConverter + { + public override Uri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return new Uri("https://default"); + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteStringValue("https://default"); + return; + } + + throw new NotSupportedException(); + } + } + + [Fact] + public static void ReferenceTypeConverter_OptOut() + { + // Per null handling opt-out, serializer handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new UriNullConverter_OptOut()); + + Uri val = JsonSerializer.Deserialize("null", options); + Assert.Null(val); + Assert.Equal("null", JsonSerializer.Serialize(val, options)); + } + + private class UriNullConverter_OptOut : UriNullConverter_SpecialCaseNull + { + public override bool HandleNull => false; + } + + [Fact] + public static void ReferenceTypeConverter_NullOptIn() + { + // Per null handling opt-in, converter handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new UriNullConverter_NullOptIn()); + + Uri val = JsonSerializer.Deserialize("null", options); + Assert.Equal(new Uri("https://default"), val); + + val = null; + Assert.Equal(@"""https://default""", JsonSerializer.Serialize(val, options)); + } + + private class UriNullConverter_NullOptIn : UriNullConverter_SpecialCaseNull + { + public override bool HandleNull => true; + } + + [Fact] + public static void ComplexReferenceTypeConverter_NoOverride() + { + // Baseline + Point_2D obj = JsonSerializer.Deserialize("null"); + Assert.Null(obj); + Assert.Equal("null", JsonSerializer.Serialize(obj)); + + // Per null handling default value for reference types (false), serializer handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new PointClassConverter_SpecialCaseNull()); + + obj = JsonSerializer.Deserialize("null", options); + Assert.Null(obj); + Assert.Equal("null", JsonSerializer.Serialize(obj)); + } + + private class PointClassConverter_SpecialCaseNull : JsonConverter + { + public override Point_2D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return new Point_2D(-1, -1); + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, Point_2D value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteStartObject(); + writer.WriteNumber("X", -1); + writer.WriteNumber("Y", -1); + writer.WriteEndObject(); + return; + } + + throw new JsonException(); + } + } + + [Fact] + public static void ComplexReferenceTypeConverter_NullOptIn() + { + // Per null handling opt-in, converter handles null. + var options = new JsonSerializerOptions(); + options.Converters.Add(new PointClassConverter_NullOptIn()); + + Point_2D obj = JsonSerializer.Deserialize("null", options); + Assert.Equal(-1, obj.X); + Assert.Equal(-1, obj.Y); + + obj = null; + JsonTestHelper.AssertJsonEqual(@"{""X"":-1,""Y"":-1}", JsonSerializer.Serialize(obj, options)); + } + + private class PointClassConverter_NullOptIn : PointClassConverter_SpecialCaseNull + { + public override bool HandleNull => true; + } + + [Fact] + public static void ConverterNotCalled_IgnoreNullValues() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new UriNullConverter_NullOptIn()); + + // Converter is not called. + ClassWithIgnoredUri obj = JsonSerializer.Deserialize(@"{""MyUri"":null}", options); + Assert.Equal(new Uri("https://microsoft.com"), obj.MyUri); + + obj.MyUri = null; + Assert.Equal("{}", JsonSerializer.Serialize(obj, options)); + } + + private class ClassWithIgnoredUri + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenNull)] + public Uri MyUri { get; set; } = new Uri("https://microsoft.com"); + } + + [Fact] + public static void ConverterWritesBadAmount() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new BadUriConverter()); + options.Converters.Add(new BadObjectConverter()); + + // Using serializer overload in Release mode uses a writer with SkipValidation = true. + var writerOptions = new JsonWriterOptions { SkipValidation = false }; + using (Utf8JsonWriter writer = new Utf8JsonWriter(new ArrayBufferWriter(), writerOptions)) + { + Assert.Throws(() => JsonSerializer.Serialize(writer, new ClassWithUri(), options)); + } + + using (Utf8JsonWriter writer = new Utf8JsonWriter(new ArrayBufferWriter(), writerOptions)) + { + Assert.Throws(() => JsonSerializer.Serialize(new StructWithObject(), options)); + } + } + + private class BadUriConverter : UriNullConverter_NullOptIn + { + public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) { } + } + + private class BadObjectConverter : JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WritePropertyName("hello"); + writer.WriteNullValue(); + } + + public override bool HandleNull => true; + } + + private class ClassWithUri + { + public Uri MyUri { get; set; } + } + + + private class StructWithObject + { + public object MyObj { get; set; } + } + + [Fact] + public static void ObjectAsRootValue() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new ObjectConverter()); + + object obj = null; + Assert.Equal(@"""NullObject""", JsonSerializer.Serialize(obj, options)); + Assert.Equal("NullObject", JsonSerializer.Deserialize("null", options)); + + options = new JsonSerializerOptions(); + options.Converters.Add(new BadObjectConverter()); + Assert.Throws(() => JsonSerializer.Serialize(obj, options)); + } + + [Fact] + public static void ObjectAsCollectionElement() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new ObjectConverter()); + + List list = new List { null }; + Assert.Equal(@"[""NullObject""]", JsonSerializer.Serialize(list, options)); + + list = JsonSerializer.Deserialize>("[null]", options); + Assert.Equal("NullObject", list[0]); + + options = new JsonSerializerOptions(); + options.Converters.Add(new BadObjectConverter()); + + list[0] = null; + Assert.Throws(() => JsonSerializer.Serialize(list, options)); + } + + public class ObjectConverter : JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return "NullObject"; + } + + throw new NotSupportedException(); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteStringValue("NullObject"); + return; + } + + throw new NotSupportedException(); + } + + public override bool HandleNull => true; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.Constructor.cs b/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.Constructor.cs index 8fce473d62993..1d02e33d7f6a0 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.Constructor.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/TestClasses/TestClasses.Constructor.cs @@ -917,6 +917,13 @@ public NullArgTester(Point_3D_Struct point3DStruct, ImmutableArray immutabl } } + public class NullArgTester_Mutable + { + public Point_3D_Struct Point3DStruct { get; set; } + public ImmutableArray ImmutableArray { get; set; } + public int Int { get; set; } + } + public class ClassWithConstructor_SimpleAndComplexParameters : ITestClassWithParameterizedCtor { public byte MyByte { get; } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 881b2d8450fe1..cebf7e44807f9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetFrameworkCurrent) true @@ -70,6 +70,7 @@ +