Fix null handling of built-in converters. (#86056)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 10 May 2023 18:18:38 +0000 (21:18 +0300)
committerGitHub <noreply@github.com>
Wed, 10 May 2023 18:18:38 +0000 (19:18 +0100)
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UriConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Null.ReadTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Null.WriteTests.cs

index bd44ed167262265a791dc675e1df8f6c181566f0..d0521e6209183af0144e3fa5cb46ea9b8de9fcc4 100644 (file)
@@ -10,7 +10,12 @@ namespace System.Text.Json.Serialization.Converters
     {
         public override void Write(Utf8JsonWriter writer, JsonArray value, JsonSerializerOptions options)
         {
-            Debug.Assert(value != null);
+            if (value is null)
+            {
+                writer.WriteNullValue();
+                return;
+            }
+
             value.WriteTo(writer, options);
         }
 
index 860009ce74b5ee977e3fcfb89fb37b04b4f4650f..6c55059ec02fa1db1a27e92e7ce3f16351c89776 100644 (file)
@@ -35,7 +35,12 @@ namespace System.Text.Json.Serialization.Converters
 
         public override void Write(Utf8JsonWriter writer, JsonObject value, JsonSerializerOptions options)
         {
-            Debug.Assert(value != null);
+            if (value is null)
+            {
+                writer.WriteNullValue();
+                return;
+            }
+
             value.WriteTo(writer, options);
         }
 
index 68ae58bb2822bc1deb90a81d0d400fa6fea9214b..af01541d1b4e2c8f65a88c3553768951b8d5336e 100644 (file)
@@ -1,7 +1,6 @@
 // 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.Text.Json.Nodes;
 using System.Text.Json.Serialization.Metadata;
 
@@ -11,12 +10,22 @@ namespace System.Text.Json.Serialization.Converters
     {
         public override void Write(Utf8JsonWriter writer, JsonValue value, JsonSerializerOptions options)
         {
-            Debug.Assert(value != null);
+            if (value is null)
+            {
+                writer.WriteNullValue();
+                return;
+            }
+
             value.WriteTo(writer, options);
         }
 
-        public override JsonValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        public override JsonValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
+            if (reader.TokenType is JsonTokenType.Null)
+            {
+                return null;
+            }
+
             JsonElement element = JsonElement.ParseValue(ref reader);
             JsonValue value = new JsonValueTrimmable<JsonElement>(element, JsonMetadataServices.JsonElementConverter, options.GetNodeOptions());
             return value;
index 97526b2f02125d0db66f11e795dcbd559b0410d6..34c4cb2a472473f9799e43aa69438c4ae25c235e 100644 (file)
@@ -30,7 +30,12 @@ namespace System.Text.Json.Serialization.Converters
 
         public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
         {
-            Debug.Assert(value?.GetType() == typeof(object));
+            if (value is null)
+            {
+                writer.WriteNullValue();
+                return;
+            }
+
             writer.WriteStartObject();
             writer.WriteEndObject();
         }
index de505fe26fdd247dfd344d7288e0963dcfdfa673..22c67ce823d599b03905690796cca4a790029cd2 100644 (file)
@@ -12,6 +12,12 @@ namespace System.Text.Json.Serialization.Converters
 
         public override void Write(Utf8JsonWriter writer, JsonDocument value, JsonSerializerOptions options)
         {
+            if (value is null)
+            {
+                writer.WriteNullValue();
+                return;
+            }
+
             value.WriteTo(writer);
         }
     }
index e50a6521abda303754b3a164fe258fe15737022c..9d582cce8e099da291b3314a7c0f2ff1ae71bdda 100644 (file)
@@ -2,33 +2,43 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics;
-using System.Runtime.CompilerServices;
 
 namespace System.Text.Json.Serialization.Converters
 {
     internal sealed class UriConverter : JsonPrimitiveConverter<Uri>
     {
-        public override Uri Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        public override Uri? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
-            string? uriString = reader.GetString();
-            if (Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out Uri? value))
-            {
-                return value;
-            }
-
-            ThrowHelper.ThrowJsonException();
-            return null;
+            return reader.TokenType is JsonTokenType.Null ? null : ReadCore(ref reader);
         }
 
         public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options)
         {
+            if (value is null)
+            {
+                writer.WriteNullValue();
+                return;
+            }
+
             writer.WriteStringValue(value.OriginalString);
         }
 
         internal override Uri ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
             Debug.Assert(reader.TokenType is JsonTokenType.PropertyName);
-            return Read(ref reader, typeToConvert, options);
+            return ReadCore(ref reader);
+        }
+
+        private static Uri ReadCore(ref Utf8JsonReader reader)
+        {
+            string? uriString = reader.GetString();
+
+            if (!Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out Uri? value))
+            {
+                ThrowHelper.ThrowJsonException();
+            }
+
+            return value;
         }
 
         internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options, bool isWritingExtensionDataProperty)
index fa4d0a07dbb9e742fbae6806611821c8ae08b3a7..052d9285533b35a992985d7c58807268f5b7f4eb 100644 (file)
@@ -15,8 +15,13 @@ namespace System.Text.Json.Serialization.Converters
         private const int MaximumEscapedVersionLength = JsonConstants.MaxExpansionFactorWhileEscaping * MaximumVersionLength;
 #endif
 
