-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add JsonNode feature #51025
Add JsonNode feature #51025
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @eiriktsarpalis, @layomia Issue DetailsImplementation of writeable JSON DOM including support for C# Fixes #47649 Notes:
|
🥳 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, made some suggestions for what look like copy/paste errors.
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/ref/System.Text.Json.InboxOnly.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonArray.IList.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.cs
Outdated
Show resolved
Hide resolved
Assert.Equal("Hello", (string)obj.MyString); | ||
|
||
// Verify other string-based types. | ||
// Since this requires a custom converter, an exception is thrown. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm isn't this why JsonStringEnumConverter
was added above on L64? Is it not honored? Also, is the specific RuntimeBinderException
known or can it vary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently the semantics don't go through the serializer when calling GetValue(), so another step is needed to get the enum. We can revisit this based on feedback, but per discussion decided to start with more restrictive semantics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future, would we need more overloads that take the options (to retrieve converters and such), or will the existing API surface suffice?
|
||
// Numbers must specify the type through a cast or assignment. | ||
Assert.IsAssignableFrom<JsonValue>(obj.MyInt); | ||
Assert.ThrowsAny<Exception>(() => obj.MyInt == 42L); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exception is thrown?
On a separate note - can I dynamically change the CLR type of a property on a dynamic object? e.g. could I say obj.MyInt = "Hello"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to RuntimeBinderException
. We could catch and throw InvalidOperation in this case; I'll look at that for consistency sake with non-dynamic.
Yes you can change the value with obj.MyInt = "Hello"
since that uses the indexer to create a new node, and then replace the previous node.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to consistency with non-dynamic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll address this after the PR. To support this will require changes to the lower-level dynamic interop in MetaDynamic.cs
and adding more extensive testing for failure cases.
src/libraries/System.Text.Json/tests/JsonNode/JsonNodeOperatorTests.cs
Outdated
Show resolved
Hide resolved
public static void SetItem_Fail() | ||
{ | ||
var jObject = new JsonObject(); | ||
Assert.Throws<ArgumentNullException>(() => jObject[null] = 42); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this test any different from NullPropertyNameFail
above?
JsonNode node1 = jObject["One"]; | ||
JsonNode node2 = jObject["Two"]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: these don't seem to be used
// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you imagine people will run into issues with the somewhat subtle semantics here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With collection initializers this can occur although I don't see an easy alternative to "force" the options to be specified upfront before "Add()".
Originally, I had a design where the JsonSerializerOptions
were used instead of JsonNodeOptions
since those options have a concept of case insensitivity. However, that dependency was removed because tying those options to DOM creation was somewhat confusing since basically only the case insensitivity flag was used and nothing else. However, using JsonSerializerOptions
like originally proposed could work in this scenario when we implement the ability to change the global options (as is scheduled for v6 I believe).
So if we add the ability to change the global JsonSerializerOptions
perhaps we also add the ability to change\specify a global JsonNodeOptions
and that could be used to address this scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see - yeah we should document this explicitly.
when we implement the ability to change the global options (as is scheduled for v6 I believe).
I'm getting more and more scared of doing this due to side effects with the defaults being changed in a way that affects all assemblies in a compilation.
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonValueOfT.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/JsonNode/JsonValueTests.cs
Outdated
Show resolved
Hide resolved
|
||
JsonNode node = JsonSerializer.Deserialize<JsonNode>("\"42\"", options); | ||
Assert.IsAssignableFrom<JsonValue>(node); | ||
Assert.Throws<InvalidOperationException>(() => node.GetValue<int>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to have this work in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes if we decide we want to ease up on the GetValue()
semantics and invoke the serializer. That would also require the JsonSerializerOptions
to be passed in for things like quoted numbers.
We could also add a new method, like Json.NET's JNode.ToObject()
, to deserialize.
src/libraries/System.Text.Json/tests/JsonNode/JsonValueTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/JsonNode/JsonValueTests.cs
Outdated
Show resolved
Hide resolved
{ | ||
JsonNode node = new JsonObject | ||
{ | ||
["[Child"] = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. Are there other characters that are interesting enough to test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean for the property name or for the value? The JsonNodeOperatorTests.cs
has every value type possible (int, uint, string, char, etc)
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonNode.cs
Outdated
Show resolved
Hide resolved
{ | ||
if (Parent != null) | ||
{ | ||
ThrowHelper.ThrowInvalidOperationException_NodeAlreadyHasParent(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could it be valid in the future to want to replace/unbind from the parent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unbinding happens when an element is removed via Remove() or replaced via an indexer.
|
||
namespace System.Text.Json.Node | ||
{ | ||
public partial class JsonObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems dynamic
semantics are only supported when using JsonObject
. What happens when other node types are treated as dynamic i.e setting/getting properties is attempted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dynamic feature allows you to call members on JsonArray
and JsonValue
(e.g. jArray.Add()
). If an invalid member is specifies, RuntimeBinderException
is thrown.
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.IDictionary.cs
Show resolved
Hide resolved
src/libraries/System.Text.Json/src/System/Text/Json/Node/JsonObject.cs
Outdated
Show resolved
Hide resolved
|
||
if (_value is JsonElement jsonElement) | ||
{ | ||
return ConvertJsonElement<T>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ConvertJsonElement
and TryConvertJsonElement
seem very similar. Could we instead call TryConvertJsonElement
here, and throw the IOE
if it returns false? Perhaps we could then delete ConvertJsonElement
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps. The reason is since we delegate to JsonElement
to obtain the values in ConvertJsonElement
we leverage the existing validation and exception logic in JsonElement
. JsonElement
in some cases throws FormatException
and in other cases InvalidOperation
which we would need to duplicate if we combine.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth it to instantiate these lazily and save on allocs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are already lazy. The JsonNodeConverterFactory
will only access these properties when given a JsonNode Type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe these properties will be initialized by the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These particular properties will be initialized eagerly by the constructor:
...ries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
Outdated
Show resolved
Hide resolved
...ies/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs
Outdated
Show resolved
Hide resolved
...t.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.Dynamic.Sample.Tests.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Implementation of writeable JSON DOM including support for C#
dynamic
.Fixes #47649
Notes:
JsonObject
is not implemented in the PR to help reduce review size. That support will come after this PR is in and will allow an extension property of the form:[JsonExtensionData] public JsonObject ExtensionData {get; set;}
MetaDynamic.cs
contains internal code to support C#dynamic
. As a follow-up to this PR, additional tests may be added to increase coverage here and\or that code refactored to reduce some lower-level expression-based code.MetaDynamic.cs
as mentioned above.JsonObject
'sSystem.Collections.Generic.Dictionary<,>
will be replaced with a dictionary that supports ordering semantics so inserts\removes are deterministic with list-based semantics; ordering is nondeterministic withDictionary<,>
.