|
// 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.CodeAnalysis;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;
namespace System.Text.Json.Nodes
{
/// <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
{
// Default options instance used when calling built-in JsonNode converters.
private protected static readonly JsonSerializerOptions s_defaultOptions = new();
private JsonNode? _parent;
private JsonNodeOptions? _options;
/// <summary>
/// The underlying JsonElement if the node is backed by a JsonElement.
/// </summary>
internal virtual JsonElement? UnderlyingElement => null;
/// <summary>
/// Options to control the behavior.
/// </summary>
public JsonNodeOptions? Options
{
get
{
if (!_options.HasValue && Parent != null)
{
// Remember the parent options; if node is re-parented later we still want to keep the
// original options since they may have affected the way the node was created as is the case
// with JsonObject's case-insensitive dictionary.
_options = Parent.Options;
}
return _options;
}
}
internal JsonNode(JsonNodeOptions? options = null)
{
_options = options;
}
/// <summary>
/// Casts to the derived <see cref="JsonArray"/> type.
/// </summary>
/// <returns>
/// A <see cref="JsonArray"/>.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The node is not a <see cref="JsonArray"/>.
/// </exception>
public JsonArray AsArray()
{
JsonArray? jArray = this as JsonArray;
if (jArray is null)
{
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray));
}
return jArray;
}
/// <summary>
/// Casts to the derived <see cref="JsonObject"/> type.
/// </summary>
/// <returns>
/// A <see cref="JsonObject"/>.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The node is not a <see cref="JsonObject"/>.
/// </exception>
public JsonObject AsObject()
{
JsonObject? jObject = this as JsonObject;
if (jObject is null)
{
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonObject));
}
return jObject;
}
/// <summary>
/// Casts to the derived <see cref="JsonValue"/> type.
/// </summary>
/// <returns>
/// A <see cref="JsonValue"/>.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The node is not a <see cref="JsonValue"/>.
/// </exception>
public JsonValue AsValue()
{
JsonValue? jValue = this as JsonValue;
if (jValue is null)
{
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonValue));
}
return jValue;
}
/// <summary>
/// Gets the parent <see cref="JsonNode"/>.
/// If there is no parent, <see langword="null"/> is returned.
/// A parent can either be a <see cref="JsonObject"/> or a <see cref="JsonArray"/>.
/// </summary>
public JsonNode? Parent
{
get
{
return _parent;
}
internal set
{
_parent = value;
}
}
/// <summary>
/// Gets the JSON path.
/// </summary>
/// <returns>The JSON Path value.</returns>
public string GetPath()
{
if (Parent == null)
{
return "$";
}
var path = new ValueStringBuilder(stackalloc char[JsonConstants.StackallocCharThreshold]);
path.Append('$');
GetPath(ref path, null);
return path.ToString();
}
internal abstract void GetPath(ref ValueStringBuilder path, JsonNode? child);
/// <summary>
/// Gets the root <see cref="JsonNode"/>.
/// </summary>
/// <remarks>
/// The current node is returned if it is a root.
/// </remarks>
public JsonNode Root
{
get
{
JsonNode? parent = Parent;
if (parent == null)
{
return this;
}
while (parent.Parent != null)
{
parent = parent.Parent;
}
return parent;
}
}
/// <summary>
/// Gets the value for the current <see cref="JsonValue"/>.
/// </summary>
/// <typeparam name="T">The type of the value to obtain from the <see cref="JsonValue"/>.</typeparam>
/// <returns>A value converted from the <see cref="JsonValue"/> instance.</returns>
/// <remarks>
/// {T} can be the type or base type of the underlying value.
/// If the underlying value is a <see cref="JsonElement"/> then {T} can also be the type of any primitive
/// value supported by current <see cref="JsonElement"/>.
/// Specifying the <see cref="object"/> type for {T} will always succeed and return the underlying value as <see cref="object"/>.<br />
/// The underlying value of a <see cref="JsonValue"/> after deserialization is an instance of <see cref="JsonElement"/>,
/// otherwise it's the value specified when the <see cref="JsonValue"/> was created.
/// </remarks>
/// <seealso cref="System.Text.Json.Nodes.JsonValue.TryGetValue"></seealso>
/// <exception cref="FormatException">
/// The current <see cref="JsonNode"/> cannot be represented as a {T}.
/// </exception>
/// <exception cref="InvalidOperationException">
/// The current <see cref="JsonNode"/> is not a <see cref="JsonValue"/> or
/// is not compatible with {T}.
/// </exception>
public virtual T GetValue<T>() =>
throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonValue)));
/// <summary>
/// Gets or sets the element at the specified index.
/// </summary>
/// <param name="index">The zero-based index of the element to get or set.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="index"/> is less than 0 or <paramref name="index"/> is greater than the number of properties.
/// </exception>
/// <exception cref="InvalidOperationException">
/// The current <see cref="JsonNode"/> is not a <see cref="JsonArray"/> or <see cref="JsonObject"/>.
/// </exception>
public JsonNode? this[int index]
{
get => GetItem(index);
set => SetItem(index, value);
}
private protected virtual JsonNode? GetItem(int index)
{
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray), nameof(JsonObject));
return null;
}
private protected virtual void SetItem(int index, JsonNode? node) =>
ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray), nameof(JsonObject));
/// <summary>
/// Gets or sets the element with the specified property name.
/// If the property is not found, <see langword="null"/> is returned.
/// </summary>
/// <param name="propertyName">The name of the property to return.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="propertyName"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="InvalidOperationException">
/// The current <see cref="JsonNode"/> is not a <see cref="JsonObject"/>.
/// </exception>
public JsonNode? this[string propertyName]
{
get
{
return AsObject().GetItem(propertyName);
}
set
{
AsObject().SetItem(propertyName, value);
}
}
/// <summary>
/// Creates a new instance of the <see cref="JsonNode"/>. All children nodes are recursively cloned.
/// </summary>
/// <returns>A new cloned instance of the current node.</returns>
public JsonNode DeepClone() => DeepCloneCore();
internal abstract JsonNode DeepCloneCore();
/// <summary>
/// Returns <see cref="JsonValueKind"/> of current instance.
/// </summary>
public JsonValueKind GetValueKind() => GetValueKindCore();
private protected abstract JsonValueKind GetValueKindCore();
/// <summary>
/// Returns property name of the current node from the parent object.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The current parent is not a <see cref="JsonObject"/>.
/// </exception>
public string GetPropertyName()
{
JsonObject? parentObject = _parent as JsonObject;
if (parentObject is null)
{
ThrowHelper.ThrowInvalidOperationException_NodeParentWrongType(nameof(JsonObject));
}
return parentObject.GetPropertyName(this);
}
/// <summary>
/// Returns index of the current node from the parent <see cref="JsonArray" />.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The current parent is not a <see cref="JsonArray"/>.
/// </exception>
public int GetElementIndex()
{
JsonArray? parentArray = _parent as JsonArray;
if (parentArray is null)
{
ThrowHelper.ThrowInvalidOperationException_NodeParentWrongType(nameof(JsonArray));
}
return parentArray.GetElementIndex(this);
}
/// <summary>
/// Compares the values of two nodes, including the values of all descendant nodes.
/// </summary>
/// <param name="node1">The <see cref="JsonNode"/> to compare.</param>
/// <param name="node2">The <see cref="JsonNode"/> to compare.</param>
/// <returns><c>true</c> if the tokens are equal; otherwise <c>false</c>.</returns>
public static bool DeepEquals(JsonNode? node1, JsonNode? node2)
{
if (node1 is null)
{
return node2 is null;
}
else if (node2 is null)
{
return false;
}
return node1.DeepEqualsCore(node2);
}
internal abstract bool DeepEqualsCore(JsonNode node);
/// <summary>
/// Replaces this node with a new value.
/// </summary>
/// <typeparam name="T">The type of value to be replaced.</typeparam>
/// <param name="value">Value that replaces this node.</param>
[RequiresUnreferencedCode(JsonValue.CreateUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonValue.CreateDynamicCodeMessage)]
public void ReplaceWith<T>(T value)
{
JsonNode? node;
switch (_parent)
{
case JsonObject jsonObject:
node = ConvertFromValue(value);
jsonObject.SetItem(GetPropertyName(), node);
return;
case JsonArray jsonArray:
node = ConvertFromValue(value);
jsonArray.SetItem(GetElementIndex(), node);
return;
}
}
internal void AssignParent(JsonNode parent)
{
if (Parent != null)
{
ThrowHelper.ThrowInvalidOperationException_NodeAlreadyHasParent();
}
JsonNode? p = parent;
while (p != null)
{
if (p == this)
{
ThrowHelper.ThrowInvalidOperationException_NodeCycleDetected();
}
p = p.Parent;
}
Parent = parent;
}
/// <summary>
/// Adaptation of the equivalent JsonValue.Create factory method extended
/// to support arbitrary <see cref="JsonElement"/> and <see cref="JsonNode"/> values.
/// TODO consider making public cf. https://github.com/dotnet/runtime/issues/70427
/// </summary>
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
internal static JsonNode? ConvertFromValue<T>(T? value, JsonNodeOptions? options = null)
{
if (value is null)
{
return null;
}
if (value is JsonNode node)
{
return node;
}
if (value is JsonElement element)
{
return JsonNodeConverter.Create(element, options);
}
var jsonTypeInfo = (JsonTypeInfo<T>)JsonSerializerOptions.Default.GetTypeInfo(typeof(T));
return JsonValue.CreateFromTypeInfo(value, jsonTypeInfo, options);
}
}
}
|