File: System\Text\Json\Nodes\JsonObject.cs
Web Access
Project: src\src\libraries\System.Text.Json\src\System.Text.Json.csproj (System.Text.Json)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace System.Text.Json.Nodes
{
    /// <summary>
    ///   Represents a mutable JSON object.
    /// </summary>
    /// <remarks>
    /// It's safe to perform multiple concurrent read operations on a <see cref="JsonObject"/>,
    /// but issues can occur if the collection is modified while it's being read.
    /// </remarks>
    [DebuggerDisplay("JsonObject[{Count}]")]
    [DebuggerTypeProxy(typeof(DebugView))]
    public sealed partial class JsonObject : JsonNode
    {
        private JsonElement? _jsonElement;
 
        internal override JsonElement? UnderlyingElement => _jsonElement;
 
        /// <summary>
        ///   Initializes a new instance of the <see cref="JsonObject"/> class that is empty.
        /// </summary>
        /// <param name="options">Options to control the behavior.</param>
        public JsonObject(JsonNodeOptions? options = null) : base(options) { }
 
        /// <summary>
        ///   Initializes a new instance of the <see cref="JsonObject"/> class that contains the specified <paramref name="properties"/>.
        /// </summary>
        /// <param name="properties">The properties to be added.</param>
        /// <param name="options">Options to control the behavior.</param>
        public JsonObject(IEnumerable<KeyValuePair<string, JsonNode?>> properties, JsonNodeOptions? options = null) : this(options)
        {
            int capacity = properties is ICollection<KeyValuePair<string, JsonNode?>> propertiesCollection ? propertiesCollection.Count : 0;
            OrderedDictionary<string, JsonNode?> dictionary = CreateDictionary(options, capacity);
 
            foreach (KeyValuePair<string, JsonNode?> node in properties)
            {
                dictionary.Add(node.Key, node.Value);
                node.Value?.AssignParent(this);
            }
 
            _dictionary = dictionary;
        }
 
        /// <summary>
        ///   Initializes a new instance of the <see cref="JsonObject"/> class that contains properties from the specified <see cref="JsonElement"/>.
        /// </summary>
        /// <returns>
        ///   The new instance of the <see cref="JsonObject"/> class that contains properties from the specified <see cref="JsonElement"/>.
        /// </returns>
        /// <param name="element">The <see cref="JsonElement"/>.</param>
        /// <param name="options">Options to control the behavior.</param>
        /// <returns>A <see cref="JsonObject"/>.</returns>
        public static JsonObject? Create(JsonElement element, JsonNodeOptions? options = null)
        {
            return element.ValueKind switch
            {
                JsonValueKind.Null => null,
                JsonValueKind.Object => new JsonObject(element, options),
                _ => throw new InvalidOperationException(SR.Format(SR.NodeElementWrongType, nameof(JsonValueKind.Object)))
            };
        }
 
        internal JsonObject(JsonElement element, JsonNodeOptions? options = null) : this(options)
        {
            Debug.Assert(element.ValueKind == JsonValueKind.Object);
            _jsonElement = element;
        }
 
        /// <summary>
        /// Gets or creates the underlying dictionary containing the properties of the object.
        /// </summary>
        private OrderedDictionary<string, JsonNode?> Dictionary => _dictionary ?? InitializeDictionary();
 
        private protected override JsonNode? GetItem(int index) => GetAt(index).Value;
        private protected override void SetItem(int index, JsonNode? value) => SetAt(index, value);
 
        internal override JsonNode DeepCloneCore()
        {
            GetUnderlyingRepresentation(out OrderedDictionary<string, JsonNode?>? dictionary, out JsonElement? jsonElement);
 
            if (dictionary is null)
            {
                return jsonElement.HasValue
                    ? new JsonObject(jsonElement.Value.Clone(), Options)
                    : new JsonObject(Options);
            }
 
            var jObject = new JsonObject(Options)
            {
                _dictionary = CreateDictionary(Options, Count)
            };
 
            foreach (KeyValuePair<string, JsonNode?> item in dictionary)
            {
                jObject.Add(item.Key, item.Value?.DeepCloneCore());
            }
 
            return jObject;
        }
 
        internal string GetPropertyName(JsonNode? node)
        {
            KeyValuePair<string, JsonNode?>? item = FindValue(node);
            return item.HasValue ? item.Value.Key : string.Empty;
        }
 
        /// <summary>
        ///   Returns the value of a property with the specified name.
        /// </summary>
        /// <param name="propertyName">The name of the property to return.</param>
        /// <param name="jsonNode">The JSON value of the property with the specified name.</param>
        /// <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 is null)
            {
                ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
            }
 
            return Dictionary.TryGetValue(propertyName, out jsonNode);
        }
 
        /// <inheritdoc/>
        public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null)
        {
            if (writer is null)
            {
                ThrowHelper.ThrowArgumentNullException(nameof(writer));
            }
 
            GetUnderlyingRepresentation(out OrderedDictionary<string, JsonNode?>? dictionary, out JsonElement? jsonElement);
 
            if (dictionary is null && jsonElement.HasValue)
            {
                // Write the element without converting to nodes.
                jsonElement.Value.WriteTo(writer);
            }
            else
            {
                writer.WriteStartObject();
 
                foreach (KeyValuePair<string, JsonNode?> entry in Dictionary)
                {
                    writer.WritePropertyName(entry.Key);
 
                    if (entry.Value is null)
                    {
                        writer.WriteNullValue();
                    }
                    else
                    {
                        entry.Value.WriteTo(writer, options);
                    }
                }
 
                writer.WriteEndObject();
            }
        }
 
        private protected override JsonValueKind GetValueKindCore() => JsonValueKind.Object;
 
        internal override bool DeepEqualsCore(JsonNode node)
        {
            switch (node)
            {
                case JsonArray:
                    return false;
                case JsonValue value:
                    // JsonValue instances have special comparison semantics, dispatch to their implementation.
                    return value.DeepEqualsCore(this);
                case JsonObject jsonObject:
                    OrderedDictionary<string, JsonNode?> currentDict = Dictionary;
                    OrderedDictionary<string, JsonNode?> otherDict = jsonObject.Dictionary;
 
                    if (currentDict.Count != otherDict.Count)
                    {
                        return false;
                    }
 
                    foreach (KeyValuePair<string, JsonNode?> item in currentDict)
                    {
                        otherDict.TryGetValue(item.Key, out JsonNode? jsonNode);
 
                        if (!DeepEquals(item.Value, jsonNode))
                        {
                            return false;
                        }
                    }
 
                    return true;
                default:
                    Debug.Fail("Impossible case");
                    return false;
            }
        }
 
        internal JsonNode? GetItem(string propertyName)
        {
            if (propertyName is null)
            {
                ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
            }
 
            if (TryGetPropertyValue(propertyName, out JsonNode? value))
            {
                return value;
            }
 
            // Return null for missing properties.
            return null;
        }
 
        internal override void GetPath(ref ValueStringBuilder path, JsonNode? child)
        {
            Parent?.GetPath(ref path, this);
 
            if (child != null)
            {
                string propertyName = FindValue(child)!.Value.Key;
                if (propertyName.AsSpan().ContainsSpecialCharacters())
                {
                    path.Append("['");
                    path.Append(propertyName);
                    path.Append("']");
                }
                else
                {
                    path.Append('.');
                    path.Append(propertyName);
                }
            }
        }
 
        internal void SetItem(string propertyName, JsonNode? value)
        {
            if (propertyName is null)
            {
                ThrowHelper.ThrowArgumentNullException(nameof(propertyName));
            }
 
            OrderedDictionary<string, JsonNode?> dict = Dictionary;
 
            if (
#if NET10_0_OR_GREATER
                !dict.TryAdd(propertyName, value, out int index)
#else
                !dict.TryAdd(propertyName, value)
#endif
                )
            {
#if !NET10_0_OR_GREATER
                int index = dict.IndexOf(propertyName);
#endif
                Debug.Assert(index >= 0);
                JsonNode? replacedValue = dict.GetAt(index).Value;
 
                if (ReferenceEquals(value, replacedValue))
                {
                    return;
                }
 
                DetachParent(replacedValue);
                dict.SetAt(index, value);
            }
 
            value?.AssignParent(this);
        }
 
        private void DetachParent(JsonNode? item)
        {
            Debug.Assert(_dictionary != null, "Cannot have detachable nodes without a materialized dictionary.");
 
            if (item != null)
            {
                item.Parent = null;
            }
        }
 
        private KeyValuePair<string, JsonNode?>? FindValue(JsonNode? value)
        {
            foreach (KeyValuePair<string, JsonNode?> item in Dictionary)
            {
                if (ReferenceEquals(item.Value, value))
                {
                    return item;
                }
            }
 
            return null;
        }
 
        [ExcludeFromCodeCoverage] // Justification = "Design-time"
        private sealed class DebugView
        {
            [DebuggerBrowsable(DebuggerBrowsableState.Never)]
            private readonly JsonObject _node;
 
            public DebugView(JsonObject node)
            {
                _node = node;
            }
 
            public string Json => _node.ToJsonString();
            public string Path => _node.GetPath();
 
            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
            private DebugViewProperty[] Items
            {
                get
                {
                    DebugViewProperty[] properties = new DebugViewProperty[_node.Count];
 
                    int i = 0;
                    foreach (KeyValuePair<string, JsonNode?> item in _node)
                    {
                        properties[i].PropertyName = item.Key;
                        properties[i].Value = item.Value;
                        i++;
                    }
 
                    return properties;
                }
            }
 
            [DebuggerDisplay("{Display,nq}")]
            private struct DebugViewProperty
            {
                [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
                public JsonNode? Value;
 
                [DebuggerBrowsable(DebuggerBrowsableState.Never)]
                public string PropertyName;
 
                [DebuggerBrowsable(DebuggerBrowsableState.Never)]
                public string Display
                {
                    get
                    {
                        if (Value == null)
                        {
                            return $"{PropertyName} = null";
                        }
 
                        if (Value is JsonValue)
                        {
                            return $"{PropertyName} = {Value.ToJsonString()}";
                        }
 
                        if (Value is JsonObject jsonObject)
                        {
                            return $"{PropertyName} = JsonObject[{jsonObject.Count}]";
                        }
 
                        JsonArray jsonArray = (JsonArray)Value;
                        return $"{PropertyName} = JsonArray[{jsonArray.Count}]";
                    }
                }
 
            }
        }
    }
}