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 bc75757b165af8..d1fe0acde4a7fe 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -660,6 +660,7 @@ public void Clear() { } public bool Contains(System.Text.Json.Nodes.JsonNode? item) { throw null; } public static System.Text.Json.Nodes.JsonArray? Create(System.Text.Json.JsonElement element, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public System.Collections.Generic.IEnumerator GetEnumerator() { throw null; } + public System.Collections.Generic.IEnumerable GetValues() { throw null; } public int IndexOf(System.Text.Json.Nodes.JsonNode? item) { throw null; } public void Insert(int index, System.Text.Json.Nodes.JsonNode? item) { } public bool Remove(System.Text.Json.Nodes.JsonNode? item) { throw null; } @@ -679,8 +680,13 @@ internal JsonNode() { } public System.Text.Json.Nodes.JsonArray AsArray() { throw null; } public System.Text.Json.Nodes.JsonObject AsObject() { throw null; } public System.Text.Json.Nodes.JsonValue AsValue() { throw null; } + public System.Text.Json.Nodes.JsonNode DeepClone() { throw null; } + public static bool DeepEquals(System.Text.Json.Nodes.JsonNode? node1, System.Text.Json.Nodes.JsonNode? node2) { throw null; } + public string GetPropertyName() { throw null; } public string GetPath() { throw null; } public virtual T GetValue() { throw null; } + public JsonValueKind GetValueKind() { throw null; } + public int GetElementIndex() { throw null; } public static explicit operator bool (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator byte (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator char (System.Text.Json.Nodes.JsonNode value) { throw null; } @@ -768,6 +774,10 @@ internal JsonNode() { } public static System.Text.Json.Nodes.JsonNode? Parse(System.ReadOnlySpan utf8Json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; } public static System.Text.Json.Nodes.JsonNode? Parse([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("Json")] string json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; } public static System.Text.Json.Nodes.JsonNode? Parse(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } + + [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Creating JsonValue instances with non-primitive types requires generating code at runtime.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("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.")] + public void ReplaceWith(T value) { throw null; } public string ToJsonString(System.Text.Json.JsonSerializerOptions? options = null) { throw null; } public override string ToString() { throw null; } public abstract void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.Text.Json.JsonSerializerOptions? options = null); 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 9042a31b9d07e5..f929db31929874 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 @@ -287,8 +287,6 @@ internal bool TextEquals(int index, ReadOnlySpan otherText, bool isPropert { CheckNotDisposed(); - int matchIndex = isPropertyName ? index - DbRow.Size : index; - byte[]? otherUtf8TextArray = null; int length = checked(otherText.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); 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 f5ad9b274728b3..3fd5af169f129d 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 @@ -48,6 +48,86 @@ public JsonArray(params JsonNode?[] items) : base() InitializeFromArray(items); } + internal override JsonNode InternalDeepClone() + { + if (_jsonElement.HasValue) + { + return new JsonArray(_jsonElement.Value.Clone(), Options); + } + + List list = List; + + var jsonArray = new JsonArray(Options) + { + _list = new List(list.Count) + }; + + for (int i = 0; i < list.Count; i++) + { + JsonNode? item = list[i]; + if (item is null) + { + jsonArray.Add(null); + } + else + { + jsonArray.Add(item.DeepClone()); + } + } + + return jsonArray; + } + + internal override bool DeepEquals(JsonNode? node) + { + switch (node) + { + case null or JsonObject: + return false; + case JsonValue value: + // JsonValueTrimmable/NonTrimmable can hold the array type so calling this method to continue the deep comparision. + return value.DeepEquals(this); + case JsonArray array: + List currentList = List; + List otherList = array.List; + + if (currentList.Count != otherList.Count) + { + return false; + } + + for (int i = 0; i < currentList.Count; i++) + { + if (!DeepEquals(currentList[i], otherList[i])) + { + return false; + } + } + + return true; + default: + Debug.Fail("Impossible case"); + return false; + } + } + + internal int GetElementIndex(JsonNode? node) + { + return List.IndexOf(node); + } + + /// + /// Returns enumerator that wraps calls to . + /// + /// The type of the value to obtain from the . + public IEnumerable GetValues() + { + foreach (JsonNode? item in List) + { + yield return item is null ? (T)(object?)null! : item.GetValue(); + } + } + private void InitializeFromArray(JsonNode?[] items) { var list = new List(items); 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 dc34dcfba82761..9150676d5122a8 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 @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace System.Text.Json.Nodes { @@ -50,12 +51,14 @@ internal JsonNode(JsonNodeOptions? options = null) /// public JsonArray AsArray() { - if (this is JsonArray jArray) + JsonArray? jArray = this as JsonArray; + + if (jArray is null) { - return jArray; + ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray)); } - throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonArray))); + return jArray; } /// @@ -69,12 +72,14 @@ public JsonArray AsArray() /// public JsonObject AsObject() { - if (this is JsonObject jObject) + JsonObject? jObject = this as JsonObject; + + if (jObject is null) { - return jObject; + ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonObject)); } - throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonObject))); + return jObject; } /// @@ -88,12 +93,14 @@ public JsonObject AsObject() /// public JsonValue AsValue() { - if (this is JsonValue jValue) + JsonValue? jValue = this as JsonValue; + + if (jValue is null) { - return jValue; + ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonValue)); } - throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonValue))); + return jValue; } /// @@ -232,6 +239,107 @@ public JsonNode? this[string propertyName] } } + /// + /// Creates a new instance of the . All children nodes are recursively cloned. + /// + public JsonNode DeepClone() + { + return InternalDeepClone(); + } + + internal abstract JsonNode InternalDeepClone(); + + /// + /// Returns of current instance. + /// + public JsonValueKind GetValueKind() + { + return this switch + { + JsonObject => JsonValueKind.Object, + JsonArray => JsonValueKind.Array, + _ => AsValue().GetInternalValueKind(), + }; + } + + /// + /// Returns property name of the current node from the parent object. + /// + /// + /// The current parent is not a . + /// + public string GetPropertyName() + { + JsonObject? jsonObject = _parent as JsonObject; + + if (jsonObject is null) + { + ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonObject)); + } + + return jsonObject.GetPropertyName(this); + } + + /// + /// Returns index of the current node from the parent . + /// + /// + /// The current parent is not a . + /// + public int GetElementIndex() + { + JsonArray? jsonArray = _parent as JsonArray; + + if (jsonArray is null) + { + ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray)); + } + + return jsonArray.GetElementIndex(this); + } + + /// + /// Compares the values of two nodes, including the values of all descendant nodes. + /// + /// The to compare. + /// The to compare. + /// true if the tokens are equal; otherwise false. + public static bool DeepEquals(JsonNode? node1, JsonNode? node2) + { + if (node1 is null) + { + return node2 is null; + } + + return node1.DeepEquals(node2); + } + + internal abstract bool DeepEquals(JsonNode? node); + + /// + /// Replaces this node with a new value. + /// + /// The type of value to be replaced. + /// Value that replaces this node. + [RequiresUnreferencedCode(JsonValue.CreateUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonValue.CreateDynamicCodeMessage)] + public void ReplaceWith(T value) + { + switch (_parent) + { + case null: + return; + case JsonObject jsonObject: + JsonValue? jsonValue = JsonValue.Create(value); + jsonObject.SetItem(GetPropertyName(), jsonValue); + return; + case JsonArray jsonArray: + JsonValue? jValue = JsonValue.Create(value); + jsonArray.SetItem(GetElementIndex(), jValue); + return; + } + } + internal void AssignParent(JsonNode parent) { if (Parent != null) 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 7beb9fdd724b85..09420fbf6895c4 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 @@ -70,6 +70,46 @@ internal JsonObject(JsonElement element, JsonNodeOptions? options = null) : this _jsonElement = element; } + internal override JsonNode InternalDeepClone() + { + if (_jsonElement.HasValue) + { + return new JsonObject(_jsonElement!.Value.Clone(), Options); + } + + if (_dictionary is not null) + { + bool caseInsensitive = Options.HasValue ? Options.Value.PropertyNameCaseInsensitive : false; + var jObject = new JsonObject(Options) + { + _dictionary = new JsonPropertyDictionary(caseInsensitive, _dictionary.Count) + }; + + foreach (KeyValuePair item in _dictionary) + { + if (item.Value is not null) + { + jObject.Add(item.Key, item.Value.DeepClone()); + } + else + { + jObject.Add(item.Key, null); + } + } + return jObject; + } + + return new JsonObject(Options); + } + + internal string GetPropertyName(JsonNode? node) + { + InitializeIfRequired(); + Debug.Assert(_dictionary != null); + KeyValuePair? item = _dictionary.FindValue(node); + return item.HasValue ? item.Value.Key : string.Empty; + } + /// /// Returns the value of a property with the specified name. /// @@ -110,6 +150,43 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio } } + internal override bool DeepEquals(JsonNode? node) + { + switch (node) + { + case null or JsonArray: + return false; + case JsonValue value: + // JsonValueTrimmable/NonTrimmable can hold the object type so calling this method to continue the deep comparision. + return value.DeepEquals(this); + case JsonObject jsonObject: + InitializeIfRequired(); + jsonObject.InitializeIfRequired(); + Debug.Assert(_dictionary is not null); + Debug.Assert(jsonObject._dictionary is not null); + + if (_dictionary.Count != jsonObject._dictionary.Count) + { + return false; + } + + foreach (KeyValuePair item in this) + { + JsonNode? jsonNode = jsonObject._dictionary[item.Key]; + + if (!DeepEquals(item.Value, jsonNode)) + { + return false; + } + } + + return true; + default: + Debug.Fail("Impossible case"); + return false; + } + } + internal JsonNode? GetItem(string propertyName) { if (TryGetPropertyValue(propertyName, out JsonNode? value)) 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 1853aad3e0f6cf..78ead4c98e4be2 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 @@ -95,6 +96,8 @@ internal override void GetPath(List path, JsonNode? child) Parent?.GetPath(path, this); } + internal abstract JsonValueKind GetInternalValueKind(); + /// /// Tries to obtain the current JSON value and returns a value that indicates whether the operation succeeded. /// @@ -117,7 +120,7 @@ private static void VerifyJsonElementIsNotArrayOrObject(ref JsonElement element) // Force usage of JsonArray and JsonObject instead of supporting those in an JsonValue. if (element.ValueKind == JsonValueKind.Object || element.ValueKind == JsonValueKind.Array) { - throw new InvalidOperationException(SR.NodeElementCannotBeObjectOrArray); + ThrowHelper.ThrowInvalidOperationException_NodeElementCannotBeObjectOrArray(); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueNotTrimmable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueNotTrimmable.cs index 611cfb8a87e0d7..c7cc8f30d77209 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueNotTrimmable.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueNotTrimmable.cs @@ -25,5 +25,25 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio JsonSerializer.Serialize(writer, _value, options); } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "The ctor is marked RequiresUnreferencedCode.")] + internal override JsonNode InternalDeepClone() => JsonSerializer.SerializeToNode(_value)!; + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "The ctor is marked RequiresUnreferencedCode.")] + internal override bool DeepEquals(JsonNode? node) + { + JsonNode? jsonNode = JsonSerializer.SerializeToNode(_value); + return DeepEquals(jsonNode, node); + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "The ctor is marked RequiresUnreferencedCode.")] + internal override JsonValueKind GetInternalValueKind() + { + JsonNode? jsonNode = JsonSerializer.SerializeToNode(_value); + return jsonNode is null ? JsonValueKind.Null : jsonNode.GetValueKind(); + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueTrimmable.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueTrimmable.cs index 5f5a354ed12f4e..6a22eb9d727f8e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueTrimmable.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueTrimmable.cs @@ -51,5 +51,100 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio JsonSerializer.Serialize(writer, _value, _jsonTypeInfo); } } + + internal override JsonNode InternalDeepClone() + { + if (_converter is not null) + { + return _value is JsonElement element + ? new JsonValueTrimmable(element.Clone(), JsonMetadataServices.JsonElementConverter, Options) + : new JsonValueTrimmable(_value, _converter, Options); + } + else + { + Debug.Assert(_jsonTypeInfo != null); + return JsonSerializer.SerializeToNode(_value, _jsonTypeInfo)!; + } + } + + internal override bool DeepEquals(JsonNode? node) + { + if (node is null) + { + return false; + } + + if (_jsonTypeInfo is not null) + { + JsonNode? jsonNode = JsonSerializer.SerializeToNode(_value, _jsonTypeInfo); + return DeepEquals(jsonNode, node); + } + else + { + if (node is JsonArray || node is JsonObject) + { + return false; + } + + if (node is JsonValueTrimmable jsonElementNodeOther && _value is JsonElement jsonElementCurrent) + { + if (jsonElementNodeOther._value.ValueKind != jsonElementCurrent.ValueKind) + { + return false; + } + + switch (jsonElementCurrent.ValueKind) + { + case JsonValueKind.String: + return jsonElementCurrent.ValueEquals(jsonElementNodeOther._value.GetString()); + case JsonValueKind.True: + case JsonValueKind.False: + return jsonElementCurrent.ValueKind == jsonElementNodeOther._value.ValueKind; + case JsonValueKind.Number: + return jsonElementCurrent.GetRawValue().Span.SequenceEqual(jsonElementNodeOther._value.GetRawValue().Span); + default: + Debug.Fail("Impossible case"); + return false; + } + } + + using var currentOutput = new PooledByteBufferWriter(JsonSerializerOptions.BufferSizeDefault); + using (var writer = new Utf8JsonWriter(currentOutput, default)) + { + WriteTo(writer); + } + + using var anotherOutput = new PooledByteBufferWriter(JsonSerializerOptions.BufferSizeDefault); + using (var writer = new Utf8JsonWriter(anotherOutput, default)) + { + node.WriteTo(writer); + } + + return currentOutput.WrittenMemory.Span.SequenceEqual(anotherOutput.WrittenMemory.Span); + } + } + + internal override JsonValueKind GetInternalValueKind() + { + if (_jsonTypeInfo is not null) + { + return JsonSerializer.SerializeToElement(_value, _jsonTypeInfo).ValueKind; + } + else + { + if (_value is JsonElement element) + { + return element.ValueKind; + } + + using var output = new PooledByteBufferWriter(JsonSerializerOptions.BufferSizeDefault); + using (var writer = new Utf8JsonWriter(output, default)) + { + WriteTo(writer); + } + + return JsonElement.ParseValue(output.WrittenMemory.Span, default).ValueKind; + } + } } } 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 90bfe5a66f06d7..de1aa750fa6bdc 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 @@ -44,6 +44,12 @@ public static void ThrowNotSupportedException_CollectionIsReadOnly() throw GetNotSupportedException_CollectionIsReadOnly(); } + [DoesNotReturn] + public static void ThrowInvalidOperationException_NodeWrongType(string typeName) + { + throw new InvalidOperationException(SR.Format(SR.NodeWrongType, typeName)); + } + public static NotSupportedException GetNotSupportedException_CollectionIsReadOnly() { return new NotSupportedException(SR.CollectionIsReadOnly); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonArrayTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonArrayTests.cs index 9e32f3298d4622..97dff770fe3bca 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonArrayTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonArrayTests.cs @@ -486,5 +486,204 @@ public static void LazyInitializationIsThreadSafe() Assert.Equal("elem1", (string)node[1]); }); } + + [Fact] + public static void DeepClone() + { + var nestedArray = new JsonArray("elem0", "elem1"); + var nestedJsonObj = new JsonObject() + { + { "Nine", 9 }, + { "Name", "def"} + }; + + var array = new JsonArray(); + array.Add(10); + array.Add("abcd"); + array.Add(null); + array.Add(true); + array.Add(false); + array.Add(JsonValue.Create(30)); + array.Add(nestedJsonObj); + array.Add(nestedArray); + + JsonArray clonedArray = array.DeepClone().AsArray(); + + Assert.True(JsonNode.DeepEquals(array, clonedArray)); + Assert.Equal(array.Count, clonedArray.Count); + Assert.Equal(10, array[0].GetValue()); + Assert.Equal("abcd", array[1].GetValue()); + Assert.Null(array[2]); + Assert.True(array[3].GetValue()); + Assert.False(array[4].GetValue()); + Assert.Equal(30, array[5].GetValue()); + + JsonObject clonedNestedJObject = array[6].AsObject(); + Assert.Equal(nestedJsonObj.Count, clonedNestedJObject.Count); + Assert.Equal(9, clonedNestedJObject["Nine"].GetValue()); + Assert.Equal("def", clonedNestedJObject["Name"].GetValue()); + + JsonArray clonedNestedArray = clonedArray[7].AsArray(); + Assert.Equal(nestedArray.Count, clonedNestedArray.Count); + Assert.Equal("elem0", clonedNestedArray[0].GetValue()); + Assert.Equal("elem1", clonedNestedArray[1].GetValue()); + + string originalJson = array.ToJsonString(); + string clonedJson = clonedArray.ToJsonString(); + + Assert.Equal(originalJson, clonedJson); + } + + [Fact] + public static void DeepCloneFromElement() + { + JsonDocument document = JsonDocument.Parse("[\"abc\", 10]"); + JsonArray jArray = JsonArray.Create(document.RootElement); + var clone = jArray.DeepClone().AsArray(); + + Assert.True(JsonNode.DeepEquals(jArray, clone)); + Assert.Equal(10, clone[1].GetValue()); + Assert.Equal("abc", clone[0].GetValue()); + } + + [Fact] + public static void DeepEquals() + { + var array = new JsonArray() { null, 10, "str" }; + var sameArray = new JsonArray() { null, 10, "str" }; + + Assert.True(JsonNode.DeepEquals(array, array)); + Assert.True(JsonNode.DeepEquals(array, sameArray)); + Assert.True(JsonNode.DeepEquals(sameArray, array)); + + Assert.False(JsonNode.DeepEquals(array, null)); + + var diffArray = new JsonArray() { null, 10, "s" }; + Assert.False(JsonNode.DeepEquals(array, diffArray)); + Assert.False(JsonNode.DeepEquals(diffArray, array)); + + diffArray = new JsonArray() { null, 10 }; + Assert.False(JsonNode.DeepEquals(array, diffArray)); + Assert.False(JsonNode.DeepEquals(diffArray, array)); + } + + [Fact] + public static void DeepEqualsWithJsonValueArrayType() + { + var array = new JsonArray(); + array.Add(JsonValue.Create(2)); + array.Add(JsonValue.Create(3)); + array.Add(JsonValue.Create(4)); + var value = JsonValue.Create(new long[] { 2, 3, 4 }); + + Assert.True(JsonNode.DeepEquals(array, value)); + } + + [Fact] + public static void DeepEqualsFromElement() + { + using JsonDocument document = JsonDocument.Parse("[1, 2, 4]"); + JsonArray array = JsonArray.Create(document.RootElement); + + using JsonDocument document2 = JsonDocument.Parse("[1, 2, 4]"); + JsonArray array2 = JsonArray.Create(document2.RootElement); + Assert.True(JsonNode.DeepEquals(array, array2)); + + using JsonDocument document3 = JsonDocument.Parse("[2, 1, 4]"); + JsonArray array3 = JsonArray.Create(document3.RootElement); + Assert.False(JsonNode.DeepEquals(array, array3)); + } + + [Fact] + public static void UpdateClonedObjectNotAffectOriginal() + { + var jArray = new JsonArray(10, 20); + + var clone = jArray.DeepClone().AsArray(); + clone[1] = 3; + + Assert.Equal(20, jArray[1].GetValue()); + } + + [Fact] + public static void GetValueKind() + { + Assert.Equal(JsonValueKind.Array, new JsonArray().GetValueKind()); + } + + [Fact] + public static void GetElementIndex() + { + var trueValue = JsonValue.Create(true); + var falseValue = JsonValue.Create(false); + var numberValue = JsonValue.Create(15); + var stringValue = JsonValue.Create("ssss"); + var nestedObject = new JsonObject(); + var nestedArray = new JsonArray(); + + var array = new JsonArray(); + array.Add(trueValue); + array.Add(falseValue); + array.Add(numberValue); + array.Add(stringValue); + array.Add(nestedObject); + array.Add(nestedArray); + + Assert.Equal(0, trueValue.GetElementIndex()); + Assert.Equal(1, falseValue.GetElementIndex()); + Assert.Equal(2, numberValue.GetElementIndex()); + Assert.Equal(3, stringValue.GetElementIndex()); + Assert.Equal(4, nestedObject.GetElementIndex()); + Assert.Equal(5, nestedArray.GetElementIndex()); + } + + [Fact] + public static void GetValues_ValueType() + { + JsonArray jsonArray = new JsonArray(1, 2, 3, 2); + + IEnumerable values = jsonArray.GetValues(); + + Assert.Equal(jsonArray.Count, values.Count()); + Assert.Equal(1, values.ElementAt(0)); + Assert.Equal(2, values.ElementAt(1)); + Assert.Equal(3, values.ElementAt(2)); + Assert.Equal(2, values.ElementAt(3)); + + jsonArray = new JsonArray(1, null); + Assert.Throws(() => jsonArray.GetValues().Count()); + } + + [Fact] + public static void GetValues_ReferenceType() + { + var student1 = new Student(); + var student2 = new Student(); + JsonArray jsonArray = new JsonArray(JsonValue.Create(student1), null, JsonValue.Create(student2)); + + IEnumerable values = jsonArray.GetValues(); + + Assert.Equal(jsonArray.Count, values.Count()); + Assert.Equal(student1, values.ElementAt(0)); + Assert.Null(values.ElementAt(1)); + Assert.Equal(student2, values.ElementAt(2)); + } + + private class Student + { + public string Name { get; set; } + } + + [Fact] + public static void ReplaceWith() + { + var jArray = new JsonArray(); + var jValue = JsonValue.Create(10); + jArray.Add(jValue); + jArray[0].ReplaceWith(5); + + Assert.Null(jValue.Parent); + Assert.Equal("[5]", jArray.ToJsonString()); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonNodeTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonNodeTests.cs index a90d59a49ccd10..3e398477c82c8e 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonNodeTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonNodeTests.cs @@ -86,5 +86,57 @@ public static void GetValue_Throws() Assert.Throws(() => JsonNode.Parse("{}").GetValue()); Assert.Throws(() => JsonNode.Parse("[]").GetValue()); } + + [Fact] + public static void GetValueKind() + { + Assert.Equal(JsonValueKind.Object, JsonNode.Parse("{}").GetValueKind()); + Assert.Equal(JsonValueKind.Array, JsonNode.Parse("[]").GetValueKind()); + Assert.Equal(JsonValueKind.Number, JsonNode.Parse("12").GetValueKind()); + Assert.Equal(JsonValueKind.String, JsonNode.Parse("\"12\"").GetValueKind()); + Assert.Equal(JsonValueKind.True, JsonNode.Parse("true").GetValueKind()); + Assert.Equal(JsonValueKind.False, JsonNode.Parse("false").GetValueKind()); + } + + [Fact] + public static void GetPropertyName() + { + JsonNode jsonNode = JsonNode.Parse("{\"a\" : \"b\"}"); + Assert.Equal("a", jsonNode["a"].GetPropertyName()); + + Assert.Throws(() => JsonNode.Parse("[]").GetPropertyName()); + Assert.Throws(() => JsonNode.Parse("5").GetPropertyName()); + } + + [Fact] + public static void GetElementIndex() + { + JsonNode jsonNode = JsonNode.Parse("[90, \"str\", true, false]"); + Assert.Equal(0, jsonNode[0].GetElementIndex()); + Assert.Equal(1, jsonNode[1].GetElementIndex()); + Assert.Equal(2, jsonNode[2].GetElementIndex()); + Assert.Equal(3, jsonNode[3].GetElementIndex()); + + Assert.Throws(() => JsonNode.Parse("{}").GetElementIndex()); + Assert.Throws(() => JsonNode.Parse("5").GetElementIndex()); + } + + + [Fact] + public static void ReplaceWith() + { + JsonNode jsonNode = JsonNode.Parse("[90, 2, 3]"); + jsonNode[1].ReplaceWith(12); + jsonNode[2].ReplaceWith("str"); + + Assert.Equal(12, jsonNode[1].GetValue()); + Assert.Equal("str", jsonNode[2].GetValue()); + + Assert.Equal("[90,12,\"str\"]", jsonNode.ToJsonString()); + + jsonNode = JsonNode.Parse("{\"a\": \"b\"}"); + jsonNode["a"].ReplaceWith("c"); + Assert.Equal("{\"a\":\"c\"}", jsonNode.ToJsonString()); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs index 8d6332e25e974b..5246739b828061 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs @@ -969,5 +969,231 @@ public static void LazyInitializationIsThreadSafe() Assert.Equal(1, (int)jObj["prop1"]); }); } + + [Fact] + public static void DeepClone() + { + var array = new JsonArray(); + array.Add(5); + array.Add(7); + var nestedJsonObj = new JsonObject() + { + { "Ten", 10 }, + { "Name", "xyz"} + }; + + var jObject = new JsonObject(); + jObject["One"] = 1; + jObject["Two"] = 2; + jObject["String"] = "ABC"; + jObject["True"] = true; + jObject["False"] = false; + jObject["Null"] = null; + jObject["value"] = JsonValue.Create(10); + jObject["array"] = array; + jObject["object"] = nestedJsonObj; + + var clone = jObject.DeepClone().AsObject(); + + Assert.True(JsonNode.DeepEquals(jObject, clone)); + + Assert.Equal(jObject.Count, clone.Count); + Assert.Equal(1, clone["One"].GetValue()); + Assert.Equal(2, clone["Two"].GetValue()); + Assert.Equal("ABC", clone["String"].GetValue()); + Assert.True(clone["True"].GetValue()); + Assert.False(clone["False"].GetValue()); + Assert.Null(clone["Null"]); + Assert.Equal(10, clone["value"].GetValue()); + + JsonArray clonedArray = clone["array"].AsArray(); + Assert.Equal(array.Count, clonedArray.Count); + Assert.Equal(5, clonedArray[0].GetValue()); + Assert.Equal(7, clonedArray[1].GetValue()); + + JsonObject clonedNestedJObject = clone["object"].AsObject(); + Assert.Equal(nestedJsonObj.Count, clonedNestedJObject.Count); + Assert.Equal(10, clonedNestedJObject["Ten"].GetValue()); + Assert.Equal("xyz", clonedNestedJObject["Name"].GetValue()); + + string originalJson = jObject.ToJsonString(); + string clonedJson = clone.ToJsonString(); + + Assert.Equal(originalJson, clonedJson); + } + + [Fact] + public static void DeepClone_FromElement() + { + JsonDocument document = JsonDocument.Parse("{\"One\": 1, \"String\": \"abc\"}"); + JsonObject jObject = JsonObject.Create(document.RootElement); + var clone = jObject.DeepClone().AsObject(); + + Assert.True(JsonNode.DeepEquals(jObject, clone)); + Assert.Equal(1, clone["One"].GetValue()); + Assert.Equal("abc", clone["String"].GetValue()); + } + + [Fact] + public static void DeepEquals() + { + var jObject = new JsonObject(); + jObject["One"] = 1; + jObject["array"] = new JsonArray() { "a", "b" }; + + var sameJObject = new JsonObject(); + sameJObject["One"] = 1; + sameJObject["array"] = new JsonArray() { "a", "b" }; + + Assert.True(JsonNode.DeepEquals(jObject, jObject)); + Assert.True(JsonNode.DeepEquals(jObject, sameJObject)); + Assert.True(JsonNode.DeepEquals(sameJObject, jObject)); + + Assert.True(JsonNode.DeepEquals(null, null)); + Assert.False(JsonNode.DeepEquals(jObject, null)); + Assert.False(JsonNode.DeepEquals(null, jObject)); + + var diffJObject = new JsonObject(); + diffJObject["One"] = 3; + + Assert.False(JsonNode.DeepEquals(diffJObject, jObject)); + Assert.False(JsonNode.DeepEquals(jObject, diffJObject)); + } + + [Fact] + public static void DeepEquals_JsonObject_With_JsonValuePOCO() + { + var jObject = new JsonObject(); + jObject["Id"] = 1; + jObject["Name"] = "First"; + var nestedObject = new JsonObject(); + nestedObject["Id"] = 2; + nestedObject["Name"] = "Last"; + nestedObject["NestedObject"] = null; + jObject["NestedObject"] = nestedObject; + + var poco = new SimpleClass() + { + Id = 1, + Name = "First", + NestedObject = new SimpleClass() + { + Id = 2, + Name = "Last", + } + }; + + Assert.True(JsonNode.DeepEquals(jObject, JsonValue.Create(poco))); + + var diffPoco = new SimpleClass() + { + Id = 1, + Name = "First", + NestedObject = new SimpleClass() + { + Id = 3, + Name = "Last", + } + }; + + Assert.False(JsonNode.DeepEquals(jObject, JsonValue.Create(diffPoco))); + } + + [Fact] + public static void DeepEquals_JsonObject_With_Dictionary() + { + var jObject = new JsonObject(); + jObject["One"] = 1; + jObject["array"] = new JsonArray() { "a", "b" }; + jObject["obj"] = new JsonObject(); + + var dictionary = new Dictionary() + { + { "One", 1 }, + { "array", new string[] { "a", "b" } }, + { "obj", new { } } + }; + + Assert.True(JsonNode.DeepEquals(jObject, JsonValue.Create(dictionary))); + + var diffDictionary = new Dictionary() + { + { "One", 1 }, + { "array", new string[] { "a", "d" } }, + { "obj", new { } } + }; + + Assert.False(JsonNode.DeepEquals(jObject, JsonValue.Create(diffDictionary))); + } + + private class SimpleClass + { + public int Id { get; set; } + + public string Name { get; set; } + + public SimpleClass NestedObject { get; set; } + } + + [Fact] + public static void DeepEqualFromElement() + { + using JsonDocument document = JsonDocument.Parse("{\"One\": 1, \"String\": \"abc\"}"); + JsonObject jObject = JsonObject.Create(document.RootElement); + + using JsonDocument document2 = JsonDocument.Parse("{\"One\": 1, \"String\": \"abc\"} "); + JsonObject jObject2 = JsonObject.Create(document2.RootElement); + Assert.True(JsonNode.DeepEquals(jObject, jObject2)); + + using JsonDocument document3 = JsonDocument.Parse("{\"One\": 3, \"String\": \"abc\"}"); + JsonObject jObject3 = JsonObject.Create(document3.RootElement); + Assert.False(JsonNode.DeepEquals(jObject, jObject3)); + + using JsonDocument document4 = JsonDocument.Parse("{\"One\": 1, \"String\": \"abc2\"} "); + JsonObject jObject4 = JsonObject.Create(document4.RootElement); + Assert.False(JsonNode.DeepEquals(jObject, jObject4)); + } + + [Fact] + public static void UpdateClonedObjectNotAffectOriginal() + { + var jObject = new JsonObject(); + jObject["One"] = 1; + jObject["Two"] = 2; + + var clone = jObject.DeepClone().AsObject(); + clone["One"] = 3; + + Assert.Equal(1, jObject["One"].GetValue()); + } + + [Fact] + public static void GetValueKind() + { + Assert.Equal(JsonValueKind.Object, new JsonObject().GetValueKind()); + } + + [Fact] + public static void GetPropertyName() + { + var jObject = new JsonObject(); + var jValue = JsonValue.Create(10); + jObject.Add("value", jValue); + + Assert.Equal("value", jValue.GetPropertyName()); + Assert.Equal("value", jObject["value"].GetPropertyName()); + } + + [Fact] + public static void ReplaceWith() + { + var jObject = new JsonObject(); + var jValue = JsonValue.Create(10); + jObject["value"] = jValue; + jObject["value"].ReplaceWith(5); + + Assert.Null(jValue.Parent); + Assert.Equal("{\"value\":5}", jObject.ToJsonString()); + } } } 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 61cd70b3a95ca3..47306ffd1d1ebd 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 @@ -3,6 +3,7 @@ using System.IO; using System.Text.Json.Serialization; +using System.Xml.Linq; using Xunit; namespace System.Text.Json.Nodes.Tests @@ -305,5 +306,129 @@ public static void WriteTo() string json = Encoding.UTF8.GetString(stream.ToArray()); Assert.Equal(Json, json); } + + [Fact] + public static void DeepCloneNotTrimmable() + { + var student = new Student() + { + Id = 1, + Name = "test" + }; + JsonValue jValue = JsonValue.Create(student); + + JsonNode clone = jValue.DeepClone(); + + Assert.True(JsonNode.DeepEquals(jValue, clone)); + + string originalJson = jValue.ToJsonString(); + string clonedJson = clone.ToJsonString(); + + Assert.Equal(originalJson, clonedJson); + } + + [Theory] + [InlineData("42")] + [InlineData("\"AB\"")] + [InlineData("\"\"")] + public static void DeepCloneTrimmable(string json) + { + using (JsonDocument document = JsonDocument.Parse(json)) + { + JsonValue jsonValue = JsonValue.Create(document.RootElement); + JsonNode clone = jsonValue.DeepClone(); + + Assert.True(JsonNode.DeepEquals(jsonValue, clone)); + string originalJson = jsonValue.ToJsonString(); + string clonedJson = clone.ToJsonString(); + + Assert.Equal(originalJson, clonedJson); + } + } + + [Fact] + public static void DeepEqualsComplexType() + { + var student = new Student() + { + Id = 10, + Name = "test" + }; + JsonValue jValue = JsonValue.Create(student); + + var jObject = new JsonObject(); + jObject.Add("Id", 10); + jObject.Add("Name", "test"); + + Assert.True(JsonNode.DeepEquals(jValue, jObject)); + Assert.True(JsonNode.DeepEquals(jObject, jValue)); + } + + [Fact] + public static void DeepEqualsPrimitiveType() + { + Assert.True(JsonNode.DeepEquals(JsonValue.Create(10), JsonValue.Create((uint)10))); + Assert.True(JsonNode.DeepEquals(JsonValue.Create(10), JsonValue.Create((ulong)10))); + Assert.True(JsonNode.DeepEquals(JsonValue.Create(10), JsonValue.Create((float)10))); + Assert.True(JsonNode.DeepEquals(JsonValue.Create(10), JsonValue.Create((decimal)10))); + Assert.True(JsonNode.DeepEquals(JsonValue.Create(10), JsonValue.Create((short)10))); + Assert.True(JsonNode.DeepEquals(JsonValue.Create(10), JsonValue.Create((ushort)10))); + Guid guid = Guid.NewGuid(); + Assert.True(JsonNode.DeepEquals(JsonValue.Create(guid), JsonValue.Create(guid.ToString()))); + Assert.False(JsonNode.DeepEquals(JsonValue.Create(10), JsonValue.Create("10"))); + } + + [Fact] + public static void DeepEqualsJsonElement() + { + JsonDocument document1 = JsonDocument.Parse("10"); + + JsonValue jsonValue1 = JsonValue.Create(document1.RootElement); + + Assert.True(JsonNode.DeepEquals(jsonValue1, JsonValue.Create(10))); + + JsonDocument document2 = JsonDocument.Parse("\"10\""); + + JsonValue jsonValue2 = JsonValue.Create(document2.RootElement); + Assert.False(JsonNode.DeepEquals(jsonValue1, jsonValue2)); + Assert.True(JsonNode.DeepEquals(jsonValue2, JsonValue.Create("10"))); + } + + [Fact] + public static void DeepEqualsJsonElement_Boolean() + { + JsonValue trueValue = JsonValue.Create(JsonDocument.Parse("true").RootElement); + JsonValue falseValue = JsonValue.Create(JsonDocument.Parse("false").RootElement); + + Assert.False(JsonNode.DeepEquals(trueValue, falseValue)); + Assert.True(JsonNode.DeepEquals(trueValue, trueValue.DeepClone())); + } + + [Fact] + public static void GetValueKind() + { + Assert.Equal(JsonValueKind.Object, JsonValue.Create(new Student()).GetValueKind()); + Assert.Equal(JsonValueKind.Array, JsonValue.Create(new Student[] { }).GetValueKind()); + + using (JsonDocument document = JsonDocument.Parse("10")) + { + JsonValue jsonValue = JsonValue.Create(document.RootElement); + Assert.Equal(JsonValueKind.Number, jsonValue.GetValueKind()); + } + } + + [Fact] + public static void DeepEquals_EscapedString() + { + JsonValue jsonValue = JsonValue.Create(JsonDocument.Parse("\"It\'s alright\"").RootElement); + JsonValue escapedJsonValue = JsonValue.Create(JsonDocument.Parse("\"It\u0027s alright\"").RootElement); + Assert.True(JsonNode.DeepEquals(escapedJsonValue, jsonValue)); + } + + private class Student + { + public int Id { get; set; } + public string Name { get; set; } + } } }