未验证 提交 a0c1fca8 编写于 作者: S Steve Harter 提交者: GitHub

Add JsonObject ordering and extension data support (#51717)

上级 7c19b1eb
......@@ -638,7 +638,7 @@ public sealed partial class JsonObject : System.Text.Json.Node.JsonNode, System.
bool System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<string, System.Text.Json.Node.JsonNode?>>.Contains(System.Collections.Generic.KeyValuePair<string, System.Text.Json.Node.JsonNode> item) { throw null; }
void System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<string, System.Text.Json.Node.JsonNode?>>.CopyTo(System.Collections.Generic.KeyValuePair<string, System.Text.Json.Node.JsonNode>[] array, int index) { }
bool System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<string, System.Text.Json.Node.JsonNode?>>.Remove(System.Collections.Generic.KeyValuePair<string, System.Text.Json.Node.JsonNode> item) { throw null; }
bool System.Collections.Generic.IDictionary<string, System.Text.Json.Node.JsonNode?>.TryGetValue(string propertyName, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Text.Json.Node.JsonNode jsonNode) { throw null; }
bool System.Collections.Generic.IDictionary<string, System.Text.Json.Node.JsonNode?>.TryGetValue(string propertyName, out System.Text.Json.Node.JsonNode? jsonNode) { throw null; }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
public bool TryGetPropertyValue(string propertyName, out System.Text.Json.Node.JsonNode? jsonNode) { throw null; }
public override void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.Text.Json.JsonSerializerOptions? options = null) { }
......
......@@ -349,7 +349,7 @@
<value>An item with the same property name '{0}' has already been added.</value>
</data>
<data name="SerializationDataExtensionPropertyInvalid" xml:space="preserve">
<value>The data extension property '{0}.{1}' does not match the required signature of IDictionary&lt;string, JsonElement&gt; or IDictionary&lt;string, object&gt;.</value>
<value>The data extension property '{0}.{1}' does not match the required signature of 'IDictionary&lt;string, JsonElement&gt;', 'IDictionary&lt;string, object&gt;' or 'JsonObject'.</value>
</data>
<data name="SerializationDuplicateTypeAttribute" xml:space="preserve">
<value>The type '{0}' cannot have more than one property that has the attribute '{1}'.</value>
......@@ -587,8 +587,8 @@
<data name="NodeWrongType" xml:space="preserve">
<value>The node must be of type '{0}'.</value>
</data>
<data name="ValueCannotBeNull" xml:space="preserve">
<value>Value cannot be null. (Parameter '{0}')</value>
<data name="NodeDuplicateKey" xml:space="preserve">
<value>An item with the same key has already been added. Key: {0}</value>
</data>
<data name="SerializerContextOptionsImmutable" xml:space="preserve">
<value>A 'JsonSerializerOptions' instance associated with a 'JsonSerializerContext' instance cannot be mutated once the context has been instantiated.</value>
......@@ -608,4 +608,16 @@
<data name="NoMetadataForType" xml:space="preserve">
<value>Metadata for type '{0}' was not provided to the serializer. The serializer method used does not support reflection-based creation of serialization-related type metadata. If using source generation, ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.</value>
</data>
<data name="NodeCollectionIsReadOnly" xml:space="preserve">
<value>Collection is read-only.</value>
</data>
<data name="NodeArrayIndexNegative" xml:space="preserve">
<value>Number was less than 0.</value>
</data>
<data name="NodeArrayTooSmall" xml:space="preserve">
<value>Destination array was not long enough.</value>
</data>
<data name="NodeJsonObjectCustomConverterNotAllowedOnExtensionProperty" xml:space="preserve">
<value>A custom converter for JsonObject is not allowed on an extension property.</value>
</data>
</root>
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFrameworks>$(NetCoreAppCurrent);netstandard2.0;netcoreapp3.0;net461</TargetFrameworks>
......@@ -58,6 +58,8 @@
<Compile Include="System\Text\Json\Node\JsonObject.cs" />
<Compile Include="System\Text\Json\Node\JsonObject.Dynamic.cs" />
<Compile Include="System\Text\Json\Node\JsonObject.IDictionary.cs" />
<Compile Include="System\Text\Json\Node\JsonObject.KeyCollection.cs" />
<Compile Include="System\Text\Json\Node\JsonObject.ValueCollection.cs" />
<Compile Include="System\Text\Json\Node\JsonValue.cs" />
<Compile Include="System\Text\Json\Node\JsonValueOfT.cs" />
<Compile Include="System\Text\Json\Node\MetaDynamic.cs" />
......
......@@ -124,6 +124,28 @@ public static string Utf8GetString(ReadOnlySpan<byte> bytes)
#endif
}
/// <summary>
/// Emulates Dictionary(IEnumerable{KeyValuePair}) on netstandard.
/// </summary>
public static Dictionary<TKey, TValue> CreateDictionaryFromCollection<TKey, TValue>(
IEnumerable<KeyValuePair<TKey, TValue>> collection,
IEqualityComparer<TKey> comparer)
where TKey : notnull
{
#if NETSTANDARD2_0 || NETFRAMEWORK
var dictionary = new Dictionary<TKey, TValue>(comparer);
foreach (KeyValuePair<TKey, TValue> item in collection)
{
dictionary.Add(item.Key, item.Value);
}
return dictionary;
#else
return new Dictionary<TKey, TValue>(collection: collection, comparer);
#endif
}
public static bool IsFinite(double value)
{
#if BUILDING_INBOX_LIBRARY
......
......@@ -258,7 +258,7 @@ public string Display
if (Value is JsonObject jsonObject)
{
return $"JsonObject[{jsonObject.Dictionary.Count}]";
return $"JsonObject[{jsonObject.Count}]";
}
JsonArray jsonArray = (JsonArray)Value;
......
......@@ -9,6 +9,8 @@ namespace System.Text.Json.Node
/// <summary>
/// The base class that represents a single node within a mutable JSON document.
/// </summary>
/// <seealso cref="JsonSerializerOptions.UnknownTypeHandling"/> to specify that a type
/// declared as an <see cref="object"/> should be deserialized as a <see cref="JsonNode"/>.
public abstract partial class JsonNode
{
private JsonNode? _parent;
......@@ -212,7 +214,6 @@ public JsonNode Root
{
return AsObject().GetItem(propertyName);
}
set
{
AsObject().SetItem(propertyName, value);
......
......@@ -8,9 +8,9 @@ namespace System.Text.Json.Node
{
public partial class JsonObject
{
internal bool TryGetMemberCallback(GetMemberBinder binder, out object? result)
private bool TryGetMemberCallback(GetMemberBinder binder, out object? result)
{
if (Dictionary.TryGetValue(binder.Name, out JsonNode? node))
if (TryGetPropertyValue(binder.Name, out JsonNode? node))
{
result = node;
return true;
......@@ -21,7 +21,7 @@ internal bool TryGetMemberCallback(GetMemberBinder binder, out object? result)
return true;
}
internal bool TrySetMemberCallback(SetMemberBinder binder, object? value)
private bool TrySetMemberCallback(SetMemberBinder binder, object? value)
{
JsonNode? node = null;
if (value != null)
......@@ -33,7 +33,7 @@ internal bool TrySetMemberCallback(SetMemberBinder binder, object? value)
}
}
Dictionary[binder.Name] = node;
this[binder.Name] = node;
return true;
}
......
......@@ -4,12 +4,24 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization.Converters;
namespace System.Text.Json.Node
{
public partial class JsonObject : IDictionary<string, JsonNode?>
{
private const int ListToDictionaryThreshold = 9;
private Dictionary<string, JsonNode?>? _dictionary;
private List<KeyValuePair<string, JsonNode?>>? _list;
/// We defer creating the comparer as long as possible in case no options were specified during creation.
/// In that case if later we are added to a parent with a non-null options, we use the parent options.
private StringComparer? _stringComparer;
private string? _lastKey;
private JsonNode? _lastValue;
/// <summary>
/// Adds an element with the provided property name and value to the <see cref="JsonObject"/>.
/// </summary>
......@@ -28,10 +40,8 @@ public void Add(string propertyName, JsonNode? value)
throw new ArgumentNullException(nameof(propertyName));
}
AddNode(propertyName, value);
value?.AssignParent(this);
Dictionary.Add(propertyName, value);
_lastKey = propertyName;
_lastValue = value;
}
/// <summary>
......@@ -40,21 +50,15 @@ public void Add(string propertyName, JsonNode? value)
/// <param name="property">
/// The KeyValuePair structure representing the property name and value to add to the <see cref="JsonObject"/>.
/// </param>
/// <exception cref="ArgumentException">
/// An element with the same property name already exists in the <see cref="JsonObject"/>.
/// </exception>
/// <exception cref="ArgumentNullException">
/// The property name of <paramref name="property"/> is <see langword="null"/>.
/// </exception>
public void Add(KeyValuePair<string, JsonNode?> property)
{
if (property.Key == null)
{
ThrowHelper.ThrowArgumentNullException_ValueCannotBeNull("propertyName");
}
JsonNode? value = property.Value;
value?.AssignParent(this);
Dictionary.Add(property);
_lastKey = property.Key;
_lastValue = value;
Add(property.Key, property.Value);
}
/// <summary>
......@@ -62,12 +66,22 @@ public void Add(KeyValuePair<string, JsonNode?> property)
/// </summary>
public void Clear()
{
foreach (JsonNode? node in Dictionary.Values)
if (_jsonElement != null)
{
Debug.Assert(_list == null);
Debug.Assert(_dictionary == null);
_jsonElement = null;
return;
}
foreach (JsonNode? node in GetValueCollection(this))
{
DetachParent(node);
}
Dictionary.Clear();
_list?.Clear();
_dictionary?.Clear();
ClearLastValueCache();
}
/// <summary>
......@@ -87,13 +101,21 @@ public bool ContainsKey(string propertyName)
throw new ArgumentNullException(nameof(propertyName));
}
return Dictionary.ContainsKey(propertyName);
return ContainsNode(propertyName);
}
/// <summary>
/// Gets the number of elements contained in <see cref="JsonObject"/>.
/// </summary>
public int Count => Dictionary.Count;
public int Count
{
get
{
CreateList();
Debug.Assert(_list != null);
return _list.Count;
}
}
/// <summary>
/// Removes the element with the specified property name from the <see cref="JsonObject"/>.
......@@ -112,26 +134,35 @@ public bool Remove(string propertyName)
throw new ArgumentNullException(nameof(propertyName));
}
if (!Dictionary.TryGetValue(propertyName, out JsonNode? item))
bool success = TryRemoveNode(propertyName, out JsonNode? removedNode);
if (success)
{
return false;
DetachParent(removedNode);
}
bool success = Dictionary.Remove(propertyName);
Debug.Assert(success);
DetachParent(item);
return true;
return success;
}
#region Explicit interface implementation
/// <summary>
/// Determines whether the <see cref="JsonObject"/> contains a specific property name and value.
/// Determines whether the <see cref="JsonObject"/> contains a specific property name and <see cref="JsonNode"/> reference.
/// </summary>
/// <param name="item">The element to locate in the <see cref="JsonObject"/>.</param>
/// <returns>
/// <see langword="true"/> if the <see cref="JsonObject"/> contains an element with the property name; otherwise, <see langword="false"/>.
/// </returns>
bool ICollection<KeyValuePair<string, JsonNode?>>.Contains(KeyValuePair<string, JsonNode?> item) => Dictionary.Contains(item);
bool ICollection<KeyValuePair<string, JsonNode?>>.Contains(KeyValuePair<string, JsonNode?> item)
{
foreach (KeyValuePair<string, JsonNode?> existing in this)
{
Debug.Assert(_stringComparer != null);
if (ReferenceEquals(item.Value, existing.Value) && _stringComparer.Equals(item.Key, existing.Key))
{
return true;
}
}
return false;
}
/// <summary>
/// Copies the elements of the <see cref="JsonObject"/> to an array of type KeyValuePair starting at the specified array index.
......@@ -150,8 +181,26 @@ public bool Remove(string propertyName)
/// The number of elements in the source ICollection is greater than the available space from <paramref name="index"/>
/// to the end of the destination <paramref name="array"/>.
/// </exception>
void ICollection<KeyValuePair<string, JsonNode?>>.CopyTo(KeyValuePair<string, JsonNode?>[] array, int index) =>
Dictionary.CopyTo(array, index);
void ICollection<KeyValuePair<string, JsonNode?>>.CopyTo(KeyValuePair<string, JsonNode?>[] array, int index)
{
if (index < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException_NodeArrayIndexNegative(nameof(index));
}
CreateList();
Debug.Assert(_list != null);
foreach (KeyValuePair<string, JsonNode?> item in _list)
{
if (index >= array.Length)
{
ThrowHelper.ThrowArgumentException_NodeArrayTooSmall(nameof(array));
}
array[index++] = item;
}
}
/// <summary>
/// Returns an enumerator that iterates through the <see cref="JsonObject"/>.
......@@ -159,7 +208,16 @@ public bool Remove(string propertyName)
/// <returns>
/// An enumerator that iterates through the <see cref="JsonObject"/>.
/// </returns>
public IEnumerator<KeyValuePair<string, JsonNode?>> GetEnumerator() => Dictionary.GetEnumerator();
public IEnumerator<KeyValuePair<string, JsonNode?>> GetEnumerator()
{
CreateList();
Debug.Assert(_list != null);
foreach (KeyValuePair<string, JsonNode?> item in _list)
{
yield return item;
}
}
/// <summary>
/// Removes a key and value from the <see cref="JsonObject"/>.
......@@ -170,27 +228,17 @@ public bool Remove(string propertyName)
/// <returns>
/// <see langword="true"/> if the element is successfully removed; otherwise, <see langword="false"/>.
/// </returns>
bool ICollection<KeyValuePair<string, JsonNode?>>.Remove(KeyValuePair<string, JsonNode?> item)
{
if (Dictionary.Remove(item))
{
JsonNode? node = item.Value;
DetachParent(node);
return true;
}
return false;
}
bool ICollection<KeyValuePair<string, JsonNode?>>.Remove(KeyValuePair<string, JsonNode?> item) => Remove(item.Key);
/// <summary>
/// Gets a collection containing the property names in the <see cref="JsonObject"/>.
/// </summary>
ICollection<string> IDictionary<string, JsonNode?>.Keys => Dictionary.Keys;
ICollection<string> IDictionary<string, JsonNode?>.Keys => GetKeyCollection(this);
/// <summary>
/// Gets a collection containing the property values in the <see cref="JsonObject"/>.
/// </summary>
ICollection<JsonNode?> IDictionary<string, JsonNode?>.Values => Dictionary.Values;
ICollection<JsonNode?> IDictionary<string, JsonNode?>.Values => GetValueCollection(this);
/// <summary>
/// Gets the value associated with the specified property name.
......@@ -206,14 +254,53 @@ public bool Remove(string propertyName)
/// <exception cref="ArgumentNullException">
/// <paramref name="propertyName"/> is <see langword="null"/>.
/// </exception>
bool IDictionary<string, JsonNode?>.TryGetValue(string propertyName, [NotNullWhen(true)] out JsonNode? jsonNode)
bool IDictionary<string, JsonNode?>.TryGetValue(string propertyName, out JsonNode? jsonNode)
{
if (propertyName == null)
{
throw new ArgumentNullException(nameof(propertyName));
}
return Dictionary.TryGetValue(propertyName, out jsonNode);
CreateList();
Debug.Assert(_list != null);
Debug.Assert(_stringComparer != null);
if (propertyName == _lastKey)
{
// Optimize for repeating sections in code:
// obj.Foo.Bar.FirstProperty = value1;
// obj.Foo.Bar.SecondProperty = value2;
jsonNode = _lastValue;
return true;
}
if (_dictionary != null)
{
bool success = _dictionary.TryGetValue(propertyName, out jsonNode);
if (success)
{
_lastKey = propertyName;
_lastValue = jsonNode;
}
return success;
}
else
{
foreach (KeyValuePair<string, JsonNode?> item in _list)
{
if (_stringComparer.Equals(propertyName, item.Key))
{
jsonNode = item.Value;
_lastKey = propertyName;
_lastValue = jsonNode;
return true;
}
}
}
jsonNode = null;
return false;
}
/// <summary>
......@@ -227,20 +314,273 @@ public bool Remove(string propertyName)
/// <returns>
/// An enumerator that iterates through the <see cref="JsonObject"/>.
/// </returns>
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Dictionary).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
{
CreateList();
Debug.Assert(_list != null);
#endregion
foreach (KeyValuePair<string, JsonNode?> item in _list)
{
yield return item;
}
}
private void DetachParent(JsonNode? item)
private void CreateStringComparer()
{
if (item != null)
bool caseInsensitive = Options?.PropertyNameCaseInsensitive == true;
if (caseInsensitive)
{
item.Parent = null;
_stringComparer = StringComparer.OrdinalIgnoreCase;
}
else
{
_stringComparer = StringComparer.Ordinal;
}
}
private void AddNode(string propertyName, JsonNode? node)
{
CreateList();
Debug.Assert(_list != null);
CreateDictionaryIfThreshold();
if (_dictionary == null)
{
// Verify there are no duplicates before adding.
VerifyListItemMissing(propertyName);
}
else
{
_dictionary.Add(propertyName, node);
}
_list.Add(new KeyValuePair<string, JsonNode?>(propertyName, node));
}
// Prevent previous child from being returned from these cached variables.
private void ClearLastValueCache()
{
_lastKey = null;
_lastValue = null;
}
private void CreateList()
{
if (_list != null)
{
return;
}
CreateStringComparer();
var list = new List<KeyValuePair<string, JsonNode?>>();
if (_jsonElement.HasValue)
{
JsonElement jElement = _jsonElement.Value;
foreach (JsonProperty jElementProperty in jElement.EnumerateObject())
{
JsonNode? node = JsonNodeConverter.Create(jElementProperty.Value, Options);
if (node != null)
{
node.Parent = this;
}
list.Add(new KeyValuePair<string, JsonNode?>(jElementProperty.Name, node));
}
_jsonElement = null;
}
_list = list;
CreateDictionaryIfThreshold();
}
private JsonNode? SetNode(string propertyName, JsonNode? node)
{
CreateList();
Debug.Assert(_list != null);
CreateDictionaryIfThreshold();
JsonNode? existing = null;
if (_dictionary != null)
{
// Fast path if item doesn't exist in dictionary.
if (JsonHelpers.TryAdd(_dictionary, propertyName, node))
{
node?.AssignParent(this);
_list.Add(new KeyValuePair<string, JsonNode?>(propertyName, node));
return null;
}
existing = _dictionary[propertyName];
if (ReferenceEquals(existing, node))
{
_lastKey = propertyName;
_lastValue = node;
// Ignore if the same value.
return null;
}
}
int i = FindNodeIndex(propertyName);
if (i >= 0)
{
if (_dictionary != null)
{
_dictionary[propertyName] = node;
}
else
{
KeyValuePair<string, JsonNode?> current = _list[i];
if (ReferenceEquals(current.Value, node))
{
// Ignore if the same value.
return null;
}
existing = current.Value;
}
node?.AssignParent(this);
_list[i] = new KeyValuePair<string, JsonNode?>(propertyName, node);
}
else
{
node?.AssignParent(this);
_dictionary?.Add(propertyName, node);
_list.Add(new KeyValuePair<string, JsonNode?>(propertyName, node));
Debug.Assert(existing == null);
}
_lastKey = propertyName;
_lastValue = node;
return existing;
}
private int FindNodeIndex(string propertyName)
{
Debug.Assert(_list != null);
Debug.Assert(_stringComparer != null);
for (int i = 0; i < _list.Count; i++)
{
KeyValuePair<string, JsonNode?> current = _list[i];
if (_stringComparer.Equals(propertyName, current.Key))
{
return i;
}
}
return -1;
}
private void CreateDictionaryIfThreshold()
{
Debug.Assert(_list != null);
Debug.Assert(_stringComparer != null);
if (_dictionary == null && _list.Count > ListToDictionaryThreshold)
{
_dictionary = JsonHelpers.CreateDictionaryFromCollection(_list, _stringComparer);
}
}
private bool ContainsNode(JsonNode? node)
{
CreateList();
foreach (JsonNode? item in GetValueCollection(this))
{
if (ReferenceEquals(item, node))
{
return true;
}
}
return false;
}
private KeyValuePair<string, JsonNode?>? FindNode(JsonNode? node)
{
CreateList();
foreach (KeyValuePair<string, JsonNode?> item in this)
{
if (ReferenceEquals(item.Value, node))
{
return item;
}
}
return null;
}
private bool ContainsNode(string propertyName)
{
if (_dictionary != null)
{
return _dictionary.ContainsKey(propertyName);
}
foreach (string item in GetKeyCollection(this))
{
Debug.Assert(_stringComparer != null);
if (_stringComparer.Equals(item, propertyName))
{
return true;
}
}
return false;
}
private bool TryRemoveNode(string propertyName, out JsonNode? existing)
{
CreateList();
Debug.Assert(_list != null);
Debug.Assert(_stringComparer != null);
if (_dictionary != null)
{
if (!_dictionary.TryGetValue(propertyName, out existing))
{
return false;
}
bool success = _dictionary.Remove(propertyName);
Debug.Assert(success);
}
for (int i = 0; i < _list.Count; i++)
{
KeyValuePair<string, JsonNode?> current = _list[i];
if (_stringComparer.Equals(current.Key, propertyName))
{
_list.RemoveAt(i);
existing = current.Value;
DetachParent(existing);
return true;
}
}
existing = null;
return false;
}
private void VerifyListItemMissing(string propertyName)
{
Debug.Assert(_dictionary == null);
Debug.Assert(_list != null);
if (ContainsNode(propertyName))
{
ThrowHelper.ThrowArgumentException_DuplicateKey(propertyName);
}
}
}
}
// 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;
using System.Collections.Generic;
namespace System.Text.Json.Node
{
public partial class JsonObject
{
private KeyCollection? _keyCollection;
private KeyCollection GetKeyCollection(JsonObject jsonObject)
{
CreateList();
return _keyCollection ??= new KeyCollection(jsonObject);
}
private sealed class KeyCollection : ICollection<string>
{
private readonly JsonObject _jObject;
public KeyCollection(JsonObject jsonObject)
{
_jObject = jsonObject;
}
public int Count => _jObject.Count;
public bool IsReadOnly => true;
IEnumerator IEnumerable.GetEnumerator()
{
foreach (KeyValuePair<string, JsonNode?> item in _jObject)
{
yield return item.Key;
}
}
public void Add(string propertyName) => throw ThrowHelper.NotSupportedException_NodeCollectionIsReadOnly();
public void Clear() => throw ThrowHelper.NotSupportedException_NodeCollectionIsReadOnly();
public bool Contains(string propertyName) => _jObject.ContainsNode(propertyName);
public void CopyTo(string[] propertyNameArray, int index)
{
if (index < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException_NodeArrayIndexNegative(nameof(index));
}
foreach (KeyValuePair<string, JsonNode?> item in _jObject)
{
if (index >= propertyNameArray.Length)
{
ThrowHelper.ThrowArgumentException_NodeArrayTooSmall(nameof(propertyNameArray));
}
propertyNameArray[index++] = item.Key;
}
}
public IEnumerator<string> GetEnumerator()
{
foreach (KeyValuePair<string, JsonNode?> item in _jObject)
{
yield return item.Key;
}
}
bool ICollection<string>.Remove(string propertyName) => throw ThrowHelper.NotSupportedException_NodeCollectionIsReadOnly();
}
}
}
// 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;
using System.Collections.Generic;
namespace System.Text.Json.Node
{
public partial class JsonObject
{
private ValueCollection? _valueCollection;
private ValueCollection GetValueCollection(JsonObject jsonObject)
{
CreateList();
return _valueCollection ??= new ValueCollection(jsonObject);
}
private sealed class ValueCollection : ICollection<JsonNode?>
{
private readonly JsonObject _jObject;
public ValueCollection(JsonObject jsonObject)
{
_jObject = jsonObject;
}
public int Count => _jObject.Count;
public bool IsReadOnly => true;
IEnumerator IEnumerable.GetEnumerator()
{
foreach (KeyValuePair<string, JsonNode?> item in _jObject)
{
yield return item.Value;
}
}
public void Add(JsonNode? jsonNode) => throw ThrowHelper.NotSupportedException_NodeCollectionIsReadOnly();
public void Clear() => throw ThrowHelper.NotSupportedException_NodeCollectionIsReadOnly();
public bool Contains(JsonNode? jsonNode) => _jObject.ContainsNode(jsonNode);
public void CopyTo(JsonNode?[] nodeArray, int index)
{
if (index < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException_NodeArrayIndexNegative(nameof(index));
}
foreach (KeyValuePair<string, JsonNode?> item in _jObject)
{
if (index >= nodeArray.Length)
{
ThrowHelper.ThrowArgumentException_NodeArrayTooSmall(nameof(nodeArray));
}
nodeArray[index++] = item.Value;
}
}
public IEnumerator<JsonNode?> GetEnumerator()
{
foreach (KeyValuePair<string, JsonNode?> item in _jObject)
{
yield return item.Value;
}
}
bool ICollection<JsonNode?>.Remove(JsonNode? node) => throw ThrowHelper.NotSupportedException_NodeCollectionIsReadOnly();
}
}
}
......@@ -11,14 +11,11 @@ namespace System.Text.Json.Node
/// <summary>
/// Represents a mutable JSON object.
/// </summary>
[DebuggerDisplay("JsonObject[{Dictionary.Count}]")]
[DebuggerDisplay("JsonObject[{Count}]")]
[DebuggerTypeProxy(typeof(DebugView))]
public sealed partial class JsonObject : JsonNode
{
private JsonElement? _jsonElement;
private IDictionary<string, JsonNode?>? _dictionary;
private string? _lastKey;
private JsonNode? _lastValue;
/// <summary>
/// Initializes a new instance of the <see cref="JsonObject"/> class that is empty.
......@@ -63,6 +60,12 @@ public JsonObject(IEnumerable<KeyValuePair<string, JsonNode?>> properties, JsonN
throw new InvalidOperationException(SR.Format(SR.NodeElementWrongType, nameof(JsonValueKind.Object)));
}
internal JsonObject(JsonElement element, JsonNodeOptions? options = null) : base(options)
{
Debug.Assert(element.ValueKind == JsonValueKind.Object);
_jsonElement = element;
}
/// <summary>
/// Returns the value of a property with the specified name.
/// </summary>
......@@ -71,22 +74,8 @@ public JsonObject(IEnumerable<KeyValuePair<string, JsonNode?>> properties, JsonN
/// <returns>
/// <see langword="true"/> if a property with the specified name was found; otherwise, <see langword="false"/>.
/// </returns>
public bool TryGetPropertyValue(string propertyName, out JsonNode? jsonNode)
{
if (propertyName == _lastKey)
{
// Optimize for repeating sections in code:
// obj.Foo.Bar.One
// obj.Foo.Bar.Two
jsonNode = _lastValue;
return true;
}
bool rc = Dictionary.TryGetValue(propertyName, out jsonNode);
_lastKey = propertyName;
_lastValue = jsonNode;
return rc;
}
public bool TryGetPropertyValue(string propertyName, out JsonNode? jsonNode) =>
((IDictionary<string, JsonNode?>)this).TryGetValue(propertyName, out jsonNode);
/// <inheritdoc/>
public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null)
......@@ -98,6 +87,7 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio
if (_jsonElement.HasValue)
{
// Write the element without converting to nodes.
_jsonElement.Value.WriteTo(writer);
}
else
......@@ -106,32 +96,16 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio
writer.WriteStartObject();
foreach (KeyValuePair<string, JsonNode?> kvp in Dictionary)
foreach (KeyValuePair<string, JsonNode?> item in this)
{
writer.WritePropertyName(kvp.Key);
JsonNodeConverter.Instance.Write(writer, kvp.Value!, options);
writer.WritePropertyName(item.Key);
JsonNodeConverter.Instance.Write(writer, item.Value, options);
}
writer.WriteEndObject();
}
}
internal JsonObject(JsonElement element, JsonNodeOptions? options = null) : base(options)
{
Debug.Assert(element.ValueKind == JsonValueKind.Object);
_jsonElement = element;
}
internal IDictionary<string, JsonNode?> Dictionary
{
get
{
CreateNodes();
Debug.Assert(_dictionary != null);
return _dictionary;
}
}
internal JsonNode? GetItem(string propertyName)
{
if (TryGetPropertyValue(propertyName, out JsonNode? value))
......@@ -147,28 +121,15 @@ internal override void GetPath(List<string> path, JsonNode? child)
{
if (child != null)
{
bool found = false;
foreach (KeyValuePair<string, JsonNode?> kvp in Dictionary)
string propertyName = FindNode(child)!.Value.Key;
if (propertyName.IndexOfAny(ReadStack.SpecialCharacters) != -1)
{
if (kvp.Value == child)
{
string propertyName = kvp.Key;
if (propertyName.IndexOfAny(ReadStack.SpecialCharacters) != -1)
{
path.Add($"['{propertyName}']");
}
else
{
path.Add($".{propertyName}");
}
found = true;
break;
}
path.Add($"['{propertyName}']");
}
else
{
path.Add($".{propertyName}");
}
Debug.Assert(found);
}
if (Parent != null)
......@@ -184,40 +145,19 @@ internal void SetItem(string propertyName, JsonNode? value)
throw new ArgumentNullException(nameof(propertyName));
}
// Do a Remove+Add instead of SetItem to unparent existing value (if any).
Remove(propertyName);
Add(propertyName, value);
JsonNode? existing = SetNode(propertyName, value);
DetachParent(existing);
}
private void CreateNodes()
private void DetachParent(JsonNode? item)
{
if (_dictionary == null)
if (item != null)
{
bool caseInsensitive = Options?.PropertyNameCaseInsensitive == true;
var dictionary = new Dictionary<string, JsonNode?>(
caseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
if (_jsonElement != null)
{
JsonElement jElement = _jsonElement.Value;
foreach (JsonProperty property in jElement.EnumerateObject())
{
JsonNode? node = JsonNodeConverter.Create(property.Value, Options);
if (node != null)
{
node.Parent = this;
}
dictionary.Add(property.Name, node);
}
// Clear since no longer needed.
_jsonElement = null;
}
_dictionary = dictionary;
item.Parent = null;
}
// Prevent previous child from being returned from these cached variables.
ClearLastValueCache();
}
[ExcludeFromCodeCoverage] // Justification = "Design-time"
......@@ -239,13 +179,13 @@ private DebugViewProperty[] Items
{
get
{
DebugViewProperty[] properties = new DebugViewProperty[_node.Dictionary.Count];
DebugViewProperty[] properties = new DebugViewProperty[_node.Count];
int i = 0;
foreach (KeyValuePair<string, JsonNode?> property in _node.Dictionary)
foreach (KeyValuePair<string, JsonNode?> item in _node)
{
properties[i].Value = property.Value;
properties[i].PropertyName = property.Key;
properties[i].PropertyName = item.Key;
properties[i].Value = item.Value;
i++;
}
......@@ -279,11 +219,11 @@ public string Display
if (Value is JsonObject jsonObject)
{
return $"{PropertyName} = JsonObject[{jsonObject.Dictionary.Count}]";
return $"{PropertyName} = JsonObject[{jsonObject.Count}]";
}
JsonArray jsonArray = (JsonArray)Value;
return $"{PropertyName} = JsonArray[{jsonArray.List.Count}]";
return $"{PropertyName} = JsonArray[{jsonArray.Count}]";
}
}
......
......@@ -4,14 +4,20 @@
namespace System.Text.Json.Serialization
{
/// <summary>
/// When placed on a property or field of type <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, any
/// properties that do not have a matching property or field are added to that Dictionary during deserialization and written during serialization.
/// When placed on a property or field of type <see cref="System.Text.Json.Node.JsonObject"/> or
/// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, any properties that do not have a
/// matching property or field are added during deserialization and written during serialization.
/// </summary>
/// <remarks>
/// The TKey value must be <see cref="string"/> and TValue must be <see cref="JsonElement"/> or <see cref="object"/>.
/// When using <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, the TKey value must be <see cref="string"/>
/// and TValue must be <see cref="JsonElement"/> or <see cref="object"/>.
///
/// During deserializing, when using <see cref="object"/> a "null" JSON value is treated as a <c>null</c> object reference, and when using
/// <see cref="JsonElement"/> a "null" is treated as a JsonElement with <see cref="JsonElement.ValueKind"/> set to <see cref="JsonValueKind.Null"/>.
/// During deserializing with a <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/> extension property with TValue as
/// <see cref="object"/>, the type of object created will either be a <see cref="System.Text.Json.Node.JsonNode"/> or a
/// <see cref="JsonElement"/> depending on the value of <see cref="System.Text.Json.JsonSerializerOptions.UnknownTypeHandling"/>.
///
/// If a <see cref="JsonElement"/> is created, a "null" JSON value is treated as a JsonElement with <see cref="JsonElement.ValueKind"/>
/// set to <see cref="JsonValueKind.Null"/>, otherwise a "null" JSON value is treated as a <c>null</c> object reference.
///
/// During serializing, the name of the extension data member is not included in the JSON;
/// the data contained within the extension data is serialized as properties of the JSON object.
......
......@@ -6,7 +6,7 @@
namespace System.Text.Json.Serialization.Converters
{
internal class JsonArrayConverter : JsonConverter<JsonArray>
internal sealed class JsonArrayConverter : JsonConverter<JsonArray>
{
public override void Write(Utf8JsonWriter writer, JsonArray value, JsonSerializerOptions options)
{
......@@ -20,8 +20,6 @@ public override void Write(Utf8JsonWriter writer, JsonArray value, JsonSerialize
{
case JsonTokenType.StartArray:
return ReadList(ref reader, options.GetNodeOptions());
case JsonTokenType.Null:
return null;
default:
Debug.Assert(false);
throw ThrowHelper.GetInvalidOperationException_ExpectedArray(reader.TokenType);
......
......@@ -6,14 +6,23 @@
namespace System.Text.Json.Serialization.Converters
{
internal class JsonNodeConverter : JsonConverter<object>
/// <summary>
/// Converter for JsonNode-derived types. The {T} value must be Object and not JsonNode
/// since we allow Object-declared members\variables to deserialize as {JsonNode}.
/// </summary>
internal sealed class JsonNodeConverter : JsonConverter<object?>
{
public static JsonNodeConverter Instance { get; } = new JsonNodeConverter();
public JsonArrayConverter ArrayConverter { get; } = new JsonArrayConverter();
public JsonObjectConverter ObjectConverter { get; } = new JsonObjectConverter();
public JsonValueConverter ValueConverter { get; } = new JsonValueConverter();
private static JsonNodeConverter? s_nodeConverter;
private static JsonArrayConverter? s_arrayConverter;
private static JsonObjectConverter? s_objectConverter;
private static JsonValueConverter? s_valueConverter;
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
public static JsonNodeConverter Instance => s_nodeConverter ??= new JsonNodeConverter();
public static JsonArrayConverter ArrayConverter => s_arrayConverter ??= new JsonArrayConverter();
public static JsonObjectConverter ObjectConverter => s_objectConverter ??= new JsonObjectConverter();
public static JsonValueConverter ValueConverter => s_valueConverter ??= new JsonValueConverter();
public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
if (value == null)
{
......@@ -41,8 +50,6 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return null;
case JsonTokenType.String:
case JsonTokenType.False:
case JsonTokenType.True:
......
......@@ -11,30 +11,19 @@ internal sealed class JsonNodeConverterFactory : JsonConverterFactory
{
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (JsonTypeInfo.ObjectType == typeToConvert)
{
if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode)
{
return JsonNodeConverter.Instance;
}
// Return the converter for System.Object which uses JsonElement.
return JsonMetadataServices.ObjectConverter;
}
if (typeof(JsonValue).IsAssignableFrom(typeToConvert))
{
return JsonNodeConverter.Instance.ValueConverter;
return JsonNodeConverter.ValueConverter;
}
if (typeof(JsonObject) == typeToConvert)
{
return JsonNodeConverter.Instance.ObjectConverter;
return JsonNodeConverter.ObjectConverter;
}
if (typeof(JsonArray) == typeToConvert)
{
return JsonNodeConverter.Instance.ArrayConverter;
return JsonNodeConverter.ArrayConverter;
}
Debug.Assert(typeof(JsonNode) == typeToConvert);
......@@ -42,7 +31,7 @@ internal sealed class JsonNodeConverterFactory : JsonConverterFactory
}
public override bool CanConvert(Type typeToConvert) =>
typeToConvert == JsonTypeInfo.ObjectType ||
typeToConvert != JsonTypeInfo.ObjectType &&
typeof(JsonNode).IsAssignableFrom(typeToConvert);
}
}
......@@ -6,8 +6,32 @@
namespace System.Text.Json.Serialization.Converters
{
internal class JsonObjectConverter : JsonConverter<JsonObject>
internal sealed class JsonObjectConverter : JsonConverter<JsonObject>
{
internal override object CreateObject(JsonSerializerOptions options)
{
return new JsonObject(options.GetNodeOptions());
}
internal override void ReadElementAndSetProperty(
object obj,
string propertyName,
ref Utf8JsonReader reader,
JsonSerializerOptions options,
ref ReadStack state)
{
bool success = JsonNodeConverter.Instance.TryRead(ref reader, typeof(JsonNode), options, ref state, out object? value);
Debug.Assert(success); // Node converters are not resumable.
Debug.Assert(obj is JsonObject);
JsonObject jObject = (JsonObject)obj;
Debug.Assert(value == null || value is JsonNode);
JsonNode? jNodeValue = (JsonNode?)value;
jObject[propertyName] = jNodeValue;
}
public override void Write(Utf8JsonWriter writer, JsonObject value, JsonSerializerOptions options)
{
Debug.Assert(value != null);
......@@ -20,8 +44,6 @@ public override void Write(Utf8JsonWriter writer, JsonObject value, JsonSerializ
{
case JsonTokenType.StartObject:
return ReadObject(ref reader, options.GetNodeOptions());
case JsonTokenType.Null:
return null;
default:
Debug.Assert(false);
throw ThrowHelper.GetInvalidOperationException_ExpectedObject(reader.TokenType);
......
......@@ -6,7 +6,7 @@
namespace System.Text.Json.Serialization.Converters
{
internal class JsonValueConverter : JsonConverter<JsonValue>
internal sealed class JsonValueConverter : JsonConverter<JsonValue>
{
public override void Write(Utf8JsonWriter writer, JsonValue value, JsonSerializerOptions options)
{
......
......@@ -54,6 +54,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
obj,
unescapedPropertyName,
ref state,
options,
out bool useExtensionProperty);
ReadPropertyValue(obj, ref state, ref reader, jsonPropertyInfo, useExtensionProperty);
......@@ -150,6 +151,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
obj,
unescapedPropertyName,
ref state,
options,
out bool useExtensionProperty);
state.Current.UseExtensionProperty = useExtensionProperty;
......
......@@ -65,7 +65,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
{
Debug.Assert(jsonPropertyInfo == state.Current.JsonTypeInfo.DataExtensionProperty);
state.Current.JsonPropertyNameAsString = dataExtKey;
JsonSerializer.CreateDataExtensionProperty(obj, jsonPropertyInfo);
JsonSerializer.CreateDataExtensionProperty(obj, jsonPropertyInfo, options);
}
ReadPropertyValue(obj, ref state, ref tempReader, jsonPropertyInfo, useExtensionProperty);
......@@ -109,7 +109,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
{
Debug.Assert(jsonPropertyInfo == state.Current.JsonTypeInfo.DataExtensionProperty);
JsonSerializer.CreateDataExtensionProperty(obj, jsonPropertyInfo);
JsonSerializer.CreateDataExtensionProperty(obj, jsonPropertyInfo, options);
object extDictionary = jsonPropertyInfo.GetValueAsObject(obj)!;
if (extDictionary is IDictionary<string, JsonElement> dict)
......@@ -199,6 +199,7 @@ private void ReadConstructorArguments(ref ReadStack state, ref Utf8JsonReader re
obj: null!,
unescapedPropertyName,
ref state,
options,
out _,
createExtensionProperty: false);
......@@ -289,6 +290,7 @@ private bool ReadConstructorArgumentsWithContinuation(ref ReadStack state, ref U
obj: null!,
unescapedPropertyName,
ref state,
options,
out bool useExtensionProperty,
createExtensionProperty: false);
......
......@@ -30,7 +30,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, byte value, JsonSe
writer.WritePropertyName(value);
}
internal override byte ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override byte ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String && (JsonNumberHandling.AllowReadingFromString & handling) != 0)
{
......
......@@ -30,7 +30,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, decimal value, Jso
writer.WritePropertyName(value);
}
internal override decimal ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override decimal ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String &&
(JsonNumberHandling.AllowReadingFromString & handling) != 0)
......
......@@ -30,7 +30,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, double value, Json
writer.WritePropertyName(value);
}
internal override double ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override double ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
......
......@@ -31,7 +31,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, short value, JsonS
writer.WritePropertyName(value);
}
internal override short ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override short ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String &&
(JsonNumberHandling.AllowReadingFromString & handling) != 0)
......
......@@ -31,7 +31,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, int value, JsonSer
writer.WritePropertyName(value);
}
internal override int ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override int ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String &&
(JsonNumberHandling.AllowReadingFromString & handling) != 0)
......
......@@ -30,7 +30,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, long value, JsonSe
writer.WritePropertyName(value);
}
internal override long ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override long ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String &&
(JsonNumberHandling.AllowReadingFromString & handling) != 0)
......
......@@ -44,7 +44,7 @@ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOption
}
}
internal override T? ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling numberHandling)
internal override T? ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling numberHandling, JsonSerializerOptions options)
{
// We do not check _converter.HandleNull, as the underlying struct cannot be null.
// A custom converter for some type T? can handle null.
......@@ -53,7 +53,7 @@ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOption
return null;
}
T value = _converter.ReadNumberWithCustomHandling(ref reader, numberHandling);
T value = _converter.ReadNumberWithCustomHandling(ref reader, numberHandling, options);
return value;
}
......
// 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;
namespace System.Text.Json.Serialization.Converters
{
internal sealed class ObjectConverter : JsonConverter<object>
internal sealed class ObjectConverter : JsonConverter<object?>
{
public ObjectConverter()
{
IsInternalConverterForNumberType = true;
}
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonElement.ParseValue(ref reader);
if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement)
{
return JsonElement.ParseValue(ref reader);
}
return JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options);
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
throw new InvalidOperationException();
}
......@@ -26,8 +33,11 @@ internal override object ReadWithQuotes(ref Utf8JsonReader reader)
return null!;
}
internal override void WriteWithQuotes(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state)
internal override void WriteWithQuotes(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state)
{
// This converter does not handle nulls.
Debug.Assert(value != null);
Type runtimeType = value.GetType();
JsonConverter runtimeConverter = options.GetConverter(runtimeType);
if (runtimeConverter == this)
......@@ -38,9 +48,14 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, object value, Json
runtimeConverter.WriteWithQuotesAsObject(writer, value, options, ref state);
}
internal override object ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override object? ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
return JsonElement.ParseValue(ref reader);
if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement)
{
return JsonElement.ParseValue(ref reader);
}
return JsonNodeConverter.Instance.Read(ref reader, typeof(object), options);
}
}
}
......@@ -30,7 +30,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, sbyte value, JsonS
writer.WritePropertyName(value);
}
internal override sbyte ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override sbyte ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String &&
(JsonNumberHandling.AllowReadingFromString & handling) != 0)
......
......@@ -31,7 +31,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, float value, JsonS
writer.WritePropertyName(value);
}
internal override float ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override float ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
......
......@@ -31,7 +31,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, ushort value, Json
writer.WritePropertyName(value);
}
internal override ushort ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override ushort ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String &&
(JsonNumberHandling.AllowReadingFromString & handling) != 0)
......
......@@ -31,7 +31,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, uint value, JsonSe
writer.WritePropertyName(value);
}
internal override uint ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override uint ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String &&
(JsonNumberHandling.AllowReadingFromString & handling) != 0)
......
......@@ -30,7 +30,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, ulong value, JsonS
writer.WritePropertyName(value);
}
internal override ulong ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal override ulong ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String &&
(JsonNumberHandling.AllowReadingFromString & handling) != 0)
......
......@@ -34,6 +34,27 @@ public abstract partial class JsonConverter
internal bool CanBePolymorphic { get; set; }
/// <summary>
/// Used to support JsonObject as an extension property in a loosely-typed, trimmable manner.
/// </summary>
internal virtual object CreateObject(JsonSerializerOptions options)
{
throw new InvalidOperationException(SR.NodeJsonObjectCustomConverterNotAllowedOnExtensionProperty);
}
/// <summary>
/// Used to support JsonObject as an extension property in a loosely-typed, trimmable manner.
/// </summary>
internal virtual void ReadElementAndSetProperty(
object obj,
string propertyName,
ref Utf8JsonReader reader,
JsonSerializerOptions options,
ref ReadStack state)
{
throw new InvalidOperationException(SR.NodeJsonObjectCustomConverterNotAllowedOnExtensionProperty);
}
internal abstract JsonPropertyInfo CreateJsonPropertyInfo();
internal abstract JsonParameterInfo CreateJsonParameterInfo();
......
......@@ -168,7 +168,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali
{
if (state.Current.NumberHandling != null)
{
value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value);
value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value, options);
}
else
{
......@@ -184,7 +184,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali
if (state.Current.NumberHandling != null)
{
value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value);
value = ReadNumberWithCustomHandling(ref reader, state.Current.NumberHandling.Value, options);
}
else
{
......@@ -443,15 +443,19 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json
return TryWrite(writer, value, options, ref state);
}
Debug.Assert(this is JsonDictionaryConverter<T>);
if (!(this is JsonDictionaryConverter<T> dictionaryConverter))
{
// If not JsonDictionaryConverter<T> then we are JsonObject.
// Avoid a type reference to JsonObject and its converter to support trimming.
Debug.Assert(TypeToConvert == typeof(Node.JsonObject));
return TryWrite(writer, value, options, ref state);
}
if (writer.CurrentDepth >= options.EffectiveMaxDepth)
{
ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth);
}
JsonDictionaryConverter<T> dictionaryConverter = (JsonDictionaryConverter<T>)this;
bool isContinuation = state.IsContinuation;
bool success;
......@@ -554,7 +558,7 @@ internal virtual void WriteWithQuotes(Utf8JsonWriter writer, [DisallowNull] T va
internal sealed override void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state)
=> WriteWithQuotes(writer, (T)value, options, ref state);
internal virtual T ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling)
internal virtual T ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling handling, JsonSerializerOptions options)
=> throw new InvalidOperationException();
internal virtual void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T value, JsonNumberHandling handling)
......
......@@ -19,6 +19,7 @@ public static partial class JsonSerializer
object obj,
ReadOnlySpan<byte> unescapedPropertyName,
ref ReadStack state,
JsonSerializerOptions options,
out bool useExtensionProperty,
bool createExtensionProperty = true)
{
......@@ -47,7 +48,7 @@ public static partial class JsonSerializer
if (createExtensionProperty)
{
CreateDataExtensionProperty(obj, dataExtProperty);
CreateDataExtensionProperty(obj, dataExtProperty, options);
}
jsonPropertyInfo = dataExtProperty;
......@@ -93,7 +94,8 @@ public static partial class JsonSerializer
internal static void CreateDataExtensionProperty(
object obj,
JsonPropertyInfo jsonPropertyInfo)
JsonPropertyInfo jsonPropertyInfo,
JsonSerializerOptions options)
{
Debug.Assert(jsonPropertyInfo != null);
......@@ -110,14 +112,26 @@ public static partial class JsonSerializer
Debug.Assert(genericArgs[0].UnderlyingSystemType == typeof(string));
Debug.Assert(
genericArgs[1].UnderlyingSystemType == JsonTypeInfo.ObjectType ||
genericArgs[1].UnderlyingSystemType == typeof(JsonElement));
genericArgs[1].UnderlyingSystemType == typeof(JsonElement) ||
genericArgs[1].UnderlyingSystemType == typeof(Node.JsonNode));
#endif
if (jsonPropertyInfo.RuntimeTypeInfo.CreateObject == null)
{
ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(jsonPropertyInfo.DeclaredPropertyType);
// Avoid a reference to the JsonNode type for trimming
if (jsonPropertyInfo.DeclaredPropertyType.FullName == JsonTypeInfo.JsonObjectTypeName)
{
extensionData = jsonPropertyInfo.ConverterBase.CreateObject(options);
}
else
{
ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(jsonPropertyInfo.DeclaredPropertyType);
}
}
else
{
extensionData = jsonPropertyInfo.RuntimeTypeInfo.CreateObject();
}
extensionData = jsonPropertyInfo.RuntimeTypeInfo.CreateObject();
jsonPropertyInfo.SetExtensionDictionaryAsObject(obj, extensionData);
}
......
......@@ -49,7 +49,7 @@ internal void RootBuiltInConvertersAndTypeInfoCreator()
private static Dictionary<Type, JsonConverter> GetDefaultSimpleConverters()
{
const int NumberOfSimpleConverters = 22;
const int NumberOfSimpleConverters = 23;
var converters = new Dictionary<Type, JsonConverter>(NumberOfSimpleConverters);
// Use a dictionary for simple converters.
......@@ -68,6 +68,7 @@ internal void RootBuiltInConvertersAndTypeInfoCreator()
Add(JsonMetadataServices.Int64Converter);
Add(new JsonElementConverter());
Add(new JsonDocumentConverter());
Add(JsonMetadataServices.ObjectConverter);
Add(JsonMetadataServices.SByteConverter);
Add(JsonMetadataServices.SingleConverter);
Add(JsonMetadataServices.StringConverter);
......
......@@ -79,11 +79,17 @@ public static partial class JsonMetadataServices
public static JsonConverter<long> Int64Converter => s_int64Converter ??= new Int64Converter();
private static JsonConverter<long>? s_int64Converter;
/// <summary>
/// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="JsonElement"/> values.
/// </summary>
internal static JsonConverter<JsonElement> JsonElementConverter => s_jsonElementConverter ??= new JsonElementConverter();
private static JsonConverter<JsonElement>? s_jsonElementConverter;
/// <summary>
/// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="object"/> values.
/// </summary>
public static JsonConverter<object> ObjectConverter => s_objectConverter ??= new ObjectConverter();
private static JsonConverter<object>? s_objectConverter;
public static JsonConverter<object?> ObjectConverter => s_objectConverter ??= new ObjectConverter();
private static JsonConverter<object?>? s_objectConverter;
/// <summary>
/// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="float"/> values.
......
......@@ -361,7 +361,10 @@ private bool NumberHandingIsApplicable()
internal JsonSerializerOptions Options { get; set; } = null!; // initialized in Init method
internal bool ReadJsonAndAddExtensionProperty(object obj, ref ReadStack state, ref Utf8JsonReader reader)
internal bool ReadJsonAndAddExtensionProperty(
object obj,
ref ReadStack state,
ref Utf8JsonReader reader)
{
object propValue = GetValueAsObject(obj)!;
......@@ -376,8 +379,20 @@ internal bool ReadJsonAndAddExtensionProperty(object obj, ref ReadStack state, r
}
else
{
JsonConverter<object> converter = (JsonConverter<object>)Options.GetConverter(JsonTypeInfo.ObjectType);
if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out object? value))
JsonConverter converter;
JsonTypeInfo? dictionaryValueInfo = RuntimeTypeInfo.ElementTypeInfo;
if (dictionaryValueInfo != null)
{
// Fast path when there is a generic type such as Dictionary<string, object>.
converter = dictionaryValueInfo.PropertyInfoForTypeInfo.ConverterBase;
}
else
{
// Slower path for non-generic types that implement IDictionary<string, object>.
converter = JsonMetadataServices.ObjectConverter;
}
if (!converter.TryReadAsObject(ref reader, Options, ref state, out object? value))
{
return false;
}
......@@ -385,14 +400,9 @@ internal bool ReadJsonAndAddExtensionProperty(object obj, ref ReadStack state, r
dictionaryObject[state.Current.JsonPropertyNameAsString!] = value;
}
}
else
else if (propValue is IDictionary<string, JsonElement> dictionaryJsonElement)
{
// Handle case where extension property is JsonElement-based.
Debug.Assert(propValue is IDictionary<string, JsonElement>);
IDictionary<string, JsonElement> dictionaryJsonElement = (IDictionary<string, JsonElement>)propValue;
JsonConverter<JsonElement> converter = (JsonConverter<JsonElement>)Options.GetConverter(typeof(JsonElement));
JsonConverter<JsonElement> converter = JsonMetadataServices.JsonElementConverter;
if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out JsonElement value))
{
return false;
......@@ -400,6 +410,13 @@ internal bool ReadJsonAndAddExtensionProperty(object obj, ref ReadStack state, r
dictionaryJsonElement[state.Current.JsonPropertyNameAsString!] = value;
}
else
{
// Avoid a type reference to JsonObject and its converter to support trimming.
Debug.Assert(propValue is Node.JsonObject);
ConverterBase.ReadElementAndSetProperty(propValue, state.Current.JsonPropertyNameAsString!, ref reader, Options, ref state);
}
return true;
}
......
......@@ -15,6 +15,8 @@ namespace System.Text.Json.Serialization.Metadata
[DebuggerDisplay("ConverterStrategy.{ConverterStrategy}, {Type.Name}")]
public partial class JsonTypeInfo
{
internal const string JsonObjectTypeName = "System.Text.Json.Node.JsonObject";
internal delegate object? ConstructorDelegate();
internal delegate T ParameterizedConstructorDelegate<T>(object[] arguments);
......@@ -506,14 +508,18 @@ private bool DetermineExtensionDataProperty(Dictionary<string, JsonPropertyInfo>
JsonPropertyInfo? jsonPropertyInfo = GetPropertyWithUniqueAttribute(Type, typeof(JsonExtensionDataAttribute), cache);
if (jsonPropertyInfo != null)
{
JsonConverter? converter = null;
Type declaredPropertyType = jsonPropertyInfo.DeclaredPropertyType;
if (typeof(IDictionary<string, object>).IsAssignableFrom(declaredPropertyType) ||
typeof(IDictionary<string, JsonElement>).IsAssignableFrom(declaredPropertyType))
typeof(IDictionary<string, JsonElement>).IsAssignableFrom(declaredPropertyType) ||
// Avoid a reference to typeof(JsonNode) to support trimming.
(declaredPropertyType.FullName == JsonObjectTypeName && ReferenceEquals(declaredPropertyType.Assembly, GetType().Assembly)))
{
JsonConverter converter = Options.GetConverter(declaredPropertyType);
converter = Options.GetConverter(declaredPropertyType);
Debug.Assert(converter != null);
}
else
if (converter == null)
{
ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(Type, jsonPropertyInfo);
}
......
......@@ -10,16 +10,30 @@ internal static partial class ThrowHelper
{
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowArgumentException_NodeValueNotAllowed(string argumentName)
public static void ThrowArgumentException_NodeValueNotAllowed(string paramName)
{
throw new ArgumentException(SR.NodeValueNotAllowed, argumentName);
throw new ArgumentException(SR.NodeValueNotAllowed, paramName);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowArgumentNullException_ValueCannotBeNull(string argumentName)
public static void ThrowArgumentException_NodeArrayTooSmall(string paramName)
{
throw new ArgumentNullException(SR.ValueCannotBeNull, argumentName);
throw new ArgumentException(SR.NodeArrayTooSmall, paramName);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowArgumentOutOfRangeException_NodeArrayIndexNegative(string paramName)
{
throw new ArgumentOutOfRangeException(paramName, SR.NodeArrayIndexNegative);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowArgumentException_DuplicateKey(string propertyName)
{
throw new ArgumentException(SR.NodeDuplicateKey, propertyName);
}
[DoesNotReturn]
......@@ -42,5 +56,17 @@ public static void ThrowInvalidOperationException_NodeElementCannotBeObjectOrArr
{
throw new InvalidOperationException(SR.NodeElementCannotBeObjectOrArray);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowNotSupportedException_NodeCollectionIsReadOnly()
{
throw NotSupportedException_NodeCollectionIsReadOnly();
}
public static NotSupportedException NotSupportedException_NodeCollectionIsReadOnly()
{
return new NotSupportedException(SR.NodeCollectionIsReadOnly);
}
}
}
......@@ -18,44 +18,72 @@ public static void KeyValuePair()
var jObject = new JsonObject();
jObject["One"] = 1;
jObject["Two"] = 2;
Test();
KeyValuePair<string, JsonNode?> kvp1 = default;
KeyValuePair<string, JsonNode?> kvp2 = default;
jObject = new JsonObject { { "One", 1 }, { "Two", 2 } };
Test();
int count = 0;
foreach (KeyValuePair<string, JsonNode?> kvp in jObject)
void Test()
{
if (count == 0)
{
kvp1 = kvp;
}
else
KeyValuePair<string, JsonNode?> kvp1 = default;
KeyValuePair<string, JsonNode?> kvp2 = default;
int count = 0;
foreach (KeyValuePair<string, JsonNode?> kvp in jObject)
{
kvp2 = kvp;
if (count == 0)
{
kvp1 = kvp;
}
else
{
kvp2 = kvp;
}
count++;
}
count++;
}
Assert.Equal(2, count);
Assert.Equal(2, count);
ICollection<KeyValuePair<string, JsonNode?>> iCollection = jObject;
Assert.True(iCollection.Contains(kvp1));
Assert.True(iCollection.Contains(kvp2));
Assert.False(iCollection.Contains(new KeyValuePair<string, JsonNode?>("?", null)));
ICollection<KeyValuePair<string, JsonNode?>> iCollection = jObject;
Assert.True(iCollection.Contains(kvp1));
Assert.True(iCollection.Contains(kvp2));
Assert.False(iCollection.Contains(new KeyValuePair<string, JsonNode?>("?", null)));
Assert.True(iCollection.Remove(kvp1));
Assert.Equal(1, jObject.Count);
Assert.True(iCollection.Remove(kvp1));
Assert.Equal(1, jObject.Count);
Assert.False(iCollection.Remove(new KeyValuePair<string, JsonNode?>("?", null)));
Assert.Equal(1, jObject.Count);
Assert.False(iCollection.Remove(new KeyValuePair<string, JsonNode?>("?", null)));
Assert.Equal(1, jObject.Count);
}
}
[Fact]
public static void IsReadOnly()
{
ICollection<KeyValuePair<string, JsonNode?>> jObject = new JsonObject();
Assert.False(jObject.IsReadOnly);
var jsonObject = new JsonObject();
ICollection<KeyValuePair<string, JsonNode?>> iCollectionOfKVP = jsonObject;
Assert.False(iCollectionOfKVP.IsReadOnly);
IDictionary<string, JsonNode?> iDictionary = jsonObject;
Assert.True(iDictionary.Keys.IsReadOnly);
Assert.True(iDictionary.Values.IsReadOnly);
}
[Fact]
public static void KeyAndValueCollections_ThrowsNotSupportedException()
{
IDictionary<string, JsonNode?> jsonObject = new JsonObject();
Assert.Throws<NotSupportedException>(() => jsonObject.Keys.Add("Hello"));
Assert.Throws<NotSupportedException>(() => jsonObject.Values.Add("Hello"));
Assert.Throws<NotSupportedException>(() => jsonObject.Keys.Clear());
Assert.Throws<NotSupportedException>(() => jsonObject.Values.Clear());
Assert.Throws<NotSupportedException>(() => jsonObject.Keys.Remove("Hello"));
Assert.Throws<NotSupportedException>(() => jsonObject.Values.Remove("Hello"));
}
[Fact]
......@@ -72,9 +100,24 @@ public static void NullPropertyValues()
[Fact]
public static void NullPropertyNameFail()
{
ArgumentNullException ex;
var jObject = new JsonObject();
Assert.Throws<ArgumentNullException>(() => jObject.Add(null, JsonValue.Create(0)));
Assert.Throws<ArgumentNullException>(() => jObject[null] = JsonValue.Create(0));
ex = Assert.Throws<ArgumentNullException>(() => jObject.Add(null, 42));
Assert.Contains("propertyName", ex.ToString());
ex = Assert.Throws<ArgumentNullException>(() => jObject[null] = 42);
Assert.Contains("propertyName", ex.ToString());
ex = Assert.Throws<ArgumentNullException>(() => jObject.ContainsKey(null));
Assert.Contains("propertyName", ex.ToString());
ex = Assert.Throws<ArgumentNullException>(() => jObject.Remove(null));
Assert.Contains("propertyName", ex.ToString());
var iDictionary = (IDictionary<string, JsonNode?>)jObject;
ex = Assert.Throws<ArgumentNullException>(() => iDictionary.TryGetValue(null, out JsonNode _));
Assert.Contains("propertyName", ex.ToString());
}
[Fact]
......@@ -86,7 +129,7 @@ public static void IEnumerable()
IEnumerable enumerable = jObject;
int count = 0;
foreach (KeyValuePair<string, JsonNode?> node in enumerable)
foreach (KeyValuePair<string, JsonNode?> kvp in enumerable)
{
count++;
}
......@@ -137,6 +180,25 @@ public static void Clear_ContainsKey()
Assert.Equal(2, jObject.Count);
}
[Fact]
public static void Clear_JsonElement()
{
const string Json = "{\"MyProperty\":42}";
JsonObject obj;
// Baseline
obj = JsonSerializer.Deserialize<JsonObject>(Json);
Assert.Equal(1, obj.Count);
obj.Clear();
Assert.Equal(0, obj.Count);
// Call clear with only JsonElement (not yet expanded to JsonNodes).
obj = JsonSerializer.Deserialize<JsonObject>(Json);
obj.Clear();
// Don't check for Count of 1 since that will create nodes.
Assert.Equal(0, obj.Count);
}
[Fact]
public static void CaseSensitivity_ReadMode()
{
......@@ -156,6 +218,41 @@ public static void CaseSensitivity_ReadMode()
Assert.Equal(42, obj["MYPROPERTY"].GetValue<int>());
}
[Fact]
public static void CaseInsensitive_Remove()
{
var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
JsonObject obj = JsonSerializer.Deserialize<JsonObject>("{\"MyProperty\":42}", options);
Assert.True(obj.ContainsKey("MyProperty"));
Assert.True(obj.ContainsKey("myproperty"));
Assert.True(obj.ContainsKey("MYPROPERTY"));
Assert.True(obj.Remove("myproperty"));
Assert.False(obj.Remove("myproperty"));
Assert.False(obj.Remove("MYPROPERTY"));
Assert.False(obj.Remove("MyProperty"));
}
[Fact]
public static void CaseSensitive_Remove()
{
var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = false };
JsonObject obj = JsonSerializer.Deserialize<JsonObject>("{\"MYPROPERTY\":42,\"myproperty\":43}", options);
Assert.False(obj.ContainsKey("MyProperty"));
Assert.True(obj.ContainsKey("MYPROPERTY"));
Assert.True(obj.ContainsKey("myproperty"));
Assert.False(obj.Remove("MyProperty"));
Assert.True(obj.Remove("MYPROPERTY"));
Assert.False(obj.Remove("MYPROPERTY"));
Assert.True(obj.Remove("myproperty"));
Assert.False(obj.Remove("myproperty"));
}
[Fact]
public static void CaseSensitivity_EditMode()
{
......@@ -172,7 +269,7 @@ public static void CaseSensitivity_EditMode()
jArray.Add(jObject);
Assert.Throws<ArgumentException>(() => jObject.Add("myproperty", 42));
// Options on parent node.
// Options on parent node (deferred creation of options until Add).
jArray = new JsonArray(options);
jObject = new JsonObject();
jArray.Add(jObject);
......@@ -184,7 +281,7 @@ public static void CaseSensitivity_EditMode()
jObject = new JsonObject();
jObject.Add("MyProperty", 42);
jArray.Add(jObject);
jObject.Add("myproperty", 42); // no exception since options were not set in time.
jObject.Add("myproperty", 42); // No exception since options were not set in time.
}
[Fact]
......@@ -285,6 +382,68 @@ public static void CopyTo()
Assert.Throws<ArgumentOutOfRangeException>(() => jObject.CopyTo(arr, -1));
}
[Fact]
public static void CopyTo_KeyAndValueCollections()
{
IDictionary<string, JsonNode?> jObject = new JsonObject();
jObject["One"] = 1;
jObject["Two"] = 2;
string[] stringArray = new string[3];
jObject.Keys.CopyTo(stringArray, 1);
Assert.Null(stringArray[0]);
Assert.Equal("One", stringArray[1]);
Assert.Equal("Two", stringArray[2]);
JsonNode[] nodeArray = new JsonNode[3];
jObject.Values.CopyTo(nodeArray, 1);
Assert.Null(nodeArray[0]);
Assert.Equal(1, nodeArray[1].GetValue<int>());
Assert.Equal(2, nodeArray[2].GetValue<int>());
Assert.Throws<ArgumentException>(() => jObject.Keys.CopyTo(stringArray, 2));
Assert.Throws<ArgumentException>(() => jObject.Values.CopyTo(nodeArray, 2));
Assert.Throws<ArgumentOutOfRangeException>(() => jObject.Keys.CopyTo(stringArray, -1));
Assert.Throws<ArgumentOutOfRangeException>(() => jObject.Values.CopyTo(nodeArray, -1));
}
[Fact]
public static void Contains_KeyAndValueCollections()
{
IDictionary<string, JsonNode?> jObject = new JsonObject();
jObject["One"] = 1;
Assert.True(jObject.Keys.Contains("One"));
Assert.False(jObject.Keys.Contains("Two"));
Assert.True(jObject.Values.Contains(jObject["One"]));
Assert.False(jObject.Values.Contains(1)); // Reference semantics causes this to be false.
}
[Fact]
public static void IEnumerable_KeyAndValueCollections()
{
IDictionary<string, JsonNode?> jObject = new JsonObject();
jObject["One"] = 1;
IEnumerable enumerable = jObject.Keys;
int count = 0;
foreach (string str in enumerable)
{
count++;
}
Assert.Equal(1, count);
enumerable = jObject.Values;
count = 0;
foreach (JsonNode node in enumerable)
{
count++;
}
Assert.Equal(1, count);
}
[Fact]
public static void CreateDom()
{
......@@ -404,7 +563,8 @@ public static void ReAddSameNode_Throws()
var jObject = new JsonObject();
jObject.Add("Prop", jValue);
Assert.Throws<InvalidOperationException>(() => jObject.Add("Prop", jValue));
ArgumentException ex = Assert.Throws<ArgumentException>(() => jObject.Add("Prop", jValue));
Assert.Contains("Prop", ex.ToString());
}
[Fact]
......@@ -484,5 +644,136 @@ public static void DynamicObject_LINQ_Convert()
string json_out = JsonSerializer.Serialize(blogPosts);
Assert.Equal(expected, json_out);
}
[Theory]
[MemberData(nameof(JObjectCollectionData))]
public static void ListToDictionaryConversions(JsonObject jObject, int count)
{
Assert.Equal(count, jObject.Count);
int i;
for (i = 0; i < count; i++)
{
Assert.Equal(i, jObject[i.ToString()].GetValue<int>());
}
i = 0;
foreach (KeyValuePair<string, JsonNode?> kvp in jObject)
{
Assert.Equal(i.ToString(), kvp.Key);
Assert.Equal(i, kvp.Value.GetValue<int>());
i++;
}
i = 0;
foreach (object o in (IEnumerable)jObject)
{
var kvp = (KeyValuePair<string, JsonNode?>)o;
Assert.Equal(i.ToString(), kvp.Key);
Assert.Equal(i, kvp.Value.GetValue<int>());
i++;
}
var dictionary = (IDictionary<string, JsonNode?>)jObject;
i = 0;
foreach (string propertyName in dictionary.Keys)
{
Assert.Equal(i.ToString(), propertyName);
i++;
}
i = 0;
foreach (JsonNode? node in dictionary.Values)
{
Assert.Equal(i, node.GetValue<int>());
i++;
}
string expectedJson = jObject.ToJsonString();
// Use indexers to replace items.
for (i = 0; i < count; i++)
{
string key = i.ToString();
// Contains does a reference comparison on JsonNode so it needs to be done before modifying.
Assert.True(jObject.Contains(new KeyValuePair<string, JsonNode?>(key, jObject[key])));
jObject[key] = JsonValue.Create(i);
jObject[key] = jObject[key]; // Should have no effect.
Assert.False(jObject.Contains(new KeyValuePair<string, JsonNode?>("MISSING", jObject[key])));
Assert.True(jObject.ContainsKey(key));
// Remove() should not affect result when missing.
bool success = jObject.Remove("MISSING");
Assert.False(success);
}
// JSON shouldn't change.
Assert.Equal(expectedJson, jObject.ToJsonString());
// Remove every other entry.
for (i = 0; i < count; i += 2)
{
bool success = dictionary.Remove(i.ToString());
Assert.True(success);
}
Assert.Equal(count / 2, jObject.Count);
// The new JSON contains half the entries.
expectedJson = jObject.ToJsonString();
// Add back every other entry (to the end)
for (i = 0; i < count; i += 2)
{
jObject.Add(i.ToString(), JsonValue.Create(i));
}
Assert.Equal(count, jObject.Count);
// The beginning part of the JSON should be the same.
string json = jObject.ToJsonString();
Assert.Contains(expectedJson.TrimEnd('}'), json);
const int ItemsToAdd = 10;
for (i = 10000; i < 10000 + ItemsToAdd; i++)
{
jObject.Add(i.ToString(), i);
}
// Original items should still be in front.
json = jObject.ToJsonString();
Assert.Contains(expectedJson.TrimEnd('}'), json);
Assert.Equal(count + ItemsToAdd, jObject.Count);
}
public static IEnumerable<object[]> JObjectCollectionData()
{
// Ensure that the list-to-dictionary threshold is hit (currently 9).
for (int i = 0; i < 20; i++)
{
yield return CreateArray(i);
}
yield return CreateArray(123);
yield return CreateArray(1000);
object[] CreateArray(int count)
{
var jObject = new JsonObject();
for (int i = 0; i < count; i++)
{
jObject[i.ToString()] = i;
}
return new object[] { jObject, count };
}
}
}
}
......@@ -191,6 +191,30 @@ public static void TryGetValue_FromDateTime(string json)
Assert.False(jValue.TryGetValue(out Guid _));
}
[Fact]
public static void TryGetValue_FromBoolean()
{
JsonValue jValue = JsonNode.Parse("true").AsValue();
Assert.True(jValue.TryGetValue(out bool _));
Assert.False(jValue.TryGetValue(out byte _));
Assert.False(jValue.TryGetValue(out short _));
Assert.False(jValue.TryGetValue(out int _));
Assert.False(jValue.TryGetValue(out long _));
Assert.False(jValue.TryGetValue(out sbyte _));
Assert.False(jValue.TryGetValue(out ushort _));
Assert.False(jValue.TryGetValue(out uint _));
Assert.False(jValue.TryGetValue(out ulong _));
Assert.False(jValue.TryGetValue(out float _));
Assert.False(jValue.TryGetValue(out double _));
Assert.False(jValue.TryGetValue(out decimal _));
Assert.False(jValue.TryGetValue(out string _));
Assert.False(jValue.TryGetValue(out char _));
Assert.False(jValue.TryGetValue(out DateTime _));
Assert.False(jValue.TryGetValue(out DateTimeOffset _));
Assert.False(jValue.TryGetValue(out Guid _));
}
[Theory]
[InlineData("\"A\"")]
[InlineData("\" \"")]
......
......@@ -69,11 +69,11 @@ public static void GetPath_SpecialCharacters()
[Fact]
public static void DetectCycles_Object()
{
var jObject = new JsonObject { };
var jObject = new JsonObject();
Assert.Throws<InvalidOperationException>(() => jObject.Add("a", jObject));
var jObject2 = new JsonObject { };
jObject.Add("a", jObject2);
var jObject2 = new JsonObject();
jObject.Add("b", jObject2);
Assert.Throws<InvalidOperationException>(() => jObject2.Add("b", jObject));
}
......
......@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using System.Reflection;
using System.Text.Json.Serialization.Tests;
using Xunit;
......@@ -144,17 +145,44 @@ public static void NullLiteral()
}
[Fact]
public static void ReadSimpleObject()
public static void InternalValueFields()
{
// Use reflection to inspect the internal state of the 3 fields that hold values.
// There is not another way to verify, and using a debug watch causes nodes to be created.
FieldInfo elementField = typeof(JsonObject).GetField("_jsonElement", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(elementField);
FieldInfo listField = typeof(JsonObject).GetField("_list", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(listField);
FieldInfo dictionaryField = typeof(JsonObject).GetField("_dictionary", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(dictionaryField);
using (MemoryStream stream = new MemoryStream(SimpleTestClass.s_data))
{
// Only JsonElement is present.
JsonNode node = JsonNode.Parse(stream);
Assert.NotNull(elementField.GetValue(node));
Assert.Null(listField.GetValue(node));
Assert.Null(dictionaryField.GetValue(node));
Test();
// Cause the single JsonElement to expand into individual JsonElement nodes.
Assert.Equal(1, node.AsObject()["MyInt16"].GetValue<int>());
Assert.Null(elementField.GetValue(node));
Assert.NotNull(listField.GetValue(node));
Assert.NotNull(dictionaryField.GetValue(node)); // The dictionary threshold was reached.
Test();
void Test()
{
string actual = node.ToJsonString();
string actual = node.ToJsonString();
// Replace the escaped "+" sign used with DateTimeOffset.
actual = actual.Replace("\\u002B", "+");
// Replace the escaped "+" sign used with DateTimeOffset.
actual = actual.Replace("\\u002B", "+");
Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual);
Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual);
}
}
}
......
......@@ -337,13 +337,16 @@ private static void GenericObjectOrJsonElementConverterTestHelper<T>(string conv
{
var options = new JsonSerializerOptions();
// Initialize the built-in converters.
JsonSerializer.Serialize("", options);
JsonConverter<T> converter = (JsonConverter<T>)options.GetConverter(typeof(T));
Assert.Equal(converterName, converter.GetType().Name);
ReadOnlySpan<byte> data = Encoding.UTF8.GetBytes(stringValue);
Utf8JsonReader reader = new Utf8JsonReader(data);
reader.Read();
T readValue = converter.Read(ref reader, typeof(T), null);
T readValue = converter.Read(ref reader, typeof(T), options);
if (readValue is JsonElement element)
{
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册