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 aee388056c8cc..9f88d70f9860b 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -75,6 +75,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + @@ -380,12 +381,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - - - - - - diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index 5a9b20ad023b8..2fcc1d79e3eae 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -118,6 +118,17 @@ internal JsonTokenType GetJsonTokenType(int index) return _parsedData.GetJsonTokenType(index); } + internal bool ValueIsEscaped(int index, bool isPropertyName) + { + CheckNotDisposed(); + + int matchIndex = isPropertyName ? index - DbRow.Size : index; + DbRow row = _parsedData.Get(matchIndex); + Debug.Assert(!isPropertyName || row.TokenType is JsonTokenType.PropertyName); + + return row.HasComplexChildren; + } + internal int GetArrayLength(int index) { CheckNotDisposed(); @@ -363,6 +374,16 @@ internal string GetNameOfPropertyValue(int index) return GetString(index - DbRow.Size, JsonTokenType.PropertyName)!; } + internal ReadOnlySpan GetPropertyNameRaw(int index) + { + CheckNotDisposed(); + + DbRow row = _parsedData.Get(index - DbRow.Size); + Debug.Assert(row.TokenType is JsonTokenType.PropertyName); + + return _utf8Json.Span.Slice(row.Location, row.SizeOrLength); + } + internal bool TryGetValue(int index, [NotNullWhen(true)] out byte[]? value) { CheckNotDisposed(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index 1ca7fff9f7e33..f5d2ff099e723 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -1164,6 +1164,13 @@ internal string GetPropertyName() return _parent.GetNameOfPropertyValue(_idx); } + internal ReadOnlySpan GetPropertyNameRaw() + { + CheckValidInstance(); + + return _parent.GetPropertyNameRaw(_idx); + } + /// /// Gets the original input data backing this value, returning it as a . /// @@ -1194,6 +1201,154 @@ internal string GetPropertyRawText() return _parent.GetPropertyRawValueAsString(_idx); } + internal bool ValueIsEscaped + { + // TODO make public https://github.com/dotnet/runtime/issues/77666 + get + { + CheckValidInstance(); + + return _parent.ValueIsEscaped(_idx, isPropertyName: false); + } + } + + internal ReadOnlySpan ValueSpan + { + // TODO make public https://github.com/dotnet/runtime/issues/77666 + get + { + CheckValidInstance(); + + return _parent.GetRawValue(_idx, includeQuotes: false).Span; + } + } + + // TODO make public https://github.com/dotnet/runtime/issues/33388 + internal static bool DeepEquals(JsonElement left, JsonElement right) + { + Debug.Assert(left._parent != null); + Debug.Assert(right._parent != null); + + JsonValueKind kind = left.ValueKind; + if (kind != right.ValueKind) + { + return false; + } + + switch (kind) + { + case JsonValueKind.Null or JsonValueKind.False or JsonValueKind.True: + return true; + + case JsonValueKind.Number: + // JSON numbers are equal if their raw representations are equal. + return left.GetRawValue().Span.SequenceEqual(right.GetRawValue().Span); + + case JsonValueKind.String: + if (right.ValueIsEscaped) + { + if (left.ValueIsEscaped) + { + // Both values are escaped, force an allocation to unescape the RHS. + return left.ValueEquals(right.GetString()); + } + + // Swap values so that unescaping is handled by the LHS. + (left, right) = (right, left); + } + + return left.ValueEquals(right.ValueSpan); + + case JsonValueKind.Array: + ArrayEnumerator rightArrayEnumerator = right.EnumerateArray(); + foreach (JsonElement leftElement in left.EnumerateArray()) + { + if (!rightArrayEnumerator.MoveNext() || !DeepEquals(leftElement, rightArrayEnumerator.Current)) + { + return false; + } + } + + return !rightArrayEnumerator.MoveNext(); + + default: + Debug.Assert(kind is JsonValueKind.Object); + ObjectEnumerator leftObjectEnumerator = left.EnumerateObject(); + ObjectEnumerator rightObjectEnumerator = right.EnumerateObject(); + + // Two JSON objects are considered equal if they define the same set of properties. + // Start optimistically with sequential comparison, but fall back to unordered + // comparison as soon as a mismatch is encountered. + + while (leftObjectEnumerator.MoveNext()) + { + if (!rightObjectEnumerator.MoveNext()) + { + return false; + } + + JsonProperty leftProp = leftObjectEnumerator.Current; + JsonProperty rightProp = rightObjectEnumerator.Current; + + if (!NameEquals(leftProp, rightProp)) + { + // We have our first mismatch, fall back to unordered comparison. + return UnorderedObjectDeepEquals(leftObjectEnumerator, rightObjectEnumerator); + } + + if (!DeepEquals(leftProp.Value, rightProp.Value)) + { + return false; + } + } + + return !rightObjectEnumerator.MoveNext(); + + static bool UnorderedObjectDeepEquals(ObjectEnumerator left, ObjectEnumerator right) + { + Dictionary rightElements = new(StringComparer.Ordinal); + do + { + JsonProperty prop = right.Current; + rightElements.TryAdd(prop.Name, prop.Value); + } + while (right.MoveNext()); + + int leftCount = 0; + do + { + JsonProperty prop = left.Current; + if (!rightElements.TryGetValue(prop.Name, out JsonElement rightElement) || !DeepEquals(prop.Value, rightElement)) + { + return false; + } + + leftCount++; + } + while (left.MoveNext()); + + return leftCount == rightElements.Count; + } + + static bool NameEquals(JsonProperty left, JsonProperty right) + { + if (right.NameIsEscaped) + { + if (left.NameIsEscaped) + { + // Both values are escaped, force an allocation to unescape the RHS. + return left.NameEquals(right.Name); + } + + // Swap values so that unescaping is handled by the LHS + (left, right) = (right, left); + } + + return left.NameEquals(right.NameSpan); + } + } + } + /// /// Compares to the string value of this element. /// @@ -1292,6 +1447,13 @@ internal bool TextEqualsHelper(ReadOnlySpan text, bool isPropertyName) return _parent.TextEquals(_idx, text, isPropertyName); } + internal bool ValueIsEscapedHelper(bool isPropertyName) + { + CheckValidInstance(); + + return _parent.ValueIsEscaped(_idx, isPropertyName); + } + /// /// Write the element into the provided writer as a JSON value. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs index 9cad409a32b7a..cac951cd9df91 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonProperty.cs @@ -18,10 +18,9 @@ public readonly struct JsonProperty public JsonElement Value { get; } private string? _name { get; } - internal JsonProperty(JsonElement value, string? name = null) + internal JsonProperty(JsonElement value) { Value = value; - _name = name; } /// @@ -94,6 +93,10 @@ internal bool EscapedNameEquals(ReadOnlySpan utf8Text) return Value.TextEqualsHelper(utf8Text, isPropertyName: true, shouldUnescape: false); } + // TODO make public https://github.com/dotnet/runtime/issues/77666 + internal bool NameIsEscaped => Value.ValueIsEscapedHelper(isPropertyName: true); + internal ReadOnlySpan NameSpan => Value.GetPropertyNameRaw(); + /// /// Write the property into the provided writer as a named JSON object property. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs index 0c95ee94fe896..815a3348d4b04 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs @@ -23,6 +23,8 @@ public sealed partial class JsonArray : JsonNode private JsonElement? _jsonElement; private List? _list; + internal override JsonElement? UnderlyingElement => _jsonElement; + /// /// Initializes a new instance of the class that is empty. /// @@ -93,11 +95,11 @@ internal override JsonNode DeepCloneCore() return jsonArray; } - internal override bool DeepEqualsCore(JsonNode? node) + internal override bool DeepEqualsCore(JsonNode node) { switch (node) { - case null or JsonObject: + case JsonObject: return false; case JsonValue value: // JsonValue instances have special comparison semantics, dispatch to their implementation. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.To.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.To.cs index 0304718c81aa2..34b12052d31d1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.To.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.To.cs @@ -44,13 +44,12 @@ public override string ToString() // Special case for string; don't quote it. if (this is JsonValue) { - if (this is JsonValue jsonString) + if (this is JsonValuePrimitive jsonString) { return jsonString.Value; } - if (this is JsonValue jsonElement && - jsonElement.Value.ValueKind == JsonValueKind.String) + if (this is JsonValueOfElement { Value.ValueKind: JsonValueKind.String } jsonElement) { return jsonElement.Value.GetString()!; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs index 70b80505b7a7d..ba611933eeee9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs @@ -14,9 +14,17 @@ namespace System.Text.Json.Nodes /// declared as an should be deserialized as a . public abstract partial class JsonNode { + // Default options instance used when calling built-in JsonNode converters. + private protected static readonly JsonSerializerOptions s_defaultOptions = new(); + private JsonNode? _parent; private JsonNodeOptions? _options; + /// + /// The underlying JsonElement if the node is backed by a JsonElement. + /// + internal virtual JsonElement? UnderlyingElement => null; + /// /// Options to control the behavior. /// @@ -300,11 +308,15 @@ public static bool DeepEquals(JsonNode? node1, JsonNode? node2) { return node2 is null; } + else if (node2 is null) + { + return false; + } return node1.DeepEqualsCore(node2); } - internal abstract bool DeepEqualsCore(JsonNode? node); + internal abstract bool DeepEqualsCore(JsonNode node); /// /// Replaces this node with a new value. @@ -375,7 +387,7 @@ internal void AssignParent(JsonNode parent) } var jsonTypeInfo = (JsonTypeInfo)JsonSerializerOptions.Default.GetTypeInfo(typeof(T)); - return new JsonValueCustomized(value, jsonTypeInfo, options); + return JsonValue.CreateFromTypeInfo(value, jsonTypeInfo, options); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs index 8756b93366f90..3de8d933a1b07 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs @@ -25,6 +25,11 @@ public partial class JsonObject : IDictionary /// public void Add(string propertyName, JsonNode? value) { + if (propertyName is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); + } + Dictionary.Add(propertyName, value); value?.AssignParent(this); } @@ -74,7 +79,15 @@ public void Clear() /// /// is . /// - public bool ContainsKey(string propertyName) => Dictionary.ContainsKey(propertyName); + public bool ContainsKey(string propertyName) + { + if (propertyName is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); + } + + return Dictionary.ContainsKey(propertyName); + } /// /// Gets the number of elements contained in . @@ -180,7 +193,15 @@ public bool Remove(string propertyName) /// /// is . /// - bool IDictionary.TryGetValue(string propertyName, out JsonNode? jsonNode) => Dictionary.TryGetValue(propertyName, out jsonNode); + bool IDictionary.TryGetValue(string propertyName, out JsonNode? jsonNode) + { + if (propertyName is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); + } + + return Dictionary.TryGetValue(propertyName, out jsonNode); + } /// /// Returns . diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IList.cs index d7032fd5125ed..372eccdcfe016 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IList.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IList.cs @@ -23,6 +23,11 @@ public partial class JsonObject : IList> /// already has a parent. public void SetAt(int index, string propertyName, JsonNode? value) { + if (propertyName is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); + } + OrderedDictionary dictionary = Dictionary; KeyValuePair existing = dictionary.GetAt(index); dictionary.SetAt(index, propertyName, value); @@ -48,7 +53,15 @@ public void SetAt(int index, JsonNode? value) /// The property name to locate. /// The index of if found; otherwise, -1. /// is null. - public int IndexOf(string propertyName) => Dictionary.IndexOf(propertyName); + public int IndexOf(string propertyName) + { + if (propertyName is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); + } + + return Dictionary.IndexOf(propertyName); + } /// Inserts a property into the object at the specified index. /// The zero-based index at which the property should be inserted. @@ -59,6 +72,11 @@ public void SetAt(int index, JsonNode? value) /// is less than 0 or greater than . public void Insert(int index, string propertyName, JsonNode? value) { + if (propertyName is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); + } + Dictionary.Insert(index, propertyName, value); value?.AssignParent(this); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs index 8b32efee897e2..667a7626b120b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs @@ -20,6 +20,8 @@ public sealed partial class JsonObject : JsonNode { private JsonElement? _jsonElement; + internal override JsonElement? UnderlyingElement => _jsonElement; + /// /// Initializes a new instance of the class that is empty. /// @@ -116,8 +118,15 @@ internal string GetPropertyName(JsonNode? node) /// /// if a property with the specified name was found; otherwise, . /// - public bool TryGetPropertyValue(string propertyName, out JsonNode? jsonNode) => - Dictionary.TryGetValue(propertyName, out jsonNode); + public bool TryGetPropertyValue(string propertyName, out JsonNode? jsonNode) + { + if (propertyName is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); + } + + return Dictionary.TryGetValue(propertyName, out jsonNode); + } /// public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) @@ -158,11 +167,11 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio private protected override JsonValueKind GetValueKindCore() => JsonValueKind.Object; - internal override bool DeepEqualsCore(JsonNode? node) + internal override bool DeepEqualsCore(JsonNode node) { switch (node) { - case null or JsonArray: + case JsonArray: return false; case JsonValue value: // JsonValue instances have special comparison semantics, dispatch to their implementation. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.CreateOverloads.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.CreateOverloads.cs index 824869192765f..439274348f4d3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.CreateOverloads.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.CreateOverloads.cs @@ -14,7 +14,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(bool value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.BooleanConverter); + public static JsonValue Create(bool value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.BooleanConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -22,7 +22,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(bool? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.BooleanConverter) : null; + public static JsonValue? Create(bool? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.BooleanConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -30,7 +30,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(byte value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.ByteConverter); + public static JsonValue Create(byte value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.ByteConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -38,7 +38,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(byte? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.ByteConverter) : null; + public static JsonValue? Create(byte? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.ByteConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -46,7 +46,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(char value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.CharConverter); + public static JsonValue Create(char value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.CharConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -54,7 +54,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(char? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.CharConverter) : null; + public static JsonValue? Create(char? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.CharConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -62,7 +62,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(DateTime value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.DateTimeConverter); + public static JsonValue Create(DateTime value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.DateTimeConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -70,7 +70,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(DateTime? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.DateTimeConverter) : null; + public static JsonValue? Create(DateTime? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.DateTimeConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -78,7 +78,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(DateTimeOffset value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.DateTimeOffsetConverter); + public static JsonValue Create(DateTimeOffset value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.DateTimeOffsetConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -86,7 +86,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(DateTimeOffset? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.DateTimeOffsetConverter) : null; + public static JsonValue? Create(DateTimeOffset? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.DateTimeOffsetConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -94,7 +94,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(decimal value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.DecimalConverter); + public static JsonValue Create(decimal value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.DecimalConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -102,7 +102,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(decimal? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.DecimalConverter) : null; + public static JsonValue? Create(decimal? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.DecimalConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -110,7 +110,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(double value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.DoubleConverter); + public static JsonValue Create(double value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.DoubleConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -118,7 +118,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(double? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.DoubleConverter) : null; + public static JsonValue? Create(double? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.DoubleConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -126,7 +126,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(Guid value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.GuidConverter); + public static JsonValue Create(Guid value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.GuidConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -134,7 +134,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(Guid? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.GuidConverter) : null; + public static JsonValue? Create(Guid? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.GuidConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -142,7 +142,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(short value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.Int16Converter); + public static JsonValue Create(short value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.Int16Converter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -150,7 +150,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(short? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.Int16Converter) : null; + public static JsonValue? Create(short? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.Int16Converter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -158,7 +158,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(int value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.Int32Converter); + public static JsonValue Create(int value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.Int32Converter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -166,7 +166,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(int? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.Int32Converter) : null; + public static JsonValue? Create(int? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.Int32Converter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -174,7 +174,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(long value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.Int64Converter); + public static JsonValue Create(long value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.Int64Converter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -182,7 +182,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(long? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.Int64Converter) : null; + public static JsonValue? Create(long? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.Int64Converter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -191,7 +191,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [CLSCompliantAttribute(false)] - public static JsonValue Create(sbyte value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.SByteConverter); + public static JsonValue Create(sbyte value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.SByteConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -200,7 +200,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [CLSCompliantAttribute(false)] - public static JsonValue? Create(sbyte? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.SByteConverter) : null; + public static JsonValue? Create(sbyte? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.SByteConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -208,7 +208,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue Create(float value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.SingleConverter); + public static JsonValue Create(float value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.SingleConverter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -216,7 +216,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(float? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.SingleConverter) : null; + public static JsonValue? Create(float? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.SingleConverter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -225,7 +225,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [return: NotNullIfNotNull(nameof(value))] - public static JsonValue? Create(string? value, JsonNodeOptions? options = null) => value != null ? new JsonValuePrimitive(value, JsonMetadataServices.StringConverter) : null; + public static JsonValue? Create(string? value, JsonNodeOptions? options = null) => value != null ? new JsonValuePrimitive(value, JsonMetadataServices.StringConverter!, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -234,7 +234,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [CLSCompliantAttribute(false)] - public static JsonValue Create(ushort value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.UInt16Converter); + public static JsonValue Create(ushort value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.UInt16Converter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -243,7 +243,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [CLSCompliantAttribute(false)] - public static JsonValue? Create(ushort? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.UInt16Converter) : null; + public static JsonValue? Create(ushort? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.UInt16Converter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -252,7 +252,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [CLSCompliantAttribute(false)] - public static JsonValue Create(uint value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.UInt32Converter); + public static JsonValue Create(uint value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.UInt32Converter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -261,7 +261,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [CLSCompliantAttribute(false)] - public static JsonValue? Create(uint? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.UInt32Converter) : null; + public static JsonValue? Create(uint? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.UInt32Converter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -270,7 +270,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [CLSCompliantAttribute(false)] - public static JsonValue Create(ulong value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.UInt64Converter); + public static JsonValue Create(ulong value, JsonNodeOptions? options = null) => new JsonValuePrimitive(value, JsonMetadataServices.UInt64Converter, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -279,7 +279,7 @@ public partial class JsonValue /// Options to control the behavior. /// The new instance of the class that contains the specified value. [CLSCompliantAttribute(false)] - public static JsonValue? Create(ulong? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.UInt64Converter) : null; + public static JsonValue? Create(ulong? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValuePrimitive(value.Value, JsonMetadataServices.UInt64Converter, options) : null; /// /// Initializes a new instance of the class that contains the specified value. @@ -287,17 +287,7 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(JsonElement value, JsonNodeOptions? options = null) - { - if (value.ValueKind == JsonValueKind.Null) - { - return null; - } - - VerifyJsonElementIsNotArrayOrObject(ref value); - - return new JsonValuePrimitive(value, JsonMetadataServices.JsonElementConverter); - } + public static JsonValue? Create(JsonElement value, JsonNodeOptions? options = null) => JsonValue.CreateFromElement(ref value, options); /// /// Initializes a new instance of the class that contains the specified value. @@ -305,22 +295,6 @@ public partial class JsonValue /// The underlying value of the new instance. /// Options to control the behavior. /// The new instance of the class that contains the specified value. - public static JsonValue? Create(JsonElement? value, JsonNodeOptions? options = null) - { - if (value == null) - { - return null; - } - - JsonElement element = value.Value; - if (element.ValueKind == JsonValueKind.Null) - { - return null; - } - - VerifyJsonElementIsNotArrayOrObject(ref element); - - return new JsonValuePrimitive(element, JsonMetadataServices.JsonElementConverter); - } + public static JsonValue? Create(JsonElement? value, JsonNodeOptions? options = null) => value is JsonElement element ? JsonValue.CreateFromElement(ref element, options) : null; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.cs index 5aeb330059056..09c21dbd5bb16 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Nodes @@ -16,7 +17,24 @@ public abstract partial class JsonValue : JsonNode internal const string CreateUnreferencedCodeMessage = "Creating JsonValue instances with non-primitive types is not compatible with trimming. It can result in non-primitive types being serialized, which may have their members trimmed."; internal const string CreateDynamicCodeMessage = "Creating JsonValue instances with non-primitive types requires generating code at runtime."; - private protected JsonValue(JsonNodeOptions? options = null) : base(options) { } + private protected JsonValue(JsonNodeOptions? options) : base(options) { } + + /// + /// Tries to obtain the current JSON value and returns a value that indicates whether the operation succeeded. + /// + /// + /// {T} can be the type or base type of the underlying value. + /// If the underlying value is a then {T} can also be the type of any primitive + /// value supported by current . + /// Specifying the type for {T} will always succeed and return the underlying value as .
+ /// The underlying value of a after deserialization is an instance of , + /// otherwise it's the value specified when the was created. + ///
+ /// + /// The type of value to obtain. + /// When this method returns, contains the parsed value. + /// if the value can be successfully obtained; otherwise, . + public abstract bool TryGetValue([NotNullWhen(true)] out T? value); /// /// Initializes a new instance of the class that contains the specified value. @@ -37,20 +55,18 @@ private protected JsonValue(JsonNodeOptions? options = null) : base(options) { } return null; } - if (value is JsonElement element) + if (value is JsonNode) { - if (element.ValueKind is JsonValueKind.Null) - { - return null; - } - - VerifyJsonElementIsNotArrayOrObject(ref element); + ThrowHelper.ThrowArgumentException_NodeValueNotAllowed(nameof(value)); + } - return new JsonValuePrimitive(element, JsonMetadataServices.JsonElementConverter, options); + if (value is JsonElement element) + { + return CreateFromElement(ref element, options); } var jsonTypeInfo = (JsonTypeInfo)JsonSerializerOptions.Default.GetTypeInfo(typeof(T)); - return new JsonValueCustomized(value, jsonTypeInfo, options); + return CreateFromTypeInfo(value, jsonTypeInfo, options); } /// @@ -76,51 +92,107 @@ private protected JsonValue(JsonNodeOptions? options = null) : base(options) { } return null; } - if (value is JsonElement element) + if (value is JsonNode) + { + ThrowHelper.ThrowArgumentException_NodeValueNotAllowed(nameof(value)); + } + + jsonTypeInfo.EnsureConfigured(); + + if (value is JsonElement element && jsonTypeInfo.EffectiveConverter.IsInternalConverter) { - if (element.ValueKind is JsonValueKind.Null) + return CreateFromElement(ref element, options); + } + + return CreateFromTypeInfo(value, jsonTypeInfo, options); + } + + internal override bool DeepEqualsCore(JsonNode otherNode) + { + if (GetValueKind() != otherNode.GetValueKind()) + { + return false; + } + + // Fall back to slow path that converts the nodes to JsonElement. + JsonElement thisElement = ToJsonElement(this, out JsonDocument? thisDocument); + JsonElement otherElement = ToJsonElement(otherNode, out JsonDocument? otherDocument); + try + { + return JsonElement.DeepEquals(thisElement, otherElement); + } + finally + { + thisDocument?.Dispose(); + otherDocument?.Dispose(); + } + + static JsonElement ToJsonElement(JsonNode node, out JsonDocument? backingDocument) + { + if (node.UnderlyingElement is { } element) { - return null; + backingDocument = null; + return element; } - VerifyJsonElementIsNotArrayOrObject(ref element); - } + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer( + options: default, + JsonSerializerOptions.BufferSizeDefault, + out PooledByteBufferWriter output); - jsonTypeInfo.EnsureConfigured(); - return new JsonValueCustomized(value, jsonTypeInfo, options); + try + { + node.WriteTo(writer); + writer.Flush(); + Utf8JsonReader reader = new(output.WrittenMemory.Span); + backingDocument = JsonDocument.ParseValue(ref reader); + return backingDocument.RootElement; + } + finally + { + Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, output); + } + } } - internal override void GetPath(ref ValueStringBuilder path, JsonNode? child) + internal sealed override void GetPath(ref ValueStringBuilder path, JsonNode? child) { Debug.Assert(child == null); Parent?.GetPath(ref path, this); } - /// - /// Tries to obtain the current JSON value and returns a value that indicates whether the operation succeeded. - /// - /// - /// {T} can be the type or base type of the underlying value. - /// If the underlying value is a then {T} can also be the type of any primitive - /// value supported by current . - /// Specifying the type for {T} will always succeed and return the underlying value as .
- /// The underlying value of a after deserialization is an instance of , - /// otherwise it's the value specified when the was created. - ///
- /// - /// The type of value to obtain. - /// When this method returns, contains the parsed value. - /// if the value can be successfully obtained; otherwise, . - public abstract bool TryGetValue([NotNullWhen(true)] out T? value); + internal static JsonValue CreateFromTypeInfo(T value, JsonTypeInfo jsonTypeInfo, JsonNodeOptions? options = null) + { + Debug.Assert(jsonTypeInfo.IsConfigured); + Debug.Assert(value != null); + + if (JsonValue.TypeIsSupportedPrimitive && + jsonTypeInfo is { EffectiveConverter.IsInternalConverter: true } && + (jsonTypeInfo.EffectiveNumberHandling & JsonNumberHandling.WriteAsString) is 0) + { + // If the type is using the built-in converter for a known primitive, + // switch to the more efficient JsonValuePrimitive implementation. + return new JsonValuePrimitive(value, jsonTypeInfo.EffectiveConverter, options); + } + + return new JsonValueCustomized(value, jsonTypeInfo, options); + } - private static void VerifyJsonElementIsNotArrayOrObject(ref JsonElement element) + internal static JsonValue? CreateFromElement(ref readonly JsonElement element, JsonNodeOptions? options = null) { + if (element.ValueKind is JsonValueKind.Null) + { + return null; + } + // Force usage of JsonArray and JsonObject instead of supporting those in an JsonValue. if (element.ValueKind is JsonValueKind.Object or JsonValueKind.Array) { ThrowHelper.ThrowInvalidOperationException_NodeElementCannotBeObjectOrArray(); } + + return new JsonValueOfElement(element, options); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfElement.cs new file mode 100644 index 0000000000000..ddccceb825033 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfElement.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.Json.Nodes +{ + /// + /// Defines a primitive JSON value that is wrapping a . + /// + internal sealed class JsonValueOfElement : JsonValue + { + public JsonValueOfElement(JsonElement value, JsonNodeOptions? options) : base(value, options) + { + Debug.Assert(value.ValueKind is JsonValueKind.False or JsonValueKind.True or JsonValueKind.Number or JsonValueKind.String); + } + + internal override JsonElement? UnderlyingElement => Value; + internal override JsonNode DeepCloneCore() => new JsonValueOfElement(Value.Clone(), Options); + private protected override JsonValueKind GetValueKindCore() => Value.ValueKind; + + internal override bool DeepEqualsCore(JsonNode otherNode) + { + if (otherNode.UnderlyingElement is JsonElement otherElement) + { + return JsonElement.DeepEquals(Value, otherElement); + } + + if (otherNode is JsonValue) + { + // Dispatch to the other value in case it knows + // how to convert JsonElement to its own type. + return otherNode.DeepEqualsCore(this); + } + + return base.DeepEqualsCore(otherNode); + } + + public override TypeToConvert GetValue() + { + if (!TryGetValue(out TypeToConvert? value)) + { + ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvertElement(Value.ValueKind, typeof(TypeToConvert)); + } + + return value; + } + + public override bool TryGetValue([NotNullWhen(true)] out TypeToConvert value) + { + bool success; + + if (Value is TypeToConvert element) + { + value = element; + return true; + } + + switch (Value.ValueKind) + { + case JsonValueKind.Number: + if (typeof(TypeToConvert) == typeof(int) || typeof(TypeToConvert) == typeof(int?)) + { + success = Value.TryGetInt32(out int result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(long) || typeof(TypeToConvert) == typeof(long?)) + { + success = Value.TryGetInt64(out long result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(double) || typeof(TypeToConvert) == typeof(double?)) + { + success = Value.TryGetDouble(out double result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(short) || typeof(TypeToConvert) == typeof(short?)) + { + success = Value.TryGetInt16(out short result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(decimal) || typeof(TypeToConvert) == typeof(decimal?)) + { + success = Value.TryGetDecimal(out decimal result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(byte) || typeof(TypeToConvert) == typeof(byte?)) + { + success = Value.TryGetByte(out byte result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(float) || typeof(TypeToConvert) == typeof(float?)) + { + success = Value.TryGetSingle(out float result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(uint) || typeof(TypeToConvert) == typeof(uint?)) + { + success = Value.TryGetUInt32(out uint result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(ushort) || typeof(TypeToConvert) == typeof(ushort?)) + { + success = Value.TryGetUInt16(out ushort result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(ulong) || typeof(TypeToConvert) == typeof(ulong?)) + { + success = Value.TryGetUInt64(out ulong result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(sbyte) || typeof(TypeToConvert) == typeof(sbyte?)) + { + success = Value.TryGetSByte(out sbyte result); + value = (TypeToConvert)(object)result; + return success; + } + break; + + case JsonValueKind.String: + if (typeof(TypeToConvert) == typeof(string)) + { + string? result = Value.GetString(); + Debug.Assert(result != null); + value = (TypeToConvert)(object)result; + return true; + } + + if (typeof(TypeToConvert) == typeof(DateTime) || typeof(TypeToConvert) == typeof(DateTime?)) + { + success = Value.TryGetDateTime(out DateTime result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(DateTimeOffset) || typeof(TypeToConvert) == typeof(DateTimeOffset?)) + { + success = Value.TryGetDateTimeOffset(out DateTimeOffset result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(Guid) || typeof(TypeToConvert) == typeof(Guid?)) + { + success = Value.TryGetGuid(out Guid result); + value = (TypeToConvert)(object)result; + return success; + } + + if (typeof(TypeToConvert) == typeof(char) || typeof(TypeToConvert) == typeof(char?)) + { + string? result = Value.GetString(); + Debug.Assert(result != null); + if (result.Length == 1) + { + value = (TypeToConvert)(object)result[0]; + return true; + } + } + break; + + case JsonValueKind.True: + case JsonValueKind.False: + if (typeof(TypeToConvert) == typeof(bool) || typeof(TypeToConvert) == typeof(bool?)) + { + value = (TypeToConvert)(object)Value.GetBoolean(); + return true; + } + break; + } + + value = default!; + return false; + } + + public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) + { + if (writer is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(writer)); + } + + Value.WriteTo(writer); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs index 8067c321e0db4..5ae4d062da131 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs @@ -12,16 +12,11 @@ internal abstract class JsonValue : JsonValue { internal readonly TValue Value; // keep as a field for direct access to avoid copies - protected JsonValue(TValue value, JsonNodeOptions? options = null) : base(options) + protected JsonValue(TValue value, JsonNodeOptions? options) : base(options) { Debug.Assert(value != null); Debug.Assert(value is not JsonElement or JsonElement { ValueKind: not JsonValueKind.Null }); - - if (value is JsonNode) - { - ThrowHelper.ThrowArgumentException_NodeValueNotAllowed(nameof(value)); - } - + Debug.Assert(value is not JsonNode); Value = value; } @@ -33,15 +28,11 @@ public override T GetValue() return returnValue; } - if (Value is JsonElement) - { - return ConvertJsonElement(); - } - // Currently we do not support other conversions. // Generics (and also boxing) do not support standard cast operators say from 'long' to 'int', // so attempting to cast here would throw InvalidCastException. - throw new InvalidOperationException(SR.Format(SR.NodeUnableToConvert, Value!.GetType(), typeof(T))); + ThrowHelper.ThrowInvalidOperationException_NodeUnableToConvert(typeof(TValue), typeof(T)); + return default!; } public override bool TryGetValue([NotNullWhen(true)] out T value) @@ -53,11 +44,6 @@ public override bool TryGetValue([NotNullWhen(true)] out T value) return true; } - if (Value is JsonElement) - { - return TryConvertJsonElement(out value); - } - // Currently we do not support other conversions. // Generics (and also boxing) do not support standard cast operators say from 'long' to 'int', // so attempting to cast here would throw InvalidCastException. @@ -65,324 +51,77 @@ public override bool TryGetValue([NotNullWhen(true)] out T value) return false; } - private protected sealed override JsonValueKind GetValueKindCore() + /// + /// Whether is a built-in type that admits primitive JsonValue representation. + /// + internal static bool TypeIsSupportedPrimitive => s_valueKind.HasValue; + private static readonly JsonValueKind? s_valueKind = DetermineValueKindForType(typeof(TValue)); + + /// + /// Determines the JsonValueKind for the value of a built-in type. + /// + private protected static JsonValueKind DetermineValueKind(TValue value) { - if (Value is JsonElement element) - { - return element.ValueKind; - } + Debug.Assert(s_valueKind is not null, "Should only be invoked for types that are supported primitives."); - Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(default, JsonSerializerOptions.BufferSizeDefault, out PooledByteBufferWriter output); - try - { - WriteTo(writer); - writer.Flush(); - return JsonElement.ParseValue(output.WrittenMemory.Span, options: default).ValueKind; - } - finally + if (value is bool boolean) { - Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, output); + // Boolean requires special handling since kind varies by value. + return boolean ? JsonValueKind.True : JsonValueKind.False; } + + return s_valueKind.Value; } - internal sealed override bool DeepEqualsCore(JsonNode? otherNode) + /// + /// Precomputes the JsonValueKind for a given built-in type where possible. + /// + private static JsonValueKind? DetermineValueKindForType(Type type) { - if (otherNode is null) + if (type.IsEnum) { - return false; + return null; // Can vary depending on converter configuration and value. } - if (Value is JsonElement thisElement && otherNode is JsonValue { Value: JsonElement otherElement }) + if (Nullable.GetUnderlyingType(type) is Type underlyingType) { - if (thisElement.ValueKind != otherElement.ValueKind) - { - return false; - } - - switch (thisElement.ValueKind) - { - case JsonValueKind.Null: - case JsonValueKind.True: - case JsonValueKind.False: - return true; - - case JsonValueKind.String: - return thisElement.ValueEquals(otherElement.GetString()); - case JsonValueKind.Number: - return thisElement.GetRawValue().Span.SequenceEqual(otherElement.GetRawValue().Span); - default: - Debug.Fail("Object and Array JsonElements cannot be contained in JsonValue."); - return false; - } + // Because JsonNode excludes null values, we can identify with the value kind of the underlying type. + return DetermineValueKindForType(underlyingType); } - using PooledByteBufferWriter thisOutput = WriteToPooledBuffer(this); - using PooledByteBufferWriter otherOutput = WriteToPooledBuffer(otherNode); - return thisOutput.WrittenMemory.Span.SequenceEqual(otherOutput.WrittenMemory.Span); - - static PooledByteBufferWriter WriteToPooledBuffer( - JsonNode node, - JsonSerializerOptions? options = null, - JsonWriterOptions writerOptions = default, - int bufferSize = JsonSerializerOptions.BufferSizeDefault) + if (type == typeof(DateTime) || type == typeof(DateTimeOffset) || type == typeof(TimeSpan) || +#if NET + type == typeof(DateOnly) || type == typeof(TimeOnly) || +#endif + type == typeof(Guid) || type == typeof(Uri) || type == typeof(Version)) { - var bufferWriter = new PooledByteBufferWriter(bufferSize); - using var writer = new Utf8JsonWriter(bufferWriter, writerOptions); - node.WriteTo(writer, options); - return bufferWriter; + return JsonValueKind.String; } - } - internal TypeToConvert ConvertJsonElement() - { - JsonElement element = (JsonElement)(object)Value!; - - switch (element.ValueKind) +#if NET + if (type == typeof(Half) || type == typeof(UInt128) || type == typeof(Int128)) { - case JsonValueKind.Number: - if (typeof(TypeToConvert) == typeof(int) || typeof(TypeToConvert) == typeof(int?)) - { - return (TypeToConvert)(object)element.GetInt32(); - } - - if (typeof(TypeToConvert) == typeof(long) || typeof(TypeToConvert) == typeof(long?)) - { - return (TypeToConvert)(object)element.GetInt64(); - } - - if (typeof(TypeToConvert) == typeof(double) || typeof(TypeToConvert) == typeof(double?)) - { - return (TypeToConvert)(object)element.GetDouble(); - } - - if (typeof(TypeToConvert) == typeof(short) || typeof(TypeToConvert) == typeof(short?)) - { - return (TypeToConvert)(object)element.GetInt16(); - } - - if (typeof(TypeToConvert) == typeof(decimal) || typeof(TypeToConvert) == typeof(decimal?)) - { - return (TypeToConvert)(object)element.GetDecimal(); - } - - if (typeof(TypeToConvert) == typeof(byte) || typeof(TypeToConvert) == typeof(byte?)) - { - return (TypeToConvert)(object)element.GetByte(); - } - - if (typeof(TypeToConvert) == typeof(float) || typeof(TypeToConvert) == typeof(float?)) - { - return (TypeToConvert)(object)element.GetSingle(); - } - - if (typeof(TypeToConvert) == typeof(uint) || typeof(TypeToConvert) == typeof(uint?)) - { - return (TypeToConvert)(object)element.GetUInt32(); - } - - if (typeof(TypeToConvert) == typeof(ushort) || typeof(TypeToConvert) == typeof(ushort?)) - { - return (TypeToConvert)(object)element.GetUInt16(); - } - - if (typeof(TypeToConvert) == typeof(ulong) || typeof(TypeToConvert) == typeof(ulong?)) - { - return (TypeToConvert)(object)element.GetUInt64(); - } - - if (typeof(TypeToConvert) == typeof(sbyte) || typeof(TypeToConvert) == typeof(sbyte?)) - { - return (TypeToConvert)(object)element.GetSByte(); - } - break; - - case JsonValueKind.String: - if (typeof(TypeToConvert) == typeof(string)) - { - return (TypeToConvert)(object)element.GetString()!; - } - - if (typeof(TypeToConvert) == typeof(DateTime) || typeof(TypeToConvert) == typeof(DateTime?)) - { - return (TypeToConvert)(object)element.GetDateTime(); - } - - if (typeof(TypeToConvert) == typeof(DateTimeOffset) || typeof(TypeToConvert) == typeof(DateTimeOffset?)) - { - return (TypeToConvert)(object)element.GetDateTimeOffset(); - } - - if (typeof(TypeToConvert) == typeof(Guid) || typeof(TypeToConvert) == typeof(Guid?)) - { - return (TypeToConvert)(object)element.GetGuid(); - } - - if (typeof(TypeToConvert) == typeof(char) || typeof(TypeToConvert) == typeof(char?)) - { - string? str = element.GetString(); - Debug.Assert(str != null); - if (str.Length == 1) - { - return (TypeToConvert)(object)str[0]; - } - } - break; - - case JsonValueKind.True: - case JsonValueKind.False: - if (typeof(TypeToConvert) == typeof(bool) || typeof(TypeToConvert) == typeof(bool?)) - { - return (TypeToConvert)(object)element.GetBoolean(); - } - break; + return JsonValueKind.Number; } - - throw new InvalidOperationException(SR.Format(SR.NodeUnableToConvertElement, - element.ValueKind, - typeof(TypeToConvert))); - } - - internal bool TryConvertJsonElement([NotNullWhen(true)] out TypeToConvert result) - { - bool success; - - JsonElement element = (JsonElement)(object)Value!; - - switch (element.ValueKind) +#endif + return Type.GetTypeCode(type) switch { - case JsonValueKind.Number: - if (typeof(TypeToConvert) == typeof(int) || typeof(TypeToConvert) == typeof(int?)) - { - success = element.TryGetInt32(out int value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(long) || typeof(TypeToConvert) == typeof(long?)) - { - success = element.TryGetInt64(out long value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(double) || typeof(TypeToConvert) == typeof(double?)) - { - success = element.TryGetDouble(out double value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(short) || typeof(TypeToConvert) == typeof(short?)) - { - success = element.TryGetInt16(out short value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(decimal) || typeof(TypeToConvert) == typeof(decimal?)) - { - success = element.TryGetDecimal(out decimal value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(byte) || typeof(TypeToConvert) == typeof(byte?)) - { - success = element.TryGetByte(out byte value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(float) || typeof(TypeToConvert) == typeof(float?)) - { - success = element.TryGetSingle(out float value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(uint) || typeof(TypeToConvert) == typeof(uint?)) - { - success = element.TryGetUInt32(out uint value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(ushort) || typeof(TypeToConvert) == typeof(ushort?)) - { - success = element.TryGetUInt16(out ushort value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(ulong) || typeof(TypeToConvert) == typeof(ulong?)) - { - success = element.TryGetUInt64(out ulong value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(sbyte) || typeof(TypeToConvert) == typeof(sbyte?)) - { - success = element.TryGetSByte(out sbyte value); - result = (TypeToConvert)(object)value; - return success; - } - break; - - case JsonValueKind.String: - if (typeof(TypeToConvert) == typeof(string)) - { - string? strResult = element.GetString(); - Debug.Assert(strResult != null); - result = (TypeToConvert)(object)strResult; - return true; - } - - if (typeof(TypeToConvert) == typeof(DateTime) || typeof(TypeToConvert) == typeof(DateTime?)) - { - success = element.TryGetDateTime(out DateTime value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(DateTimeOffset) || typeof(TypeToConvert) == typeof(DateTimeOffset?)) - { - success = element.TryGetDateTimeOffset(out DateTimeOffset value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(Guid) || typeof(TypeToConvert) == typeof(Guid?)) - { - success = element.TryGetGuid(out Guid value); - result = (TypeToConvert)(object)value; - return success; - } - - if (typeof(TypeToConvert) == typeof(char) || typeof(TypeToConvert) == typeof(char?)) - { - string? str = element.GetString(); - Debug.Assert(str != null); - if (str.Length == 1) - { - result = (TypeToConvert)(object)str[0]; - return true; - } - } - break; - - case JsonValueKind.True: - case JsonValueKind.False: - if (typeof(TypeToConvert) == typeof(bool) || typeof(TypeToConvert) == typeof(bool?)) - { - result = (TypeToConvert)(object)element.GetBoolean(); - return true; - } - break; - } - - result = default!; - return false; + TypeCode.Boolean => JsonValueKind.Undefined, // Can vary dependending on value. + TypeCode.SByte => JsonValueKind.Number, + TypeCode.Byte => JsonValueKind.Number, + TypeCode.Int16 => JsonValueKind.Number, + TypeCode.UInt16 => JsonValueKind.Number, + TypeCode.Int32 => JsonValueKind.Number, + TypeCode.UInt32 => JsonValueKind.Number, + TypeCode.Int64 => JsonValueKind.Number, + TypeCode.UInt64 => JsonValueKind.Number, + TypeCode.Single => JsonValueKind.Number, + TypeCode.Double => JsonValueKind.Number, + TypeCode.Decimal => JsonValueKind.Number, + TypeCode.String => JsonValueKind.String, + TypeCode.Char => JsonValueKind.String, + _ => null, + }; } [ExcludeFromCodeCoverage] // Justification = "Design-time" diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfTCustomized.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfTCustomized.cs index 916f7e9bcfc29..589a8ec8e5556 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfTCustomized.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfTCustomized.cs @@ -7,18 +7,25 @@ namespace System.Text.Json.Nodes { /// - /// A JsonValue encapsulating arbitrary types using custom JsonTypeInfo metadata. + /// A JsonValue that encapsulates arbitrary .NET type configurations. + /// Paradoxically, instances of this type can be of any JsonValueKind + /// (including objects and arrays) and introspecting these values is + /// generally slower compared to the other JsonValue implementations. /// internal sealed class JsonValueCustomized : JsonValue { private readonly JsonTypeInfo _jsonTypeInfo; + private JsonValueKind? _valueKind; - public JsonValueCustomized(TValue value, JsonTypeInfo jsonTypeInfo, JsonNodeOptions? options = null) : base(value, options) + public JsonValueCustomized(TValue value, JsonTypeInfo jsonTypeInfo, JsonNodeOptions? options = null): base(value, options) { Debug.Assert(jsonTypeInfo.IsConfigured); _jsonTypeInfo = jsonTypeInfo; } + private protected override JsonValueKind GetValueKindCore() => _valueKind ??= ComputeValueKind(); + internal override JsonNode DeepCloneCore() => JsonSerializer.SerializeToNode(Value, _jsonTypeInfo)!; + public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) { if (writer is null) @@ -37,9 +44,25 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio jsonTypeInfo.Serialize(writer, Value); } - internal override JsonNode DeepCloneCore() + /// + /// Computes the JsonValueKind of the value by serializing it and reading the resultant JSON. + /// + private JsonValueKind ComputeValueKind() { - return JsonSerializer.SerializeToNode(Value, _jsonTypeInfo)!; + Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(options: default, JsonSerializerOptions.BufferSizeDefault, out PooledByteBufferWriter output); + try + { + WriteTo(writer); + writer.Flush(); + Utf8JsonReader reader = new(output.WrittenMemory.Span); + bool success = reader.Read(); + Debug.Assert(success); + return JsonReaderHelper.ToValueKind(reader.TokenType); + } + finally + { + Utf8JsonWriterCache.ReturnWriterAndBuffer(writer, output); + } } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfTPrimitive.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfTPrimitive.cs index 87be927a29222..fce1d5fbf04cf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfTPrimitive.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfTPrimitive.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Diagnostics; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Nodes { @@ -12,15 +12,31 @@ namespace System.Text.Json.Nodes ///
internal sealed class JsonValuePrimitive : JsonValue { - // Default value used when calling into the converter. - private static readonly JsonSerializerOptions s_defaultOptions = new(); - private readonly JsonConverter _converter; + private readonly JsonValueKind _valueKind; - public JsonValuePrimitive(TValue value, JsonConverter converter, JsonNodeOptions? options = null) : base(value, options) + public JsonValuePrimitive(TValue value, JsonConverter converter, JsonNodeOptions? options) : base(value, options) { + Debug.Assert(TypeIsSupportedPrimitive, $"The type {typeof(TValue)} is not a supported primitive."); Debug.Assert(converter is { IsInternalConverter: true, ConverterStrategy: ConverterStrategy.Value }); + _converter = converter; + _valueKind = DetermineValueKind(value); + } + + private protected override JsonValueKind GetValueKindCore() => _valueKind; + internal override JsonNode DeepCloneCore() => new JsonValuePrimitive(Value, _converter, Options); + + internal override bool DeepEqualsCore(JsonNode otherNode) + { + if (otherNode is JsonValue otherValue && otherValue.TryGetValue(out TValue? v)) + { + // Because TValue is equatable and otherNode returns a matching + // type we can short circuit the comparison in this case. + return EqualityComparer.Default.Equals(Value, v); + } + + return base.DeepEqualsCore(otherNode); } public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) @@ -42,14 +58,5 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio converter.Write(writer, Value, options); } } - - internal override JsonNode DeepCloneCore() - { - // Primitive JsonValue's are generally speaking immutable so we don't need to do much here. - // For the case of JsonElement clone the instance since it could be backed by pooled buffers. - return Value is JsonElement element - ? new JsonValuePrimitive(element.Clone(), JsonMetadataServices.JsonElementConverter, Options) - : new JsonValuePrimitive(Value, _converter, Options); - } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs index 3728ab26ad6f3..af2e3a785728e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs @@ -73,7 +73,7 @@ public override void Write(Utf8JsonWriter writer, JsonNode? value, JsonSerialize node = new JsonArray(element, options); break; default: - node = new JsonValuePrimitive(element, JsonMetadataServices.JsonElementConverter, options); + node = JsonValue.CreateFromElement(ref element, options); break; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs index 51253ddd70c82..97dbea8bbf7a9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs @@ -28,8 +28,7 @@ public override void Write(Utf8JsonWriter writer, JsonValue? value, JsonSerializ } JsonElement element = JsonElement.ParseValue(ref reader); - JsonValue value = new JsonValuePrimitive(element, JsonMetadataServices.JsonElementConverter, options.GetNodeOptions()); - return value; + return JsonValue.CreateFromElement(ref element, options.GetNodeOptions()); } internal override JsonSchema? GetSchema(JsonNumberHandling _) => JsonSchema.True; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs index d5803e65b2cc8..9aed8af362e9d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs @@ -20,6 +20,7 @@ internal sealed class NullableConverter : JsonConverter where T : struct public NullableConverter(JsonConverter elementConverter) { _elementConverter = elementConverter; + IsInternalConverter = elementConverter.IsInternalConverter; IsInternalConverterForNumberType = elementConverter.IsInternalConverterForNumberType; ConverterStrategy = elementConverter.ConverterStrategy; } 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 f67431c36d56a..91b516058e157 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 @@ -496,15 +496,7 @@ static string ReadAsStringMetadataValue(JsonNode? jsonNode) return value; } - JsonValueKind metadataValueKind = jsonNode switch - { - null => JsonValueKind.Null, - JsonObject => JsonValueKind.Object, - JsonArray => JsonValueKind.Array, - JsonValue element => element.Value.ValueKind, - _ => JsonValueKind.Undefined, - }; - + JsonValueKind metadataValueKind = jsonNode?.GetValueKind() ?? JsonValueKind.Null; Debug.Assert(metadataValueKind != JsonValueKind.Undefined); ThrowHelper.ThrowJsonException_MetadataValueWasNotString(metadataValueKind); return null!; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 29f7dcdcca323..f337adcb567b0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -505,6 +505,7 @@ public JsonNumberHandling? NumberHandling } } + internal JsonNumberHandling EffectiveNumberHandling => _numberHandling ?? Options.NumberHandling; private JsonNumberHandling? _numberHandling; /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs index 9fe72311c7cf5..025a4215ba472 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs @@ -70,5 +70,17 @@ public static NotSupportedException GetNotSupportedException_CollectionIsReadOnl { return new NotSupportedException(SR.CollectionIsReadOnly); } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_NodeUnableToConvert(Type sourceType, Type destinationType) + { + throw new InvalidOperationException(SR.Format(SR.NodeUnableToConvert, sourceType, destinationType)); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_NodeUnableToConvertElement(JsonValueKind valueKind, Type destinationType) + { + throw new InvalidOperationException(SR.Format(SR.NodeUnableToConvertElement, valueKind, destinationType)); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs index ad0bddbd3fd1c..94505ff194149 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Text.Json.Serialization; -using System.Xml.Linq; +using System.Text.Json.Serialization.Metadata; using Xunit; namespace System.Text.Json.Nodes.Tests @@ -417,6 +419,20 @@ public static void GetValueKind() } } + [Theory] + [InlineData(JsonNumberHandling.Strict, JsonValueKind.Number)] + [InlineData(JsonNumberHandling.AllowReadingFromString, JsonValueKind.Number)] + [InlineData(JsonNumberHandling.AllowNamedFloatingPointLiterals, JsonValueKind.Number)] + [InlineData(JsonNumberHandling.WriteAsString, JsonValueKind.String)] + [InlineData(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals, JsonValueKind.String)] + public static void GetValueKind_NumberHandling(JsonNumberHandling numberHandling, JsonValueKind expectedKind) + { + JsonSerializerOptions options = new(JsonSerializerOptions.Default) { NumberHandling = numberHandling }; + JsonTypeInfo typeInfo = (JsonTypeInfo)options.GetTypeInfo(typeof(int)); + JsonValue value = JsonValue.Create(42, typeInfo); + Assert.Equal(expectedKind, value.GetValueKind()); + } + [Fact] public static void DeepEquals_EscapedString() { @@ -430,5 +446,87 @@ private class Student public int Id { get; set; } public string? Name { get; set; } } + + [Theory] + [MemberData(nameof(GetPrimitiveTypes))] + public static void PrimitiveTypes_ReturnExpectedTypeKind(T value, JsonValueKind expectedKind) + { + JsonNode node = JsonValue.Create(value); + Assert.Equal(expectedKind, node.GetValueKind()); + } + + [Theory] + [MemberData(nameof(GetPrimitiveTypes))] + public static void PrimitiveTypes_EqualThemselves(T value, JsonValueKind _) + { + JsonNode node = JsonValue.Create(value); + Assert.True(JsonNode.DeepEquals(node, node)); + } + + [Theory] + [MemberData(nameof(GetPrimitiveTypes))] + public static void PrimitiveTypes_EqualClonedValue(T value, JsonValueKind _) + { + JsonNode node = JsonValue.Create(value); + JsonNode clone = node.DeepClone(); + + Assert.True(JsonNode.DeepEquals(clone, clone)); + Assert.True(JsonNode.DeepEquals(node, clone)); + Assert.True(JsonNode.DeepEquals(clone, node)); + } + + [Theory] + [MemberData(nameof(GetPrimitiveTypes))] + public static void PrimitiveTypes_EqualDeserializedValue(T value, JsonValueKind _) + { + JsonNode node = JsonValue.Create(value); + JsonNode clone = JsonSerializer.Deserialize(node.ToJsonString()); + + Assert.True(JsonNode.DeepEquals(clone, clone)); + Assert.True(JsonNode.DeepEquals(node, clone)); + Assert.True(JsonNode.DeepEquals(clone, node)); + } + + public static IEnumerable GetPrimitiveTypes() + { + yield return Wrap(false, JsonValueKind.False); + yield return Wrap(true, JsonValueKind.True); + yield return Wrap((bool?)false, JsonValueKind.False); + yield return Wrap((bool?)true, JsonValueKind.True); + yield return Wrap((byte)42, JsonValueKind.Number); + yield return Wrap((sbyte)42, JsonValueKind.Number); + yield return Wrap((short)42, JsonValueKind.Number); + yield return Wrap((ushort)42, JsonValueKind.Number); + yield return Wrap(42, JsonValueKind.Number); + yield return Wrap((int?)42, JsonValueKind.Number); + yield return Wrap((uint)42, JsonValueKind.Number); + yield return Wrap((long)42, JsonValueKind.Number); + yield return Wrap((ulong)42, JsonValueKind.Number); + yield return Wrap(42.0f, JsonValueKind.Number); + yield return Wrap(42.0, JsonValueKind.Number); + yield return Wrap(42.0m, JsonValueKind.Number); + yield return Wrap('A', JsonValueKind.String); + yield return Wrap((char?)'A', JsonValueKind.String); + yield return Wrap("A", JsonValueKind.String); + yield return Wrap(new byte[] { 1, 2, 3 }, JsonValueKind.String); + yield return Wrap(new DateTimeOffset(2024, 06, 20, 10, 29, 0, TimeSpan.Zero), JsonValueKind.String); + yield return Wrap(new DateTime(2024, 06, 20, 10, 29, 0), JsonValueKind.String); + yield return Wrap(Guid.Empty, JsonValueKind.String); + yield return Wrap((Guid?)Guid.Empty, JsonValueKind.String); + yield return Wrap(new Uri("http://example.com"), JsonValueKind.String); + yield return Wrap(new Version(1, 2, 3, 4), JsonValueKind.String); + yield return Wrap(BindingFlags.Public, JsonValueKind.Number); + yield return Wrap((BindingFlags?)BindingFlags.Public, JsonValueKind.Number); +#if NET + yield return Wrap(Half.MaxValue, JsonValueKind.Number); + yield return Wrap((Int128)42, JsonValueKind.Number); + yield return Wrap((Int128)42, JsonValueKind.Number); + yield return Wrap((Memory)new byte[] { 1, 2, 3 }, JsonValueKind.String); + yield return Wrap((ReadOnlyMemory)new byte[] { 1, 2, 3 }, JsonValueKind.String); + yield return Wrap(new DateOnly(2024, 06, 20), JsonValueKind.String); + yield return Wrap(new TimeOnly(10, 29), JsonValueKind.String); +#endif + static object[] Wrap(T value, JsonValueKind expectedKind) => [value, expectedKind]; + } } }