From 63f26c73d6f922f3cff97c7ae2d3141a4678b8f2 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Fri, 2 Apr 2021 11:44:04 -0500 Subject: [PATCH] Add JsonNode feature --- .../ref/System.Text.Json.InboxOnly.cs | 13 + .../System.Text.Json/ref/System.Text.Json.cs | 167 ++++++ .../ref/System.Text.Json.csproj | 4 + .../src/Resources/Strings.resx | 30 ++ .../src/System.Text.Json.csproj | 24 + .../Text/Json/Document/JsonDocument.Parse.cs | 60 ++- .../Text/Json/Document/JsonElement.Parse.cs | 19 + .../System/Text/Json/Node/JsonArray.IList.cs | 165 ++++++ .../src/System/Text/Json/Node/JsonArray.cs | 270 ++++++++++ .../System/Text/Json/Node/JsonNode.Dynamic.cs | 18 + .../Text/Json/Node/JsonNode.Operators.cs | 420 +++++++++++++++ .../System/Text/Json/Node/JsonNode.Parse.cs | 129 +++++ .../src/System/Text/Json/Node/JsonNode.To.cs | 67 +++ .../src/System/Text/Json/Node/JsonNode.cs | 248 +++++++++ .../System/Text/Json/Node/JsonNodeOptions.cs | 16 + .../Text/Json/Node/JsonObject.Dynamic.cs | 53 ++ .../Text/Json/Node/JsonObject.IDictionary.cs | 244 +++++++++ .../src/System/Text/Json/Node/JsonObject.cs | 299 +++++++++++ .../src/System/Text/Json/Node/JsonValue.cs | 68 +++ .../src/System/Text/Json/Node/JsonValueOfT.cs | 375 ++++++++++++++ .../src/System/Text/Json/Node/MetaDynamic.cs | 433 ++++++++++++++++ .../Converters/Node/JsonArrayConverter.cs | 37 ++ .../Converters/Node/JsonNodeConverter.cs | 85 +++ .../Node/JsonNodeConverterFactory.cs | 47 ++ .../Converters/Node/JsonObjectConverter.cs | 38 ++ .../Converters/Node/JsonValueConverter.cs | 24 + .../JsonSerializerOptions.Converters.cs | 6 +- .../Serialization/JsonSerializerOptions.cs | 24 + .../Serialization/JsonUnknownTypeHandling.cs | 20 + .../src/System/Text/Json/ThrowHelper.Node.cs | 50 ++ .../src/System/Text/Json/ThrowHelper.cs | 10 + .../System.Text.Json/tests/JsonNode/Common.cs | 132 +++++ .../tests/JsonNode/DynamicTests.cs | 286 +++++++++++ .../tests/JsonNode/JsonArrayTests.cs | 432 ++++++++++++++++ .../tests/JsonNode/JsonNodeOperatorTests.cs | 223 ++++++++ .../tests/JsonNode/JsonNodeTests.cs | 71 +++ .../tests/JsonNode/JsonObjectTests.cs | 484 ++++++++++++++++++ .../tests/JsonNode/JsonValueTests.cs | 291 +++++++++++ .../tests/JsonNode/ParentPathRootTests.cs | 91 ++++ .../tests/JsonNode/ParseTests.cs | 217 ++++++++ .../tests/JsonNode/SerializerInteropTests.cs | 65 +++ .../tests/JsonNode/ToStringTests.cs | 32 ++ ...stomConverterTests.Dynamic.Sample.Tests.cs | 84 +-- .../tests/Serialization/OptionsTests.cs | 5 + .../tests/Serialization/Stream.WriteTests.cs | 2 +- .../tests/System.Text.Json.Tests.csproj | 11 + 46 files changed, 5846 insertions(+), 43 deletions(-) create mode 100644 src/libraries/System.Text.Json/ref/System.Text.Json.InboxOnly.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Dynamic.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Operators.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Parse.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.To.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNodeOptions.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.Dynamic.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValue.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValueOfT.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Node/MetaDynamic.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverterFactory.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownTypeHandling.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/Common.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/DynamicTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/JsonArrayTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/JsonNodeOperatorTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/JsonNodeTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/JsonObjectTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/JsonValueTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/ParentPathRootTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/ParseTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/SerializerInteropTests.cs create mode 100644 src/libraries/System.Text.Json/tests/JsonNode/ToStringTests.cs diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.InboxOnly.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.InboxOnly.cs new file mode 100644 index 0000000000000..407ad74e8066b --- /dev/null +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.InboxOnly.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the https://aka.ms/api-review process. +// ------------------------------------------------------------------------------ + +namespace System.Text.Json.Node +{ + public partial class JsonNode : System.Dynamic.IDynamicMetaObjectProvider + { + System.Dynamic.DynamicMetaObject System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject(System.Linq.Expressions.Expression parameter) { throw null; } + } +} diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 48a4c8c5f9e51..56e275b7039d6 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -234,6 +234,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } public bool WriteIndented { get { throw null; } set { } } public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; } } @@ -468,6 +469,167 @@ public void WriteStringValue(string? value) { } public void WriteStringValue(System.Text.Json.JsonEncodedText value) { } } } +namespace System.Text.Json.Node +{ + public sealed partial class JsonArray : System.Text.Json.Node.JsonNode, System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.Generic.IList, System.Collections.IEnumerable + { + public JsonArray(System.Text.Json.Node.JsonNodeOptions? options = default(System.Text.Json.Node.JsonNodeOptions?)) { } + public JsonArray(System.Text.Json.Node.JsonNodeOptions options, params System.Text.Json.Node.JsonNode?[] items) { } + public JsonArray(params System.Text.Json.Node.JsonNode?[] items) { } + public int Count { get { throw null; } } + public static System.Text.Json.Node.JsonArray? Create(System.Text.Json.JsonElement element, System.Text.Json.Node.JsonNodeOptions? options = default(System.Text.Json.Node.JsonNodeOptions?)) { throw null; } + bool System.Collections.Generic.ICollection.IsReadOnly { get { throw null; } } + public void Add(System.Text.Json.Node.JsonNode? item) { } + public void Add(T value) { } + public void Clear() { } + public bool Contains(System.Text.Json.Node.JsonNode? item) { throw null; } + public System.Collections.Generic.IEnumerator GetEnumerator() { throw null; } + public int IndexOf(System.Text.Json.Node.JsonNode? item) { throw null; } + public void Insert(int index, System.Text.Json.Node.JsonNode? item) { } + public bool Remove(System.Text.Json.Node.JsonNode? item) { throw null; } + public void RemoveAt(int index) { } + void System.Collections.Generic.ICollection.CopyTo(System.Text.Json.Node.JsonNode?[]? array, int index) { } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public override void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.Text.Json.JsonSerializerOptions? options = null) { } + } + public abstract partial class JsonNode + { + internal JsonNode() { } + public System.Text.Json.Node.JsonNode? this[int index] { get { throw null; } set { } } + public System.Text.Json.Node.JsonNode? this[string propertyName] { get { throw null; } set { } } + public System.Text.Json.Node.JsonNodeOptions? Options { get { throw null; } } + public System.Text.Json.Node.JsonNode? Parent { get { throw null; } } + public System.Text.Json.Node.JsonNode Root { get { throw null; } } + public System.Text.Json.Node.JsonArray AsArray() { throw null; } + public System.Text.Json.Node.JsonObject AsObject() { throw null; } + public System.Text.Json.Node.JsonValue AsValue() { throw null; } + public string GetPath() { throw null; } + public virtual TValue GetValue<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]TValue>() { throw null; } + public static explicit operator bool(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator byte(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator char(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator System.DateTime(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator System.DateTimeOffset(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator decimal(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator double(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator System.Guid(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator short(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator int(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator long(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator bool?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator byte?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator char?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator System.DateTimeOffset?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator System.DateTime?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator decimal?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator double?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator System.Guid?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator short?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator int?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator long?(System.Text.Json.Node.JsonNode? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static explicit operator sbyte?(System.Text.Json.Node.JsonNode? value) { throw null; } + public static explicit operator float?(System.Text.Json.Node.JsonNode? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static explicit operator ushort?(System.Text.Json.Node.JsonNode? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static explicit operator uint?(System.Text.Json.Node.JsonNode? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static explicit operator ulong?(System.Text.Json.Node.JsonNode? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static explicit operator sbyte(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator float(System.Text.Json.Node.JsonNode value) { throw null; } + public static explicit operator string?(System.Text.Json.Node.JsonNode? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static explicit operator ushort(System.Text.Json.Node.JsonNode value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static explicit operator uint(System.Text.Json.Node.JsonNode value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static explicit operator ulong(System.Text.Json.Node.JsonNode value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(bool value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(byte value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(char value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(System.DateTime value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(System.DateTimeOffset value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(decimal value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(double value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(System.Guid value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(short value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(int value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(long value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(bool? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(byte? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(char? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(System.DateTimeOffset? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(System.DateTime? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(decimal? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(double? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(System.Guid? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(short? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(int? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(long? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static implicit operator System.Text.Json.Node.JsonNode?(sbyte? value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(float? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static implicit operator System.Text.Json.Node.JsonNode?(ushort? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static implicit operator System.Text.Json.Node.JsonNode?(uint? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static implicit operator System.Text.Json.Node.JsonNode?(ulong? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static implicit operator System.Text.Json.Node.JsonNode(sbyte value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode(float value) { throw null; } + public static implicit operator System.Text.Json.Node.JsonNode?(string? value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static implicit operator System.Text.Json.Node.JsonNode(ushort value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static implicit operator System.Text.Json.Node.JsonNode(uint value) { throw null; } + [System.CLSCompliantAttribute(false)] + public static implicit operator System.Text.Json.Node.JsonNode(ulong value) { throw null; } + public static System.Text.Json.Node.JsonNode? Parse(string json, System.Text.Json.Node.JsonNodeOptions? nodeOptions = default(System.Text.Json.Node.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; } + public static System.Text.Json.Node.JsonNode? Parse(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.Node.JsonNodeOptions? nodeOptions = default(System.Text.Json.Node.JsonNodeOptions?)) { throw null; } + public static System.Text.Json.Node.JsonNode? Parse(System.IO.Stream utf8Json, System.Text.Json.Node.JsonNodeOptions? nodeOptions = default(System.Text.Json.Node.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; } + public static System.Text.Json.Node.JsonNode? Parse(System.ReadOnlySpan utf8Json, System.Text.Json.Node.JsonNodeOptions? nodeOptions = default(System.Text.Json.Node.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions)) { throw null; } + public string ToJsonString(System.Text.Json.JsonSerializerOptions? options = null) { throw null; } + public override string ToString() { throw null; } + public abstract void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.Text.Json.JsonSerializerOptions? options = null); + } + public partial struct JsonNodeOptions + { + private int _dummyPrimitive; + public bool PropertyNameCaseInsensitive { readonly get { throw null; } set { } } + } + public sealed partial class JsonObject : System.Text.Json.Node.JsonNode, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public JsonObject(System.Text.Json.Node.JsonNodeOptions? options = default(System.Text.Json.Node.JsonNodeOptions?)) { } + public JsonObject(System.Collections.Generic.IEnumerable> properties, System.Text.Json.Node.JsonNodeOptions? options = default(System.Text.Json.Node.JsonNodeOptions?)) { } + public int Count { get { throw null; } } + public static System.Text.Json.Node.JsonObject? Create(System.Text.Json.JsonElement element, System.Text.Json.Node.JsonNodeOptions? options = default(System.Text.Json.Node.JsonNodeOptions?)) { throw null; } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + public void Add(System.Collections.Generic.KeyValuePair property) { } + public void Add(string propertyName, System.Text.Json.Node.JsonNode? value) { } + public void Clear() { } + public bool ContainsKey(string propertyName) { throw null; } + public System.Collections.Generic.IEnumerator> GetEnumerator() { throw null; } + public bool Remove(string propertyName) { throw null; } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int index) { } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.IDictionary.TryGetValue(string propertyName, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] 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) { } + } + public abstract partial class JsonValue : System.Text.Json.Node.JsonNode + { + private protected JsonValue(JsonNodeOptions? options = null) { throw null; } + public static System.Text.Json.Node.JsonValue? Create(T value, System.Text.Json.Node.JsonNodeOptions? options = default(System.Text.Json.Node.JsonNodeOptions?)) { throw null; } + public abstract bool TryGetValue<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]T>([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out T? value); + } +} namespace System.Text.Json.Serialization { public abstract partial class JsonAttribute : System.Attribute @@ -556,6 +718,11 @@ public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy? namingPolicy = public override bool CanConvert(System.Type typeToConvert) { throw null; } public override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; } } + public enum JsonUnknownTypeHandling + { + JsonElement = 0, + JsonNode = 1, + } public abstract partial class ReferenceHandler { protected ReferenceHandler() { } diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.csproj b/src/libraries/System.Text.Json/ref/System.Text.Json.csproj index d041bece0a671..7bb438f0863e5 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.csproj @@ -6,6 +6,9 @@ + + + @@ -13,6 +16,7 @@ + diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 47c651107dfa3..b67fca1c899a7 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -560,4 +560,34 @@ The converter '{0}' cannot return an instance of JsonConverterFactory. + + The result type '{0}' of the dynamic binding produced by the object with type '{1}' for the binder '{2}' is not compatible with the result type '{3}' expected by the call site. + + + The element must be of type '{0}' + + + The element cannot be an object or array. + + + The node already has a parent. + + + A node cycle was detected. + + + A value of type '{0}' cannot be converted to a '{1}'. + + + An element of type '{0}' cannot be converted to a '{1}'. + + + A JsonNode cannot be used as a value. + + + The node must be of type '{0}'. + + + Value cannot be null. (Parameter '{0}') + diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index c84780fae369a..beecf2e976166 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -44,6 +44,20 @@ + + + + + + + + + + + + + + @@ -95,6 +109,11 @@ + + + + + @@ -169,6 +188,7 @@ + @@ -189,6 +209,7 @@ + @@ -259,6 +280,9 @@ + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs index e81c52ea548df..8df0c30e76b7e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.Parse.cs @@ -137,6 +137,39 @@ public static JsonDocument Parse(Stream utf8Json, JsonDocumentOptions options = } } + internal static JsonDocument ParseValue(Stream utf8Json, JsonDocumentOptions options) + { + Debug.Assert(utf8Json != null); + + ArraySegment drained = ReadToEnd(utf8Json); + Debug.Assert(drained.Array != null); + + byte[] owned = new byte[drained.Count]; + Buffer.BlockCopy(drained.Array, 0, owned, 0, drained.Count); + + // Holds document content, clear it before returning it. + drained.AsSpan().Clear(); + ArrayPool.Shared.Return(drained.Array); + + return ParseUnrented(owned.AsMemory(), options.GetReaderOptions()); + } + + internal static JsonDocument ParseValue(ReadOnlySpan utf8Json, JsonDocumentOptions options) + { + Debug.Assert(utf8Json != null); + + byte[] owned = new byte[utf8Json.Length]; + utf8Json.CopyTo(owned); + + return ParseUnrented(owned.AsMemory(), options.GetReaderOptions()); + } + + internal static JsonDocument ParseValue(string json, JsonDocumentOptions options) + { + Debug.Assert(json != null); + return ParseValue(json.AsMemory(), options); + } + /// /// Parse a as UTF-8-encoded data representing a single JSON value into a /// JsonDocument. The Stream will be read to completion. @@ -230,6 +263,31 @@ public static JsonDocument Parse(ReadOnlyMemory json, JsonDocumentOptions } } + internal static JsonDocument ParseValue(ReadOnlyMemory json, JsonDocumentOptions options) + { + ReadOnlySpan jsonChars = json.Span; + int expectedByteCount = JsonReaderHelper.GetUtf8ByteCount(jsonChars); + byte[] owned; + byte[] utf8Bytes = ArrayPool.Shared.Rent(expectedByteCount); + + try + { + int actualByteCount = JsonReaderHelper.GetUtf8FromText(jsonChars, utf8Bytes); + Debug.Assert(expectedByteCount == actualByteCount); + + owned = new byte[actualByteCount]; + Buffer.BlockCopy(utf8Bytes, 0, owned, 0, actualByteCount); + } + finally + { + // Holds document content, clear it before returning it. + utf8Bytes.AsSpan(0, expectedByteCount).Clear(); + ArrayPool.Shared.Return(utf8Bytes); + } + + return ParseUnrented(owned.AsMemory(), options.GetReaderOptions()); + } + /// /// Parse text representing a single JSON value into a JsonDocument. /// @@ -639,7 +697,7 @@ private static JsonDocument Parse( private static JsonDocument ParseUnrented( ReadOnlyMemory utf8Json, JsonReaderOptions readerOptions, - JsonTokenType tokenType) + JsonTokenType tokenType = JsonTokenType.None) { // These tokens should already have been processed. Debug.Assert( diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.Parse.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.Parse.cs index b02d6fa893691..d96fdc45412df 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.Parse.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.Parse.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; namespace System.Text.Json { @@ -52,6 +53,24 @@ public static JsonElement ParseValue(ref Utf8JsonReader reader) return document.RootElement; } + internal static JsonElement ParseValue(Stream utf8Json, JsonDocumentOptions options) + { + JsonDocument document = JsonDocument.ParseValue(utf8Json, options); + return document.RootElement; + } + + internal static JsonElement ParseValue(ReadOnlySpan utf8Json, JsonDocumentOptions options) + { + JsonDocument document = JsonDocument.ParseValue(utf8Json, options); + return document.RootElement; + } + + internal static JsonElement ParseValue(string json, JsonDocumentOptions options) + { + JsonDocument document = JsonDocument.ParseValue(json, options); + return document.RootElement; + } + /// /// Attempts to parse one JSON value (including objects or arrays) from the provided reader. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs new file mode 100644 index 0000000000000..70b3b8f204c57 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs @@ -0,0 +1,165 @@ +// 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 sealed partial class JsonArray : JsonNode, IList + { + /// + /// Gets the number of elements contained in the . + /// + public int Count => List.Count; + + /// + /// Adds a to the end of the . + /// + /// + /// The to be added to the end of the . + /// + public void Add(JsonNode? item) + { + if (item != null) + { + item.AssignParent(this); + } + + List.Add(item); + } + + /// + /// Removes all elements from the . + /// + public void Clear() + { + for (int i = 0; i < List.Count; i++) + { + DetachParent(List[i]); + } + + List.Clear(); + } + + /// + /// Determines whether an element is in the . + /// + /// The object to locate in the . + /// + /// if is found in the ; otherwise, . + /// + public bool Contains(JsonNode? item) => List.Contains(item); + + /// + /// Returns the zero-based index of the first occurrence of a in the or in a portion of it. + /// + /// The to locate in the . + /// + /// The zero-based index of the first occurrence of within the range of elements in the + /// that extends from to the last element, if found; otherwise, -1. + /// + public int IndexOf(JsonNode? item) => List.IndexOf(item); + + /// + /// Inserts an element into the at the specified index. + /// + /// The zero-based index at which should be inserted. + /// The to insert. + /// + /// is less than 0 or is greater then . + /// + public void Insert(int index, JsonNode? item) + { + item?.AssignParent(this); + List.Insert(index, item); + } + + /// + /// Removes the first occurrence of a specific from the . + /// + /// + /// The to remove from the . + /// + /// + /// if item is successfully removed; otherwise, . + /// + public bool Remove(JsonNode? item) + { + if (List.Remove(item)) + { + DetachParent(item); + return true; + } + + return false; + } + + /// + /// Removes the element at the specified index of the . + /// + /// The zero-based index of the element to remove. + /// + /// is less than 0 or is greater then . + /// + public void RemoveAt(int index) + { + JsonNode? item = List[index]; + List.RemoveAt(index); + DetachParent(item); + } + + #region Explicit interface implementation + + /// + /// Copies the entire to a compatible one-dimensional array, + /// starting at the specified index of the target array. + /// + /// + /// The one-dimensional that is the destination of the elements copied + /// from . The Array must have zero-based indexing. + /// + /// The zero-based index in at which copying begins. + /// + /// + /// is . + /// + /// + /// is less than 0. + /// + /// + /// The number of elements in the source ICollection is greater than the available space from + /// to the end of the destination . + /// + void ICollection.CopyTo(JsonNode?[] array, int index) => List.CopyTo(array, index); + + /// + /// Returns an enumerator that iterates through the . + /// + /// A for the . + public IEnumerator GetEnumerator() => List.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through the . + /// + /// + /// A for the . + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)List).GetEnumerator(); + + /// + /// Returns . + /// + bool ICollection.IsReadOnly => false; + + #endregion + + private void DetachParent(JsonNode? item) + { + if (item != null) + { + item.Parent = null; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.cs new file mode 100644 index 0000000000000..7e324e025cfbc --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.cs @@ -0,0 +1,270 @@ +// 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; +using System.Text.Json.Serialization.Converters; + +namespace System.Text.Json.Node +{ + /// + /// Represents a mutable JSON array. + /// + [DebuggerDisplay("JsonArray[{List.Count}]")] + [DebuggerTypeProxy(typeof(DebugView))] + public sealed partial class JsonArray : JsonNode + { + private JsonElement? _jsonElement; + private List? _list; + + /// + /// Initializes a new instance of the class that is empty. + /// + /// Options to control the behavior. + public JsonArray(JsonNodeOptions? options = null) : base(options) { } + + /// + /// Initializes a new instance of the class that contains items from the specified params array. + /// + /// Options to control the behavior. + /// The items to add to the new . + public JsonArray(JsonNodeOptions options, params JsonNode?[] items) : base(options) + { + InitializeFromArray(items); + } + + /// + /// Initializes a new instance of the class that contains items from the specified array. + /// + /// The items to add to the new . + public JsonArray(params JsonNode?[] items) : base() + { + InitializeFromArray(items); + } + + private void InitializeFromArray(JsonNode?[] items) + { + var list = new List(items); + + for (int i = 0; i < items.Length; i++) + { + items[i]?.AssignParent(this); + } + + _list = list; + } + + /// + /// Initializes a new instance of the class that contains items from the specified . + /// + /// + /// The new instance of the class that contains items from the specified . + /// + /// The . + /// Options to control the behavior. + /// + /// The is not a . + /// + public static JsonArray? Create(JsonElement element, JsonNodeOptions? options = null) + { + if (element.ValueKind == JsonValueKind.Null) + { + return null; + } + + if (element.ValueKind == JsonValueKind.Array) + { + return new JsonArray(element, options); + } + + throw new InvalidOperationException(SR.Format(SR.NodeElementWrongType, nameof(JsonValueKind.Array))); + } + + internal JsonArray (JsonElement element, JsonNodeOptions? options = null) : base(options) + { + Debug.Assert(element.ValueKind == JsonValueKind.Array); + _jsonElement = element; + } + + /// + /// Adds an object to the end of the . + /// + /// The type of object to be added. + /// + /// The object to be added to the end of the . + /// + public void Add(T? value) + { + if (value == null) + { + Add(null); + } + else + { + JsonNode? jNode = value as JsonNode; + if (jNode == null) + { + jNode = new JsonValue(value); + } + + // Call the IList.Add() implementation. + Add(jNode); + } + } + + internal List List + { + get + { + CreateNodes(); + Debug.Assert(_list != null); + return _list; + } + } + + internal JsonNode? GetItem(int index) + { + return List[index]; + } + + internal void SetItem(int index, JsonNode? value) + { + value?.AssignParent(this); + List[index] = value; + } + + internal override void GetPath(List path, JsonNode? child) + { + if (child != null) + { + int index = List.IndexOf(child); + path.Add($"[{index}]"); + } + + Parent?.GetPath(path, this); + } + + /// + public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_jsonElement.HasValue) + { + _jsonElement.Value.WriteTo(writer); + } + else + { + Debug.Assert(_list != null); + + options ??= JsonSerializerOptions.s_defaultOptions; + + writer.WriteStartArray(); + + for (int i = 0; i < _list.Count; i++) + { + JsonNodeConverter.Default.Write(writer, _list[i]!, options); + } + + writer.WriteEndArray(); + } + } + + private void CreateNodes() + { + if (_list == null) + { + List list; + + if (_jsonElement == null) + { + list = new List(); + } + else + { + JsonElement jElement = _jsonElement.Value; + Debug.Assert(jElement.ValueKind == JsonValueKind.Array); + + list = new List(jElement.GetArrayLength()); + + foreach (JsonElement element in jElement.EnumerateArray()) + { + list.Add(JsonNodeConverter.Create(element, Options)); + } + + // Clear since no longer needed. + _jsonElement = null; + } + + _list = list; + } + } + + [ExcludeFromCodeCoverage] // Justification = "Design-time" + private class DebugView + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private JsonArray _node; + + public DebugView(JsonArray node) + { + _node = node; + } + + public string Json => _node.ToJsonString(); + public string Path => _node.GetPath(); + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + private DebugViewItem[] Items + { + get + { + DebugViewItem[] properties = new DebugViewItem[_node.List.Count]; + + for (int i = 0; i < _node.List.Count; i++) + { + properties[i].Value = _node.List[i]; + } + + return properties; + } + } + + [DebuggerDisplay("{Display,nq}")] + private struct DebugViewItem + { + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public JsonNode? Value; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public string Display + { + get + { + if (Value == null) + { + return $"null"; + } + + if (Value is JsonValue) + { + return Value.ToJsonString(); + } + + if (Value is JsonObject jsonObject) + { + return $"JsonObject[{jsonObject.Dictionary.Count}]"; + } + + JsonArray jsonArray = (JsonArray)Value; + return $"JsonArray[{jsonArray.List.Count}]"; + } + } + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Dynamic.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Dynamic.cs new file mode 100644 index 0000000000000..89fd661477289 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Dynamic.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Dynamic; +using System.Linq.Expressions; +using System.Reflection; + +namespace System.Text.Json.Node +{ + public partial class JsonNode : IDynamicMetaObjectProvider + { + internal virtual MethodInfo? TryGetMemberMethodInfo => null; + internal virtual MethodInfo? TrySetMemberMethodInfo => null; + + DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter) => + new MetaDynamic(parameter, this); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Operators.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Operators.cs new file mode 100644 index 0000000000000..977489cc2fd5c --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Operators.cs @@ -0,0 +1,420 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Node +{ + public partial class JsonNode + { + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(bool value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(bool? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(byte value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(byte? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(char value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(char? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(DateTime value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(DateTime? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(DateTimeOffset value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(DateTimeOffset? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(decimal value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(decimal? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(double value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(double? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(Guid value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(Guid? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(short value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(short? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(int value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(int? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(long value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(long? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static implicit operator JsonNode(sbyte value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static implicit operator JsonNode?(sbyte? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(float value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(float? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(string? value) => (value == null ? null : new JsonValue(value)); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static implicit operator JsonNode(ushort value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static implicit operator JsonNode?(ushort? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static implicit operator JsonNode(uint value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static implicit operator JsonNode?(uint? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static implicit operator JsonNode(ulong value) => new JsonValue(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static implicit operator JsonNode?(ulong? value) => value.HasValue ? new JsonValue(value.Value) : null; + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator bool(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator bool?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator byte(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator byte?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator char(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator char?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator DateTime(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator DateTime?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator DateTimeOffset(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator DateTimeOffset?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator decimal(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator decimal?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator double(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator double?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator Guid(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator Guid?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator short(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator short?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator int(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator int?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator long(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator long?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static explicit operator sbyte(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static explicit operator sbyte?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator float(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator float?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator string?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static explicit operator ushort(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static explicit operator ushort?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static explicit operator uint(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static explicit operator uint?(JsonNode? value) => value?.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static explicit operator ulong(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + [System.CLSCompliantAttribute(false)] + public static explicit operator ulong?(JsonNode? value) => value?.GetValue(); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Parse.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Parse.cs new file mode 100644 index 0000000000000..8210c4c87f7f1 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.Parse.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Text.Json.Serialization.Converters; + +namespace System.Text.Json.Node +{ + public abstract partial class JsonNode + { + /// + /// Parses one JSON value (including objects or arrays) from the provided reader. + /// + /// The reader to read. + /// Options to control the behavior. + /// + /// The from the reader. + /// + /// + /// + /// If the property of + /// is or , the + /// reader will be advanced by one call to to determine + /// the start of the value. + /// + /// + /// Upon completion of this method will be positioned at the + /// final token in the JSON value. If an exception is thrown, the reader is reset to the state it was in when the method was called. + /// + /// + /// This method makes a copy of the data the reader acted on, so there is no caller + /// requirement to maintain data integrity beyond the return of this method. + /// + /// + /// + /// is using unsupported options. + /// + /// + /// The current token does not start or represent a value. + /// + /// + /// A value could not be read from the reader. + /// + public static JsonNode? Parse( + ref Utf8JsonReader reader, + JsonNodeOptions? nodeOptions = null) + { + JsonElement element = JsonElement.ParseValue(ref reader); + return JsonNodeConverter.Create(element, nodeOptions); + } + + /// + /// Parse text representing a single JSON value. + /// + /// JSON text to parse. + /// Options to control the node behavior after parsing. + /// Options to control the document behavior during parsing. + /// + /// A representation of the JSON value. + /// + /// + /// is . + /// + /// + /// does not represent a valid single JSON value. + /// + public static JsonNode? Parse( + string json, + JsonNodeOptions? nodeOptions = null, + JsonDocumentOptions documentOptions = default(JsonDocumentOptions)) + { + if (json == null) + { + throw new ArgumentNullException(nameof(json)); + } + + JsonElement element = JsonElement.ParseValue(json, documentOptions); + return JsonNodeConverter.Create(element, nodeOptions); + } + + /// + /// Parse text representing a single JSON value. + /// + /// JSON text to parse. + /// Options to control the node behavior after parsing. + /// Options to control the document behavior during parsing. + /// + /// A representation of the JSON value. + /// + /// + /// does not represent a valid single JSON value. + /// + public static JsonNode? Parse( + ReadOnlySpan utf8Json, + JsonNodeOptions? nodeOptions = null, + JsonDocumentOptions documentOptions = default(JsonDocumentOptions)) + { + JsonElement element = JsonElement.ParseValue(utf8Json, documentOptions); + return JsonNodeConverter.Create(element, nodeOptions); + } + + /// + /// Parse a as UTF-8-encoded data representing a single JSON value into a + /// . The Stream will be read to completion. + /// + /// JSON text to parse. + /// Options to control the node behavior after parsing. + /// Options to control the document behavior during parsing. + /// + /// A representation of the JSON value. + /// + /// + /// does not represent a valid single JSON value. + /// + public static JsonNode? Parse( + Stream utf8Json, + JsonNodeOptions? nodeOptions = null, + JsonDocumentOptions documentOptions = default) + { + if (utf8Json == null) + { + throw new ArgumentNullException(nameof(utf8Json)); + } + + JsonElement element = JsonElement.ParseValue(utf8Json, documentOptions); + return JsonNodeConverter.Create(element, nodeOptions); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.To.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.To.cs new file mode 100644 index 0000000000000..ad6fe42766233 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.To.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace System.Text.Json.Node +{ + public abstract partial class JsonNode + { + /// + /// Converts the current instance to string in JSON format. + /// + /// Options to control the serialization behavior. + /// JSON representation of current instance. + public string ToJsonString(JsonSerializerOptions? options = null) + { + var output = new ArrayBufferWriter(); + using (var writer = new Utf8JsonWriter(output, options == null ? default(JsonWriterOptions) : options.GetWriterOptions())) + { + WriteTo(writer, options); + } + return JsonHelpers.Utf8GetString(output.WrittenSpan); + } + + /// + /// Gets a string representation for the current value appropriate to the node type. + /// + /// A string representation for the current value appropriate to the node type. + public override string ToString() + { + // Special case for string; don't quote it. + if (this is JsonValue) + { + if (this is JsonValue jsonString) + { + return jsonString.Value; + } + + if (this is JsonValue jsonElement && + jsonElement.Value.ValueKind == JsonValueKind.String) + { + return jsonElement.Value.GetString()!; + } + } + + var options = new JsonWriterOptions { Indented = true }; + var output = new ArrayBufferWriter(); + + using (var writer = new Utf8JsonWriter(output, options)) + { + WriteTo(writer); + } + + return JsonHelpers.Utf8GetString(output.WrittenSpan); + } + + /// + /// Write the into the provided as JSON. + /// + /// The . + /// + /// The parameter is . + /// + /// Options to control the serialization behavior. + public abstract void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.cs new file mode 100644 index 0000000000000..330ea8bc21c1c --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.cs @@ -0,0 +1,248 @@ +// 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.CodeAnalysis; + +namespace System.Text.Json.Node +{ + /// + /// The base class that represents a single node within a mutable JSON document. + /// + public abstract partial class JsonNode + { + internal const DynamicallyAccessedMemberTypes MembersAccessedOnRead = + DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields; + + private JsonNode? _parent; + private JsonNodeOptions? _options; + + /// + /// Options to control the behavior. + /// + public JsonNodeOptions? Options + { + get + { + if (!_options.HasValue && Parent != null) + { + return Parent.Options; + } + + return _options; + } + + private set + { + _options = value; + } + } + + internal JsonNode(JsonNodeOptions? options = null) + { + Options = options; + } + + /// + /// Casts to the derived type. + /// + /// + /// A . + /// + /// + /// The node is not a . + /// + public JsonArray AsArray() + { + if (this is JsonArray jArray) + { + return jArray; + } + + throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonArray))); + } + + /// + /// Casts to the derived type. + /// + /// + /// A . + /// + /// + /// The node is not a . + /// + public JsonObject AsObject() + { + if (this is JsonObject jObject) + { + return jObject; + } + + throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonObject))); + } + + /// + /// Casts to the derived type. + /// + /// + /// A . + /// + /// + /// The node is not a . + /// + public JsonValue AsValue() + { + if (this is JsonValue jValue) + { + return jValue; + } + + throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonValue))); + } + + /// + /// Gets the parent . + /// If there is no parent, is returned. + /// A parent can either be a or a . + /// + public JsonNode? Parent + { + get + { + return _parent; + } + internal set + { + _parent = value; + } + } + + /// + /// Gets the JSON path. + /// + /// The JSON Path value. + public string GetPath() + { + if (Parent == null) + { + return "$"; + } + + var path = new List(); + GetPath(path, null); + + var sb = new StringBuilder("$"); + for (int i = path.Count - 1; i >= 0; i--) + { + sb.Append(path[i]); + } + + return sb.ToString(); + } + + internal abstract void GetPath(List path, JsonNode? child); + + /// + /// Gets the root . + /// If the current is a root, is returned. + /// + public JsonNode Root + { + get + { + JsonNode? parent = Parent; + if (parent == null) + { + return this; + } + + while (parent.Parent != null) + { + parent = parent.Parent; + } + + return parent; + } + } + + /// + /// Gets the value for the current . + /// + /// + /// The current cannot be represented as a {TValue}. + /// + /// + /// The current is not a or + /// is not compatible with {TValue}. + /// + public virtual TValue GetValue<[DynamicallyAccessedMembers(MembersAccessedOnRead)] TValue>() => + throw new InvalidOperationException(SR.Format(SR.NodeWrongType, nameof(JsonValue))); + + /// + /// Gets or sets the element at the specified index. + /// + /// The zero-based index of the element to get or set. + /// + /// is less than 0 or is greater then the number of properties. + /// + /// + /// The current is not a . + /// + public JsonNode? this[int index] + { + get + { + return AsArray().GetItem(index); + } + set + { + AsArray().SetItem(index, value); + } + } + + /// + /// Gets or sets the element with the specified property name. + /// If the property is not found, is returned. + /// + /// The name of the property to return. + /// + /// is . + /// + /// + /// The current is not a . + /// + public JsonNode? this[string propertyName] + { + get + { + return AsObject().GetItem(propertyName); + } + + set + { + AsObject().SetItem(propertyName, value); + } + } + + 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; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNodeOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNodeOptions.cs new file mode 100644 index 0000000000000..a0f1750945e29 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNodeOptions.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Node +{ + /// + /// Options to control behavior. + /// + public struct JsonNodeOptions + { + /// + /// Specifies whether property names on are case insensitive. + /// + public bool PropertyNameCaseInsensitive { get; set; } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.Dynamic.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.Dynamic.cs new file mode 100644 index 0000000000000..41fb9f0970553 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.Dynamic.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Dynamic; +using System.Reflection; + +namespace System.Text.Json.Node +{ + public partial class JsonObject + { + internal bool TryGetMemberCallback(GetMemberBinder binder, out object? result) + { + if (Dictionary.TryGetValue(binder.Name, out JsonNode? node)) + { + result = node; + return true; + } + + // Return null for missing properties. + result = null; + return true; + } + + internal bool TrySetMemberCallback(SetMemberBinder binder, object? value) + { + JsonNode? node = null; + if (value != null) + { + node = value as JsonNode; + if (node == null) + { + node = new JsonValue(value, Options); + } + } + + Dictionary[binder.Name] = node; + return true; + } + + private static MethodInfo GetMethod(string name) => typeof(JsonObject).GetMethod( + name, BindingFlags.Instance | BindingFlags.NonPublic)!; + + private static MethodInfo? s_TryGetMember; + internal override MethodInfo? TryGetMemberMethodInfo => + s_TryGetMember ?? + (s_TryGetMember = GetMethod(nameof(TryGetMemberCallback))); + + private static MethodInfo? s_TrySetMember; + internal override MethodInfo? TrySetMemberMethodInfo => + s_TrySetMember ?? + (s_TrySetMember = GetMethod(nameof(TrySetMemberCallback))); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs new file mode 100644 index 0000000000000..58dd99f8584b0 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs @@ -0,0 +1,244 @@ +// 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; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.Json.Node +{ + public partial class JsonObject : IDictionary + { + /// + /// Adds an element with the provided property name and value to the . + /// + /// The property name of the element to add. + /// The value of the element to add. + /// + /// is . + /// + /// + /// An element with the same property name already exists in the . + /// + public void Add(string propertyName, JsonNode? value) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + value?.AssignParent(this); + Dictionary.Add(propertyName, value); + _lastKey = propertyName; + _lastValue = value; + } + + /// + /// Adds the specified property to the . + /// + /// + /// The KeyValuePair structure representing the property name and value to add to the . + /// + /// + /// The property name of is . + /// + public void Add(KeyValuePair property) + { + if (property.Key == null) + { + ThrowHelper.ThrowArgumentNullException_ValueCannotBeNull("propertyName"); + } + + Dictionary.Add(property); + JsonNode? value = property.Value; + value?.AssignParent(this); + } + + /// + /// Removes all elements from the . + /// + public void Clear() + { + foreach (JsonNode? node in Dictionary.Values) + { + DetachParent(node); + } + + Dictionary.Clear(); + } + + /// + /// Determines whether the contains an element with the specified property name. + /// + /// The property name to locate in the . + /// + /// if the contains an element with the specified property name; otherwise, . + /// + /// + /// is . + /// + public bool ContainsKey(string propertyName) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + return Dictionary.ContainsKey(propertyName); + } + + /// + /// Gets the number of elements contained in . + /// + public int Count => Dictionary.Count; + + /// + /// Removes the element with the specified property name from the . + /// + /// The property name of the value to get. + /// + /// if the element is successfully removed; otherwise, . + /// + /// + /// is . + /// + public bool Remove(string propertyName) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + if (!Dictionary.TryGetValue(propertyName, out JsonNode? item)) + { + return false; + } + + bool success = Dictionary.Remove(propertyName); + Debug.Assert(success); + DetachParent(item); + return true; + } + + #region Explicit interface implementation + /// + /// Determines whether the contains a specific property name and value. + /// + /// The element to locate in the . + /// + /// if the contains an element with the property name; otherwise, . + /// + bool ICollection>.Contains(KeyValuePair item) => Dictionary.Contains(item); + + /// + /// Copies the elements of the to an array of type KeyValuePair starting at the specified array index. + /// + /// + /// The one-dimensional Array that is the destination of the elements copied from . + /// + /// The zero-based index in at which copying begins. + /// + /// is . + /// + /// + /// is less than 0. + /// + /// + /// The number of elements in the source ICollection is greater than the available space from + /// to the end of the destination . + /// + void ICollection>.CopyTo(KeyValuePair[] array, int index) => + Dictionary.CopyTo(array, index); + + /// + /// Returns an enumerator that iterates through the . + /// + /// + /// An enumerator that iterates through the . + /// + public IEnumerator> GetEnumerator() => Dictionary.GetEnumerator(); + + /// + /// Removes a key and value from the . + /// + /// + /// The KeyValuePair structure representing the property name and value to remove from the . + /// + /// + /// if the element is successfully removed; otherwise, . + /// + bool ICollection>.Remove(KeyValuePair item) + { + if (Dictionary.Remove(item)) + { + JsonNode? node = item.Value; + DetachParent(node); + return true; + } + + return false; + } + + /// + /// Gets a collection containing the property names in the . + /// + ICollection IDictionary.Keys => Dictionary.Keys; + + /// + /// Gets a collection containing the property values in the . + /// + ICollection IDictionary.Values => Dictionary.Values; + + /// + /// Gets the value associated with the specified property name. + /// + /// The property name of the value to get. + /// + /// When this method returns, contains the value associated with the specified property name, if the property name is found; + /// otherwise, . + /// + /// + /// if the contains an element with the specified property name; otherwise, . + /// + /// + /// is . + /// + bool IDictionary.TryGetValue(string propertyName, [NotNullWhen(true)] out JsonNode? jsonNode) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + return Dictionary.TryGetValue(propertyName, out jsonNode); + } + + /// + /// Returns . + /// + bool ICollection>.IsReadOnly => false; + + /// + /// Returns an enumerator that iterates through . + /// + /// + /// An enumerator that iterates through the . + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Dictionary).GetEnumerator(); + + #endregion + + private void DetachParent(JsonNode? item) + { + if (item != null) + { + item.Parent = null; + } + + // Prevent previous child from being returned from these cached variables. + _lastKey = null; + _lastValue = null; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.cs new file mode 100644 index 0000000000000..e519d203f2dbc --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.cs @@ -0,0 +1,299 @@ +// 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; +using System.Text.Json.Serialization.Converters; + +namespace System.Text.Json.Node +{ + /// + /// Represents a mutable JSON object. + /// + [DebuggerDisplay("JsonObject[{Dictionary.Count}]")] + [DebuggerTypeProxy(typeof(DebugView))] + public sealed partial class JsonObject : JsonNode + { + private JsonElement? _jsonElement; + private IDictionary? _dictionary; + private string? _lastKey; + private JsonNode? _lastValue; + + /// + /// Initializes a new instance of the class that is empty. + /// + /// Options to control the behavior. + public JsonObject(JsonNodeOptions? options = null) : base(options) { } + + /// + /// Initializes a new instance of the class that contains properties from the specified . + /// + /// The properties to be added. + /// Options to control the behavior. + public JsonObject(IEnumerable> properties, JsonNodeOptions? options = null) + { + foreach (KeyValuePair node in properties) + { + Add(node.Key, node.Value); + } + } + + /// + /// Initializes a new instance of the class that contains properties from the specified . + /// + /// + /// The new instance of the class that contains properties from the specified . + /// + /// The . + /// Options to control the behavior. + /// A . + public static JsonObject? Create(JsonElement element, JsonNodeOptions? options = null) + { + if (element.ValueKind == JsonValueKind.Null) + { + return null; + } + + if (element.ValueKind == JsonValueKind.Object) + { + return new JsonObject(element, options); + } + + throw new InvalidOperationException(SR.Format(SR.NodeElementWrongType, nameof(JsonValueKind.Object))); + } + + /// + /// Returns the value of a property with the specified name. + /// + /// The name of the property to return. + /// The JSON value of the property with the specified name. + /// + /// if a property with the specified name was found; otherwise, . + /// + 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 override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_jsonElement.HasValue) + { + _jsonElement.Value.WriteTo(writer); + } + else + { + options ??= JsonSerializerOptions.s_defaultOptions; + + writer.WriteStartObject(); + + foreach (KeyValuePair kvp in Dictionary) + { + writer.WritePropertyName(kvp.Key); + JsonNodeConverter.Default.Write(writer, kvp.Value!, options); + } + + writer.WriteEndObject(); + } + } + + internal JsonObject(JsonElement element, JsonNodeOptions? options = null) : base(options) + { + Debug.Assert(element.ValueKind == JsonValueKind.Object); + _jsonElement = element; + } + + internal IDictionary Dictionary + { + get + { + CreateNodes(); + Debug.Assert(_dictionary != null); + return _dictionary; + } + } + + internal JsonNode? GetItem(string propertyName) + { + if (TryGetPropertyValue(propertyName, out JsonNode? value)) + { + return value; + } + + // Return null for missing properties. + return null; + } + + internal override void GetPath(List path, JsonNode? child) + { + if (child != null) + { + bool found = false; + + foreach (KeyValuePair kvp in Dictionary) + { + if (kvp.Value == child) + { + string propertyName = kvp.Key; + if (propertyName.IndexOfAny(ReadStack.SpecialCharacters) != -1) + { + path.Add($"['{propertyName}']"); + } + else + { + path.Add($".{propertyName}"); + } + + found = true; + break; + } + } + + Debug.Assert(found); + } + + if (Parent != null) + { + Parent.GetPath(path, this); + } + } + + internal void SetItem(string propertyName, JsonNode? value) + { + if (propertyName == null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + value?.AssignParent(this); + + Dictionary[propertyName] = value; + _lastKey = propertyName; + _lastValue = value; + } + + private void CreateNodes() + { + if (_dictionary == null) + { + bool caseInsensitive = false; + if (Options?.PropertyNameCaseInsensitive == true) + { + caseInsensitive = true; + } + + var dictionary = new Dictionary( + 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; + } + } + + [ExcludeFromCodeCoverage] // Justification = "Design-time" + private class DebugView + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private 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.Dictionary.Count]; + + int i = 0; + foreach (KeyValuePair property in _node.Dictionary) + { + properties[i].Value = property.Value; + properties[i].PropertyName = property.Key; + 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.Dictionary.Count}]"; + } + + JsonArray jsonArray = (JsonArray)Value; + return $"{PropertyName} = JsonArray[{jsonArray.List.Count}]"; + } + } + + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValue.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValue.cs new file mode 100644 index 0000000000000..17b19ecdd2d88 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValue.cs @@ -0,0 +1,68 @@ +// 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.Node +{ + /// + /// Represents a mutable JSON value. + /// + public abstract partial class JsonValue : JsonNode + { + private protected JsonValue(JsonNodeOptions? options = null) : base(options) { } + + /// + /// Initializes a new instance of the class that contains the specified value. + /// + /// + /// The new instance of the class that contains the specified value. + /// + /// The type of value to be added. + /// The value to add. + /// Options to control the behavior. + /// The new instance of the class that contains the specified value. + public static JsonValue? Create(T? value, JsonNodeOptions? options = null) + { + if (value == null) + { + return null; + } + + if (value is JsonElement element) + { + if (element.ValueKind == JsonValueKind.Null) + { + return null; + } + + if (element.ValueKind == JsonValueKind.Object || element.ValueKind == JsonValueKind.Array) + { + throw new InvalidOperationException(SR.NodeElementCannotBeObjectOrArray); + } + } + + return new JsonValue(value, options); + } + + internal override void GetPath(List path, JsonNode? child) + { + Debug.Assert(child == null); + + if (Parent != null) + { + Parent.GetPath(path, this); + } + } + + /// + /// Tries to obtain the current JSON value and returns a value that indicates whether the operation succeeded. + /// + /// The type of value to obtain. + /// When this method returns, contains the parsed value. + /// if the value can be successfully obtained; otherwise, . + public abstract bool TryGetValue<[DynamicallyAccessedMembers(MembersAccessedOnRead)]T>([NotNullWhen(true)] out T? value); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValueOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValueOfT.cs new file mode 100644 index 0000000000000..3f43c4bcd9de2 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValueOfT.cs @@ -0,0 +1,375 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.Json.Node +{ + [DebuggerDisplay("{ToJsonString(),nq}")] + [DebuggerTypeProxy(typeof(JsonValue<>.DebugView))] + internal sealed partial class JsonValue : JsonValue + { + internal readonly TValue _value; // keep as a field for direct access to avoid copies + + public JsonValue(TValue value, JsonNodeOptions? options = null) : base(options) + { + Debug.Assert(value != null); + Debug.Assert(!(value is JsonElement) || ((JsonElement)(object)value).ValueKind != JsonValueKind.Null); + + if (value is JsonNode) + { + ThrowHelper.ThrowArgumentException_NodeValueNotAllowed(nameof(value)); + } + + _value = value; + } + + public TValue Value + { + get + { + return _value; + } + } + + public override T GetValue<[DynamicallyAccessedMembers(MembersAccessedOnRead)] T>() + { + Type returnType = typeof(T); + + // If no conversion is needed, just return the raw value. + if (_value is T returnValue) + { + return returnValue; + } + + if (_value is JsonElement jsonElement) + { + return ConvertJsonElement(); + } + + // Currently we do not support other conversions. + // Generics (and also boxing) do not support standard cast operators say from 'long' to 'int', + // so attempting to cast here would throw InvalidCastException. + throw new InvalidOperationException(SR.Format(SR.NodeUnableToConvert, _value!.GetType(), typeof(T))); + } + + public override bool TryGetValue<[DynamicallyAccessedMembers(MembersAccessedOnRead)] T>([NotNullWhen(true)] out T value) + { + // If no conversion is needed, just return the raw value. + if (_value is T returnValue) + { + value = returnValue; + return true; + } + + if (_value is JsonElement jsonElement) + { + return TryConvertJsonElement(out value); + } + + // Currently we do not support other conversions. + // Generics (and also boxing) do not support standard cast operators say from 'long' to 'int', + // so attempting to cast here would throw InvalidCastException. + value = default!; + return false; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:ystem.Text.Json.Node.JsonValue.WriteTo(Utf8JsonWriter,JsonSerializerOptions): 'inputType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicFields', 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Text.Json.JsonSerializer.Serialize(Utf8JsonWriter,Object,Type,JsonSerializerOptions)'. The return value of method 'System.Object.GetType()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.", + Justification = "The 'inputType' parameter if obtained by calling System.Object.GetType().")] + public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_value is JsonElement jsonElement) + { + jsonElement.WriteTo(writer); + } + else + { + JsonSerializer.Serialize(writer, _value, _value!.GetType(), options); + } + } + + internal TypeToConvert ConvertJsonElement() + { + JsonElement element = (JsonElement)(object)_value!; + Type returnType = typeof(TypeToConvert); + Type? underlyingType = Nullable.GetUnderlyingType(returnType); + returnType = underlyingType ?? returnType; + + switch (element.ValueKind) + { + case JsonValueKind.Number: + if (returnType == typeof(int)) + { + return (TypeToConvert)(object)element.GetInt32(); + } + + if (returnType == typeof(long)) + { + return (TypeToConvert)(object)element.GetInt64(); + } + + if (returnType == typeof(double)) + { + return (TypeToConvert)(object)element.GetDouble(); + } + + if (returnType == typeof(short)) + { + return (TypeToConvert)(object)element.GetInt16(); + } + + if (returnType == typeof(decimal)) + { + return (TypeToConvert)(object)element.GetDecimal(); + } + + if (returnType == typeof(byte)) + { + return (TypeToConvert)(object)element.GetByte(); + } + + if (returnType == typeof(float)) + { + return (TypeToConvert)(object)element.GetSingle(); + } + + else if (returnType == typeof(uint)) + { + return (TypeToConvert)(object)element.GetUInt32(); + } + + if (returnType == typeof(ushort)) + { + return (TypeToConvert)(object)element.GetUInt16(); + } + + if (returnType == typeof(ulong)) + { + return (TypeToConvert)(object)element.GetUInt64(); + } + + if (returnType == typeof(sbyte)) + { + return (TypeToConvert)(object)element.GetSByte(); + } + break; + + case JsonValueKind.String: + if (returnType == typeof(string)) + { + return (TypeToConvert)(object)element.GetString()!; + } + + if (returnType == typeof(DateTime)) + { + return (TypeToConvert)(object)element.GetDateTime(); + } + + if (returnType == typeof(DateTimeOffset)) + { + return (TypeToConvert)(object)element.GetDateTimeOffset(); + } + + if (returnType == typeof(Guid)) + { + return (TypeToConvert)(object)element.GetGuid(); + } + + if (returnType == typeof(char)) + { + string? strResult = element.GetString(); + Debug.Assert(strResult != null); + if (strResult.Length >= 1) + { + return (TypeToConvert)(object)strResult[0]; + } + + throw ThrowHelper.GetFormatException(); + } + break; + + case JsonValueKind.True: + case JsonValueKind.False: + if (returnType == typeof(bool)) + { + return (TypeToConvert)(object)element.GetBoolean(); + } + break; + } + + throw new InvalidOperationException(SR.Format(SR.NodeUnableToConvertElement, + element.ValueKind, + typeof(TypeToConvert)) + ); + } + + internal bool TryConvertJsonElement([NotNullWhen(true)] out TypeToConvert result) + { + bool success; + + JsonElement element = (JsonElement)(object)_value!; + Type returnType = typeof(TypeToConvert); + Type? underlyingType = Nullable.GetUnderlyingType(returnType); + returnType = underlyingType ?? returnType; + + switch (element.ValueKind) + { + case JsonValueKind.Number: + if (returnType == typeof(int)) + { + success = element.TryGetInt32(out int value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(long)) + { + success = element.TryGetInt64(out long value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(double)) + { + success = element.TryGetDouble(out double value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(short)) + { + success = element.TryGetInt16(out short value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(decimal)) + { + success = element.TryGetDecimal(out decimal value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(byte)) + { + success = element.TryGetByte(out byte value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(float)) + { + success = element.TryGetSingle(out float value); + result = (TypeToConvert)(object)value; + return success; + } + + else if (returnType == typeof(uint)) + { + success = element.TryGetUInt32(out uint value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(ushort)) + { + success = element.TryGetUInt16(out ushort value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(ulong)) + { + success = element.TryGetUInt64(out ulong value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(sbyte)) + { + success = element.TryGetSByte(out sbyte value); + result = (TypeToConvert)(object)value; + return success; + } + break; + + case JsonValueKind.String: + if (returnType == typeof(string)) + { + string? strResult = element.GetString(); + Debug.Assert(strResult != null); + result = (TypeToConvert)(object)strResult; + return true; + } + + if (returnType == typeof(DateTime)) + { + success = element.TryGetDateTime(out DateTime value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(DateTimeOffset)) + { + success = element.TryGetDateTimeOffset(out DateTimeOffset value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(Guid)) + { + success = element.TryGetGuid(out Guid value); + result = (TypeToConvert)(object)value; + return success; + } + + if (returnType == typeof(char)) + { + string? strResult = element.GetString(); + Debug.Assert(strResult != null); + if (strResult.Length >= 1) + { + result = (TypeToConvert)(object)strResult[0]; + return true; + } + } + break; + + case JsonValueKind.True: + case JsonValueKind.False: + if (returnType == typeof(bool)) + { + result = (TypeToConvert)(object)element.GetBoolean(); + return true; + } + break; + } + + result = default!; + return false; + } + + [ExcludeFromCodeCoverage] // Justification = "Design-time" + [DebuggerDisplay("{Json,nq}")] + private class DebugView + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + public JsonValue _node; + + public DebugView(JsonValue node) + { + _node = node; + } + + public string Json => _node.ToJsonString(); + public string Path => _node.GetPath(); + public TValue? Value => _node.Value; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Node/MetaDynamic.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Node/MetaDynamic.cs new file mode 100644 index 0000000000000..b479a3895e962 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Node/MetaDynamic.cs @@ -0,0 +1,433 @@ +// 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.ObjectModel; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Dynamic; + +namespace System.Text.Json.Node +{ + // The bulk of this code was pulled from src/libraries/System.Linq.Expressions/src/System/Dynamic/DynamicObject.cs + // and then refactored. + internal sealed class MetaDynamic : DynamicMetaObject + { + private static readonly ConstantExpression NullExpression = Expression.Constant(null); + private static readonly DefaultExpression EmptyExpression = Expression.Empty(); + private static readonly ConstantExpression Int1Expression = Expression.Constant((object)1); + + private JsonNode Dynamic { get; } + internal MetaDynamic(Expression expression, JsonNode dynamicObject) + : base(expression, BindingRestrictions.Empty, dynamicObject) + { + Dynamic = dynamicObject; + } + + public override DynamicMetaObject BindGetMember(GetMemberBinder binder) + { + MethodInfo? methodInfo = Dynamic.TryGetMemberMethodInfo; + if (methodInfo == null) + { + return base.BindGetMember(binder); + } + + return CallMethodWithResult( + methodInfo, + binder, + s_noArgs, + (MetaDynamic @this, GetMemberBinder b, DynamicMetaObject? e) => b.FallbackGetMember(@this, e) + ); + } + + public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value) + { + MethodInfo? methodInfo = Dynamic.TrySetMemberMethodInfo; + if (methodInfo == null) + { + return base.BindSetMember(binder, value); + } + + DynamicMetaObject localValue = value; + + return CallMethodReturnLast( + methodInfo, + binder, + s_noArgs, + value.Expression, + (MetaDynamic @this, SetMemberBinder b, DynamicMetaObject? e) => b.FallbackSetMember(@this, localValue, e) + ); + } + + private delegate DynamicMetaObject Fallback(MetaDynamic @this, TBinder binder, DynamicMetaObject? errorSuggestion); + +#pragma warning disable CA1825 // used in reference comparison, requires unique object identity + private static readonly Expression[] s_noArgs = new Expression[0]; +#pragma warning restore CA1825 + + private static ReadOnlyCollection GetConvertedArgs(params Expression[] args) + { + var paramArgs = new Expression[args.Length]; + + for (int i = 0; i < args.Length; i++) + { + paramArgs[i] = Expression.Convert(args[i], typeof(object)); + } + + return new ReadOnlyCollection(paramArgs); + } + + /// + /// Helper method for generating expressions that assign byRef call + /// parameters back to their original variables. + /// + private static Expression ReferenceArgAssign(Expression callArgs, Expression[] args) + { + ReadOnlyCollectionBuilder? block = null; + + for (int i = 0; i < args.Length; i++) + { + ParameterExpression variable = (ParameterExpression)args[i]; + + if (variable.IsByRef) + { + if (block == null) + block = new ReadOnlyCollectionBuilder(); + + block.Add( + Expression.Assign( + variable, + Expression.Convert( + Expression.ArrayIndex( + callArgs, + Int1Expression + ), + variable.Type + ) + ) + ); + } + } + + if (block != null) + return Expression.Block(block); + else + return EmptyExpression; + } + + /// + /// Helper method for generating arguments for calling methods + /// on DynamicObject. parameters is either a list of ParameterExpressions + /// to be passed to the method as an object[], or NoArgs to signify that + /// the target method takes no object[] parameter. + /// + private static Expression[] BuildCallArgs(TBinder binder, Expression[] parameters, Expression arg0, Expression? arg1) + where TBinder : DynamicMetaObjectBinder + { + if (!ReferenceEquals(parameters, s_noArgs)) + return arg1 != null ? new Expression[] { Constant(binder), arg0, arg1 } : new Expression[] { Constant(binder), arg0 }; + else + return arg1 != null ? new Expression[] { Constant(binder), arg1 } : new Expression[] { Constant(binder) }; + } + + private static ConstantExpression Constant(TBinder binder) + { + return Expression.Constant(binder, typeof(TBinder)); + } + + /// + /// Helper method for generating a MetaObject which calls a + /// specific method on Dynamic that returns a result + /// + private DynamicMetaObject CallMethodWithResult(MethodInfo method, TBinder binder, Expression[] args, Fallback fallback) + where TBinder : DynamicMetaObjectBinder + { + return CallMethodWithResult(method, binder, args, fallback, null); + } + + /// + /// Helper method for generating a MetaObject which calls a + /// specific method on Dynamic that returns a result + /// + private DynamicMetaObject CallMethodWithResult(MethodInfo method, TBinder binder, Expression[] args, Fallback fallback, Fallback? fallbackInvoke) + where TBinder : DynamicMetaObjectBinder + { + // + // First, call fallback to do default binding + // This produces either an error or a call to a .NET member + // + DynamicMetaObject fallbackResult = fallback(this, binder, null); + + DynamicMetaObject callDynamic = BuildCallMethodWithResult(method, binder, args, fallbackResult, fallbackInvoke); + + // + // Now, call fallback again using our new MO as the error + // When we do this, one of two things can happen: + // 1. Binding will succeed, and it will ignore our call to + // the dynamic method, OR + // 2. Binding will fail, and it will use the MO we created + // above. + // + return fallback(this, binder, callDynamic); + } + + private DynamicMetaObject BuildCallMethodWithResult(MethodInfo method, TBinder binder, Expression[] args, DynamicMetaObject fallbackResult, Fallback? fallbackInvoke) + where TBinder : DynamicMetaObjectBinder + { + ParameterExpression result = Expression.Parameter(typeof(object), null); + ParameterExpression callArgs = Expression.Parameter(typeof(object[]), null); + ReadOnlyCollection callArgsValue = GetConvertedArgs(args); + + var resultMO = new DynamicMetaObject(result, BindingRestrictions.Empty); + + // Need to add a conversion if calling TryConvert + if (binder.ReturnType != typeof(object)) + { + Debug.Assert(binder is ConvertBinder && fallbackInvoke == null); + + UnaryExpression convert = Expression.Convert(resultMO.Expression, binder.ReturnType); + // will always be a cast or unbox + Debug.Assert(convert.Method == null); + + // Prepare a good exception message in case the convert will fail + string convertFailed = SR.Format(SR.NodeDynamicObjectResultNotAssignable, + "{0}", + this.Value.GetType(), + binder.GetType(), + binder.ReturnType + ); + + Expression condition; + // If the return type can not be assigned null then just check for type assignability otherwise allow null. + if (binder.ReturnType.IsValueType && Nullable.GetUnderlyingType(binder.ReturnType) == null) + { + condition = Expression.TypeIs(resultMO.Expression, binder.ReturnType); + } + else + { + condition = Expression.OrElse( + Expression.Equal(resultMO.Expression, NullExpression), + Expression.TypeIs(resultMO.Expression, binder.ReturnType)); + } + + Expression checkedConvert = Expression.Condition( + condition, + convert, + Expression.Throw( + Expression.New( + CachedReflectionInfo.InvalidCastException_Ctor_String, + new TrueReadOnlyCollection( + Expression.Call( + CachedReflectionInfo.String_Format_String_ObjectArray, + Expression.Constant(convertFailed), + Expression.NewArrayInit( + typeof(object), + new TrueReadOnlyCollection( + Expression.Condition( + Expression.Equal(resultMO.Expression, NullExpression), + Expression.Constant("null"), + Expression.Call( + resultMO.Expression, + CachedReflectionInfo.Object_GetType + ), + typeof(object) + ) + ) + ) + ) + ) + ), + binder.ReturnType + ), + binder.ReturnType + ); + + resultMO = new DynamicMetaObject(checkedConvert, resultMO.Restrictions); + } + + if (fallbackInvoke != null) + { + resultMO = fallbackInvoke(this, binder, resultMO); + } + + var callDynamic = new DynamicMetaObject( + Expression.Block( + new TrueReadOnlyCollection(result, callArgs), + new TrueReadOnlyCollection( + Expression.Assign(callArgs, Expression.NewArrayInit(typeof(object), callArgsValue)), + Expression.Condition( + Expression.Call( + GetLimitedSelf(), + method, + BuildCallArgs( + binder, + args, + callArgs, + result + ) + ), + Expression.Block( + ReferenceArgAssign(callArgs, args), + resultMO.Expression + ), + fallbackResult.Expression, + binder.ReturnType + ) + ) + ), + GetRestrictions().Merge(resultMO.Restrictions).Merge(fallbackResult.Restrictions) + ); + return callDynamic; + } + + private DynamicMetaObject CallMethodReturnLast(MethodInfo method, TBinder binder, Expression[] args, Expression value, Fallback fallback) + where TBinder : DynamicMetaObjectBinder + { + // + // First, call fallback to do default binding + // This produces either an error or a call to a .NET member + // + DynamicMetaObject fallbackResult = fallback(this, binder, null); + + // + // Build a new expression like: + // { + // object result; + // TrySetMember(payload, result = value) ? result : fallbackResult + // } + // + + ParameterExpression result = Expression.Parameter(typeof(object), null); + ParameterExpression callArgs = Expression.Parameter(typeof(object[]), null); + ReadOnlyCollection callArgsValue = GetConvertedArgs(args); + + var callDynamic = new DynamicMetaObject( + Expression.Block( + new TrueReadOnlyCollection(result, callArgs), + new TrueReadOnlyCollection( + Expression.Assign(callArgs, Expression.NewArrayInit(typeof(object), callArgsValue)), + Expression.Condition( + Expression.Call( + GetLimitedSelf(), + method, + BuildCallArgs( + binder, + args, + callArgs, + Expression.Assign(result, Expression.Convert(value, typeof(object))) + ) + ), + Expression.Block( + ReferenceArgAssign(callArgs, args), + result + ), + fallbackResult.Expression, + typeof(object) + ) + ) + ), + GetRestrictions().Merge(fallbackResult.Restrictions) + ); + + // + // Now, call fallback again using our new MO as the error + // When we do this, one of two things can happen: + // 1. Binding will succeed, and it will ignore our call to + // the dynamic method, OR + // 2. Binding will fail, and it will use the MO we created + // above. + // + return fallback(this, binder, callDynamic); + } + + /// + /// Returns a Restrictions object which includes our current restrictions merged + /// with a restriction limiting our type + /// + private BindingRestrictions GetRestrictions() + { + Debug.Assert(Restrictions == BindingRestrictions.Empty, "We don't merge, restrictions are always empty"); + + return GetTypeRestriction(this); + } + + /// + /// Returns our Expression converted to DynamicObject + /// + private Expression GetLimitedSelf() + { + // Convert to DynamicObject rather than LimitType, because + // the limit type might be non-public. + if (AreEquivalent(Expression.Type, Value.GetType())) + { + return Expression; + } + return Expression.Convert(Expression, Value.GetType()); + } + + private static bool AreEquivalent(Type? t1, Type? t2) => t1 != null && t1.IsEquivalentTo(t2); + + private new object Value => base.Value!; + + // It is okay to throw NotSupported from this binder. This object + // is only used by DynamicObject.GetMember--it is not expected to + // (and cannot) implement binding semantics. It is just so the DO + // can use the Name and IgnoreCase properties. + private sealed class GetBinderAdapter : GetMemberBinder + { + internal GetBinderAdapter(InvokeMemberBinder binder) + : base(binder.Name, binder.IgnoreCase) + { + } + + public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject? errorSuggestion) + { + throw new NotSupportedException(); + } + } + + private sealed class TrueReadOnlyCollection : ReadOnlyCollection + { + /// + /// Creates instance of TrueReadOnlyCollection, wrapping passed in array. + /// !!! DOES NOT COPY THE ARRAY !!! + /// + public TrueReadOnlyCollection(params T[] list) + : base(list) + { + } + } + + internal static BindingRestrictions GetTypeRestriction(DynamicMetaObject obj) + { + Debug.Assert(obj != null); + if (obj.Value == null && obj.HasValue) + { + return BindingRestrictions.GetInstanceRestriction(obj.Expression, null); + } + else + { + return BindingRestrictions.GetTypeRestriction(obj.Expression, obj.LimitType); + } + } + + internal static partial class CachedReflectionInfo + { + private static MethodInfo? s_String_Format_String_ObjectArray; + public static MethodInfo String_Format_String_ObjectArray => + s_String_Format_String_ObjectArray ?? + (s_String_Format_String_ObjectArray = typeof(string).GetMethod(nameof(string.Format), new Type[] { typeof(string), typeof(object[]) })!); + + private static ConstructorInfo? s_InvalidCastException_Ctor_String; + public static ConstructorInfo InvalidCastException_Ctor_String => + s_InvalidCastException_Ctor_String ?? + (s_InvalidCastException_Ctor_String = typeof(InvalidCastException).GetConstructor(new Type[] { typeof(string) })!); + + private static MethodInfo? s_Object_GetType; + public static MethodInfo Object_GetType => + s_Object_GetType ?? + (s_Object_GetType = typeof(object).GetMethod(nameof(object.GetType))!); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs new file mode 100644 index 0000000000000..75e4402bc9d8d --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Node; + +namespace System.Text.Json.Serialization.Converters +{ + internal class JsonArrayConverter : JsonConverter + { + public override void Write(Utf8JsonWriter writer, JsonArray value, JsonSerializerOptions options) + { + Debug.Assert(value != null); + value.WriteTo(writer, options); + } + + public override JsonArray? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.StartArray: + return ReadList(ref reader, options.GetNodeOptions()); + case JsonTokenType.Null: + return null; + default: + Debug.Assert(false); + throw ThrowHelper.GetInvalidOperationException_ExpectedArray(reader.TokenType); + } + } + + public JsonArray ReadList(ref Utf8JsonReader reader, JsonNodeOptions? options = null) + { + JsonElement jElement = JsonElement.ParseValue(ref reader); + return new JsonArray(jElement, options); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs new file mode 100644 index 0000000000000..fe59499848420 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Node; + +namespace System.Text.Json.Serialization.Converters +{ + internal class JsonNodeConverter : JsonConverter + { + public static JsonNodeConverter Default { get; } = new JsonNodeConverter(); + public JsonArrayConverter ArrayConverter { get; } = new JsonArrayConverter(); + public JsonObjectConverter ObjectConverter { get; } = new JsonObjectConverter(); + public JsonValueConverter ValueConverter { get; } = new JsonValueConverter(); + public ObjectConverter ElementConverter { get; } = new ObjectConverter(); + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + if (value is JsonObject jsonObject) + { + ObjectConverter.Write(writer, jsonObject, options); + } + else if (value is JsonArray jsonArray) + { + ArrayConverter.Write(writer, (JsonArray)value, options); + } + else + { + Debug.Assert(value is JsonValue); + ValueConverter.Write(writer, (JsonValue)value, options); + } + } + } + + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.Null: + return null; + case JsonTokenType.String: + case JsonTokenType.False: + case JsonTokenType.True: + case JsonTokenType.Number: + return ValueConverter.Read(ref reader, typeToConvert, options); + case JsonTokenType.StartArray: + return ArrayConverter.Read(ref reader, typeToConvert, options); + case JsonTokenType.StartObject: + return ObjectConverter.Read(ref reader, typeToConvert, options); + default: + Debug.Assert(false); + throw new JsonException(); + } + } + + public static JsonNode? Create(JsonElement element, JsonNodeOptions? options) + { + JsonNode? node; + + switch (element.ValueKind) + { + case JsonValueKind.Null: + node = null; + break; + case JsonValueKind.Object: + node = new JsonObject(element, options); + break; + case JsonValueKind.Array: + node = new JsonArray(element, options); + break; + default: + node = new JsonValue(element, options); + break; + } + + return node; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverterFactory.cs new file mode 100644 index 0000000000000..b656edf49d7b8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverterFactory.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Node; + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class JsonNodeConverterFactory : JsonConverterFactory + { + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + if (JsonClassInfo.ObjectType == typeToConvert) + { + if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode) + { + return JsonNodeConverter.Default; + } + + // Return the converter for System.Object which uses JsonElement. + return JsonNodeConverter.Default.ElementConverter; + } + + if (typeof(JsonValue).IsAssignableFrom(typeToConvert)) + { + return JsonNodeConverter.Default.ValueConverter; + } + + if (typeof(JsonObject) == typeToConvert) + { + return JsonNodeConverter.Default.ObjectConverter; + } + + if (typeof(JsonArray) == typeToConvert) + { + return JsonNodeConverter.Default.ArrayConverter; + } + + Debug.Assert(typeof(JsonNode) == typeToConvert); + return JsonNodeConverter.Default; + } + + public override bool CanConvert(Type typeToConvert) => + typeToConvert == JsonClassInfo.ObjectType || + typeof(JsonNode).IsAssignableFrom(typeToConvert); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs new file mode 100644 index 0000000000000..949c643e3995c --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Node; + +namespace System.Text.Json.Serialization.Converters +{ + internal class JsonObjectConverter : JsonConverter + { + public override void Write(Utf8JsonWriter writer, JsonObject value, JsonSerializerOptions options) + { + Debug.Assert(value != null); + value.WriteTo(writer, options); + } + + public override JsonObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + return ReadObject(ref reader, options.GetNodeOptions()); + case JsonTokenType.Null: + return null; + default: + Debug.Assert(false); + throw ThrowHelper.GetInvalidOperationException_ExpectedObject(reader.TokenType); + } + } + + public JsonObject ReadObject(ref Utf8JsonReader reader, JsonNodeOptions? options) + { + JsonElement jElement = JsonElement.ParseValue(ref reader); + JsonObject jObject = new JsonObject(jElement, options); + return jObject; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs new file mode 100644 index 0000000000000..6e433e0fd2ca7 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Node; + +namespace System.Text.Json.Serialization.Converters +{ + internal class JsonValueConverter : JsonConverter + { + public override void Write(Utf8JsonWriter writer, JsonValue value, JsonSerializerOptions options) + { + Debug.Assert(value != null); + value.WriteTo(writer, options); + } + + public override JsonValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonElement element = JsonElement.ParseValue(ref reader); + JsonValue value = new JsonValue(element, options.GetNodeOptions()); + return value; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 226f86f171a56..1f2e51dd9fc75 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -18,6 +18,8 @@ public sealed partial class JsonSerializerOptions // The global list of built-in simple converters. private static readonly Dictionary s_defaultSimpleConverters = GetDefaultSimpleConverters(); + private static readonly JsonNodeConverterFactory s_JsonNodeConverterFactory = new JsonNodeConverterFactory(); + // The global list of built-in converters that override CanConvert(). private static readonly JsonConverter[] s_defaultFactoryConverters = new JsonConverter[] { @@ -26,6 +28,7 @@ public sealed partial class JsonSerializerOptions // Nullable converter should always be next since it forwards to any nullable type. new NullableConverterFactory(), new EnumConverterFactory(), + s_JsonNodeConverterFactory, // IAsyncEnumerable takes precedence over IEnumerable. new IAsyncEnumerableConverterFactory(), // IEnumerable should always be second to last since they can convert any IEnumerable. @@ -39,7 +42,7 @@ public sealed partial class JsonSerializerOptions private static Dictionary GetDefaultSimpleConverters() { - const int NumberOfSimpleConverters = 23; + const int NumberOfSimpleConverters = 22; var converters = new Dictionary(NumberOfSimpleConverters); // Use a dictionary for simple converters. @@ -58,7 +61,6 @@ private static Dictionary GetDefaultSimpleConverters() Add(new Int64Converter()); Add(new JsonElementConverter()); Add(new JsonDocumentConverter()); - Add(new ObjectConverter()); Add(new SByteConverter()); Add(new SingleConverter()); Add(new StringConverter()); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 8a8fe1d9cc1af..37074da094802 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Text.Json.Serialization; using System.Text.Encodings.Web; +using System.Text.Json.Node; namespace System.Text.Json { @@ -34,6 +35,7 @@ public sealed partial class JsonSerializerOptions private JavaScriptEncoder? _encoder; private JsonIgnoreCondition _defaultIgnoreCondition; private JsonNumberHandling _numberHandling; + private JsonUnknownTypeHandling _unknownTypeHandling; private int _defaultBufferSize = BufferSizeDefault; private int _maxDepth; @@ -76,6 +78,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _encoder = options._encoder; _defaultIgnoreCondition = options._defaultIgnoreCondition; _numberHandling = options._numberHandling; + _unknownTypeHandling = options._unknownTypeHandling; _defaultBufferSize = options._defaultBufferSize; _maxDepth = options._maxDepth; @@ -457,6 +460,19 @@ public JsonCommentHandling ReadCommentHandling } } + /// + /// Defines how deserializing a type declared as an is handled during deserialization. + /// + public JsonUnknownTypeHandling UnknownTypeHandling + { + get => _unknownTypeHandling; + set + { + VerifyMutable(); + _unknownTypeHandling = value; + } + } + /// /// Defines whether JSON should pretty print which includes: /// indenting nested JSON tokens, adding new lines, and adding white space between property names and values. @@ -547,6 +563,14 @@ internal bool TypeIsCached(Type type) return _classes.ContainsKey(type); } + internal JsonNodeOptions GetNodeOptions() + { + return new JsonNodeOptions + { + PropertyNameCaseInsensitive = PropertyNameCaseInsensitive + }; + } + internal JsonReaderOptions GetReaderOptions() { return new JsonReaderOptions diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownTypeHandling.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownTypeHandling.cs new file mode 100644 index 0000000000000..5d2ea224bb5ad --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownTypeHandling.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Defines how deserializing a type declared as an is handled during deserialization. + /// + public enum JsonUnknownTypeHandling + { + /// + /// A type declared as is deserialized as a . + /// + JsonElement = 0, + /// + /// A type declared as is deserialized as a . + /// + JsonNode = 1 + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs new file mode 100644 index 0000000000000..cedff91b56286 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; + +namespace System.Text.Json +{ + internal static partial class ThrowHelper + { + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_NodeElementCannotBeObjectOrArray() + { + throw new InvalidOperationException(SR.NodeElementCannotBeObjectOrArray); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_NodeAlreadyHasParent() + { + throw new InvalidOperationException(SR.NodeAlreadyHasParent); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowInvalidOperationException_NodeCycleDetected() + { + throw new InvalidOperationException(SR.NodeCycleDetected); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowArgumentException_NodeValueNotAllowed(string argumentName) + { + throw new ArgumentException(SR.NodeValueNotAllowed, argumentName); + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowArgumentNullException_ValueCannotBeNull(string argumentName) + { + throw new ArgumentNullException(SR.ValueCannotBeNull, argumentName); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index f51b9e7a1bc6d..2b6b85878c9ae 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -197,6 +197,16 @@ public static void ThrowInvalidOperationOrArgumentException(ReadOnlySpan p } } + public static InvalidOperationException GetInvalidOperationException_ExpectedArray(JsonTokenType tokenType) + { + return GetInvalidOperationException("array", tokenType); + } + + public static InvalidOperationException GetInvalidOperationException_ExpectedObject(JsonTokenType tokenType) + { + return GetInvalidOperationException("object", tokenType); + } + public static InvalidOperationException GetInvalidOperationException_ExpectedNumber(JsonTokenType tokenType) { return GetInvalidOperationException("number", tokenType); diff --git a/src/libraries/System.Text.Json/tests/JsonNode/Common.cs b/src/libraries/System.Text.Json/tests/JsonNode/Common.cs new file mode 100644 index 0000000000000..74677e86f4464 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/Common.cs @@ -0,0 +1,132 @@ +// 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; + +namespace System.Text.Json.Node.Tests +{ + public static partial class JsonNodeTests + { + internal const string ExpectedDomJson = "{\"MyString\":\"Hello!\",\"MyNull\":null,\"MyBoolean\":false,\"MyArray\":[2,3,42]," + + "\"MyInt\":43,\"MyDateTime\":\"2020-07-08T00:00:00\",\"MyGuid\":\"ed957609-cdfe-412f-88c1-02daca1b4f51\"," + + "\"MyObject\":{\"MyString\":\"Hello!!\"},\"Child\":{\"ChildProp\":1}}"; + + internal const string Linq_Query_Json = @" + [ + { + ""OrderId"":100, ""Customer"": + { + ""Name"":""Customer1"", + ""City"":""Fargo"" + } + }, + { + ""OrderId"":200, ""Customer"": + { + ""Name"":""Customer2"", + ""City"":""Redmond"" + } + }, + { + ""OrderId"":300, ""Customer"": + { + ""Name"":""Customer3"", + ""City"":""Fargo"" + } + } + ]"; + + /// + /// Helper class simulating external library + /// + internal static class EmployeesDatabase + { + private static int s_id = 0; + public static KeyValuePair GetNextEmployee() + { + var employee = new JsonObject() + { + { "name", "John" } , + { "surname", "Smith"}, + { "age", 45 } + }; + + return new KeyValuePair("employee" + s_id++, employee); + } + + public static IEnumerable> GetTenBestEmployees() + { + for (int i = 0; i < 10; i++) + yield return GetNextEmployee(); + } + + /// + /// Returns following JsonObject: + /// { + /// { "name" : "John" } + /// { "phone numbers" : { "work" : "425-555-0123", "home": "425-555-0134" } } + /// { + /// "reporting employees" : + /// { + /// "software developers" : + /// { + /// "full time employees" : /JsonObject of 3 employees from database/ + /// "intern employees" : /JsonObject of 2 employees from database/ + /// }, + /// "HR" : /JsonObject of 10 employees from database/ + /// } + /// + /// + public static JsonObject GetManager() + { + var manager = GetNextEmployee().Value as JsonObject; + + manager.Add + ( + "phone numbers", + new JsonObject() + { + { "work", "425-555-0123" }, { "home", "425-555-0134" } + } + ); + + manager.Add + ( + "reporting employees", new JsonObject + { + { + "software developers", new JsonObject + { + { + "full time employees", new JsonObject + { + EmployeesDatabase.GetNextEmployee(), + EmployeesDatabase.GetNextEmployee(), + EmployeesDatabase.GetNextEmployee(), + } + }, + { + "intern employees", new JsonObject + { + EmployeesDatabase.GetNextEmployee(), + EmployeesDatabase.GetNextEmployee(), + } + } + } + }, + { + "HR", new JsonObject + { + { + "full time employees", new JsonObject(EmployeesDatabase.GetTenBestEmployees()) + } + } + } + } + ); + + return manager; + } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/JsonNode/DynamicTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/DynamicTests.cs new file mode 100644 index 0000000000000..753855d124732 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/DynamicTests.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if BUILDING_INBOX_LIBRARY + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using Microsoft.CSharp.RuntimeBinder; +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class DynamicTests + { + [Fact] + public static void ImplicitOperators() + { + dynamic jObj = new JsonObject(); + + // Dynamic objects do not support object initializers. + + // Primitives + jObj.MyString = "Hello!"; + Assert.IsAssignableFrom(jObj.MyString); + + jObj.MyNull = null; + jObj.MyBoolean = false; + + // Nested array + jObj.MyArray = new JsonArray(2, 3, 42); + + // Additional primitives + jObj.MyInt = 43; + jObj.MyDateTime = new DateTime(2020, 7, 8); + jObj.MyGuid = new Guid("ed957609-cdfe-412f-88c1-02daca1b4f51"); + + // Nested objects + jObj.MyObject = new JsonObject(); + jObj.MyObject.MyString = "Hello!!"; + + jObj.Child = new JsonObject(); + jObj.Child.ChildProp = 1; + + var options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + + string json = jObj.ToJsonString(options); + JsonTestHelper.AssertJsonEqual(JsonNodeTests.ExpectedDomJson, json); + } + + private enum MyCustomEnum + { + Default = 0, + FortyTwo = 42, + Hello = 77 + } + + [Fact] + public static void Primitives_UnknownTypeHandling() + { + var options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + options.Converters.Add(new JsonStringEnumConverter()); + + dynamic obj = JsonSerializer.Deserialize(Serialization.Tests.DynamicTests.Json, options); + Assert.IsAssignableFrom(obj); + + // JsonValue created from a JSON string. + Assert.IsAssignableFrom(obj.MyString); + Assert.Equal("Hello", (string)obj.MyString); + + // Verify other string-based types. + // Since this requires a custom converter, an exception is thrown. + Assert.ThrowsAny(() => (MyCustomEnum)obj.MyString); + + Assert.Equal(Serialization.Tests.DynamicTests.MyDateTime, (DateTime)obj.MyDateTime); + Assert.Equal(Serialization.Tests.DynamicTests.MyGuid, (Guid)obj.MyGuid); + + // JsonValue created from a JSON bool. + Assert.IsAssignableFrom(obj.MyBoolean); + bool b = (bool)obj.MyBoolean; + Assert.True(b); + + // Numbers must specify the type through a cast or assignment. + Assert.IsAssignableFrom(obj.MyInt); + Assert.ThrowsAny(() => obj.MyInt == 42L); + Assert.Equal(42L, (long)obj.MyInt); + Assert.Equal((byte)42, (byte)obj.MyInt); + + // Verify floating point. + obj = JsonSerializer.Deserialize("4.2", options); + Assert.IsAssignableFrom(obj); + + double dbl = (double)obj; + Assert.Equal(4.2, dbl); + } + + [Fact] + public static void Array_UnknownTypeHandling() + { + var options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + options.Converters.Add(new JsonStringEnumConverter()); + + dynamic obj = JsonSerializer.Deserialize(Serialization.Tests.DynamicTests.Json, options); + Assert.IsAssignableFrom(obj); + Assert.IsAssignableFrom(obj.MyArray); + + Assert.Equal(2, obj.MyArray.Count); + Assert.Equal(1, (int)obj.MyArray[0]); + Assert.Equal(2, (int)obj.MyArray[1]); + + int count = 0; + foreach (object value in obj.MyArray) + { + count++; + } + Assert.Equal(2, count); + + obj.MyArray[0] = 10; + Assert.IsAssignableFrom(obj.MyArray[0]); + + Assert.Equal(10, (int)obj.MyArray[0]); + } + + [Fact] + public static void CreateDom_UnknownTypeHandling() + { + var options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + + string GuidJson = $"{Serialization.Tests.DynamicTests.MyGuid.ToString("D")}"; + string GuidJsonWithQuotes = $"\"{GuidJson}\""; + + // We can't convert an unquoted string to a Guid + dynamic dynamicString = JsonValue.Create(GuidJson); + Assert.Throws(() => (Guid)dynamicString); + + string json; + + // Number (JsonElement) + using (JsonDocument doc = JsonDocument.Parse($"{decimal.MaxValue}")) + { + dynamic dynamicNumber = JsonValue.Create(doc.RootElement); + Assert.Equal(decimal.MaxValue, (decimal)dynamicNumber); + json = dynamicNumber.ToJsonString(options); + Assert.Equal(decimal.MaxValue.ToString(), json); + } + + // Boolean + dynamic dynamicBool = JsonValue.Create(true); + Assert.True((bool)dynamicBool); + json = dynamicBool.ToJsonString(options); + Assert.Equal("true", json); + + // Array + dynamic arr = new JsonArray(); + arr.Add(1); + arr.Add(2); + json = arr.ToJsonString(options); + Assert.Equal("[1,2]", json); + + // Object + dynamic dynamicObject = new JsonObject(); + dynamicObject.One = 1; + dynamicObject.Two = 2; + + json = dynamicObject.ToJsonString(options); + JsonTestHelper.AssertJsonEqual("{\"One\":1,\"Two\":2}", json); + } + + /// + /// Use a mutable DOM with the 'dynamic' keyword. + /// + [Fact] + public static void UnknownTypeHandling_Object() + { + var options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + + dynamic obj = JsonSerializer.Deserialize(Serialization.Tests.DynamicTests.Json, options); + Assert.IsAssignableFrom(obj); + + // Change some primitives. + obj.MyString = "Hello!"; + obj.MyBoolean = false; + obj.MyInt = 43; + + // Add nested objects. + // Use JsonObject; ExpandoObject should not be used since it doesn't have the same semantics including + // null handling and case-sensitivity that respects JsonSerializerOptions.PropertyNameCaseInsensitive. + dynamic myObject = new JsonObject(); + myObject.MyString = "Hello!!"; + obj.MyObject = myObject; + + dynamic child = new JsonObject(); + child.ChildProp = 1; + obj.Child = child; + + // Modify number elements. + dynamic arr = obj.MyArray; + arr[0] = (int)arr[0] + 1; + arr[1] = (int)arr[1] + 1; + + // Add an element. + arr.Add(42); + + string json = obj.ToJsonString(options); + JsonTestHelper.AssertJsonEqual(JsonNodeTests.ExpectedDomJson, json); + } + + [Fact] + public static void ConvertArrayTo() + { + dynamic obj = JsonSerializer.Deserialize("[42]"); + Assert.Equal(42, (int)obj[0]); + + IList ilist = obj; + Assert.NotNull(ilist); + Assert.Equal(42, (int)ilist[0]); + } + + [Fact] + public static void UnknownTypeHandling_CaseSensitivity() + { + var options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + dynamic obj = JsonSerializer.Deserialize("{\"MyProperty\":42}", options); + + Assert.IsType(obj); + Assert.IsAssignableFrom(obj.MyProperty); + + //dynamic temp = obj.MyProperty; + //int sdfsfd = temp; + int sdfsfdsdf = (int)obj.MyProperty; + + var sdf = obj.MyProperty; + + Assert.Equal(42, (int)obj.MyProperty); + Assert.Null(obj.myProperty); + Assert.Null(obj.MYPROPERTY); + + options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + options.PropertyNameCaseInsensitive = true; + obj = JsonSerializer.Deserialize("{\"MyProperty\":42}", options); + + Assert.Equal(42, (int)obj.MyProperty); + Assert.Equal(42, (int)obj.myproperty); + Assert.Equal(42, (int)obj.MYPROPERTY); + } + + [Fact] + public static void MissingProperty_UnknownTypeHandling() + { + var options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + dynamic obj = JsonSerializer.Deserialize("{}", options); + Assert.Equal(null, obj.NonExistingProperty); + } + + [Fact] + public static void Linq_UnknownTypeHandling() + { + var options = new JsonSerializerOptions(); + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; + + IEnumerable allOrders = JsonSerializer.Deserialize>(JsonNodeTests.Linq_Query_Json, options); + IEnumerable orders = allOrders.Where(o => ((string)o.Customer.City) == "Fargo"); + + Assert.Equal(2, orders.Count()); + Assert.Equal(100, (int)orders.ElementAt(0).OrderId); + Assert.Equal(300, (int)orders.ElementAt(1).OrderId); + Assert.Equal("Customer1", (string)orders.ElementAt(0).Customer.Name); + Assert.Equal("Customer3", (string)orders.ElementAt(1).Customer.Name); + + // Verify methods can be called as well. + Assert.Equal(100, orders.ElementAt(0).OrderId.GetValue()); + Assert.Equal(300, orders.ElementAt(1).OrderId.GetValue()); + Assert.Equal("Customer1", orders.ElementAt(0).Customer.Name.GetValue()); + Assert.Equal("Customer3", orders.ElementAt(1).Customer.Name.GetValue()); + } + } +} +#endif diff --git a/src/libraries/System.Text.Json/tests/JsonNode/JsonArrayTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/JsonArrayTests.cs new file mode 100644 index 0000000000000..bb4e5ec9dfade --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/JsonArrayTests.cs @@ -0,0 +1,432 @@ +// 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; +using System.IO; +using System.Linq; +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class JsonArrayTests + { + [Fact] + public static void FromElement() + { + using (JsonDocument document = JsonDocument.Parse("[42]")) + { + JsonArray jArray = JsonArray.Create(document.RootElement); + Assert.Equal(42, jArray[0].GetValue()); + } + + using (JsonDocument document = JsonDocument.Parse("null")) + { + JsonArray jArray = JsonArray.Create(document.RootElement); + Assert.Null(jArray); + } + } + + [Theory] + [InlineData("42")] + [InlineData("{}")] + public static void FromElement_WrongNodeTypeThrows(string json) + { + using (JsonDocument document = JsonDocument.Parse(json)) + Assert.Throws(() => JsonArray.Create(document.RootElement)); + } + + [Fact] + public static void WriteTo_Validation() + { + Assert.Throws(() => new JsonArray().WriteTo(null)); + } + + [Fact] + public static void WriteTo() + { + const string Json = "[42]"; + + JsonArray jArray = JsonNode.Parse(Json).AsArray(); + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream); + jArray.WriteTo(writer); + writer.Flush(); + + string json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(Json, json); + } + + [Fact] + public static void WriteTo_Options() + { + JsonArray jArray = new JsonArray(42); + + // Baseline. + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream); + jArray.WriteTo(writer); + writer.Flush(); + string json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("[42]", json); + + var options = new JsonSerializerOptions + { + NumberHandling = Serialization.JsonNumberHandling.WriteAsString + }; + + // With options. + stream = new MemoryStream(); + writer = new Utf8JsonWriter(stream); + jArray.WriteTo(writer, options); + writer.Flush(); + json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("[\"42\"]", json); + } + + [Fact] + public static void Clear() + { + var jArray = new JsonArray(42, 43); + Assert.Equal(2, jArray.Count); + + JsonNode node = jArray[0]; + jArray.Clear(); + Assert.Equal(0, jArray.Count); + jArray.Add(node); + Assert.Equal(1, jArray.Count); + } + + [Fact] + public static void AddOverloads() + { + var jArray = new JsonArray(); + jArray.Add((object)null); + jArray.Add((int?)null); + jArray.Add(1); + Assert.Equal(3, jArray.Count); + } + + [Fact] + public static void IsReadOnly() + { + Assert.False(((IList)new JsonArray()).IsReadOnly); + } + + [Fact] + public static void IEnumerable() + { + IEnumerable jArray = new JsonArray(1, 2); + + int count = 0; + foreach (JsonNode? node in jArray) + { + count++; + } + + Assert.Equal(2, count); + } + + [Fact] + public static void Contains_IndexOf_Remove_Insert() + { + JsonNode node1 = 1; + JsonNode node2 = 2; + JsonNode node3 = 3; + + var jArray = new JsonArray(node1, node2, node3); + Assert.Equal(3, jArray.Count); + + Assert.Equal(0, jArray.IndexOf(node1)); + Assert.Equal(1, jArray.IndexOf(node2)); + Assert.Equal(2, jArray.IndexOf(node3)); + + // Remove + bool success = jArray.Remove(node2); + Assert.True(success); + Assert.Equal(2, jArray.Count); + + Assert.Equal(0, jArray.IndexOf(node1)); + Assert.Equal(-1, jArray.IndexOf(node2)); + Assert.Equal(1, jArray.IndexOf(node3)); + + Assert.False(jArray.Remove(node2)); // remove an already removed node. + Assert.Equal(2, jArray.Count); + + // Contains + Assert.True(jArray.Contains(node1)); + Assert.False(jArray.Contains(node2)); + Assert.True(jArray.Contains(node3)); + + // Insert + jArray.Insert(1, node2); + Assert.Equal(3, jArray.Count); + + Assert.Equal(0, jArray.IndexOf(node1)); + Assert.Equal(1, jArray.IndexOf(node2)); + Assert.Equal(2, jArray.IndexOf(node3)); + } + + [Fact] + public static void CopyTo() + { + JsonNode node1 = 1; + JsonNode node2 = 2; + + IList jArray = new JsonArray(node1, node2, null); + var arr = new JsonNode[4]; + jArray.CopyTo(arr, 0); + + Assert.Same(node1, arr[0]); + Assert.Same(node2, arr[1]); + Assert.Null(arr[2]); + + arr = new JsonNode[5]; + jArray.CopyTo(arr, 1); + Assert.Null(arr[0]); + Assert.Same(node1, arr[1]); + Assert.Same(node2, arr[2]); + Assert.Null(arr[3]); + Assert.Null(arr[4]); + + arr = new JsonNode[3]; + Assert.Throws(() => jArray.CopyTo(arr, 1)); + } + + [Fact] + public static void ReAddSameNode_Throws() + { + var jValue = JsonValue.Create(1); + + var jArray = new JsonArray(jValue); + Assert.Throws(() => jArray.Add(jValue)); + } + + [Fact] + public static void ReAddRemovedNode() + { + var jValue = JsonValue.Create(1); + + var jArray = new JsonArray(jValue); + Assert.Equal(1, jArray.Count); + jArray.Remove(jValue); + Assert.Equal(0, jArray.Count); + jArray.Add(jValue); + Assert.Equal(1, jArray.Count); + } + + [Fact] + public static void CreatingJsonArrayFromNodeArray() + { + JsonNode[] expected = { "sushi", "pasta", "cucumber soup" }; + + var dishesJsonArray = new JsonArray(expected); + Assert.Equal(3, dishesJsonArray.Count); + + for (int i = 0; i < dishesJsonArray.Count; i++) + { + Assert.Equal(expected[i], dishesJsonArray[i]); + } + } + + [Fact] + public static void CreatingJsonArrayFromArrayOfStrings() + { + var strangeWords = new string[] + { + "supercalifragilisticexpialidocious", + "gladiolus", + "albumen", + "smaragdine", + }; + + var strangeWordsJsonArray = new JsonArray(); + strangeWords.Where(word => word.Length < 10). + ToList().ForEach(str => strangeWordsJsonArray.Add(JsonValue.Create(str))); + + Assert.Equal(2, strangeWordsJsonArray.Count); + + string[] expected = { "gladiolus", "albumen" }; + + for (int i = 0; i < strangeWordsJsonArray.Count; i++) + { + Assert.Equal(expected[i], strangeWordsJsonArray[i].GetValue()); + } + } + + [Fact] + public static void CreatingNestedJsonArray() + { + var vertices = new JsonArray() + { + new JsonArray + { + new JsonArray + { + new JsonArray { 0, 0, 0 }, + new JsonArray { 0, 0, 1 } + }, + new JsonArray + { + new JsonArray { 0, 1, 0 }, + new JsonArray { 0, 1, 1 } + } + }, + new JsonArray + { + new JsonArray + { + new JsonArray { 1, 0, 0 }, + new JsonArray { 1, 0, 1 } + }, + new JsonArray + { + new JsonArray { 1, 1, 0 }, + new JsonArray { 1, 1, 1 } + } + }, + }; + + var jArray = (JsonArray)vertices[0]; + Assert.Equal(2, jArray.Count()); + jArray = (JsonArray)jArray[1]; + Assert.Equal(2, jArray.Count()); + jArray = (JsonArray)jArray[0]; + Assert.Equal(3, jArray.Count()); + + Assert.Equal(0, (int)jArray[0]); + Assert.Equal(1, (int)jArray[1]); + Assert.Equal(0, (int)jArray[2]); + } + + [Fact] + public static void NullHandling() + { + var jArray = new JsonArray() { "to be replaced" }; + + jArray[0] = null; + Assert.Equal(1, jArray.Count); + Assert.Null(jArray[0]); + + jArray.Add(null); + Assert.Equal(2, jArray.Count); + Assert.Null(jArray[1]); + + jArray.Add(null); + Assert.Equal(3, jArray.Count); + Assert.Null(jArray[2]); + + jArray.Insert(3, null); + Assert.Equal(4, jArray.Count); + Assert.Null(jArray[3]); + + jArray.Insert(4, null); + Assert.Equal(5, jArray.Count); + Assert.Null(jArray[4]); + + Assert.True(jArray.Contains(null)); + Assert.Equal(0, jArray.IndexOf(null)); + + jArray.Remove(null); + Assert.Equal(4, jArray.Count); + } + + [Fact] + public static void AccesingNestedJsonArray() + { + var issues = new JsonObject + { + { "features", new JsonArray { "new functionality 1", "new functionality 2" } }, + { "bugs", new JsonArray { "bug 123", "bug 4566", "bug 821" } }, + { "tests", new JsonArray { "code coverage" } }, + }; + + issues["bugs"].AsArray().Add("bug 12356"); + issues["features"].AsArray()[0] = "feature 1569"; + issues["features"].AsArray()[1] = "feature 56134"; + + Assert.Equal("bug 12356", (string)issues["bugs"][3]); + Assert.Equal("feature 1569", (string)issues["features"][0]); + Assert.Equal("feature 56134", (string)issues["features"][1]); + } + + [Fact] + public static void Insert() + { + var jArray = new JsonArray() { 1 }; + Assert.Equal(1, jArray.Count); + + jArray.Insert(0, 0); + + Assert.Equal(2, jArray.Count); + Assert.Equal(0, (int)jArray[0]); + Assert.Equal(1, (int)jArray[1]); + + jArray.Insert(2, 3); + + Assert.Equal(3, jArray.Count); + Assert.Equal(0, (int)jArray[0]); + Assert.Equal(1, (int)jArray[1]); + Assert.Equal(3, (int)jArray[2]); + + jArray.Insert(2, 2); + + Assert.Equal(4, jArray.Count); + Assert.Equal(0, (int)jArray[0]); + Assert.Equal(1, (int)jArray[1]); + Assert.Equal(2, (int)jArray[2]); + Assert.Equal(3, (int)jArray[3]); + } + + [Fact] + public static void HeterogeneousArray() + { + var mixedTypesArray = new JsonArray { 1, "value", true, null, 2.3, new JsonObject() }; + + Assert.Equal(1, mixedTypesArray[0].GetValue()); + Assert.Equal("value", mixedTypesArray[1].GetValue()); + Assert.True(mixedTypesArray[2].GetValue()); + Assert.Null(mixedTypesArray[3]); + Assert.Equal(2.3, mixedTypesArray[4].GetValue()); + Assert.IsType(mixedTypesArray[5]); + + mixedTypesArray.Add(false); + mixedTypesArray.Insert(4, "another"); + mixedTypesArray.Add(null); + + Assert.False(mixedTypesArray[7].GetValue()); + Assert.Equal("another", mixedTypesArray[4].GetValue()); + Assert.Null(mixedTypesArray[8]); + } + + [Fact] + public static void OutOfRangeException() + { + Assert.Throws(() => new JsonArray()[-1]); + Assert.Throws(() => new JsonArray()[0]); + Assert.Throws(() => new JsonArray()[1]); + Assert.Throws(() => + { + var jArray = new JsonArray { 1, 2, 3 }; + jArray.Insert(4, 17); + }); + Assert.Throws(() => + { + var jArray = new JsonArray { 1, 2, 3 }; + jArray.Insert(-1, 17); + }); + } + + [Fact] + public static void GetJsonArrayIEnumerable() + { + IEnumerable jArray = new JsonArray() { 1, "value" }; + IEnumerator jArrayEnumerator = jArray.GetEnumerator(); + + Assert.True(jArrayEnumerator.MoveNext()); + Assert.Equal(1, ((JsonValue)jArrayEnumerator.Current).GetValue()); + Assert.True(jArrayEnumerator.MoveNext()); + Assert.Equal("value", ((JsonValue)jArrayEnumerator.Current).GetValue()); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/JsonNode/JsonNodeOperatorTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/JsonNodeOperatorTests.cs new file mode 100644 index 0000000000000..492ebc46663a1 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/JsonNodeOperatorTests.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class OperatorTests + { + private const string ExpectedPrimitiveJson = + @"{" + + @"""MyInt16"":1," + + @"""MyInt32"":2," + + @"""MyInt64"":3," + + @"""MyUInt16"":4," + + @"""MyUInt32"":5," + + @"""MyUInt64"":6," + + @"""MyByte"":7," + + @"""MySByte"":8," + + @"""MyChar"":""a""," + + @"""MyString"":""Hello""," + + @"""MyBooleanTrue"":true," + + @"""MyBooleanFalse"":false," + + @"""MySingle"":1.1," + + @"""MyDouble"":2.2," + + @"""MyDecimal"":3.3," + + @"""MyDateTime"":""2019-01-30T12:01:02Z""," + + @"""MyDateTimeOffset"":""2019-01-30T12:01:02+01:00""," + + @"""MyGuid"":""1b33498a-7b7d-4dda-9c13-f6aa4ab449a6""" + // note lowercase + @"}"; + + [Fact] + public static void ImplicitOperators_FromProperties() + { + var jObject = new JsonObject(); + jObject["MyInt16"] = (short)1; + jObject["MyInt32"] = 2; + jObject["MyInt64"] = (long)3; + jObject["MyUInt16"] = (ushort)4; + jObject["MyUInt32"] = (uint)5; + jObject["MyUInt64"] = (ulong)6; + jObject["MyByte"] = (byte)7; + jObject["MySByte"] = (sbyte)8; + jObject["MyChar"] = 'a'; + jObject["MyString"] = "Hello"; + jObject["MyBooleanTrue"] = true; + jObject["MyBooleanFalse"] = false; + jObject["MySingle"] = 1.1f; + jObject["MyDouble"] = 2.2d; + jObject["MyDecimal"] = 3.3m; + jObject["MyDateTime"] = new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc); + jObject["MyDateTimeOffset"] = new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0)); + jObject["MyGuid"] = new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6"); + + string expected = ExpectedPrimitiveJson; + +#if !BUILDING_INBOX_LIBRARY + // The reader uses "G9" format which causes temp to be 1.10000002 in this case. + expected.Replace("1.1", "1.10000002"); + + // The reader uses "G17" format which causes temp to be 2.2000000000000002 in this case. + expected.Replace("2.2", "2.2000000000000002"); +#endif + + string json = jObject.ToJsonString(); + Assert.Equal(expected, json); + } + + [Fact] + public static void ExplicitOperators_FromProperties() + { + JsonObject jObject = JsonNode.Parse(ExpectedPrimitiveJson).AsObject(); + Assert.Equal(1, (short)jObject["MyInt16"]); + Assert.Equal(2, (int)jObject["MyInt32"]); + Assert.Equal(3, (long)jObject["MyInt64"]); + Assert.Equal(4, (ushort)jObject["MyUInt16"]); + Assert.Equal(5, (uint)jObject["MyUInt32"]); + Assert.Equal(6, (ulong)jObject["MyUInt64"]); + Assert.Equal(7, (byte)jObject["MyByte"]); + Assert.Equal(8, (sbyte)jObject["MySByte"]); + Assert.Equal('a', (char)jObject["MyChar"]); + Assert.Equal("Hello", (string)jObject["MyString"]); + Assert.True((bool)jObject["MyBooleanTrue"]); + Assert.False((bool)jObject["MyBooleanFalse"]); + Assert.Equal(1.1f, (float)jObject["MySingle"]); + Assert.Equal(2.2d, (double)jObject["MyDouble"]); + Assert.Equal(3.3m, (decimal)jObject["MyDecimal"]); + Assert.Equal(new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc), (DateTime)jObject["MyDateTime"]); + Assert.Equal(new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0)), (DateTimeOffset)jObject["MyDateTimeOffset"]); + Assert.Equal(new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6"), (Guid)jObject["MyGuid"]); + } + + [Fact] + public static void ExplicitOperators_FromValues() + { + Assert.Equal(1, (short)(JsonNode)(short)1); + Assert.Equal(2, (int)(JsonNode)2); + Assert.Equal(3, (long)(JsonNode)(long)3); + Assert.Equal(4, (ushort)(JsonNode)(ushort)4); + Assert.Equal(5, (uint)(JsonNode)(uint)5); + Assert.Equal(6, (ulong)(JsonNode)(ulong)6); + Assert.Equal(7, (byte)(JsonNode)(byte)7); + Assert.Equal(8, (sbyte)(JsonNode)(sbyte)8); + Assert.Equal('a', (char)(JsonNode)'a'); + Assert.Equal("Hello", (string)(JsonNode)"Hello"); + Assert.True((bool)(JsonNode)true); + Assert.False((bool)(JsonNode)false); + Assert.Equal(1.1f, (float)(JsonNode)1.1f); + Assert.Equal(2.2d, (double)(JsonNode)2.2d); + Assert.Equal(3.3m, (decimal)(JsonNode)3.3m); + Assert.Equal(new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc), + (DateTime)(JsonNode)new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc)); + Assert.Equal(new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0)), + (DateTimeOffset)(JsonNode)new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0))); + Assert.Equal(new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6"), + (Guid)(JsonNode)new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6")); + } + + [Fact] + public static void ExplicitOperators_FromNullValues() + { + Assert.Null((byte?)(JsonValue)null); + Assert.Null((short?)(JsonValue)null); + Assert.Null((int?)(JsonValue)null); + Assert.Null((long?)(JsonValue)null); + Assert.Null((sbyte?)(JsonValue)null); + Assert.Null((ushort?)(JsonValue)null); + Assert.Null((uint?)(JsonValue)null); + Assert.Null((ulong?)(JsonValue)null); + Assert.Null((char?)(JsonValue)null); + Assert.Null((string)(JsonValue)null); + Assert.Null((bool?)(JsonValue)null); + Assert.Null((float?)(JsonValue)null); + Assert.Null((double?)(JsonValue)null); + Assert.Null((decimal?)(JsonValue)null); + Assert.Null((DateTime?)(JsonValue)null); + Assert.Null((DateTimeOffset?)(JsonValue)null); + Assert.Null((Guid?)(JsonValue)null); + } + + [Fact] + public static void ExplicitOperators_FromNullableValues() + { + Assert.NotNull((byte?)(JsonValue)(byte)42); + Assert.NotNull((short?)(JsonValue)(short)42); + Assert.NotNull((int?)(JsonValue)42); + Assert.NotNull((long?)(JsonValue)(long)42); + Assert.NotNull((sbyte?)(JsonValue)(sbyte)42); + Assert.NotNull((ushort?)(JsonValue)(ushort)42); + Assert.NotNull((uint?)(JsonValue)(uint)42); + Assert.NotNull((ulong?)(JsonValue)(ulong)42); + Assert.NotNull((char?)(JsonValue)'a'); + Assert.NotNull((string)(JsonValue)""); + Assert.NotNull((bool?)(JsonValue)true); + Assert.NotNull((float?)(JsonValue)(float)42); + Assert.NotNull((double?)(JsonValue)(double)42); + Assert.NotNull((decimal?)(JsonValue)(decimal)42); + Assert.NotNull((DateTime?)(JsonValue)new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc)); + Assert.NotNull((DateTimeOffset?)(JsonValue)new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0))); + Assert.NotNull((Guid?)(JsonValue)new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6")); + } + + [Fact] + public static void ImplicitOperators_FromNullValues() + { + Assert.Null((JsonValue?)(byte?)null); + Assert.Null((JsonValue?)(short?)null); + Assert.Null((JsonValue?)(int?)null); + Assert.Null((JsonValue?)(long?)null); + Assert.Null((JsonValue?)(sbyte?)null); + Assert.Null((JsonValue?)(ushort?)null); + Assert.Null((JsonValue?)(uint?)null); + Assert.Null((JsonValue?)(ulong?)null); + Assert.Null((JsonValue?)(char?)null); + Assert.Null((JsonValue?)(string)null); + Assert.Null((JsonValue?)(bool?)null); + Assert.Null((JsonValue?)(float?)null); + Assert.Null((JsonValue?)(double?)null); + Assert.Null((JsonValue?)(decimal?)null); + Assert.Null((JsonValue?)(DateTime?)null); + Assert.Null((JsonValue?)(DateTimeOffset?)null); + Assert.Null((JsonValue?)(Guid?)null); + } + + [Fact] + public static void ImplicitOperators_FromNullableValues() + { + Assert.NotNull((JsonValue?)(byte?)42); + Assert.NotNull((JsonValue?)(short?)42); + Assert.NotNull((JsonValue?)(int?)42); + Assert.NotNull((JsonValue?)(long?)42); + Assert.NotNull((JsonValue?)(sbyte?)42); + Assert.NotNull((JsonValue?)(ushort?)42); + Assert.NotNull((JsonValue?)(uint?)42); + Assert.NotNull((JsonValue?)(ulong?)42); + Assert.NotNull((JsonValue?)(char?)'a'); + Assert.NotNull((JsonValue?)(bool?)true); + Assert.NotNull((JsonValue?)(float?)42); + Assert.NotNull((JsonValue?)(double?)42); + Assert.NotNull((JsonValue?)(decimal?)42); + Assert.NotNull((JsonValue?)(DateTime?)new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc)); + Assert.NotNull((JsonValue?)(DateTimeOffset?)new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0))); + Assert.NotNull((JsonValue?)(Guid?)new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6")); + } + + [Fact] + public static void CastsNotSupported() + { + // Since generics and boxing do not support casts, we get InvalidCastExceptions here. + Assert.Throws(() => (byte)(JsonNode)(long)3); // narrowing + Assert.Throws(() => (long)(JsonNode)(byte)3); // widening + } + + [Fact] + public static void Boxing() + { + var node = JsonValue.Create(42); + Assert.Equal(42, node.GetValue()); + + Assert.Equal(42, node.GetValue()); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/JsonNode/JsonNodeTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/JsonNodeTests.cs new file mode 100644 index 0000000000000..eb764beebc118 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/JsonNodeTests.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static partial class JsonNodeTests + { + [Fact] + public static void JsonTypes_Deserialize() + { + var options = new JsonSerializerOptions(); + + Assert.IsType(JsonSerializer.Deserialize("{}", options)); + Assert.IsType(JsonNode.Parse("{}")); + Assert.IsType(JsonNode.Parse(ToUtf8("{}"))); + Assert.IsType(JsonSerializer.Deserialize("{}", options)); + + Assert.IsType(JsonSerializer.Deserialize("[]", options)); + Assert.IsType(JsonNode.Parse("[]")); + Assert.IsType(JsonNode.Parse(ToUtf8("[]"))); + Assert.IsType(JsonSerializer.Deserialize("[]", options)); + + Assert.IsAssignableFrom(JsonSerializer.Deserialize("true", options)); + Assert.IsAssignableFrom(JsonNode.Parse("true")); + Assert.IsAssignableFrom(JsonNode.Parse(ToUtf8("true"))); + Assert.IsType(JsonSerializer.Deserialize("true", options)); + + Assert.IsAssignableFrom(JsonSerializer.Deserialize("0", options)); + Assert.IsAssignableFrom(JsonNode.Parse("0")); + Assert.IsAssignableFrom(JsonNode.Parse(ToUtf8("0"))); + Assert.IsType(JsonSerializer.Deserialize("0", options)); + + Assert.IsAssignableFrom(JsonSerializer.Deserialize("1.2", options)); + Assert.IsAssignableFrom(JsonNode.Parse("1.2")); + Assert.IsAssignableFrom(JsonNode.Parse(ToUtf8("1.2"))); + Assert.IsType(JsonSerializer.Deserialize("1.2", options)); + + Assert.IsAssignableFrom(JsonSerializer.Deserialize("\"str\"", options)); + Assert.IsAssignableFrom(JsonNode.Parse("\"str\"")); + Assert.IsAssignableFrom(JsonNode.Parse(ToUtf8("\"str\""))); + Assert.IsType(JsonSerializer.Deserialize("\"str\"", options)); + } + + [Fact] + public static void AsMethods_Throws() + { + Assert.Throws(() => JsonNode.Parse("{}").AsArray()); + Assert.Throws(() => JsonNode.Parse("{}").AsValue()); + Assert.Throws(() => JsonNode.Parse("[]").AsObject()); + Assert.Throws(() => JsonNode.Parse("[]").AsValue()); + Assert.Throws(() => JsonNode.Parse("1").AsArray()); + Assert.Throws(() => JsonNode.Parse("1").AsObject()); + } + + [Fact] + public static void NullHandling() + { + var options = new JsonSerializerOptions(); + JsonNode obj = JsonSerializer.Deserialize("null", options); + Assert.Null(obj); + } + + private static byte[] ToUtf8(string value) + { + return Encoding.UTF8.GetBytes(value); + } + } +} + diff --git a/src/libraries/System.Text.Json/tests/JsonNode/JsonObjectTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/JsonObjectTests.cs new file mode 100644 index 0000000000000..58b69998136b0 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/JsonObjectTests.cs @@ -0,0 +1,484 @@ +// 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; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization.Tests; +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class JsonObjectTests + { + [Fact] + public static void KeyValuePair() + { + var jObject = new JsonObject(); + jObject["One"] = 1; + jObject["Two"] = 2; + + KeyValuePair kvp1 = default; + KeyValuePair kvp2 = default; + + int count = 0; + foreach (KeyValuePair kvp in jObject) + { + if (count == 0) + { + kvp1 = kvp; + } + else + { + kvp2 = kvp; + } + + count++; + } + + Assert.Equal(2, count); + + ICollection> iCollection = jObject; + Assert.True(iCollection.Contains(kvp1)); + Assert.True(iCollection.Contains(kvp2)); + Assert.False(iCollection.Contains(new KeyValuePair("?", null))); + + Assert.True(iCollection.Remove(kvp1)); + Assert.Equal(1, jObject.Count); + + Assert.False(iCollection.Remove(new KeyValuePair("?", null))); + Assert.Equal(1, jObject.Count); + } + + [Fact] + public static void IsReadOnly() + { + ICollection> jObject = new JsonObject(); + Assert.False(jObject.IsReadOnly); + } + + [Fact] + public static void NullPropertyValues() + { + var jObject = new JsonObject(); + jObject["One"] = null; + jObject.Add("Two", null); + Assert.Equal(2, jObject.Count); + Assert.Null(jObject["One"]); + Assert.Null(jObject["Two"]); + } + + [Fact] + public static void NullPropertyNameFail() + { + var jObject = new JsonObject(); + Assert.Throws(() => jObject.Add(null, JsonValue.Create(0))); + Assert.Throws(() => jObject[null] = JsonValue.Create(0)); + } + + [Fact] + public static void IEnumerable() + { + var jObject = new JsonObject(); + jObject["One"] = 1; + jObject["Two"] = 2; + + IEnumerable enumerable = jObject; + int count = 0; + foreach (KeyValuePair node in enumerable) + { + count++; + } + + Assert.Equal(2, count); + } + + [Fact] + public static void MissingProperty() + { + var options = new JsonSerializerOptions(); + JsonObject jObject = JsonSerializer.Deserialize("{}", options); + Assert.Null(jObject["NonExistingProperty"]); + Assert.False(jObject.Remove("NonExistingProperty")); + } + + [Fact] + public static void SetItem_Fail() + { + var jObject = new JsonObject(); + Assert.Throws(() => jObject[null] = 42); + } + + [Fact] + public static void IDictionary_KeyValuePair() + { + IDictionary jObject = new JsonObject(); + jObject.Add(new KeyValuePair("MyProperty", 42)); + Assert.Equal(42, jObject["MyProperty"].GetValue()); + + Assert.Equal(1, jObject.Keys.Count); + Assert.Equal(1, jObject.Values.Count); + } + + [Fact] + public static void Clear_ContainsKey() + { + var jObject = new JsonObject(); + jObject.Add("One", 1); + jObject.Add("Two", 2); + Assert.Equal(2, jObject.Count); + JsonNode node1 = jObject["One"]; + JsonNode node2 = jObject["Two"]; + + Assert.True(jObject.ContainsKey("One")); + Assert.True(jObject.ContainsKey("Two")); + Assert.False(jObject.ContainsKey("?")); + + jObject.Clear(); + Assert.Equal(0, jObject.Count); + + jObject.Add("One", 1); + jObject.Add("Two", 2); + Assert.Equal(2, jObject.Count); + } + + [Fact] + public static void CaseSensitivity_ReadMode() + { + var options = new JsonSerializerOptions(); + JsonObject obj = JsonSerializer.Deserialize("{\"MyProperty\":42}", options); + + Assert.Equal(42, obj["MyProperty"].GetValue()); + Assert.Null(obj["myproperty"]); + Assert.Null(obj["MYPROPERTY"]); + + options = new JsonSerializerOptions(); + options.PropertyNameCaseInsensitive = true; + obj = JsonSerializer.Deserialize("{\"MyProperty\":42}", options); + + Assert.Equal(42, obj["MyProperty"].GetValue()); + Assert.Equal(42, obj["myproperty"].GetValue()); + Assert.Equal(42, obj["MYPROPERTY"].GetValue()); + } + + [Fact] + public static void CaseSensitivity_EditMode() + { + var jArray = new JsonArray(); + var jObject = new JsonObject(); + jObject.Add("MyProperty", 42); + jObject.Add("myproperty", 42); // No exception + + // Options on direct node. + var options = new JsonNodeOptions { PropertyNameCaseInsensitive = true }; + jArray = new JsonArray(); + jObject = new JsonObject(options); + jObject.Add("MyProperty", 42); + jArray.Add(jObject); + Assert.Throws(() => jObject.Add("myproperty", 42)); + + // Options on parent node. + jArray = new JsonArray(options); + jObject = new JsonObject(); + jArray.Add(jObject); + jObject.Add("MyProperty", 42); + Assert.Throws(() => jObject.Add("myproperty", 42)); + + // Dictionary is created when Add is called for the first time, so we need to be added first. + jArray = new JsonArray(options); + jObject = new JsonObject(); + jObject.Add("MyProperty", 42); + jArray.Add(jObject); + jObject.Add("myproperty", 42); // no exception since options were not set in time. + } + + [Fact] + public static void NamingPoliciesAreNotUsed() + { + const string Json = "{\"myProperty\":42}"; + + var options = new JsonSerializerOptions(); + options.PropertyNamingPolicy = new SimpleSnakeCasePolicy(); + + JsonObject obj = JsonSerializer.Deserialize(Json, options); + string json = obj.ToJsonString(); + JsonTestHelper.AssertJsonEqual(Json, json); + } + + [Fact] + public static void FromElement() + { + using (JsonDocument document = JsonDocument.Parse("{\"myProperty\":42}")) + { + JsonObject jObject = JsonObject.Create(document.RootElement); + Assert.Equal(42, jObject["myProperty"].GetValue()); + } + + using (JsonDocument document = JsonDocument.Parse("null")) + { + JsonObject? jObject = JsonObject.Create(document.RootElement); + Assert.Null(jObject); + } + } + + [Theory] + [InlineData("42")] + [InlineData("[]")] + public static void FromElement_WrongNodeTypeThrows(string json) + { + using (JsonDocument document = JsonDocument.Parse(json)) + Assert.Throws(() => JsonObject.Create(document.RootElement)); + } + + [Fact] + public static void WriteTo_Validation() + { + Assert.Throws(() => new JsonObject().WriteTo(null)); + } + + [Fact] + public static void WriteTo() + { + const string Json = "{\"MyProperty\":42}"; + + JsonObject jObject = JsonNode.Parse(Json).AsObject(); + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream); + jObject.WriteTo(writer); + writer.Flush(); + + string json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(Json, json); + } + + [Fact] + public static void CopyTo() + { + JsonNode node1 = 1; + JsonNode node2 = 2; + + IDictionary jObject = new JsonObject(); + jObject["One"] = node1; + jObject["Two"] = node2; + jObject["null"] = null; + + var arr = new KeyValuePair[4]; + jObject.CopyTo(arr, 0); + + Assert.Same(node1, arr[0].Value); + Assert.Same(node2, arr[1].Value); + Assert.Null(arr[2].Value); + + arr = new KeyValuePair[5]; + jObject.CopyTo(arr, 1); + Assert.Null(arr[0].Key); + Assert.Null(arr[0].Value); + Assert.NotNull(arr[1].Key); + Assert.Same(node1, arr[1].Value); + Assert.NotNull(arr[2].Key); + Assert.Same(node2, arr[2].Value); + Assert.NotNull(arr[3].Key); + Assert.Null(arr[3].Value); + Assert.Null(arr[4].Key); + Assert.Null(arr[4].Value); + + arr = new KeyValuePair[3]; + Assert.Throws(() => jObject.CopyTo(arr, 1)); + } + + [Fact] + public static void CreateDom() + { + var jObj = new JsonObject + { + // Primitives + ["MyString"] = JsonValue.Create("Hello!"), + ["MyNull"] = null, + ["MyBoolean"] = JsonValue.Create(false), + + // Nested array + ["MyArray"] = new JsonArray + ( + JsonValue.Create(2), + JsonValue.Create(3), + JsonValue.Create(42) + ), + + // Additional primitives + ["MyInt"] = JsonValue.Create(43), + ["MyDateTime"] = JsonValue.Create(new DateTime(2020, 7, 8)), + ["MyGuid"] = JsonValue.Create(new Guid("ed957609-cdfe-412f-88c1-02daca1b4f51")), + + // Nested objects + ["MyObject"] = new JsonObject + { + ["MyString"] = JsonValue.Create("Hello!!") + }, + + ["Child"] = new JsonObject + { + ["ChildProp"] = JsonValue.Create(1) + } + }; + + string json = jObj.ToJsonString(); + JsonTestHelper.AssertJsonEqual(JsonNodeTests.ExpectedDomJson, json); + } + + [Fact] + public static void CreateDom_ImplicitOperators() + { + var jObj = new JsonObject + { + // Primitives + ["MyString"] = "Hello!", + ["MyNull"] = null, + ["MyBoolean"] = false, + + // Nested array + ["MyArray"] = new JsonArray(2, 3, 42), + + // Additional primitives + ["MyInt"] = 43, + ["MyDateTime"] = new DateTime(2020, 7, 8), + ["MyGuid"] = new Guid("ed957609-cdfe-412f-88c1-02daca1b4f51"), + + // Nested objects + ["MyObject"] = new JsonObject + { + ["MyString"] = "Hello!!" + }, + + ["Child"] = new JsonObject() + { + ["ChildProp"] = 1 + } + }; + + string json = jObj.ToJsonString(); + JsonTestHelper.AssertJsonEqual(JsonNodeTests.ExpectedDomJson, json); + } + + [Fact] + public static void EditDom() + { + const string Json = + "{\"MyString\":\"Hello\",\"MyNull\":null,\"MyBoolean\":true,\"MyArray\":[1,2],\"MyInt\":42,\"MyDateTime\":\"2020-07-08T00:00:00\",\"MyGuid\":\"ed957609-cdfe-412f-88c1-02daca1b4f51\",\"MyObject\":{\"MyString\":\"World\"}}"; + + JsonNode obj = JsonSerializer.Deserialize(Json); + Verify(); + + // Verify the values are round-trippable. + ((JsonArray)obj["MyArray"]).RemoveAt(2); + Verify(); + + void Verify() + { + // Change some primitives. + obj["MyString"] = JsonValue.Create("Hello!"); + obj["MyBoolean"] = JsonValue.Create(false); + obj["MyInt"] = JsonValue.Create(43); + + // Add nested objects. + obj["MyObject"] = new JsonObject(); + obj["MyObject"]["MyString"] = JsonValue.Create("Hello!!"); + + obj["Child"] = new JsonObject(); + obj["Child"]["ChildProp"] = JsonValue.Create(1); + + // Modify number elements. + obj["MyArray"][0] = JsonValue.Create(2); + obj["MyArray"][1] = JsonValue.Create(3); + + // Add an element. + ((JsonArray)obj["MyArray"]).Add(JsonValue.Create(42)); + + string json = obj.ToJsonString(); + JsonTestHelper.AssertJsonEqual(JsonNodeTests.ExpectedDomJson, json); + } + } + + [Fact] + public static void ReAddSameNode_Throws() + { + var jValue = JsonValue.Create(1); + + var jObject = new JsonObject(); + jObject.Add("Prop", jValue); + Assert.Throws(() => jObject.Add("Prop", jValue)); + } + + [Fact] + public static void ReAddRemovedNode() + { + var jValue = JsonValue.Create(1); + + var jObject = new JsonObject(); + jObject.Add("Prop", jValue); + Assert.Equal(1, jObject.Count); + jObject.Remove("Prop"); + Assert.Equal(0, jObject.Count); + jObject.Add("Prop", jValue); + Assert.Equal(1, jObject.Count); + } + + [Fact] + public static void DynamicObject_LINQ_Query() + { + JsonArray allOrders = JsonSerializer.Deserialize(JsonNodeTests.Linq_Query_Json); + IEnumerable orders = allOrders.Where(o => o["Customer"]["City"].GetValue() == "Fargo"); + + Assert.Equal(2, orders.Count()); + Assert.Equal(100, orders.ElementAt(0)["OrderId"].GetValue()); + Assert.Equal(300, orders.ElementAt(1)["OrderId"].GetValue()); + Assert.Equal("Customer1", orders.ElementAt(0)["Customer"]["Name"].GetValue()); + Assert.Equal("Customer3", orders.ElementAt(1)["Customer"]["Name"].GetValue()); + } + + private class BlogPost + { + public string Title { get; set; } + public string AuthorName { get; set; } + public string AuthorTwitter { get; set; } + public string Body { get; set; } + public DateTime PostedDate { get; set; } + } + + [Fact] + public static void DynamicObject_LINQ_Convert() + { + string json = @" + [ + { + ""Title"": ""TITLE."", + ""Author"": + { + ""Name"": ""NAME."", + ""Mail"": ""MAIL."", + ""Picture"": ""/PICTURE.png"" + }, + ""Date"": ""2021-01-20T19:30:00"", + ""BodyHtml"": ""Content."" + } + ]"; + + JsonArray arr = JsonSerializer.Deserialize(json); + + // Convert nested JSON to a flat POCO. + IList blogPosts = arr.Select(p => new BlogPost + { + Title = p["Title"].GetValue(), + AuthorName = p["Author"]["Name"].GetValue(), + AuthorTwitter = p["Author"]["Mail"].GetValue(), + PostedDate = p["Date"].GetValue(), + Body = p["BodyHtml"].GetValue() + }).ToList(); + + const string expected = "[{\"Title\":\"TITLE.\",\"AuthorName\":\"NAME.\",\"AuthorTwitter\":\"MAIL.\",\"Body\":\"Content.\",\"PostedDate\":\"2021-01-20T19:30:00\"}]"; + + string json_out = JsonSerializer.Serialize(blogPosts); + Assert.Equal(expected, json_out); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/JsonValueTests.cs new file mode 100644 index 0000000000000..2b5e87cd8a869 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/JsonValueTests.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class JsonValueTests + { + [Fact] + public static void CreateFromNull() + { + Assert.Null(JsonValue.Create((object)null)); + } + + [Fact] + public static void CreateFromNode_Fail() + { + Assert.Throws(() => JsonValue.Create(new JsonArray())); + Assert.Throws(() => JsonValue.Create(new JsonObject())); + Assert.Throws(() => JsonValue.Create(JsonValue.Create(42))); + } + + [Fact] + public static void GetValue_Throws() + { + Assert.Throws(() => JsonNode.Parse("{}").GetValue()); + Assert.Throws(() => JsonNode.Parse("[]").GetValue()); + } + + private class Polymorphic_Base { } + private class Polymorphic_Derived : Polymorphic_Base { } + + [Fact] + public static void Polymorphic() + { + JsonValue value; + + Polymorphic_Base baseClass = new Polymorphic_Derived(); + value = JsonValue.Create(baseClass); + Assert.Same(baseClass, value.GetValue()); + + Polymorphic_Derived derivedClass = new Polymorphic_Derived(); + value = JsonValue.Create(derivedClass); + Assert.Same(derivedClass, value.GetValue()); + + Assert.Same(derivedClass, value.GetValue()); + } + + [Fact] + public static void QuotedNumbers_Deserialize() + { + var options = new JsonSerializerOptions(); + options.NumberHandling = JsonNumberHandling.AllowReadingFromString | + JsonNumberHandling.AllowNamedFloatingPointLiterals; + + JsonNode node = JsonSerializer.Deserialize("\"42\"", options); + Assert.IsAssignableFrom(node); + Assert.Throws(() => node.GetValue()); + + // A second pass is needed to obtain the quoted number. + Assert.Equal(42, JsonSerializer.Deserialize(node.ToJsonString(), options)); + + node = JsonSerializer.Deserialize("\"NaN\"", options); + Assert.IsAssignableFrom(node); + Assert.Equal(double.NaN, JsonSerializer.Deserialize(node.ToJsonString(), options)); + Assert.Equal(float.NaN, JsonSerializer.Deserialize(node.ToJsonString(), options)); + } + + [Fact] + public static void QuotedNumbers_Serialize() + { + var options = new JsonSerializerOptions(); + options.NumberHandling = JsonNumberHandling.WriteAsString; + + JsonValue obj = JsonValue.Create(42); + string json = obj.ToJsonString(options); + Assert.Equal("\"42\"", json); + + obj = JsonValue.Create(double.NaN); + json = obj.ToJsonString(options); + Assert.Equal("\"NaN\"", json); + } + + [Fact] + public static void FromElement() + { + using (JsonDocument document = JsonDocument.Parse("42")) + { + JsonValue jValue = JsonValue.Create(document.RootElement); + Assert.Equal(42, jValue.GetValue()); + } + + using (JsonDocument document = JsonDocument.Parse("null")) + { + JsonValue? jValue = JsonValue.Create(document.RootElement); + Assert.Null(jValue); + } + } + + [Fact] + public static void TryGetValue_FromString() + { + JsonValue jValue = JsonNode.Parse("\"MyString\"").AsValue(); + + Assert.True(jValue.TryGetValue(out string _)); + Assert.True(jValue.TryGetValue(out char _)); + 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 bool _)); + Assert.False(jValue.TryGetValue(out float _)); + Assert.False(jValue.TryGetValue(out double _)); + Assert.False(jValue.TryGetValue(out decimal _)); + Assert.False(jValue.TryGetValue(out DateTime _)); + Assert.False(jValue.TryGetValue(out DateTimeOffset _)); + Assert.False(jValue.TryGetValue(out Guid _)); + } + + [Fact] + public static void TryGetValue_FromNumber() + { + JsonValue jValue = JsonNode.Parse("42").AsValue(); + + Assert.True(jValue.TryGetValue(out byte _)); + Assert.True(jValue.TryGetValue(out short _)); + Assert.True(jValue.TryGetValue(out int _)); + Assert.True(jValue.TryGetValue(out long _)); + Assert.True(jValue.TryGetValue(out sbyte _)); + Assert.True(jValue.TryGetValue(out ushort _)); + Assert.True(jValue.TryGetValue(out uint _)); + Assert.True(jValue.TryGetValue(out ulong _)); + Assert.True(jValue.TryGetValue(out float _)); + Assert.True(jValue.TryGetValue(out double _)); + Assert.True(jValue.TryGetValue(out decimal _)); + Assert.False(jValue.TryGetValue(out bool _)); + 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 _)); + } + + [Fact] + public static void TryGetValue_FromGuid() + { + JsonValue jValue = JsonNode.Parse("\"ed957609-cdfe-412f-88c1-02daca1b4f51\"").AsValue(); + + Assert.True(jValue.TryGetValue(out Guid _)); + Assert.True(jValue.TryGetValue(out string _)); + Assert.True(jValue.TryGetValue(out char _)); + 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 bool _)); + Assert.False(jValue.TryGetValue(out DateTime _)); + Assert.False(jValue.TryGetValue(out DateTimeOffset _)); + } + + [Theory] + [InlineData("\"2020-07-08T00:00:00\"")] + [InlineData("\"2019-01-30T12:01:02+01:00\"")] + public static void TryGetValue_FromDateTime(string json) + { + JsonValue jValue = JsonNode.Parse(json).AsValue(); + + Assert.True(jValue.TryGetValue(out DateTime _)); + Assert.True(jValue.TryGetValue(out DateTimeOffset _)); + Assert.True(jValue.TryGetValue(out string _)); + Assert.True(jValue.TryGetValue(out char _)); + 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 bool _)); + Assert.False(jValue.TryGetValue(out Guid _)); + } + + [Theory] + [InlineData("\"A\"")] + [InlineData("\"AB\"")] + public static void FromElement_Char(string json) + { + using (JsonDocument document = JsonDocument.Parse(json)) + { + JsonValue jValue = JsonValue.Create(document.RootElement); + Assert.Equal('A', jValue.GetValue()); + + bool success = jValue.TryGetValue(out char ch); + Assert.True(success); + Assert.Equal('A', ch); + } + } + + [Theory] + [InlineData("\"A\"", "A")] + [InlineData("\"AB\"", "AB")] + public static void FromElement_ToElement(string json, string expected) + { + using (JsonDocument document = JsonDocument.Parse(json)) + { + JsonValue jValue = JsonValue.Create(document.RootElement); + + // Obtain the internal element + + JsonElement element = jValue.GetValue(); + Assert.Equal(expected, element.GetString()); + + bool success = jValue.TryGetValue(out element); + Assert.True(success); + Assert.Equal(expected, element.GetString()); + } + } + + [Fact] + public static void FromElement_Char_Fail() + { + using (JsonDocument document = JsonDocument.Parse("\"\"")) + { + JsonValue jValue = JsonValue.Create(document.RootElement); + Assert.Throws(() => jValue.GetValue()); + + bool success = jValue.TryGetValue(out char ch); + Assert.False(success); + Assert.Equal(default(char), ch); + } + + using (JsonDocument document = JsonDocument.Parse("42")) + { + JsonValue jValue = JsonValue.Create(document.RootElement); + Assert.Throws(() => jValue.GetValue()); + + bool success = jValue.TryGetValue(out char ch); + Assert.False(success); + Assert.Equal(default(char), ch); + } + } + + [Theory] + [InlineData("{}")] + [InlineData("[]")] + public static void FromElement_WrongNodeTypeThrows(string json) + { + using (JsonDocument document = JsonDocument.Parse(json)) + Assert.Throws(() => JsonValue.Create(document.RootElement)); + } + + [Fact] + public static void WriteTo_Validation() + { + Assert.Throws(() => JsonValue.Create(42).WriteTo(null)); + } + + [Fact] + public static void WriteTo() + { + const string Json = "42"; + + JsonValue jObject = JsonNode.Parse(Json).AsValue(); + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream); + jObject.WriteTo(writer); + writer.Flush(); + + string json = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(Json, json); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/JsonNode/ParentPathRootTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/ParentPathRootTests.cs new file mode 100644 index 0000000000000..d1d85097ca40b --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/ParentPathRootTests.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class ParentPathRootTests + { + [Fact] + public static void GetPathAndRoot() + { + JsonNode node; + + node = JsonValue.Create(1); + Assert.Equal("$", node.GetPath()); + Assert.Same(node, node.Root); + + node = new JsonObject(); + Assert.Equal("$", node.GetPath()); + Assert.Same(node, node.Root); + + node = new JsonArray(); + Assert.Equal("$", node.GetPath()); + Assert.Same(node, node.Root); + + node = new JsonObject + { + ["Child"] = 1 + }; + Assert.Equal("$.Child", node["Child"].GetPath()); + + node = new JsonObject + { + ["Child"] = new JsonArray { 1, 2, 3 } + }; + Assert.Equal("$.Child[1]", node["Child"][1].GetPath()); + Assert.Same(node, node["Child"][1].Root); + + node = new JsonObject + { + ["Child"] = new JsonArray { 1, 2, 3 } + }; + Assert.Equal("$.Child[2]", node["Child"][2].GetPath()); + Assert.Same(node, node["Child"][2].Root); + + node = new JsonArray + { + new JsonObject + { + ["Child"] = 42 + } + }; + Assert.Equal("$[0].Child", node[0]["Child"].GetPath()); + Assert.Same(node, node[0]["Child"].Root); + } + + [Fact] + public static void GetPath_SpecialCharacters() + { + JsonNode node = new JsonObject + { + ["[Child"] = 1 + }; + + Assert.Equal("$['[Child']", node["[Child"].GetPath()); + } + + [Fact] + public static void DetectCycles_Object() + { + var jObject = new JsonObject { }; + Assert.Throws(() => jObject.Add("a", jObject)); + + var jObject2 = new JsonObject { }; + jObject.Add("a", jObject2); + Assert.Throws(() => jObject2.Add("b", jObject)); + } + + [Fact] + public static void DetectCycles_Array() + { + var jArray = new JsonArray { }; + Assert.Throws(() => jArray.Add(jArray)); + + var jArray2 = new JsonArray { }; + jArray.Add(jArray2); + Assert.Throws(() => jArray2.Add(jArray)); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/JsonNode/ParseTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/ParseTests.cs new file mode 100644 index 0000000000000..1180c64514931 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/ParseTests.cs @@ -0,0 +1,217 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Text.Json.Serialization.Tests; +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class ParseTests + { + [Fact] + public static void Parse() + { + JsonObject jObject = JsonNode.Parse(JsonNodeTests.ExpectedDomJson).AsObject(); + + Assert.Equal("Hello!", jObject["MyString"].GetValue()); + Assert.Null(jObject["MyNull"]); + Assert.False(jObject["MyBoolean"].GetValue()); + Assert.Equal("ed957609-cdfe-412f-88c1-02daca1b4f51", jObject["MyGuid"].GetValue()); + Assert.IsType(jObject["MyArray"]); + Assert.IsType(jObject["MyObject"]); + + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + Assert.Equal(43, jObject["MyInt"].GetValue()); + + DateTime dt = JsonNode.Parse("\"2020-07-08T01:02:03\"").GetValue(); + Assert.Equal(2020, dt.Year); + Assert.Equal(7, dt.Month); + Assert.Equal(8, dt.Day); + Assert.Equal(1, dt.Hour); + Assert.Equal(2, dt.Minute); + Assert.Equal(3, dt.Second); + + DateTimeOffset dtOffset = JsonNode.Parse("\"2020-07-08T01:02:03+01:15\"").GetValue(); + Assert.Equal(2020, dtOffset.Year); + Assert.Equal(7, dtOffset.Month); + Assert.Equal(8, dtOffset.Day); + Assert.Equal(1, dtOffset.Hour); + Assert.Equal(2, dtOffset.Minute); + Assert.Equal(3, dtOffset.Second); + Assert.Equal(new TimeSpan(1,15,0), dtOffset.Offset); + } + + [Fact] + public static void Parse_TryGetPropertyValue() + { + JsonObject jObject = JsonNode.Parse(JsonNodeTests.ExpectedDomJson).AsObject(); + + JsonNode? node; + + Assert.True(jObject.TryGetPropertyValue("MyString", out node)); + Assert.Equal("Hello!", node.GetValue()); + + Assert.True(jObject.TryGetPropertyValue("MyNull", out node)); + Assert.Null(node); + + Assert.True(jObject.TryGetPropertyValue("MyBoolean", out node)); + Assert.False(node.GetValue()); + + Assert.True(jObject.TryGetPropertyValue("MyArray", out node)); + Assert.IsType(node); + + Assert.True(jObject.TryGetPropertyValue("MyInt", out node)); + Assert.Equal(43, node.GetValue()); + + Assert.True(jObject.TryGetPropertyValue("MyDateTime", out node)); + Assert.Equal("2020-07-08T00:00:00", node.GetValue()); + + Assert.True(jObject.TryGetPropertyValue("MyGuid", out node)); + Assert.Equal("ed957609-cdfe-412f-88c1-02daca1b4f51", node.AsValue().GetValue().ToString()); + + Assert.True(jObject.TryGetPropertyValue("MyObject", out node)); + Assert.IsType(node); + } + + [Fact] + public static void Parse_TryGetValue() + { + Assert.True(JsonNode.Parse("\"Hello\"").AsValue().TryGetValue(out string? _)); + Assert.True(JsonNode.Parse("true").AsValue().TryGetValue(out bool? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out byte? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out sbyte? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out short? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out ushort? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out int? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out uint? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out long? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out ulong? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out decimal? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out float? _)); + Assert.True(JsonNode.Parse("42").AsValue().TryGetValue(out double? _)); + Assert.True(JsonNode.Parse("\"2020-07-08T00:00:00\"").AsValue().TryGetValue(out DateTime? _)); + Assert.True(JsonNode.Parse("\"ed957609-cdfe-412f-88c1-02daca1b4f51\"").AsValue().TryGetValue(out Guid? _)); + Assert.True(JsonNode.Parse("\"2020-07-08T01:02:03+01:15\"").AsValue().TryGetValue(out DateTimeOffset? _)); + + JsonValue? jValue = JsonNode.Parse("\"Hello!\"").AsValue(); + Assert.False(jValue.TryGetValue(out int _)); + Assert.False(jValue.TryGetValue(out DateTime _)); + Assert.False(jValue.TryGetValue(out DateTimeOffset _)); + Assert.False(jValue.TryGetValue(out Guid _)); + } + + [Fact] + public static void Parse_Fail() + { + JsonObject jObject = JsonNode.Parse(JsonNodeTests.ExpectedDomJson).AsObject(); + + Assert.Throws(() => jObject["MyString"].GetValue()); + Assert.Throws(() => jObject["MyBoolean"].GetValue()); + Assert.Throws(() => jObject["MyGuid"].GetValue()); + Assert.Throws(() => jObject["MyInt"].GetValue()); + Assert.Throws(() => jObject["MyDateTime"].GetValue()); + Assert.Throws(() => jObject["MyObject"].GetValue()); + Assert.Throws(() => jObject["MyArray"].GetValue()); + } + + [Fact] + public static void NullReference_Fail() + { + Assert.Throws(() => JsonSerializer.Deserialize((string)null)); + Assert.Throws(() => JsonNode.Parse((string)null)); + Assert.Throws(() => JsonNode.Parse((Stream)null)); + } + + [Fact] + public static void NullLiteral() + { + Assert.Null(JsonSerializer.Deserialize("null")); + Assert.Null(JsonNode.Parse("null")); + + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes("null"))) + { + Assert.Null(JsonNode.Parse(stream)); + } + } + + [Fact] + public static void ReadSimpleObject() + { + using (MemoryStream stream = new MemoryStream(SimpleTestClass.s_data)) + { + JsonNode node = JsonNode.Parse(stream); + + string actual = node.ToJsonString(); + // Replace the escaped "+" sign used with DateTimeOffset. + actual = actual.Replace("\\u002B", "+"); + + Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual); + } + } + + [Fact] + public static void ReadSimpleObjectWithTrailingTrivia() + { + byte[] data = Encoding.UTF8.GetBytes(SimpleTestClass.s_json + " /* Multi\r\nLine Comment */\t"); + using (MemoryStream stream = new MemoryStream(data)) + { + var options = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip + }; + + JsonNode node = JsonNode.Parse(stream, nodeOptions: null, options); + + string actual = node.ToJsonString(); + // Replace the escaped "+" sign used with DateTimeOffset. + actual = actual.Replace("\\u002B", "+"); + + Assert.Equal(SimpleTestClass.s_json.StripWhitespace(), actual); + } + } + + [Fact] + public static void ReadPrimitives() + { + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(@"1"))) + { + int i = JsonNode.Parse(stream).AsValue().GetValue(); + Assert.Equal(1, i); + } + } + + [Fact] + public static void ParseThenEdit() + { + const string Expected = "{\"MyString\":null,\"Node\":42,\"Array\":[43],\"Value\":44,\"IntValue\":45,\"Object\":{\"Property\":46}}"; + + JsonNode node = JsonNode.Parse(Expected); + Assert.Equal(Expected, node.ToJsonString()); + + // Change a primitive + node["IntValue"] = 1; + const string ExpectedAfterEdit1 = "{\"MyString\":null,\"Node\":42,\"Array\":[43],\"Value\":44,\"IntValue\":1,\"Object\":{\"Property\":46}}"; + Assert.Equal(ExpectedAfterEdit1, node.ToJsonString()); + + // Change element + node["Array"][0] = 2; + const string ExpectedAfterEdit2 = "{\"MyString\":null,\"Node\":42,\"Array\":[2],\"Value\":44,\"IntValue\":1,\"Object\":{\"Property\":46}}"; + Assert.Equal(ExpectedAfterEdit2, node.ToJsonString()); + + // Change property + node["MyString"] = "3"; + const string ExpectedAfterEdit3 = "{\"MyString\":\"3\",\"Node\":42,\"Array\":[2],\"Value\":44,\"IntValue\":1,\"Object\":{\"Property\":46}}"; + Assert.Equal(ExpectedAfterEdit3, node.ToJsonString()); + } + } +} + diff --git a/src/libraries/System.Text.Json/tests/JsonNode/SerializerInteropTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/SerializerInteropTests.cs new file mode 100644 index 0000000000000..3bfb6f7d113b9 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/SerializerInteropTests.cs @@ -0,0 +1,65 @@ +// 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.Text.Json.Serialization.Tests.Schemas.OrderPayload; +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class SerializerInteropTests + { + [Fact] + public static void CompareResultsAgainstSerializer() + { + List obj = Serialization.Tests.StreamTests.PopulateLargeObject(2); + string expected = JsonSerializer.Serialize(obj); + + JsonArray jArray = JsonSerializer.Deserialize(expected); + string actual = jArray.ToJsonString(); + Assert.Equal(expected, actual); + + jArray = JsonNode.Parse(expected).AsArray(); + actual = jArray.ToJsonString(); + Assert.Equal(expected, actual); + } + + private class Poco + { + public string MyString { get; set; } + public JsonNode Node { get; set; } + public JsonArray Array { get; set; } + public JsonValue Value { get; set; } + public JsonValue IntValue { get; set; } + public JsonObject Object { get; set; } + } + + [Fact] + public static void NodesAsPocoProperties() + { + const string Expected = "{\"MyString\":null,\"Node\":42,\"Array\":[43],\"Value\":44,\"IntValue\":45,\"Object\":{\"Property\":46}}"; + + var poco = new Poco + { + Node = 42, + Array = new JsonArray(43), + Value = (JsonValue)44, + IntValue = (JsonValue)45, + Object = new JsonObject + { + ["Property"] = 46 + } + }; + + string json = JsonSerializer.Serialize(poco); + Assert.Equal(Expected, json); + + poco = JsonSerializer.Deserialize(json); + Assert.Equal(42, (int)poco.Node); + Assert.Equal(43, (int)poco.Array[0]); + Assert.Equal(44, (int)poco.Value); + Assert.Equal(45, (int)poco.IntValue); + Assert.Equal(46, (int)poco.Object["Property"]); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/JsonNode/ToStringTests.cs b/src/libraries/System.Text.Json/tests/JsonNode/ToStringTests.cs new file mode 100644 index 0000000000000..b1992e2277916 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/JsonNode/ToStringTests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Text.Json.Node.Tests +{ + public static class ToStringTests + { + internal const string JsonWithWhitespace = "{\r\n \"MyString\": \"Hello!\",\r\n \"MyNull\": null,\r\n \"MyBoolean\": false,\r\n \"MyArray\": [\r\n 2,\r\n 3,\r\n 42\r\n ],\r\n \"MyInt\": 43,\r\n \"MyDateTime\": \"2020-07-08T00:00:00\",\r\n \"MyGuid\": \"ed957609-cdfe-412f-88c1-02daca1b4f51\",\r\n \"MyObject\": {\r\n \"MyString\": \"Hello!!\"\r\n },\r\n \"Child\": {\r\n \"ChildProp\": 1\r\n }\r\n}"; + + [Fact] + public static void NodeToString() + { + JsonNode node = JsonNode.Parse(JsonNodeTests.ExpectedDomJson); + string json = node.ToString(); + Assert.Equal(JsonWithWhitespace, json); + } + + [Fact] + public static void NodeToString_StringValuesNotQuoted() + { + JsonNode node = JsonNode.Parse("\"Hello\""); + string json = node.ToString(); + Assert.Equal("Hello", json); + + node = JsonValue.Create("Hello"); + json = node.ToString(); + Assert.Equal("Hello", json); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Dynamic.Sample.Tests.cs b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Dynamic.Sample.Tests.cs index 511267cdc7dda..d4284c5517aef 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Dynamic.Sample.Tests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Dynamic.Sample.Tests.cs @@ -26,14 +26,17 @@ private enum MyCustomEnum public static void VerifyPrimitives() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + + // To prevent calling the actual EnableDynamicTypes() just use the extension class directly + JsonSerializerExtensions.EnableDynamicTypes(options); + options.Converters.Add(new JsonStringEnumConverter()); dynamic obj = JsonSerializer.Deserialize(DynamicTests.Json, options); - Assert.IsType(obj); + Assert.IsType(obj); // JsonDynamicString has an implicit cast to string. - Assert.IsType(obj.MyString); + Assert.IsType(obj.MyString); Assert.Equal("Hello", obj.MyString); // Verify other string-based types. @@ -71,14 +74,14 @@ public static void VerifyPrimitives() public static void VerifyArray() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); options.Converters.Add(new JsonStringEnumConverter()); dynamic obj = JsonSerializer.Deserialize(DynamicTests.Json, options); - Assert.IsType(obj); + Assert.IsType(obj); - Assert.IsType(obj); - Assert.IsType(obj.MyArray); + Assert.IsType(obj); + Assert.IsType(obj.MyArray); Assert.Equal(2, obj.MyArray.Count); Assert.Equal(1, (int)obj.MyArray[0]); @@ -101,7 +104,7 @@ public static void VerifyArray() public static void JsonDynamicTypes_Serialize() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); // Guid (string) string GuidJson = $"{DynamicTests.MyGuid.ToString("D")}"; @@ -134,14 +137,15 @@ public static void JsonDynamicTypes_Serialize() Assert.Equal("true", json); // Array - dynamic arr = new JsonDynamicArray(options); + dynamic arr = new System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicArray(options); arr.Add(1); arr.Add(2); + json = JsonSerializer.Serialize(arr, options); Assert.Equal("[1,2]", json); // Object - dynamic dynamicObject = new JsonDynamicObject(options); + dynamic dynamicObject = new System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject(options); dynamicObject["One"] = 1; dynamicObject["Two"] = 2; @@ -153,14 +157,14 @@ public static void JsonDynamicTypes_Serialize() public static void JsonDynamicTypes_Deserialize() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); JsonSerializer.Deserialize("{}", options); - JsonSerializer.Deserialize("[]", options); + JsonSerializer.Deserialize("[]", options); JsonSerializer.Deserialize("true", options); JsonSerializer.Deserialize("0", options); JsonSerializer.Deserialize("1.2", options); - JsonSerializer.Deserialize("{}", options); + JsonSerializer.Deserialize("{}", options); JsonSerializer.Deserialize("\"str\"", options); } @@ -168,13 +172,13 @@ public static void JsonDynamicTypes_Deserialize() public static void JsonDynamicTypes_Deserialize_AsObject() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); - Assert.IsType(JsonSerializer.Deserialize("[]", options)); + Assert.IsType(JsonSerializer.Deserialize("[]", options)); Assert.IsType(JsonSerializer.Deserialize("true", options)); Assert.IsType(JsonSerializer.Deserialize("0", options)); Assert.IsType(JsonSerializer.Deserialize("1.2", options)); - Assert.IsType(JsonSerializer.Deserialize("{}", options)); + Assert.IsType(JsonSerializer.Deserialize("{}", options)); Assert.IsType(JsonSerializer.Deserialize("\"str\"", options)); } @@ -185,10 +189,10 @@ public static void JsonDynamicTypes_Deserialize_AsObject() public static void VerifyMutableDom_UsingDynamicKeyword() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); dynamic obj = JsonSerializer.Deserialize(DynamicTests.Json, options); - Assert.IsType(obj); + Assert.IsType(obj); // Change some primitives. obj.MyString = "Hello!"; @@ -198,11 +202,11 @@ public static void VerifyMutableDom_UsingDynamicKeyword() // Add nested objects. // Use JsonDynamicObject; ExpandoObject should not be used since it doesn't have the same semantics including // null handling and case-sensitivity that respects JsonSerializerOptions.PropertyNameCaseInsensitive. - dynamic myObject = new JsonDynamicObject(options); + dynamic myObject = new System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject(options); myObject.MyString = "Hello!!"; obj.MyObject = myObject; - dynamic child = new JsonDynamicObject(options); + dynamic child = new System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject(options); child.ChildProp = 1; obj.Child = child; @@ -225,9 +229,9 @@ public static void VerifyMutableDom_UsingDynamicKeyword() public static void VerifyMutableDom_WithoutUsingDynamicKeyword() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); - JsonDynamicObject obj = (JsonDynamicObject)JsonSerializer.Deserialize(DynamicTests.Json, options); + System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject obj = (System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject)JsonSerializer.Deserialize(DynamicTests.Json, options); // Change some primitives. obj["MyString"] = "Hello!"; @@ -235,19 +239,19 @@ public static void VerifyMutableDom_WithoutUsingDynamicKeyword() obj["MyInt"] = 43; // Add nested objects. - obj["MyObject"] = new JsonDynamicObject(options) + obj["MyObject"] = new System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject(options) { ["MyString"] = "Hello!!" }; - obj["Child"] = new JsonDynamicObject(options) + obj["Child"] = new System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject(options) { ["ChildProp"] = 1 }; // Modify number elements. - var arr = (JsonDynamicArray)obj["MyArray"]; - var elem = (JsonDynamicNumber)arr[0]; + var arr = (System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicArray)obj["MyArray"]; + var elem = (System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicNumber)arr[0]; elem.SetValue(elem.GetValue() + 1); elem = (JsonDynamicNumber)arr[1]; elem.SetValue(elem.GetValue() + 1); @@ -267,13 +271,13 @@ public static void VerifyMutableDom_WithoutUsingDynamicKeyword() public static void VerifyMutableDom_WithoutUsingDynamicKeyword_JsonDynamicType() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); - JsonDynamicObject obj = (JsonDynamicObject)JsonSerializer.Deserialize(DynamicTests.Json, options); + System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject obj = (System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject)JsonSerializer.Deserialize(DynamicTests.Json, options); Verify(); // Verify the values are round-trippable. - ((JsonDynamicArray)obj["MyArray"]).RemoveAt(2); + ((System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicArray)obj["MyArray"]).RemoveAt(2); Verify(); void Verify() @@ -284,17 +288,17 @@ void Verify() ((JsonDynamicType)obj["MyInt"]).SetValue(43); // Add nested objects. - obj["MyObject"] = new JsonDynamicObject(options) + obj["MyObject"] = new System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject(options) { ["MyString"] = new JsonDynamicString("Hello!!", options) }; - obj["Child"] = new JsonDynamicObject(options) + obj["Child"] = new System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicObject(options) { ["ChildProp"] = new JsonDynamicNumber(1, options) }; // Modify number elements. - var arr = (JsonDynamicArray)obj["MyArray"]; + var arr = (System.Text.Json.Serialization.Samples.JsonSerializerExtensions.JsonDynamicArray)obj["MyArray"]; ((JsonDynamicType)arr[0]).SetValue(2); ((JsonDynamicType)arr[1]).SetValue(3); @@ -310,7 +314,8 @@ void Verify() public static void DynamicObject_MissingProperty() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); + dynamic obj = JsonSerializer.Deserialize("{}", options); // We return null here; ExpandoObject throws for missing properties. @@ -321,7 +326,8 @@ public static void DynamicObject_MissingProperty() public static void DynamicObject_CaseSensitivity() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); + dynamic obj = JsonSerializer.Deserialize("{\"MyProperty\":42}", options); Assert.Equal(42, (int)obj.MyProperty); @@ -329,7 +335,7 @@ public static void DynamicObject_CaseSensitivity() Assert.Null(obj.MYPROPERTY); options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); options.PropertyNameCaseInsensitive = true; obj = JsonSerializer.Deserialize("{\"MyProperty\":42}", options); @@ -344,7 +350,7 @@ public static void NamingPoliciesAreNotUsed() const string Json = "{\"myProperty\":42}"; var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); options.PropertyNamingPolicy = new SimpleSnakeCasePolicy(); dynamic obj = JsonSerializer.Deserialize(Json, options); @@ -357,7 +363,7 @@ public static void NamingPoliciesAreNotUsed() public static void NullHandling() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); dynamic obj = JsonSerializer.Deserialize("null", options); Assert.Null(obj); @@ -367,7 +373,7 @@ public static void NullHandling() public static void QuotedNumbers_Deserialize() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); options.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals; @@ -385,7 +391,7 @@ public static void QuotedNumbers_Deserialize() public static void QuotedNumbers_Serialize() { var options = new JsonSerializerOptions(); - options.EnableDynamicTypes(); + JsonSerializerExtensions.EnableDynamicTypes(options); options.NumberHandling = JsonNumberHandling.WriteAsString; dynamic obj = 42L; diff --git a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs index b0fa10a473ce8..53c7fdd1dd91d 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs @@ -590,6 +590,7 @@ private static JsonSerializerOptions GetFullyPopulatedOptionsInstance() options.ReadCommentHandling = JsonCommentHandling.Disallow; options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; options.NumberHandling = JsonNumberHandling.AllowReadingFromString; + options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; } else { @@ -641,6 +642,10 @@ private static void VerifyOptionsEqual(JsonSerializerOptions options, JsonSerial { Assert.Equal(options.NumberHandling, newOptions.NumberHandling); } + else if (property.Name == "UnknownTypeHandling") + { + Assert.Equal(options.UnknownTypeHandling, newOptions.UnknownTypeHandling); + } else { Assert.True(false, $"Public option was added to JsonSerializerOptions but not copied in the copy ctor: {property.Name}"); diff --git a/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs b/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs index b7e90b6085e02..06481bf70fed6 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/Stream.WriteTests.cs @@ -456,7 +456,7 @@ private static List GenerateListOfSize(int size) return list; } - private static List PopulateLargeObject(int size) + internal static List PopulateLargeObject(int size) { List orders = new List(size); for (int i = 0; i < size; i++) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 26116948c4310..3c3a945d01528 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -25,6 +25,17 @@ + + + + + + + + + + +