-        public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        public override Version? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
+            if (reader.TokenType is JsonTokenType.Null)
+            {
+                return null;
+            }
+
             if (reader.TokenType != JsonTokenType.String)
             {
                 ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType);
@@ -73,6 +78,12 @@ namespace System.Text.Json.Serialization.Converters
 
         public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)
         {
+            if (value is null)
+            {
+                writer.WriteNullValue();
+                return;
+            }
+
 #if NETCOREAPP
             Span<char> span = stackalloc char[MaximumVersionLength];
             bool formattedSuccessfully = value.TryFormat(span, out int charsWritten);
index 9b5ecf8031ff3919bb43e9ae6b1819b0bcde0d57..41de7e6edc63c68c1a2ad91883938d081e1d9d03 100644 (file)
@@ -256,5 +256,44 @@ namespace System.Text.Json.Serialization.Tests
                 Assert.Null(JsonSerializer.Deserialize("null", type, options));
             }
         }
+
+        [Theory]
+        [MemberData(nameof(GetBuiltInConvertersForNullableTypes))]
+        public static void ReadNullValue_BuiltInConverter<T>(JsonConverter<T> converter)
+        {
+            var reader = new Utf8JsonReader("null"u8);
+            Assert.True(reader.Read());
+            Assert.Equal(JsonTokenType.Null, reader.TokenType);
+
+            T? result = converter.Read(ref reader, typeof(T), JsonSerializerOptions.Default);
+            Assert.True(result is null or JsonDocument { RootElement.ValueKind: JsonValueKind.Null } or JsonElement { ValueKind: JsonValueKind.Null });
+        }
+
+        [Theory]
+        [MemberData(nameof(GetBuiltInConvertersForNullableTypes))]
+        public static void DeserializeNullValue_BuiltInConverter<T>(JsonConverter<T> converter)
+        {
+            _ = converter; // Not needed here.
+
+            T? value = JsonSerializer.Deserialize<T>("null");
+            AssertNull(value);
+
+            T[]? array = JsonSerializer.Deserialize<T[]>("[null]");
+            AssertNull(array[0]);
+
+            List<T>? list = JsonSerializer.Deserialize<List<T>>("[null]");
+            AssertNull(list[0]);
+
+            GenericRecord<T>? record = JsonSerializer.Deserialize<GenericRecord<T>>("""{"Value":null}""");
+            AssertNull(record.Value);
+
+            Dictionary<string, T>? dictionary = JsonSerializer.Deserialize<Dictionary<string, T>>("""{"Key":null}""");
+            AssertNull(dictionary["Key"]);
+
+            static void AssertNull(T? result) => Assert.True(
+                result is null
+                       or JsonDocument { RootElement.ValueKind: JsonValueKind.Null }
+                       or JsonElement { ValueKind: JsonValueKind.Null });
+        }
     }
 }
index dec9dee5c0eee1185e80a6593c9b96dc3b2ac758..7de51c9146c54f6876674778eadf68c532db512e 100644 (file)
@@ -2,6 +2,9 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json.Serialization.Metadata;
 using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
@@ -273,5 +276,58 @@ namespace System.Text.Json.Serialization.Tests
             json = JsonSerializer.Serialize<int[,]>(arr, options);
             Assert.Equal("null", json);
         }
+
+        [Theory]
+        [MemberData(nameof(GetBuiltInConvertersForNullableTypes))]
+        public static void WriteNullValue_BuiltInConverter<T>(JsonConverter<T> converter)
+        {
+            T @null = default;
+            Assert.Null(@null);
+
+            using var stream = new Utf8MemoryStream();
+            using var writer = new Utf8JsonWriter(stream);
+
+            converter.Write(writer, @null, JsonSerializerOptions.Default);
+            writer.Flush();
+
+            Assert.Equal("null", stream.AsString());
+        }
+
+        [Theory]
+        [MemberData(nameof(GetBuiltInConvertersForNullableTypes))]
+        public static void SerializeNullValue_BuiltInConverter<T>(JsonConverter<T> converter)
+        {
+            _ = converter; // Not needed here.
+
+            T @null = default;
+            Assert.Null(@null);
+
+            string json = JsonSerializer.Serialize(@null);
+            Assert.Equal("null", json);
+
+            json = JsonSerializer.Serialize(new T[] { @null });
+            Assert.Equal("[null]", json);
+
+            json = JsonSerializer.Serialize(new List<T> { @null });
+            Assert.Equal("[null]", json);
+
+            json = JsonSerializer.Serialize(new GenericRecord<T>(@null));
+            Assert.Equal("""{"Value":null}""", json);
+
+            json = JsonSerializer.Serialize(new Dictionary<string, T> { ["Key"] = @null });
+            Assert.Equal("""{"Key":null}""", json);
+        }
+
+        public static IEnumerable<object?[]> GetBuiltInConvertersForNullableTypes()
+        {
+            return typeof(JsonMetadataServices)
+                .GetProperties(BindingFlags.Public | BindingFlags.Static)
+                .Where(prop =>
+                    prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(JsonConverter<>) &&
+                    !prop.PropertyType.GetGenericArguments()[0].IsValueType)
+                .Select(prop => new object?[] { prop.GetValue(null) });
+        }
+
+        public record GenericRecord<T>(T Value);
     }
 }