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 c42ad28bc83279..135345f8e856c7 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -465,7 +465,7 @@ public JsonSerializerOptions() { } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } public System.Text.Json.Serialization.ReferenceHandling ReferenceHandling { get { throw null; } set { } } public bool WriteIndented { get { throw null; } set { } } - public System.Text.Json.Serialization.JsonConverter? GetConverter(System.Type typeToConvert) { throw null; } + public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; } } public sealed partial class JsonString : System.Text.Json.JsonNode, System.IEquatable { diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 226f99b2b3927c..11363d9fa5e01e 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -357,9 +357,6 @@ The type '{0}' is not supported. - - The type '{0}' on '{1}' is not supported. - '{0}' is invalid after '/' at the beginning of the comment. Expected either '/' or '*'. @@ -485,4 +482,10 @@ Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandling to ReferenceHandling.Default. + + The converter '{0}' cannot return a null value. + + + The unsupported member type is located on type '{0}'. + \ No newline at end of file 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 68bf3d616528ba..461572bf70c7e9 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -119,6 +119,8 @@ + + @@ -129,7 +131,6 @@ - @@ -138,7 +139,6 @@ - diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs index d44a0b9316c09b..0f0c7711f02940 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Unescaping.cs @@ -42,11 +42,13 @@ public static bool TryGetUnescapedBase64Bytes(ReadOnlySpan utf8Source, int // TODO: Similar to escaping, replace the unescaping logic with publicly shipping APIs from https://github.com/dotnet/runtime/issues/27919 public static string GetUnescapedString(ReadOnlySpan utf8Source, int idx) { - byte[]? unescapedArray = null; + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = utf8Source.Length; + byte[]? pooledName = null; - Span utf8Unescaped = utf8Source.Length <= JsonConstants.StackallocThreshold ? - stackalloc byte[utf8Source.Length] : - (unescapedArray = ArrayPool.Shared.Rent(utf8Source.Length)); + Span utf8Unescaped = length <= JsonConstants.StackallocThreshold ? + stackalloc byte[length] : + (pooledName = ArrayPool.Shared.Rent(length)); Unescape(utf8Source, utf8Unescaped, idx, out int written); Debug.Assert(written > 0); @@ -56,15 +58,40 @@ public static string GetUnescapedString(ReadOnlySpan utf8Source, int idx) string utf8String = TranscodeHelper(utf8Unescaped); - if (unescapedArray != null) + if (pooledName != null) { utf8Unescaped.Clear(); - ArrayPool.Shared.Return(unescapedArray); + ArrayPool.Shared.Return(pooledName); } return utf8String; } + public static ReadOnlySpan GetUnescapedSpan(ReadOnlySpan utf8Source, int idx) + { + // The escaped name is always >= than the unescaped, so it is safe to use escaped name for the buffer length. + int length = utf8Source.Length; + byte[]? pooledName = null; + + Span utf8Unescaped = length <= JsonConstants.StackallocThreshold ? + stackalloc byte[length] : + (pooledName = ArrayPool.Shared.Rent(length)); + + Unescape(utf8Source, utf8Unescaped, idx, out int written); + Debug.Assert(written > 0); + + ReadOnlySpan propertyName = utf8Unescaped.Slice(0, written).ToArray(); + Debug.Assert(!propertyName.IsEmpty); + + if (pooledName != null) + { + new Span(pooledName, 0, written).Clear(); + ArrayPool.Shared.Return(pooledName); + } + + return propertyName; + } + public static bool UnescapeAndCompare(ReadOnlySpan utf8Source, ReadOnlySpan other) { Debug.Assert(utf8Source.Length >= other.Length && utf8Source.Length / JsonConstants.MaxExpansionFactorWhileEscaping <= other.Length); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs index 69448f1ba83336..c6d032cfef00ce 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ClassType.cs @@ -13,6 +13,8 @@ namespace System.Text.Json /// internal enum ClassType : byte { + // Default - no class type. + None = 0x0, // JsonObjectConverter<> - objects with properties. Object = 0x1, // JsonConverter<> - simple values. @@ -23,7 +25,5 @@ internal enum ClassType : byte Enumerable = 0x8, // JsonDictionaryConverter<,> - dictionary types. Dictionary = 0x10, - // Invalid (not used directly for serialization) - Invalid = 0x20 } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs index 3d1de1343519ae..27214b3c10a2b7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs @@ -14,7 +14,7 @@ namespace System.Text.Json.Serialization.Converters /// /// Converter factory for all IEnumerable types. /// - internal class JsonIEnumerableConverterFactory : JsonConverterFactory + internal class IEnumerableConverterFactory : JsonConverterFactory { private static readonly IDictionaryConverter s_converterForIDictionary = new IDictionaryConverter(); private static readonly IEnumerableConverter s_converterForIEnumerable = new IEnumerableConverter(); @@ -43,10 +43,9 @@ public override bool CanConvert(Type typeToConvert) [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.ListOfTConverter`2")] [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.QueueOfTConverter`2")] [PreserveDependency(".ctor", "System.Text.Json.Serialization.Converters.StackOfTConverter`2")] - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - JsonConverter? converter = null; - Type converterType; + Type converterType = null!; Type[] genericArgs; Type? elementType = null; Type? actualTypeToConvert; @@ -57,7 +56,7 @@ public override bool CanConvert(Type typeToConvert) // Verify that we don't have a multidimensional array. if (typeToConvert.GetArrayRank() > 1) { - return null; + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); } converterType = typeof(ArrayConverter<,>); @@ -80,7 +79,7 @@ public override bool CanConvert(Type typeToConvert) } else { - return null; + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); } } // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary @@ -94,7 +93,7 @@ public override bool CanConvert(Type typeToConvert) } else { - return null; + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); } } // IDictionary or deriving from IDictionary @@ -108,7 +107,7 @@ public override bool CanConvert(Type typeToConvert) } else { - return null; + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); } } // IReadOnlyDictionary or deriving from IReadOnlyDictionary @@ -122,7 +121,7 @@ public override bool CanConvert(Type typeToConvert) } else { - return null; + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert); } } // Immutable non-dictionaries from System.Collections.Immutable, e.g. ImmutableStack @@ -213,25 +212,24 @@ public override bool CanConvert(Type typeToConvert) converterType = typeof(IEnumerableConverter<>); } - if (converterType != null) - { - Type genericType; - if (converterType.GetGenericArguments().Length == 1) - { - genericType = converterType.MakeGenericType(typeToConvert); - } - else - { - genericType = converterType.MakeGenericType(typeToConvert, elementType!); - } + Debug.Assert(converterType != null); - converter = (JsonConverter)Activator.CreateInstance( - genericType, - BindingFlags.Instance | BindingFlags.Public, - binder: null, - args: null, - culture: null)!; + Type genericType; + if (converterType.GetGenericArguments().Length == 1) + { + genericType = converterType.MakeGenericType(typeToConvert); } + else + { + genericType = converterType.MakeGenericType(typeToConvert, elementType!); + } + + JsonConverter converter = (JsonConverter)Activator.CreateInstance( + genericType, + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: null, + culture: null)!; return converter; } 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 f2de18853f91dd..0c2d13974e892b 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 @@ -10,7 +10,7 @@ namespace System.Text.Json.Serialization.Converters /// /// Default base class implementation of JsonObjectConverter{T}. /// - internal sealed class ObjectDefaultConverter : JsonObjectConverter + internal sealed class ObjectDefaultConverter : JsonObjectConverter where T : notnull { internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value) { @@ -273,16 +273,10 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) { // Minimize boxing for structs by only boxing once here - object? objectValue = value; + object objectValue = value!; if (!state.SupportContinuation) { - if (objectValue == null) - { - writer.WriteNullValue(); - return true; - } - writer.WriteStartObject(); if (options.ReferenceHandling.ShouldWritePreservedReferences()) @@ -338,12 +332,6 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer { if (!state.Current.ProcessedStartToken) { - if (objectValue == null) - { - writer.WriteNullValue(); - return true; - } - writer.WriteStartObject(); if (options.ReferenceHandling.ShouldWritePreservedReferences()) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs index 796bd86d16b8a7..1e9fc14cd83fa2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs @@ -20,11 +20,8 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer Type valueTypeToConvert = typeToConvert.GetGenericArguments()[0]; - JsonConverter? valueConverter = options.GetConverter(valueTypeToConvert); - if (valueConverter == null) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(valueTypeToConvert); - } + JsonConverter valueConverter = options.GetConverter(valueTypeToConvert); + Debug.Assert(valueConverter != null); return CreateValueConverter(valueTypeToConvert, valueConverter); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs index 87d2a2e981c4cc..d60f02ffa7166b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs @@ -17,28 +17,19 @@ private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInf return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(propertyInfo, options); } - JsonConverter? converter; - ClassType classType = GetClassType( + JsonConverter converter = GetConverter( propertyType, parentClassType, propertyInfo, - out Type? runtimeType, - out Type? _, - out converter, + out Type runtimeType, options); - if (converter == null) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(propertyType, parentClassType, propertyInfo); - } - return CreateProperty( declaredPropertyType: propertyType, runtimePropertyType: runtimeType, propertyInfo, parentClassType, converter, - classType, options); } @@ -48,7 +39,6 @@ internal static JsonPropertyInfo CreateProperty( PropertyInfo? propertyInfo, Type parentClassType, JsonConverter converter, - ClassType classType, JsonSerializerOptions options) { // Create the JsonPropertyInfo instance. @@ -58,7 +48,7 @@ internal static JsonPropertyInfo CreateProperty( parentClassType, declaredPropertyType, runtimePropertyType, - runtimeClassType: classType, + runtimeClassType: converter.ClassType, propertyInfo, converter, options); @@ -74,9 +64,8 @@ internal static JsonPropertyInfo CreateProperty( /// internal static JsonPropertyInfo CreatePolicyProperty( Type declaredPropertyType, - Type? runtimePropertyType, + Type runtimePropertyType, JsonConverter converter, - ClassType classType, JsonSerializerOptions options) { return CreateProperty( @@ -85,7 +74,6 @@ internal static JsonPropertyInfo CreatePolicyProperty( propertyInfo: null, // Not a real property so this is null. parentClassType: typeof(object), // a dummy value (not used) converter : converter, - classType : classType, options); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs index a6f6961c523241..3f8b95e33a41f9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs @@ -2,7 +2,6 @@ // 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.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -115,14 +114,15 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) Type = type; Options = options; - ClassType = GetClassType( - type, - parentClassType: type, - propertyInfo: null, - out Type? runtimeType, - out Type? elementType, - out JsonConverter? converter, - options); + JsonConverter converter = GetConverter( + Type, + parentClassType: null, // A ClassInfo never has a "parent" class. + propertyInfo: null, // A ClassInfo never has a "parent" property. + out Type runtimeType, + Options); + + ClassType = converter.ClassType; + PolicyProperty = CreatePolicyProperty(Type, runtimeType, converter, Options); switch (ClassType) { @@ -188,34 +188,29 @@ public JsonClassInfo(Type type, JsonSerializerOptions options) PropertyCache = cache; cache.Values.CopyTo(cacheArray, 0); PropertyCacheArray = cacheArray; - - // Create the policy property. - PolicyProperty = CreatePolicyProperty(type, runtimeType, converter!, ClassType, options); } break; case ClassType.Enumerable: case ClassType.Dictionary: { - ElementType = elementType; - PolicyProperty = CreatePolicyProperty(type, runtimeType, converter!, ClassType, options); - CreateObject = options.MemberAccessorStrategy.CreateConstructor(PolicyProperty.RuntimePropertyType!); + ElementType = converter.ElementType; + CreateObject = options.MemberAccessorStrategy.CreateConstructor(runtimeType); } break; case ClassType.Value: case ClassType.NewValue: { CreateObject = options.MemberAccessorStrategy.CreateConstructor(type); - PolicyProperty = CreatePolicyProperty(type, runtimeType, converter!, ClassType, options); } break; - case ClassType.Invalid: + case ClassType.None: { ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); } break; default: Debug.Fail($"Unexpected class type: {ClassType}"); - break; + throw new InvalidOperationException(); } } @@ -228,11 +223,8 @@ private bool DetermineExtensionDataProperty(Dictionary if (typeof(IDictionary).IsAssignableFrom(declaredPropertyType) || typeof(IDictionary).IsAssignableFrom(declaredPropertyType)) { - JsonConverter? converter = Options.GetConverter(declaredPropertyType); - if (converter == null) - { - ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(declaredPropertyType); - } + JsonConverter converter = Options.GetConverter(declaredPropertyType); + Debug.Assert(converter != null); } else { @@ -516,24 +508,16 @@ public static ulong GetKey(ReadOnlySpan propertyName) // - runtime type, // - element type (if the type is a collection), // - the converter (either native or custom), if one exists. - public static ClassType GetClassType( + public static JsonConverter GetConverter( Type type, - Type parentClassType, + Type? parentClassType, PropertyInfo? propertyInfo, - out Type? runtimeType, - out Type? elementType, - out JsonConverter? converter, + out Type runtimeType, JsonSerializerOptions options) { Debug.Assert(type != null); - converter = options.DetermineConverter(parentClassType, type, propertyInfo); - if (converter == null) - { - runtimeType = null; - elementType = null; - return ClassType.Invalid; - } + JsonConverter converter = options.DetermineConverter(parentClassType, type, propertyInfo)!; // The runtimeType is the actual value being assigned to the property. // There are three types to consider for the runtimeType: @@ -569,14 +553,13 @@ public static ClassType GetClassType( } else { - throw ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type, parentClassType, propertyInfo); + runtimeType = default!; + ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(type); } } } - elementType = converter.ElementType; - - return converter.ClassType; + return converter; } } } 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 1bc90ae29a17d6..8d7b4bbb9a915d 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 @@ -20,17 +20,6 @@ internal JsonConverter() { } internal abstract ClassType ClassType { get; } - // Whether the converter should handle the null value. - internal virtual bool HandleNullValue - { - get - { - // 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. - return TypeToConvert.IsValueType; - } - } - /// /// Can direct Read or Write methods be called (for performance). /// @@ -47,6 +36,22 @@ internal virtual bool HandleNullValue 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. + /// + internal bool IsValueType { get; set; } + + /// + /// Loosely-typed ReadCore() that forwards to strongly-typed ReadCore(). + /// + internal abstract object? ReadCoreAsObject(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state); + // For polymorphic cases, the concrete type to create. internal virtual Type RuntimeType => TypeToConvert; @@ -59,7 +64,11 @@ internal bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) // This is used internally to quickly determine the type being converted for JsonConverter. internal abstract Type TypeToConvert { get; } - internal abstract bool TryReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value); internal abstract bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state); + + /// + /// Loosely-typed WriteCore() that forwards to strongly-typed WriteCore(). + /// + internal abstract bool WriteCoreAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs index 352c32ed859c75..63faf511eadf00 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs @@ -23,7 +23,7 @@ internal sealed override ClassType ClassType { get { - return ClassType.Invalid; + return ClassType.None; } } @@ -48,18 +48,23 @@ internal override JsonPropertyInfo CreateJsonPropertyInfo() internal sealed override Type? ElementType => null; - internal JsonConverter? GetConverterInternal(Type typeToConvert, JsonSerializerOptions options) + internal JsonConverter GetConverterInternal(Type typeToConvert, JsonSerializerOptions options) { Debug.Assert(CanConvert(typeToConvert)); - return CreateConverter(typeToConvert, options); + + JsonConverter? converter = CreateConverter(typeToConvert, options); + if (converter == null) + { + ThrowHelper.ThrowInvalidOperationException_SerializerConverterFactoryReturnsNull(GetType()); + } + + return converter!; } - internal sealed override bool TryReadAsObject( + internal sealed override object ReadCoreAsObject( ref Utf8JsonReader reader, - Type typeToConvert, JsonSerializerOptions options, - ref ReadStack state, - out object? value) + ref ReadStack state) { // We should never get here. Debug.Assert(false); @@ -67,7 +72,11 @@ internal sealed override bool TryReadAsObject( throw new InvalidOperationException(); } - internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state) + internal sealed override bool TryWriteAsObject( + Utf8JsonWriter writer, + object? value, + JsonSerializerOptions options, + ref WriteStack state) { // We should never get here. Debug.Assert(false); @@ -76,5 +85,17 @@ internal sealed override bool TryWriteAsObject(Utf8JsonWriter writer, object? va } internal sealed override Type TypeToConvert => null!; + + internal sealed override bool WriteCoreAsObject( + Utf8JsonWriter writer, + object? value, + JsonSerializerOptions options, + ref WriteStack state) + { + // We should never get here. + Debug.Assert(false); + + throw new InvalidOperationException(); + } } } 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 new file mode 100644 index 00000000000000..e1cc141f9439b9 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.ReadCore.cs @@ -0,0 +1,110 @@ +// 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. + +namespace System.Text.Json.Serialization +{ + public partial class JsonConverter + { + internal sealed override object? ReadCoreAsObject( + ref Utf8JsonReader reader, + JsonSerializerOptions options, + ref ReadStack state) + { + return ReadCore(ref reader, options, ref state); + } + + internal T ReadCore( + ref Utf8JsonReader reader, + JsonSerializerOptions options, + ref ReadStack state) + { + try + { + if (!state.IsContinuation) + { + if (!SingleValueReadWithReadAhead(ClassType, ref reader, ref state)) + { + if (state.SupportContinuation) + { + // If a Stream-based scenaio, return the actual value previously found; + // this may or may not be the final pass through here. + state.BytesConsumed += reader.BytesConsumed; + if (state.Current.ReturnValue == null) + { + // Avoid returning null for value types. + return default!; + } + + return (T)state.Current.ReturnValue!; + } + else + { + // Read more data until we have the full element. + state.BytesConsumed += reader.BytesConsumed; + return default!; + } + } + } + else + { + // For a continuation, read ahead here to avoid having to build and then tear + // down the call stack if there is more than one buffer fetch necessary. + if (!SingleValueReadWithReadAhead(ClassType.Value, ref reader, ref state)) + { + state.BytesConsumed += reader.BytesConsumed; + return default!; + } + } + + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.PolicyProperty!; + bool success = TryRead(ref reader, jsonPropertyInfo.RuntimePropertyType!, options, ref state, out T value); + if (success) + { + // Read any trailing whitespace. This will throw if JsonCommentHandling=Disallow. + // Avoiding setting ReturnValue for the final block; reader.Read() returns 'false' even when this is the final block. + if (!reader.Read() && !reader.IsFinalBlock) + { + // This method will re-enter if so set `ReturnValue` which will be returned during re-entry. + state.Current.ReturnValue = value; + } + } + + state.BytesConsumed += reader.BytesConsumed; + return value; + } + catch (JsonReaderException ex) + { + ThrowHelper.ReThrowWithPath(state, ex); + return default; + } + catch (FormatException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) + { + ThrowHelper.ReThrowWithPath(state, reader, ex); + return default; + } + catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) + { + ThrowHelper.ReThrowWithPath(state, reader, ex); + return default; + } + catch (JsonException ex) + { + ThrowHelper.AddJsonExceptionInformation(state, reader, ex); + throw; + } + catch (NotSupportedException ex) + { + // If the message already contains Path, just re-throw. This could occur in serializer re-entry cases. + // To get proper Path semantics in re-entry cases, APIs that take 'state' need to be used. + if (ex.Message.Contains(" Path: ")) + { + throw; + } + + ThrowHelper.ThrowNotSupportedException(state, reader, ex); + return default; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs new file mode 100644 index 00000000000000..8486c5b525d2e1 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs @@ -0,0 +1,59 @@ +// 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. + +namespace System.Text.Json.Serialization +{ + public partial class JsonConverter + { + internal sealed override bool WriteCoreAsObject( + Utf8JsonWriter writer, + object? value, + JsonSerializerOptions options, + ref WriteStack state) + { + // Value types can never have a null except for Nullable. + if (value == null && IsValueType && Nullable.GetUnderlyingType(TypeToConvert) == null) + { + ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert); + } + + T actualValue = (T)value!; + return WriteCore(writer, actualValue, options, ref state); + } + + internal bool WriteCore( + Utf8JsonWriter writer, + T value, + JsonSerializerOptions options, + ref WriteStack state) + { + try + { + return TryWrite(writer, value, options, ref state); + } + catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) + { + ThrowHelper.ReThrowWithPath(state, ex); + throw; + } + catch (JsonException ex) + { + ThrowHelper.AddJsonExceptionInformation(state, ex); + throw; + } + catch (NotSupportedException ex) + { + // If the message already contains Path, just re-throw. This could occur in serializer re-entry cases. + // To get proper Path semantics in re-entry cases, APIs that take 'state' need to be used. + if (ex.Message.Contains(" Path: ")) + { + throw; + } + + ThrowHelper.ThrowNotSupportedException(state, ex); + return default; + } + } + } +} 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 1e2c19119c93a9..0bd354c2714458 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 @@ -11,7 +11,7 @@ namespace System.Text.Json.Serialization /// Converts an object or value to or from JSON. /// /// The to convert. - public abstract class JsonConverter : JsonConverter + public abstract partial class JsonConverter : JsonConverter { /// /// When overidden, constructs a new instance. @@ -21,7 +21,8 @@ protected internal JsonConverter() // Today only typeof(object) can have polymorphic writes. // In the future, this will be check for !IsSealed (and excluding value types). CanBePolymorphic = TypeToConvert == typeof(object); - + IsValueType = TypeToConvert.IsValueType; + HandleNullValue = ShouldHandleNullValue; IsInternalConverter = GetType().Assembly == typeof(JsonConverter).Assembly; CanUseDirectReadOrWrite = !CanBePolymorphic && IsInternalConverter && ClassType == ClassType.Value; } @@ -48,6 +49,11 @@ internal sealed override JsonPropertyInfo CreateJsonPropertyInfo() 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; + /// /// Is the converter built-in. /// @@ -68,22 +74,6 @@ internal virtual bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerO return true; } - // This non-generic API is sealed as it just forwards to the generic version. - internal sealed override bool TryReadAsObject(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value) - { - bool success = TryRead(ref reader, typeToConvert, options, ref state, out T valueOfT); - if (success) - { - value = valueOfT; - } - else - { - value = default; - } - - return success; - } - // Provide a default implementation for value converters. internal virtual bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T value) { @@ -212,7 +202,7 @@ internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions opt { if (writer.CurrentDepth >= options.EffectiveMaxDepth) { - ThrowHelper.ThrowInvalidOperationException_SerializerCycleDetected(options.MaxDepth); + ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.MaxDepth); } if (CanBePolymorphic) @@ -242,6 +232,16 @@ internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions opt } } } + else + { + // We do not pass null values to converters unless HandleNullValue 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; + } + } if (ClassType == ClassType.Value) { @@ -284,7 +284,7 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json if (writer.CurrentDepth >= options.EffectiveMaxDepth) { - ThrowHelper.ThrowInvalidOperationException_SerializerCycleDetected(options.MaxDepth); + ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.MaxDepth); } bool success; 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 205b45f3c2aa88..e2aabd2ceacb97 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 @@ -20,7 +20,7 @@ public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, Jso } ReadStack state = default; - state.InitializeRoot(typeToConvert, options); + state.Initialize(typeToConvert, options, supportContinuation: false); TryRead(ref reader, typeToConvert, options, ref state, out T value); return value; } @@ -34,7 +34,7 @@ public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializer } WriteStack state = default; - state.InitializeRoot(typeof(T), options, supportContinuation: false); + state.Initialize(typeof(T), options, supportContinuation: false); TryWrite(writer, value, options, ref state); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs index 3f233790ee385a..0e08de715bf7ad 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs @@ -58,7 +58,7 @@ internal static bool ResolveMetadata( else if (metadata == MetadataPropertyName.Ref) { state.Current.JsonPropertyName = propertyName.ToArray(); - if (converter.TypeToConvert.IsValueType) + if (converter.IsValueType) { ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert); } 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 ffc8c8846def18..701044f70e0ecd 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 @@ -36,7 +36,7 @@ internal static JsonPropertyInfo LookupProperty( { int idx = propertyName.IndexOf(JsonConstants.BackSlash); Debug.Assert(idx != -1); - unescapedPropertyName = GetUnescapedString(propertyName, idx); + unescapedPropertyName = JsonReaderHelper.GetUnescapedSpan(propertyName, idx); } else { 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 1c5113b3f8bef3..bfda0563165d3e 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 @@ -2,21 +2,34 @@ // 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; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Converters; + namespace System.Text.Json { public static partial class JsonSerializer { - private static object? ReadCore( - Type returnType, - JsonSerializerOptions options, - ref Utf8JsonReader reader) + private static TValue ReadCore(ref Utf8JsonReader reader, Type returnType, JsonSerializerOptions options) { ReadStack state = default; - state.InitializeRoot(returnType, options); + state.Initialize(returnType, options, supportContinuation: false); + JsonConverter jsonConverter = state.Current.JsonPropertyInfo!.ConverterBase; + return ReadCore(jsonConverter, ref reader, options, ref state); + } - ReadCore(options, ref reader, ref state); + private static TValue ReadCore(JsonConverter jsonConverter, ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state) + { + if (jsonConverter is JsonConverter converter) + { + // Call the strongly-typed ReadCore that will not box structs. + return converter.ReadCore(ref reader, options, ref state); + } - return state.Current.ReturnValue; + // The non-generic API was called or we have a polymorphic case where TValue is not equal to the T in JsonConverter. + object? value = jsonConverter.ReadCoreAsObject(ref reader, options, ref state); + Debug.Assert(value == null || value is TValue); + return (TValue)value!; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs index 93a4f5194b1833..af9c932b6006a3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace System.Text.Json { @@ -23,7 +24,15 @@ public static partial class JsonSerializer [return: MaybeNull] public static TValue Deserialize(ReadOnlySpan utf8Json, JsonSerializerOptions? options = null) { - return (TValue)ParseCore(utf8Json, typeof(TValue), options)!; + if (options == null) + { + options = JsonSerializerOptions.s_defaultOptions; + } + + var readerState = new JsonReaderState(options.GetReaderOptions()); + var reader = new Utf8JsonReader(utf8Json, isFinalBlock: true, readerState); + + return ReadCore(ref reader, typeof(TValue), options); } /// @@ -44,13 +53,10 @@ public static TValue Deserialize(ReadOnlySpan utf8Json, JsonSerial public static object? Deserialize(ReadOnlySpan utf8Json, Type returnType, JsonSerializerOptions? options = null) { if (returnType == null) + { throw new ArgumentNullException(nameof(returnType)); + } - return ParseCore(utf8Json, returnType, options); - } - - private static object? ParseCore(ReadOnlySpan utf8Json, Type returnType, JsonSerializerOptions? options) - { if (options == null) { options = JsonSerializerOptions.s_defaultOptions; @@ -58,12 +64,8 @@ public static TValue Deserialize(ReadOnlySpan utf8Json, JsonSerial var readerState = new JsonReaderState(options.GetReaderOptions()); var reader = new Utf8JsonReader(utf8Json, isFinalBlock: true, readerState); - object? result = ReadCore(returnType, options, ref reader); - - // The reader should have thrown if we have remaining bytes. - Debug.Assert(reader.BytesConsumed == utf8Json.Length); - return result; + return ReadCore(ref reader, returnType, options); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs index 2e426a1393346f..8902537450bcc4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs @@ -34,7 +34,9 @@ public static ValueTask DeserializeAsync( CancellationToken cancellationToken = default) { if (utf8Json == null) + { throw new ArgumentNullException(nameof(utf8Json)); + } return ReadAsync(utf8Json, typeof(TValue), options, cancellationToken); } @@ -76,8 +78,8 @@ public static ValueTask DeserializeAsync( private static async ValueTask ReadAsync( Stream utf8Json, Type returnType, - JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) + JsonSerializerOptions? options, + CancellationToken cancellationToken) { if (options == null) { @@ -85,10 +87,9 @@ private static async ValueTask ReadAsync( } ReadStack state = default; - state.InitializeRoot(returnType, options); + state.Initialize(returnType, options, supportContinuation: true); - // Ensures converters support contination due to having to re-populate the buffer from a Stream. - state.SupportContinuation = true; + JsonConverter converter = state.Current.JsonPropertyInfo!.ConverterBase; var readerState = new JsonReaderState(options.GetReaderOptions()); @@ -153,12 +154,13 @@ private static async ValueTask ReadAsync( } // Process the data available - ReadCore( + TValue value = ReadCore( ref readerState, isFinalBlock, new ReadOnlySpan(buffer, start, bytesInBuffer), options, - ref state); + ref state, + converter); Debug.Assert(state.BytesConsumed <= bytesInBuffer); int bytesConsumed = checked((int)state.BytesConsumed); @@ -167,7 +169,10 @@ private static async ValueTask ReadAsync( if (isFinalBlock) { - break; + // The reader should have thrown if we have remaining bytes. + Debug.Assert(bytesInBuffer == 0); + + return value; } // Check if we need to shift or expand the buffer because there wasn't enough data to complete deserialization. @@ -198,19 +203,15 @@ private static async ValueTask ReadAsync( new Span(buffer, 0, clearMax).Clear(); ArrayPool.Shared.Return(buffer); } - - // The reader should have thrown if we have remaining bytes. - Debug.Assert(bytesInBuffer == 0); - - return (TValue)state.Current.ReturnValue!; } - private static void ReadCore( + private static TValue ReadCore( ref JsonReaderState readerState, bool isFinalBlock, ReadOnlySpan buffer, JsonSerializerOptions options, - ref ReadStack state) + ref ReadStack state, + JsonConverter converterBase) { var reader = new Utf8JsonReader(buffer, isFinalBlock, readerState); @@ -221,12 +222,10 @@ private static void ReadCore( state.ReadAhead = !isFinalBlock; state.BytesConsumed = 0; - ReadCore( - options, - ref reader, - ref state); + TValue value = ReadCore(converterBase, ref reader, options, ref state); readerState = reader.CurrentState; + return value!; } } } 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 f5379c748131cc..80416001bcf7b7 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 @@ -5,9 +5,14 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace System.Text.Json { + /// + /// Provides functionality to serialize objects or value types to JSON and + /// deserialize JSON into objects or value types. + /// public static partial class JsonSerializer { /// @@ -30,7 +35,12 @@ public static partial class JsonSerializer [return: MaybeNull] public static TValue Deserialize(string json, JsonSerializerOptions? options = null) { - return (TValue)Deserialize(json, typeof(TValue), options)!; + if (json == null) + { + throw new ArgumentNullException(nameof(json)); + } + + return Deserialize(json, typeof(TValue), options)!; } /// @@ -53,8 +63,6 @@ public static TValue Deserialize(string json, JsonSerializerOptions? opt /// public static object? Deserialize(string json, Type returnType, JsonSerializerOptions? options = null) { - const long ArrayPoolMaxSizeBeforeUsingNormalAlloc = 1024 * 1024; - if (json == null) { throw new ArgumentNullException(nameof(json)); @@ -65,12 +73,20 @@ public static TValue Deserialize(string json, JsonSerializerOptions? opt throw new ArgumentNullException(nameof(returnType)); } + object? value = Deserialize(json, returnType, options)!; + + return value; + } + + private static TValue Deserialize(string json, Type returnType, JsonSerializerOptions? options) + { + const long ArrayPoolMaxSizeBeforeUsingNormalAlloc = 1024 * 1024; + if (options == null) { options = JsonSerializerOptions.s_defaultOptions; } - object? result; byte[]? tempArray = null; // For performance, avoid obtaining actual byte count unless memory usage is higher than the threshold. @@ -88,10 +104,13 @@ public static TValue Deserialize(string json, JsonSerializerOptions? opt var readerState = new JsonReaderState(options.GetReaderOptions()); var reader = new Utf8JsonReader(utf8, isFinalBlock: true, readerState); - result = ReadCore(returnType, options, ref reader); + + TValue value = ReadCore(ref reader, returnType, options); // The reader should have thrown if we have remaining bytes. Debug.Assert(reader.BytesConsumed == actualByteCount); + + return value; } finally { @@ -101,8 +120,6 @@ public static TValue Deserialize(string json, JsonSerializerOptions? opt ArrayPool.Shared.Return(tempArray); } } - - return result; } } } 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 df534fa8f4691f..aee59161880b42 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 @@ -5,6 +5,7 @@ using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace System.Text.Json { @@ -13,16 +14,20 @@ public static partial class JsonSerializer /// /// Internal version that allows re-entry with preserving ReadStack so that JsonPath works correctly. /// - internal static T Deserialize(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, string? propertyName = null) + internal static TValue Deserialize(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, string? propertyName = null) { if (options == null) { throw new ArgumentNullException(nameof(options)); } - state.Current.InitializeReEntry(typeof(T), options, propertyName); + state.Current.InitializeReEntry(typeof(TValue), options, propertyName); - T value = (T)ReadCoreReEntry(options, ref reader, ref state)!; + JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo!; + + JsonConverter converter = (JsonConverter)jsonPropertyInfo.ConverterBase; + bool success = converter.TryRead(ref reader, jsonPropertyInfo.RuntimePropertyType!, options, ref state, out TValue value); + Debug.Assert(success); // Clear the current property state since we are done processing it. state.Current.EndProperty(); @@ -71,7 +76,15 @@ internal static T Deserialize(ref Utf8JsonReader reader, JsonSerializerOption [return: MaybeNull] public static TValue Deserialize(ref Utf8JsonReader reader, JsonSerializerOptions? options = null) { - return (TValue)ReadValueCore(ref reader, typeof(TValue), options)!; + if (options == null) + { + options = JsonSerializerOptions.s_defaultOptions; + } + + ReadStack state = default; + state.Initialize(typeof(TValue), options, supportContinuation: false); + + return ReadValueCore(options, ref reader, ref state); } /// @@ -118,24 +131,19 @@ public static TValue Deserialize(ref Utf8JsonReader reader, JsonSerializ public static object? Deserialize(ref Utf8JsonReader reader, Type returnType, JsonSerializerOptions? options = null) { if (returnType == null) + { throw new ArgumentNullException(nameof(returnType)); + } - return ReadValueCore(ref reader, returnType, options); - } - - private static object? ReadValueCore(ref Utf8JsonReader reader, Type returnType, JsonSerializerOptions? options) - { if (options == null) { options = JsonSerializerOptions.s_defaultOptions; } ReadStack state = default; - state.InitializeRoot(returnType, options); - - ReadValueCore(options, ref reader, ref state); + state.Initialize(returnType, options, supportContinuation: false); - return state.Current.ReturnValue; + return ReadValueCore(options, ref reader, ref state); } private static void CheckSupportedOptions(JsonReaderOptions readerOptions, string paramName) @@ -146,7 +154,7 @@ private static void CheckSupportedOptions(JsonReaderOptions readerOptions, strin } } - private static void ReadValueCore(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) + private static TValue ReadValueCore(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack state) { JsonReaderState readerState = reader.CurrentState; CheckSupportedOptions(readerState.Options, nameof(reader)); @@ -325,10 +333,13 @@ private static void ReadValueCore(JsonSerializerOptions options, ref Utf8JsonRea var newReader = new Utf8JsonReader(rentedSpan, originalReaderOptions); - ReadCore(options, ref newReader, ref state); + JsonConverter jsonConverter = state.Current.JsonPropertyInfo!.ConverterBase; + TValue value = ReadCore(jsonConverter, ref newReader, options, ref state); // The reader should have thrown if we have remaining bytes. Debug.Assert(newReader.BytesConsumed == length); + + return value; } catch (JsonException) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs deleted file mode 100644 index 50deaa6ababff5..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs +++ /dev/null @@ -1,114 +0,0 @@ -// 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.Diagnostics; -using System.Text.Json.Serialization; - -namespace System.Text.Json -{ - /// - /// Provides functionality to serialize objects or value types to JSON and - /// deserialize JSON into objects or value types. - /// - public static partial class JsonSerializer - { - private static void ReadCore( - JsonSerializerOptions options, - ref Utf8JsonReader reader, - ref ReadStack state) - { - try - { - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo!.PolicyProperty!; - JsonConverter converter = jsonPropertyInfo.ConverterBase; - - if (!state.IsContinuation) - { - if (!JsonConverter.SingleValueReadWithReadAhead(converter.ClassType, ref reader, ref state)) - { - // Read more data until we have the full element. - state.BytesConsumed += reader.BytesConsumed; - return; - } - } - else - { - // For a continuation, read ahead here to avoid having to build and then tear - // down the call stack if there is more than one buffer fetch necessary. - if (!JsonConverter.SingleValueReadWithReadAhead(ClassType.Value, ref reader, ref state)) - { - state.BytesConsumed += reader.BytesConsumed; - return; - } - } - - bool success = converter.TryReadAsObject(ref reader, jsonPropertyInfo.RuntimePropertyType!, options, ref state, out object? value); - if (success) - { - state.Current.ReturnValue = value; - - // Read any trailing whitespace. - // If additional whitespace exists after this read, the subsequent call to reader.Read() will throw. - reader.Read(); - } - - state.BytesConsumed += reader.BytesConsumed; - } - catch (JsonReaderException ex) - { - // Re-throw with Path information. - ThrowHelper.ReThrowWithPath(state, ex); - } - catch (FormatException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) - { - ThrowHelper.ReThrowWithPath(state, reader, ex); - } - catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) - { - ThrowHelper.ReThrowWithPath(state, reader, ex); - } - catch (JsonException ex) - { - ThrowHelper.AddExceptionInformation(state, reader, ex); - throw; - } - } - - internal static object? ReadCoreReEntry( - JsonSerializerOptions options, - ref Utf8JsonReader reader, - ref ReadStack state) - { - JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo!; - JsonConverter converter = jsonPropertyInfo.ConverterBase; - bool success = converter.TryReadAsObject(ref reader, jsonPropertyInfo.RuntimePropertyType!, options, ref state, out object? value); - Debug.Assert(success); - return value; - } - - private static ReadOnlySpan GetUnescapedString(ReadOnlySpan utf8Source, int idx) - { - // The escaped name is always longer than the unescaped, so it is safe to use escaped name for the buffer length. - int length = utf8Source.Length; - byte[]? pooledName = null; - - Span unescapedName = length <= JsonConstants.StackallocThreshold ? - stackalloc byte[length] : - (pooledName = ArrayPool.Shared.Rent(length)); - - JsonReaderHelper.Unescape(utf8Source, unescapedName, idx, out int written); - ReadOnlySpan propertyName = unescapedName.Slice(0, written).ToArray(); - - if (pooledName != null) - { - // We clear the array because it is "user data" (although a property name). - new Span(pooledName, 0, written).Clear(); - ArrayPool.Shared.Return(pooledName); - } - - return propertyName; - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs index 0639fe0827979a..0047d43c5cd038 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.ByteArray.cs @@ -14,7 +14,7 @@ public static partial class JsonSerializer /// Options to control the conversion behavior. public static byte[] SerializeToUtf8Bytes(TValue value, JsonSerializerOptions? options = null) { - return WriteCoreBytes(value, typeof(TValue), options); + return WriteCoreBytes(value, typeof(TValue), options); } /// @@ -26,8 +26,35 @@ public static byte[] SerializeToUtf8Bytes(TValue value, JsonSerializerOp /// Options to control the conversion behavior. public static byte[] SerializeToUtf8Bytes(object? value, Type inputType, JsonSerializerOptions? options = null) { - VerifyValueAndType(value, inputType); - return WriteCoreBytes(value, inputType, options); + if (inputType == null) + { + throw new ArgumentNullException(nameof(inputType)); + } + + if (value != null && !inputType.IsAssignableFrom(value.GetType())) + { + ThrowHelper.ThrowArgumentException_DeserializeWrongType(inputType, value); + } + + return WriteCoreBytes(value!, inputType, options); + } + + private static byte[] WriteCoreBytes(TValue value, Type inputType, JsonSerializerOptions? options) + { + if (options == null) + { + options = JsonSerializerOptions.s_defaultOptions; + } + + using (var output = new PooledByteBufferWriter(options.DefaultBufferSize)) + { + using (var writer = new Utf8JsonWriter(output, options.GetWriterOptions())) + { + WriteCore(writer, value, inputType, options); + } + + return output.WrittenMemory.ToArray(); + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs index 1550edb0b77725..0b5061e624470b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs @@ -23,7 +23,7 @@ internal static MetadataPropertyName WriteReferenceForObject( MetadataPropertyName metadataToWrite; // If the jsonConverter supports immutable dictionaries or value types, don't write any metadata - if (!jsonConverter.CanHaveIdMetadata || jsonConverter.TypeToConvert.IsValueType) + if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType) { metadataToWrite = MetadataPropertyName.NoMetadata; } @@ -53,7 +53,7 @@ internal static MetadataPropertyName WriteReferenceForCollection( MetadataPropertyName metadataToWrite; // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata - if (!jsonConverter.CanHaveIdMetadata || jsonConverter.TypeToConvert.IsValueType) + if (!jsonConverter.CanHaveIdMetadata || jsonConverter.IsValueType) { writer.WriteStartArray(); metadataToWrite = MetadataPropertyName.NoMetadata; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs index 31d2b2180d86cd..e045d3f9f87d93 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs @@ -9,103 +9,48 @@ namespace System.Text.Json { public static partial class JsonSerializer { - private static void VerifyValueAndType(object? value, Type type) + private static void WriteCore(Utf8JsonWriter writer, TValue value, Type inputType, JsonSerializerOptions options) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - else if (value != null) - { - if (!type.IsAssignableFrom(value.GetType())) - { - ThrowHelper.ThrowArgumentException_DeserializeWrongType(type, value); - } - } - } - - private static byte[] WriteCoreBytes(object? value, Type type, JsonSerializerOptions? options) - { - if (options == null) - { - options = JsonSerializerOptions.s_defaultOptions; - } - - byte[] result; - - using (var output = new PooledByteBufferWriter(options.DefaultBufferSize)) - { - WriteCore(output, value, type, options); - result = output.WrittenMemory.ToArray(); - } - - return result; - } - - private static string WriteCoreString(object? value, Type type, JsonSerializerOptions? options) - { - if (options == null) - { - options = JsonSerializerOptions.s_defaultOptions; - } - - string result; - - using (var output = new PooledByteBufferWriter(options.DefaultBufferSize)) - { - WriteCore(output, value, type, options); - result = JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); - } - - return result; - } - - private static void WriteValueCore(Utf8JsonWriter writer, object? value, Type type, JsonSerializerOptions? options) - { - if (options == null) - { - options = JsonSerializerOptions.s_defaultOptions; - } + Debug.Assert(writer != null); - if (writer == null) + // We treat typeof(object) special and allow polymorphic behavior. + if (inputType == typeof(object) && value != null) { - throw new ArgumentNullException(nameof(writer)); + inputType = value!.GetType(); } - WriteCore(writer, value, type, options); - } + WriteStack state = default; + state.Initialize(inputType, options, supportContinuation: false); + JsonConverter jsonConverter = state.Current.JsonClassInfo!.PolicyProperty!.ConverterBase; - private static void WriteCore(PooledByteBufferWriter output, object? value, Type type, JsonSerializerOptions options) - { - using (var writer = new Utf8JsonWriter(output, options.GetWriterOptions())) - { - WriteCore(writer, value, type, options); - } + bool success = WriteCore(jsonConverter, writer, value, options, ref state); + Debug.Assert(success); } - private static void WriteCore(Utf8JsonWriter writer, object? value, Type type, JsonSerializerOptions options) + private static bool WriteCore( + JsonConverter jsonConverter, + Utf8JsonWriter writer, + TValue value, + JsonSerializerOptions options, + ref WriteStack state) { - Debug.Assert(type != null || value == null); Debug.Assert(writer != null); - if (value == null) + bool success; + + if (jsonConverter is JsonConverter converter) { - writer.WriteNullValue(); + // Call the strongly-typed WriteCore that will not box structs. + success = converter.WriteCore(writer, value, options, ref state); } else { - // We treat typeof(object) special and allow polymorphic behavior. - if (type == typeof(object)) - { - type = value.GetType(); - } - - WriteStack state = default; - state.InitializeRoot(type!, options, supportContinuation: false); - WriteCore(writer, value, options, ref state, state.Current.JsonClassInfo!.PolicyProperty!.ConverterBase); + // The non-generic API was called or we have a polymorphic case where TValue is not equal to the T in JsonConverter. + success = jsonConverter.WriteCoreAsObject(writer, value, options, ref state); } writer.Flush(); + return success; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs index 980ca9aa7839f8..563eeb4ed1ae15 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs @@ -39,14 +39,24 @@ public static Task SerializeAsync(Stream utf8Json, TValue value, JsonSer public static Task SerializeAsync(Stream utf8Json, object? value, Type inputType, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { if (utf8Json == null) + { throw new ArgumentNullException(nameof(utf8Json)); + } + + if (inputType == null) + { + throw new ArgumentNullException(nameof(inputType)); + } - VerifyValueAndType(value, inputType); + if (value != null && !inputType.IsAssignableFrom(value.GetType())) + { + ThrowHelper.ThrowArgumentException_DeserializeWrongType(inputType, value); + } - return WriteAsyncCore(utf8Json, value, inputType, options, cancellationToken); + return WriteAsyncCore(utf8Json, value!, inputType, options, cancellationToken); } - private static async Task WriteAsyncCore(Stream utf8Json, object? value, Type inputType, JsonSerializerOptions? options, CancellationToken cancellationToken) + private static async Task WriteAsyncCore(Stream utf8Json, TValue value, Type inputType, JsonSerializerOptions? options, CancellationToken cancellationToken) { if (options == null) { @@ -58,18 +68,16 @@ private static async Task WriteAsyncCore(Stream utf8Json, object? value, Type in using (var bufferWriter = new PooledByteBufferWriter(options.DefaultBufferSize)) using (var writer = new Utf8JsonWriter(bufferWriter, writerOptions)) { - if (value == null) + // We treat typeof(object) special and allow polymorphic behavior. + if (inputType == typeof(object) && value != null) { - writer.WriteNullValue(); - writer.Flush(); - - await bufferWriter.WriteToStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); - - return; + inputType = value!.GetType(); } WriteStack state = default; - state.InitializeRoot(inputType, options, supportContinuation: true); + state.Initialize(inputType, options, supportContinuation: true); + + JsonConverter converterBase = state.Current.JsonClassInfo!.PolicyProperty!.ConverterBase; bool isFinalBlock; @@ -78,14 +86,8 @@ private static async Task WriteAsyncCore(Stream utf8Json, object? value, Type in // todo: determine best value here // https://github.com/dotnet/runtime/issues/32356 state.FlushThreshold = (int)(bufferWriter.Capacity * .9); - isFinalBlock = WriteCore( - writer, - value, - options, - ref state, - state.Current.JsonClassInfo!.PolicyProperty!.ConverterBase); - - writer.Flush(); + + isFinalBlock = WriteCore(converterBase, writer, value, options, ref state); await bufferWriter.WriteToStreamAsync(utf8Json, cancellationToken).ConfigureAwait(false); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs index 6464b1c6b7b2d4..c17c18e38d38fd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs @@ -18,7 +18,7 @@ public static partial class JsonSerializer /// public static string Serialize(TValue value, JsonSerializerOptions? options = null) { - return WriteCoreString(value, typeof(TValue), options); + return Serialize(value, typeof(TValue), options); } /// @@ -34,8 +34,35 @@ public static string Serialize(TValue value, JsonSerializerOptions? opti /// public static string Serialize(object? value, Type inputType, JsonSerializerOptions? options = null) { - VerifyValueAndType(value, inputType); - return WriteCoreString(value, inputType, options); + if (inputType == null) + { + throw new ArgumentNullException(nameof(inputType)); + } + + if (value != null && !inputType.IsAssignableFrom(value.GetType())) + { + ThrowHelper.ThrowArgumentException_DeserializeWrongType(inputType, value); + } + + return Serialize(value, inputType, options); + } + + private static string Serialize(TValue value, Type inputType, JsonSerializerOptions? options) + { + if (options == null) + { + options = JsonSerializerOptions.s_defaultOptions; + } + + using (var output = new PooledByteBufferWriter(options.DefaultBufferSize)) + { + using (var writer = new Utf8JsonWriter(output, options.GetWriterOptions())) + { + WriteCore(writer, value, inputType, options); + } + + return JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span); + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs index bf31e422263448..0f77a32c17dda8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs @@ -36,7 +36,7 @@ internal static void Serialize(Utf8JsonWriter writer, T value, JsonSerializer /// public static void Serialize(Utf8JsonWriter writer, TValue value, JsonSerializerOptions? options = null) { - WriteValueCore(writer, value, typeof(TValue), options); + Serialize(writer, value, typeof(TValue), options); } /// @@ -51,8 +51,32 @@ public static void Serialize(Utf8JsonWriter writer, TValue value, JsonSe /// public static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerOptions? options = null) { - VerifyValueAndType(value, inputType); - WriteValueCore(writer, value, inputType, options); + if (inputType == null) + { + throw new ArgumentNullException(nameof(inputType)); + } + + if (value != null && !inputType.IsAssignableFrom(value.GetType())) + { + ThrowHelper.ThrowArgumentException_DeserializeWrongType(inputType, value); + } + + Serialize(writer, value, inputType, options); + } + + private static void Serialize(Utf8JsonWriter writer, TValue value, Type type, JsonSerializerOptions? options) + { + if (options == null) + { + options = JsonSerializerOptions.s_defaultOptions; + } + + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + WriteCore(writer, value, type, options); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs deleted file mode 100644 index a1999acc1829ef..00000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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.Text.Json.Serialization; - -namespace System.Text.Json -{ - public static partial class JsonSerializer - { - // There are three conditions to consider for an object (primitive value, enumerable or object) being processed here: - // 1) The object type was specified as the root-level return type to a Deserialize method. - // 2) The object is a property on a parent object. - // 3) The object is an element in an enumerable. - private static bool WriteCore( - Utf8JsonWriter writer, - object? value, - JsonSerializerOptions options, - ref WriteStack state, - JsonConverter jsonConverter) - { - try - { - return jsonConverter.TryWriteAsObject(writer, value, options, ref state); - } - catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException) - { - ThrowHelper.ReThrowWithPath(state, ex); - throw; - } - catch (JsonException ex) - { - ThrowHelper.AddExceptionInformation(state, ex); - throw; - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 2de29b4f5358c4..18f94668c95d7e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -54,7 +54,7 @@ private static List GetDefaultConverters() converters.Add(new KeyValuePairConverterFactory()); // IEnumerable should always be last since they can convert any IEnumerable. - converters.Add(new JsonIEnumerableConverterFactory()); + converters.Add(new IEnumerableConverterFactory()); // Object should always be last since it converts any type. converters.Add(new ObjectConverterFactory()); @@ -72,30 +72,36 @@ private static List GetDefaultConverters() /// public IList Converters { get; } - internal JsonConverter? DetermineConverter(Type parentClassType, Type runtimePropertyType, PropertyInfo? propertyInfo) + internal JsonConverter DetermineConverter(Type? parentClassType, Type runtimePropertyType, PropertyInfo? propertyInfo) { - JsonConverter? converter = null; + JsonConverter converter = null!; // Priority 1: attempt to get converter from JsonConverterAttribute on property. if (propertyInfo != null) { + Debug.Assert(parentClassType != null); + JsonConverterAttribute? converterAttribute = (JsonConverterAttribute?) - GetAttributeThatCanHaveMultiple(parentClassType, typeof(JsonConverterAttribute), propertyInfo); + GetAttributeThatCanHaveMultiple(parentClassType!, typeof(JsonConverterAttribute), propertyInfo); if (converterAttribute != null) { - converter = GetConverterFromAttribute(converterAttribute, typeToConvert: runtimePropertyType, classTypeAttributeIsOn: parentClassType, propertyInfo); + converter = GetConverterFromAttribute(converterAttribute, typeToConvert: runtimePropertyType, classTypeAttributeIsOn: parentClassType!, propertyInfo); } } if (converter == null) { converter = GetConverter(runtimePropertyType); + Debug.Assert(converter != null); } if (converter is JsonConverterFactory factory) { converter = factory.GetConverterInternal(runtimePropertyType, this); + + // A factory cannot return null; GetConverterInternal checked for that. + Debug.Assert(converter != null); } return converter; @@ -106,12 +112,13 @@ private static List GetDefaultConverters() /// /// The type to return a converter for. /// - /// The first converter that supports the given type, or null if there is no converter. + /// The converter for the given type. /// - public JsonConverter? GetConverter(Type typeToConvert) + public JsonConverter GetConverter(Type typeToConvert) { if (_converters.TryGetValue(typeToConvert, out JsonConverter? converter)) { + Debug.Assert(converter != null); return converter; } @@ -143,6 +150,7 @@ private static List GetDefaultConverters() { if (s_defaultSimpleConverters.TryGetValue(typeToConvert, out JsonConverter? foundConverter)) { + Debug.Assert(foundConverter != null); converter = foundConverter; } else @@ -155,6 +163,9 @@ private static List GetDefaultConverters() break; } } + + // Since the object and IEnumerable converters cover all types, we should have a converter. + Debug.Assert(converter != null); } } @@ -162,19 +173,17 @@ private static List GetDefaultConverters() if (converter is JsonConverterFactory factory) { converter = factory.GetConverterInternal(typeToConvert, this); - // Allow null converters from the factory. This will result in a NotSupportedException later - // and with a nice exception that indicates the parent type. + + // A factory cannot return null; GetConverterInternal checked for that. + Debug.Assert(converter != null); } - if (converter != null) - { - Type converterTypeToConvert = converter.TypeToConvert!; + Type converterTypeToConvert = converter.TypeToConvert; - if (!converterTypeToConvert.IsAssignableFrom(typeToConvert) && - !typeToConvert.IsAssignableFrom(converterTypeToConvert)) - { - ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(converter.GetType(), typeToConvert); - } + if (!converterTypeToConvert.IsAssignableFrom(typeToConvert) && + !typeToConvert.IsAssignableFrom(converterTypeToConvert)) + { + ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(converter.GetType(), typeToConvert); } // Only cache the value once (de)serialization has occurred since new converters can be added that may change the result. 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 512d40fefb9d53..cf58f5b95becd6 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 @@ -329,15 +329,15 @@ internal MemberAccessor MemberAccessorStrategy } } - internal JsonClassInfo GetOrAddClass(Type classType) + internal JsonClassInfo GetOrAddClass(Type type) { _haveTypesBeenCreated = true; // todo: for performance and reduced instances, consider using the converters and JsonClassInfo from s_defaultOptions by cloning (or reference directly if no changes). // https://github.com/dotnet/runtime/issues/32357 - if (!_classes.TryGetValue(classType, out JsonClassInfo? result)) + if (!_classes.TryGetValue(type, out JsonClassInfo? result)) { - result = _classes.GetOrAdd(classType, new JsonClassInfo(classType, this)); + result = _classes.GetOrAdd(type, new JsonClassInfo(type, this)); } return result; 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 15a1e24f332deb..5332509163f1dc 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 @@ -19,7 +19,7 @@ public override sealed T Read(ref Utf8JsonReader reader, Type typeToConvert, Jso } ReadStack state = default; - state.InitializeRoot(typeToConvert, options); + state.Initialize(typeToConvert, options, supportContinuation: false); TryRead(ref reader, typeToConvert, options, ref state, out T value); return value; } @@ -33,7 +33,7 @@ public override sealed void Write(Utf8JsonWriter writer, T value, JsonSerializer } WriteStack state = default; - state.InitializeRoot(typeof(T), options, supportContinuation: false); + state.Initialize(typeof(T), options, supportContinuation: false); TryWrite(writer, value, options, ref state); } } 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 3ff46d8e2d4f21..38443feb930feb 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 @@ -71,20 +71,21 @@ private void AddCurrent() _count++; } - public void InitializeRoot(Type type, JsonSerializerOptions options) + public void Initialize(Type type, JsonSerializerOptions options, bool supportContinuation) { JsonClassInfo jsonClassInfo = options.GetOrAddClass(type); - Debug.Assert(jsonClassInfo.ClassType != ClassType.Invalid); Current.JsonClassInfo = jsonClassInfo; // The initial JsonPropertyInfo will be used to obtain the converter. - Current.JsonPropertyInfo = jsonClassInfo.PolicyProperty!; + Current.JsonPropertyInfo = jsonClassInfo.PolicyProperty; if (options.ReferenceHandling.ShouldReadPreservedReferences()) { ReferenceResolver = new DefaultReferenceResolver(writing: false); } + + SupportContinuation = supportContinuation; } public void Push() @@ -113,7 +114,7 @@ public void Push() Current.Reset(); Current.JsonClassInfo = jsonClassInfo; - Current.JsonPropertyInfo = jsonClassInfo.PolicyProperty!; + Current.JsonPropertyInfo = jsonClassInfo.PolicyProperty; } } else if (_continuationCount == 1) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index dfdfd19376c587..d869afd0530851 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -59,10 +59,9 @@ public void EndElement() public void InitializeReEntry(Type type, JsonSerializerOptions options, string? propertyName) { JsonClassInfo jsonClassInfo = options.GetOrAddClass(type); - Debug.Assert(jsonClassInfo.ClassType != ClassType.Invalid); // The initial JsonPropertyInfo will be used to obtain the converter. - JsonPropertyInfo = jsonClassInfo.PolicyProperty!; + JsonPropertyInfo = jsonClassInfo.PolicyProperty; // Set for exception handling calculation of JsonPath. JsonPropertyNameAsString = propertyName; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index 4debd5ed506a56..3481859a6db837 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -64,18 +64,17 @@ private void AddCurrent() } /// - /// Initializes the state for the first type being serialized. + /// Initialize the state without delayed initialization of the JsonClassInfo. /// - public void InitializeRoot(Type type, JsonSerializerOptions options, bool supportContinuation) + public void Initialize(Type type, JsonSerializerOptions options, bool supportContinuation) { JsonClassInfo jsonClassInfo = options.GetOrAddClass(type); - Debug.Assert(jsonClassInfo.ClassType != ClassType.Invalid); Current.JsonClassInfo = jsonClassInfo; if ((jsonClassInfo.ClassType & (ClassType.Enumerable | ClassType.Dictionary)) == 0) { - Current.DeclaredJsonPropertyInfo = jsonClassInfo.PolicyProperty!; + Current.DeclaredJsonPropertyInfo = jsonClassInfo.PolicyProperty; } if (options.ReferenceHandling.ShouldWritePreservedReferences()) @@ -103,7 +102,7 @@ public void Push() Current.Reset(); Current.JsonClassInfo = jsonClassInfo; - Current.DeclaredJsonPropertyInfo = jsonClassInfo.PolicyProperty!; + Current.DeclaredJsonPropertyInfo = jsonClassInfo.PolicyProperty; } } else if (_continuationCount == 1) 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 f2c701d1127435..20d9924951b5b7 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 @@ -19,29 +19,11 @@ public static void ThrowArgumentException_DeserializeWrongType(Type type, object throw new ArgumentException(SR.Format(SR.DeserializeWrongType, type, value.GetType())); } - [MethodImpl(MethodImplOptions.NoInlining)] - public static NotSupportedException GetNotSupportedException_SerializationNotSupported(Type propertyType, Type? parentType, MemberInfo? memberInfo) - { - if (parentType != null && parentType != typeof(object) && memberInfo != null) - { - return new NotSupportedException(SR.Format(SR.SerializationNotSupported, propertyType, $"{parentType}.{memberInfo.Name}")); - } - - return new NotSupportedException(SR.Format(SR.SerializationNotSupportedType, propertyType)); - } - [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static NotSupportedException ThrowNotSupportedException_SerializationNotSupported(Type propertyType, Type? parentType = null, MemberInfo? memberInfo = null) + public static void ThrowNotSupportedException_SerializationNotSupported(Type propertyType) { - throw GetNotSupportedException_SerializationNotSupported(propertyType, parentType, memberInfo); - } - - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowInvalidOperationException_SerializerCycleDetected(int maxDepth) - { - throw new JsonException(SR.Format(SR.SerializerCycleDetected, maxDepth)); + throw new NotSupportedException(SR.Format(SR.SerializationNotSupportedType, propertyType)); } [DoesNotReturn] @@ -71,6 +53,13 @@ public static void ThrowJsonException_SerializationConverterWrite(JsonConverter? throw ex; } + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowJsonException_SerializerCycleDetected(int maxDepth) + { + throw new JsonException(SR.Format(SR.SerializerCycleDetected, maxDepth)); + } + [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowJsonException(string? message = null) @@ -151,6 +140,12 @@ public static void ThrowInvalidOperationException_SerializerDictionaryKeyNull(Ty throw new InvalidOperationException(SR.Format(SR.SerializerDictionaryKeyNull, policyType)); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_SerializerConverterFactoryReturnsNull(Type converterType) + { + throw new InvalidOperationException(SR.Format(SR.SerializerConverterFactoryReturnsNull, converterType)); + } + [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static void ReThrowWithPath(in ReadStack state, JsonReaderException ex) @@ -179,11 +174,11 @@ public static void ReThrowWithPath(in ReadStack state, JsonReaderException ex) public static void ReThrowWithPath(in ReadStack state, in Utf8JsonReader reader, Exception ex) { JsonException jsonException = new JsonException(null, ex); - AddExceptionInformation(state, reader, jsonException); + AddJsonExceptionInformation(state, reader, jsonException); throw jsonException; } - public static void AddExceptionInformation(in ReadStack state, in Utf8JsonReader reader, JsonException ex) + public static void AddJsonExceptionInformation(in ReadStack state, in Utf8JsonReader reader, JsonException ex) { long lineNumber = reader.CurrentState._lineNumber; ex.LineNumber = lineNumber; @@ -221,11 +216,11 @@ public static void AddExceptionInformation(in ReadStack state, in Utf8JsonReader public static void ReThrowWithPath(in WriteStack state, Exception ex) { JsonException jsonException = new JsonException(null, ex); - AddExceptionInformation(state, jsonException); + AddJsonExceptionInformation(state, jsonException); throw jsonException; } - public static void AddExceptionInformation(in WriteStack state, JsonException ex) + public static void AddJsonExceptionInformation(in WriteStack state, JsonException ex) { string path = state.PropertyPath(); ex.Path = path; @@ -272,6 +267,68 @@ public static void ThrowInvalidOperationException_SerializationDataExtensionProp throw new InvalidOperationException(SR.Format(SR.SerializationDataExtensionPropertyInvalid, jsonClassInfo.Type, jsonPropertyInfo.PropertyInfo?.Name)); } + [DoesNotReturn] + public static void ThrowNotSupportedException(in ReadStack state, in Utf8JsonReader reader, NotSupportedException ex) + { + string message = ex.Message; + + // The caller should check to ensure path is not already set. + Debug.Assert(!message.Contains(" Path: ")); + + // Obtain the type to show in the message. + Type? propertyType = state.Current.JsonPropertyInfo?.RuntimePropertyType; + if (propertyType == null) + { + propertyType = state.Current.JsonClassInfo.Type; + } + + if (!message.Contains(propertyType.ToString())) + { + if (message.Length > 0) + { + message += " "; + } + + message += SR.Format(SR.SerializationNotSupportedParentType, propertyType); + } + + long lineNumber = reader.CurrentState._lineNumber; + long bytePositionInLine = reader.CurrentState._bytePositionInLine; + message += $" Path: {state.JsonPath()} | LineNumber: {lineNumber} | BytePositionInLine: {bytePositionInLine}."; + + throw new NotSupportedException(message, ex); + } + + [DoesNotReturn] + public static void ThrowNotSupportedException(in WriteStack state, NotSupportedException ex) + { + string message = ex.Message; + + // The caller should check to ensure path is not already set. + Debug.Assert(!message.Contains(" Path: ")); + + // Obtain the type to show in the message. + Type? propertyType = state.Current.DeclaredJsonPropertyInfo?.RuntimePropertyType; + if (propertyType == null) + { + propertyType = state.Current.JsonClassInfo.Type; + } + + if (!message.Contains(propertyType.ToString())) + { + if (message.Length > 0) + { + message += " "; + } + + message += SR.Format(SR.SerializationNotSupportedParentType, propertyType); + } + + message += $" Path: {state.PropertyPath()}."; + + throw new NotSupportedException(message, ex); + } + [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowNotSupportedException_DeserializeNoParameterlessConstructor(Type invalidType) diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs index 6cb58bba09166b..18128ff8403df4 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.BadConverters.cs @@ -177,16 +177,16 @@ public static void ConverterThatReturnsNullFail() var options = new JsonSerializerOptions(); options.Converters.Add(new ConverterFactoryThatReturnsNull()); - // A null return value from CreateConverter() will generate a NotSupportedException with the type name. - NotSupportedException ex = Assert.Throws(() => JsonSerializer.Serialize(0, options)); - Assert.Contains(typeof(int).ToString(), ex.Message); + // A null return value from CreateConverter() will generate a InvalidOperationException with the type name. + InvalidOperationException ex = Assert.Throws(() => JsonSerializer.Serialize(0, options)); + Assert.Contains(typeof(ConverterFactoryThatReturnsNull).ToString(), ex.Message); - ex = Assert.Throws(() => JsonSerializer.Deserialize("0", options)); - Assert.Contains(typeof(int).ToString(), ex.Message); + ex = Assert.Throws(() => JsonSerializer.Deserialize("0", options)); + Assert.Contains(typeof(ConverterFactoryThatReturnsNull).ToString(), ex.Message); // This will invoke the Nullable converter which should detect a null converter. - ex = Assert.Throws(() => JsonSerializer.Deserialize("0", options)); - Assert.Contains(typeof(int).ToString(), ex.Message); + ex = Assert.Throws(() => JsonSerializer.Deserialize("0", options)); + Assert.Contains(typeof(ConverterFactoryThatReturnsNull).ToString(), ex.Message); } private class Level1 diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Callback.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Callback.cs index d0f00a3c7ee306..acfaf71912463e 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Callback.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Callback.cs @@ -58,5 +58,105 @@ public static void ConverterWithCallback() int expectedLength = JsonSerializer.Serialize(customer).Length; Assert.Equal(@"[{""CreditLimit"":0,""Name"":""MyNameHello!"",""Address"":{""City"":null}}," + $"{expectedLength}]", result); } + + /// + /// A converter that calls back in the serializer with not supported types. + /// + private class PocoWithNotSupportedChildConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + return typeof(ChildPocoWithConverter).IsAssignableFrom(typeToConvert); + } + + public override ChildPocoWithConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); + Debug.Assert(reader.GetString() == "Child"); + + reader.Read(); + Debug.Assert(reader.TokenType == JsonTokenType.StartObject); + + // The options are not passed here as that would cause an infinite loop. + ChildPocoWithNoConverter value = JsonSerializer.Deserialize(ref reader); + + // Should not get here due to exception. + Debug.Assert(false); + return default; + } + + public override void Write(Utf8JsonWriter writer, ChildPocoWithConverter value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WritePropertyName("Child"); + + JsonSerializer.Serialize(writer, value.Child); + + // Should not get here due to exception. + Debug.Assert(false); + } + } + + private class TopLevelPocoWithNoConverter + { + public ChildPocoWithConverter Child { get; set; } + } + + private class ChildPocoWithConverter + { + public ChildPocoWithNoConverter Child { get; set; } + } + + private class ChildPocoWithNoConverter + { + public ChildPocoWithNoConverterAndInvalidProperty InvalidProperty { get; set; } + } + + private class ChildPocoWithNoConverterAndInvalidProperty + { + public int[,] NotSupported { get; set; } + } + + [Fact] + public static void ConverterWithReentryFail() + { + const string Json = @"{""Child"":{""Child"":{""InvalidProperty"":{""NotSupported"":[1]}}}}"; + + NotSupportedException ex; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new PocoWithNotSupportedChildConverter()); + + // This verifies: + // - Path does not flow through to custom converters that re-enter the serializer. + // - "Path:" is not repeated due to having two try\catch blocks (the second block does not append "Path:" again). + + ex = Assert.Throws(() => JsonSerializer.Deserialize(Json, options)); + Assert.Contains(typeof(int[,]).ToString(), ex.ToString()); + Assert.Contains(typeof(ChildPocoWithNoConverterAndInvalidProperty).ToString(), ex.ToString()); + Assert.Contains("Path: $.InvalidProperty | LineNumber: 0 | BytePositionInLine: 20.", ex.ToString()); + Assert.Equal(2, ex.ToString().Split(new string[] { "Path:" }, StringSplitOptions.None).Length); + + var poco = new TopLevelPocoWithNoConverter() + { + Child = new ChildPocoWithConverter() + { + Child = new ChildPocoWithNoConverter() + { + InvalidProperty = new ChildPocoWithNoConverterAndInvalidProperty() + { + NotSupported = new int[,] { { 1, 2 } } + } + } + } + }; + + ex = Assert.Throws(() => JsonSerializer.Serialize(poco, options)); + Assert.Contains(typeof(int[,]).ToString(), ex.ToString()); + Assert.Contains(typeof(ChildPocoWithNoConverterAndInvalidProperty).ToString(), ex.ToString()); + Assert.Contains("Path: $.InvalidProperty.", ex.ToString()); + Assert.Equal(2, ex.ToString().Split(new string[] { "Path:" }, StringSplitOptions.None).Length); + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.cs index 5cdf6aa7aaa222..490053d684eb0f 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.cs @@ -101,5 +101,24 @@ public static void VerifyConverterWithTrailingWhitespace() Assert.Null(c); } + + [Fact] + public static void VerifyConverterWithTrailingComments() + { + string json = "{} //"; + byte[] utf8 = Encoding.UTF8.GetBytes(json); + + // Disallow comments + var options = new JsonSerializerOptions(); + options.Converters.Add(new ConverterReturningNull()); + Assert.Throws(() => JsonSerializer.Deserialize(utf8, options)); + + // Skip comments + options = new JsonSerializerOptions(); + options.Converters.Add(new ConverterReturningNull()); + options.ReadCommentHandling = JsonCommentHandling.Skip; + Customer c = JsonSerializer.Deserialize(utf8, options); + Assert.Null(c); + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs index ba9729e8e9d3dd..7d776b93e175b3 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/DictionaryTests.cs @@ -1616,17 +1616,11 @@ public static void DictionaryNotSupported() { string json = @"{""MyDictionary"":{""Key"":""Value""}}"; - try - { - JsonSerializer.Deserialize(json); - Assert.True(false, "Expected NotSupportedException to be thrown."); - } - catch (NotSupportedException e) - { - // The exception should contain className.propertyName and the invalid type. - Assert.Contains("ClassWithNotSupportedDictionary.MyDictionary", e.Message); - Assert.Contains("Dictionary`2[System.Int32,System.Int32]", e.Message); - } + NotSupportedException ex = Assert.Throws(() => JsonSerializer.Deserialize(json)); + + // The exception contains the type. + Assert.Contains(typeof(Dictionary).ToString(), ex.Message); + Assert.DoesNotContain("Path: ", ex.Message); } [Fact] diff --git a/src/libraries/System.Text.Json/tests/Serialization/ExceptionTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ExceptionTests.cs index 32a11928687eb6..ae5a45b9431383 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ExceptionTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ExceptionTests.cs @@ -430,6 +430,11 @@ public class ChildClass public ChildClass[] Children { get; set; } } + private class ClassWithPropertyToClassWithInvalidArray + { + public ClassWithInvalidArray Inner { get; set; } = new ClassWithInvalidArray(); + } + private class ClassWithInvalidArray { public int[,] UnsupportedArray { get; set; } @@ -441,21 +446,85 @@ private class ClassWithInvalidDictionary } [Fact] - public static void ClassWithUnsupportedCollectionTypes() + public static void ClassWithUnsupportedArray() { - Exception ex; + Exception ex = Assert.Throws(() => + JsonSerializer.Deserialize(@"{""UnsupportedArray"":[]}")); + + // The exception contains the type. + Assert.Contains(typeof(int[,]).ToString(), ex.Message); - ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""UnsupportedArray"":[]}")); + ex = Assert.Throws(() => JsonSerializer.Serialize(new ClassWithInvalidArray())); + Assert.Contains(typeof(int[,]).ToString(), ex.Message); + Assert.DoesNotContain("Path: ", ex.Message); + } + + [Fact] + public static void ClassWithUnsupportedArrayInProperty() + { + Exception ex = Assert.Throws(() => + JsonSerializer.Deserialize(@"{""Inner"":{""UnsupportedArray"":[]}}")); + + // The exception contains the type and Path. + Assert.Contains(typeof(int[,]).ToString(), ex.Message); + Assert.Contains("Path: $.Inner | LineNumber: 0 | BytePositionInLine: 10.", ex.Message); + + ex = Assert.Throws(() => + JsonSerializer.Serialize(new ClassWithPropertyToClassWithInvalidArray())); + + Assert.Contains(typeof(int[,]).ToString(), ex.Message); + Assert.Contains(typeof(ClassWithInvalidArray).ToString(), ex.Message); + Assert.Contains("Path: $.Inner.", ex.Message); + + // The original exception contains the type. + Assert.NotNull(ex.InnerException); + Assert.Contains(typeof(int[,]).ToString(), ex.InnerException.Message); + Assert.DoesNotContain("Path: ", ex.InnerException.Message); + } - // The exception should contain the parent type and the property name. - Assert.Contains("ClassWithInvalidArray.UnsupportedArray", ex.ToString()); + [Fact] + public static void ClassWithUnsupportedDictionary() + { + Exception ex = Assert.Throws(() => + JsonSerializer.Deserialize(@"{""UnsupportedDictionary"":{}}")); - ex = Assert.Throws(() => JsonSerializer.Deserialize(@"{""UnsupportedDictionary"":{}}")); - Assert.Contains("System.Int32[,]", ex.ToString()); + Assert.Contains("System.Int32[,]", ex.Message); - // The exception for element types do not contain the parent type and the property name + // The exception for element types does not contain the parent type and the property name // since the verification occurs later and is no longer bound to the parent type. - Assert.DoesNotContain("ClassWithInvalidDictionary.UnsupportedDictionary", ex.ToString()); + Assert.DoesNotContain("ClassWithInvalidDictionary.UnsupportedDictionary", ex.Message); + + // The exception for element types includes Path. + Assert.Contains("Path: $.UnsupportedDictionary", ex.Message); + + // Serializing works for unsupported types if the property is null; elements are not verified until serialization occurs. + var obj = new ClassWithInvalidDictionary(); + string json = JsonSerializer.Serialize(obj); + Assert.Equal(@"{""UnsupportedDictionary"":null}", json); + + obj.UnsupportedDictionary = new Dictionary(); + ex = Assert.Throws(() => JsonSerializer.Serialize(obj)); + + // The exception contains the type and Path. + Assert.Contains(typeof(int[,]).ToString(), ex.Message); + Assert.Contains("Path: $.UnsupportedDictionary", ex.Message); + + // The original exception contains the type. + Assert.NotNull(ex.InnerException); + Assert.Contains(typeof(int[,]).ToString(), ex.InnerException.Message); + Assert.DoesNotContain("Path: ", ex.InnerException.Message); + } + + [Fact] + public static void UnsupportedTypeFromRoot() + { + Exception ex = Assert.Throws(() => + JsonSerializer.Deserialize(@"[]")); + + Assert.Contains(typeof(int[,]).ToString(), ex.Message); + + // Root-level Types (not from a property) do not include the Path. + Assert.DoesNotContain("Path: $", ex.Message); } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs index 81bc7e7b610dd5..84b81684781a91 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Null.ReadTests.cs @@ -192,5 +192,43 @@ public static async Task ParseNullStringShouldThrowJsonExceptionAsync() await Assert.ThrowsAsync(async () => await JsonSerializer.DeserializeAsync(stream)); } } + + [Fact] + public static void DeserializeDictionaryWithNullValues() + { + { + Dictionary dict = JsonSerializer.Deserialize>(@"{""key"":null}"); + Assert.Null(dict["key"]); + } + + { + Dictionary dict = JsonSerializer.Deserialize>(@"{""key"":null}"); + Assert.Null(dict["key"]); + } + + { + Dictionary> dict = JsonSerializer.Deserialize>>(@"{""key"":null}"); + Assert.Null(dict["key"]); + } + + { + Dictionary> dict = JsonSerializer.Deserialize>>(@"{""key"":null}"); + Assert.Null(dict["key"]); + } + } + + [Fact] + public static void InvalidRootOnRead() + { + Assert.Throws(() => JsonSerializer.Deserialize("null")); + + var options = new JsonSerializerOptions + { + IgnoreNullValues = true + }; + + // We still throw when we have an unsupported root. + Assert.Throws(() => JsonSerializer.Deserialize("null", options)); + } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs index c9ed94577dbaf1..f85564a212c1c9 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Null.WriteTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Xunit; namespace System.Text.Json.Serialization.Tests @@ -225,27 +226,52 @@ public static void SerializeDictionaryWithNullValues() } [Fact] - public static void DeserializeDictionaryWithNullValues() + public static void WritePocoArray() { - { - Dictionary dict = JsonSerializer.Deserialize>(@"{""key"":null}"); - Assert.Null(dict["key"]); - } + var input = new MyPoco[] { null, new MyPoco { Foo = "foo" } }; + + string json = JsonSerializer.Serialize(input, new JsonSerializerOptions { Converters = { new MyPocoConverter() } }); + Assert.Equal("[null,{\"Foo\":\"foo\"}]", json); + } + private class MyPoco + { + public string Foo { get; set; } + } + + private class MyPocoConverter : JsonConverter + { + public override MyPoco Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - Dictionary dict = JsonSerializer.Deserialize>(@"{""key"":null}"); - Assert.Null(dict["key"]); + throw new NotImplementedException(); } + public override void Write(Utf8JsonWriter writer, MyPoco value, JsonSerializerOptions options) { - Dictionary> dict = JsonSerializer.Deserialize>>(@"{""key"":null}"); - Assert.Null(dict["key"]); + if (value == null) + { + throw new InvalidOperationException("The custom converter should never get called with null value."); + } + + writer.WriteStartObject(); + writer.WriteString(nameof(value.Foo), value.Foo); + writer.WriteEndObject(); } + } + + [Fact] + public static void InvalidRootOnWrite() + { + int[,] arr = null; + Assert.Throws(() => JsonSerializer.Serialize(arr)); + var options = new JsonSerializerOptions { - Dictionary> dict = JsonSerializer.Deserialize>>(@"{""key"":null}"); - Assert.Null(dict["key"]); - } + IgnoreNullValues = true + }; + + // We still throw when we have an unsupported root. + Assert.Throws(() => JsonSerializer.Serialize(arr, options)); } } } diff --git a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs index 44fd8f7ac8a57b..b4714219b0ae83 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs @@ -203,6 +203,7 @@ public static void ReadCommentHandling() options.ReadCommentHandling = JsonCommentHandling.Skip; int value = JsonSerializer.Deserialize("1 /* commment */", options); + Assert.Equal(1, value); } [Theory] diff --git a/src/libraries/System.Text.Json/tests/Serialization/ReadValueTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ReadValueTests.cs index 6f27fce48dada6..29081800b69590 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ReadValueTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ReadValueTests.cs @@ -14,13 +14,34 @@ namespace System.Text.Json.Serialization.Tests public static partial class ReadValueTests { [Fact] - public static void NullTypeThrows() + public static void NullReturnTypeThrows() { - Assert.ThrowsAny(() => + ArgumentNullException ex = Assert.Throws(() => { Utf8JsonReader reader = default; - JsonSerializer.Deserialize(ref reader, null); + JsonSerializer.Deserialize(ref reader, returnType: null); }); + + Assert.Contains("returnType", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.Deserialize("", returnType: null)); + Assert.Contains("returnType", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.Deserialize(new byte[] { 1 }, returnType: null)); + Assert.Contains("returnType", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.DeserializeAsync(new MemoryStream(), returnType: null)); + Assert.Contains("returnType", ex.ToString()); + } + + [Fact] + public static void NullJsonThrows() + { + ArgumentNullException ex = Assert.Throws(() => JsonSerializer.Deserialize(json: null, returnType: typeof(string))); + Assert.Contains("json", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.DeserializeAsync(utf8Json: null, returnType: null)); + Assert.Contains("utf8Json", ex.ToString()); } [Fact] diff --git a/src/libraries/System.Text.Json/tests/Serialization/WriteValueTests.cs b/src/libraries/System.Text.Json/tests/Serialization/WriteValueTests.cs index 22bf2b449259a2..fb84ada75914bc 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/WriteValueTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/WriteValueTests.cs @@ -15,8 +15,76 @@ public static partial class WriteValueTests [Fact] public static void NullWriterThrows() { - Assert.Throws(() => JsonSerializer.Serialize(null, 1)); - Assert.Throws(() => JsonSerializer.Serialize(null, 1, typeof(int))); + ArgumentNullException ex; + + ex = Assert.Throws(() => JsonSerializer.Serialize(null, 1)); + Assert.Contains("writer", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.Serialize(null, 1, typeof(int))); + Assert.Contains("writer", ex.ToString()); + } + + [Fact] + public async static void NullInputTypeThrows() + { + ArgumentException ex; + Utf8JsonWriter writer = new Utf8JsonWriter(new MemoryStream()); + + ex = Assert.Throws(() => JsonSerializer.Serialize(writer: writer, value: null, inputType: null)); + Assert.Contains("inputType", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.Serialize(writer, value: null, inputType: null)); + Assert.Contains("inputType", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.Serialize(1, inputType: null)); + Assert.Contains("inputType", ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.SerializeToUtf8Bytes(null, inputType: null)); + Assert.Contains("inputType", ex.ToString()); + + ex = await Assert.ThrowsAsync(async () => await JsonSerializer.SerializeAsync(new MemoryStream(), null, inputType: null)); + Assert.Contains("inputType", ex.ToString()); + } + + [Fact] + public async static void NullValueWithValueTypeThrows() + { + JsonException ex; + + Utf8JsonWriter writer = new Utf8JsonWriter(new MemoryStream()); + ex = Assert.Throws(() => JsonSerializer.Serialize(writer: writer, value: null, inputType: typeof(int))); + Assert.Contains(typeof(int).ToString(), ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.Serialize(value: null, inputType: typeof(int))); + Assert.Contains(typeof(int).ToString(), ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.SerializeToUtf8Bytes(value: null, inputType: typeof(int))); + Assert.Contains(typeof(int).ToString(), ex.ToString()); + + ex = await Assert.ThrowsAsync(async () => await JsonSerializer.SerializeAsync(new MemoryStream(), value: null, inputType: typeof(int))); + Assert.Contains(typeof(int).ToString(), ex.ToString()); + } + + [Fact] + public async static void NullValueWithNullableSuccess() + { + byte[] nullUtf8Literal = Encoding.UTF8.GetBytes("null"); + + var stream = new MemoryStream(); + Utf8JsonWriter writer = new Utf8JsonWriter(stream); + JsonSerializer.Serialize(writer: writer, value: null, inputType: typeof(int?)); + byte[] jsonBytes = stream.ToArray(); + Assert.Equal(nullUtf8Literal, jsonBytes); + + string jsonString = JsonSerializer.Serialize(value: null, inputType: typeof(int?)); + Assert.Equal("null", jsonString); + + jsonBytes = JsonSerializer.SerializeToUtf8Bytes(value: null, inputType: typeof(int?)); + Assert.Equal(nullUtf8Literal, jsonBytes); + + stream = new MemoryStream(); + await JsonSerializer.SerializeAsync(stream, value: null, inputType: typeof(int?)); + Assert.Equal(nullUtf8Literal, stream.ToArray()); } [Fact]