-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Allow trailing trivia in JsonSerializer.Parse and Read #37500 #37549
Changes from all commits
64da14f
e2eb92c
66a0e5d
cd324b7
a6da3d4
bbb432a
f3b4513
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,15 +53,21 @@ public string PropertyPath | |
| { | ||
| get | ||
| { | ||
| StringBuilder path = new StringBuilder(); | ||
| StringBuilder path; | ||
|
|
||
| if (_previous == null || _index == 0) | ||
| { | ||
| path.Append($"[{Current.JsonClassInfo.Type.FullName}]"); | ||
| // No path if we've walked beyond the end of our JSON document | ||
| if (Current.JsonClassInfo == null) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am just wondering if this is the only case we would want to return |
||
| { | ||
| return "<none>"; | ||
| } | ||
|
|
||
| path = new StringBuilder($"[{Current.JsonClassInfo.Type.FullName}]"); | ||
| } | ||
| else | ||
| { | ||
| path.Append($"[{_previous[0].JsonClassInfo.Type.FullName}]"); | ||
| path = new StringBuilder($"[{_previous[0].JsonClassInfo.Type.FullName}]"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried various levels of invalid nesting or unexpected types and could not elicit an NPE down this path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great. Good to know :) |
||
|
|
||
| for (int i = 0; i < _index; i++) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,37 @@ public static void ReadSimpleClass() | |
| obj.Verify(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("", " ")] | ||
| [InlineData("", "\t ")] | ||
| [InlineData("", "//Single Line Comment\r\n")] | ||
| [InlineData("", "/* Multi\nLine Comment */")] | ||
| [InlineData("", "\t\t\t\n// Both\n/* Comments */")] | ||
| [InlineData(" ", "")] | ||
| [InlineData("\t ", "")] | ||
| [InlineData(" \t", " \n")] | ||
| [InlineData("// Leading Comment\n", "")] | ||
| [InlineData("/* Multi\nLine\nComment */ ", "")] | ||
| [InlineData("/* Multi\nLine\nComment */ ", "\t// trailing comment\n ")] | ||
| public static void ReadSimpleClassIgnoresLeadingOrTrailingTrivia(string leadingTrivia, string trailingTrivia) | ||
| { | ||
| { | ||
| var options = new JsonSerializerOptions(); | ||
| options.ReadCommentHandling = JsonCommentHandling.Skip; | ||
|
|
||
| SimpleTestClass obj = JsonSerializer.Parse<SimpleTestClass>(leadingTrivia + SimpleTestClass.s_json + trailingTrivia, options); | ||
| obj.Verify(); | ||
| } | ||
|
|
||
| { | ||
| var options = new JsonSerializerOptions(); | ||
| options.ReadCommentHandling = JsonCommentHandling.Allow; | ||
|
|
||
| SimpleTestClass obj = JsonSerializer.Parse<SimpleTestClass>(leadingTrivia + SimpleTestClass.s_json + trailingTrivia, options); | ||
| obj.Verify(); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public static void ReadSimpleClassWithObject() | ||
| { | ||
|
|
@@ -31,6 +62,18 @@ public static void ReadSimpleClassWithObject() | |
| obj.Verify(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("", "//Single Line Comment\r\n")] | ||
| [InlineData("", "/* Multi\nLine Comment */")] | ||
| [InlineData("", "\t\t\t\n// Both\n/* Comments */")] | ||
| [InlineData("// Leading Comment\n", "")] | ||
| [InlineData("/* Multi\nLine\nComment */ ", "")] | ||
| [InlineData("/* Multi\nLine\nComment */ ", "\t// trailing comment\n ")] | ||
| public static void ReadClassWithCommentsThrowsIfDisallowed(string leadingTrivia, string trailingTrivia) | ||
| { | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Parse<ClassWithComplexObjects>(leadingTrivia + ClassWithComplexObjects.s_json + trailingTrivia)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public static void ReadSimpleClassWithObjectArray() | ||
| { | ||
|
|
@@ -63,6 +106,34 @@ public static void ReadClassWithComplexObjects() | |
| obj.Verify(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("", " ")] | ||
| [InlineData("", "\t ")] | ||
| [InlineData("", "//Single Line Comment\r\n")] | ||
| [InlineData("", "/* Multi\nLine Comment */")] | ||
| [InlineData("", "\t\t\t\n// Both\n/* Comments */")] | ||
| [InlineData(" ", "")] | ||
| [InlineData("\t ", "")] | ||
| [InlineData(" \t", " \n")] | ||
| [InlineData("// Leading Comment\n", "")] | ||
| [InlineData("/* Multi\nLine\nComment */ ", "")] | ||
| [InlineData("/* Multi\nLine\nComment */ ", "\t// trailing comment\n ")] | ||
|
watfordgnf marked this conversation as resolved.
|
||
| public static void ReadComplexClassIgnoresLeadingOrTrailingTrivia(string leadingTrivia, string trailingTrivia) | ||
| { | ||
| var options = new JsonSerializerOptions(); | ||
| options.ReadCommentHandling = JsonCommentHandling.Skip; | ||
|
watfordgnf marked this conversation as resolved.
|
||
|
|
||
| ClassWithComplexObjects obj = JsonSerializer.Parse<ClassWithComplexObjects>(leadingTrivia + ClassWithComplexObjects.s_json + trailingTrivia, options); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As an aside (unrelated to this PR), we should consider adding tests where we have comments in-between the JSON objects as well to make sure the serializer continues to handle that. We have such tests for the reader/document, but I am not sure we have them for the serializer. |
||
| obj.Verify(); | ||
|
|
||
| // Throws due to JsonDocument.TryParse not supporting Allow | ||
| //var options = new JsonSerializerOptions(); | ||
| //options.ReadCommentHandling = JsonCommentHandling.Allow; | ||
| // | ||
| //obj = JsonSerializer.Parse<ClassWithComplexObjects>(leadingTrivia + ClassWithComplexObjects.s_json + trailingTrivia, options); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think the most intuitive solution here would be that the serializer rejects |
||
| //obj.Verify(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public static void ReadEmpty() | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,23 @@ public static async Task ReadSimpleObjectAsync() | |
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public static async Task ReadSimpleObjectWithTrailingTriviaAsync() | ||
| { | ||
| byte[] data = Encoding.UTF8.GetBytes(SimpleTestClass.s_json + " /* Multi\r\nLine Comment */\t"); | ||
| using (MemoryStream stream = new MemoryStream(data)) | ||
| { | ||
| JsonSerializerOptions options = new JsonSerializerOptions | ||
| { | ||
| DefaultBufferSize = 1, | ||
| ReadCommentHandling = JsonCommentHandling.Skip, | ||
| }; | ||
|
|
||
| SimpleTestClass obj = await JsonSerializer.ReadAsync<SimpleTestClass>(stream, options); | ||
| obj.Verify(); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public static async Task ReadPrimitivesAsync() | ||
| { | ||
|
|
@@ -46,5 +63,21 @@ public static async Task ReadPrimitivesAsync() | |
| Assert.Equal(1, i); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public static async Task ReadPrimitivesWithTrailingTriviaAsync() | ||
| { | ||
| using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(" 1\t// Comment\r\n/* Multi\r\nLine */"))) | ||
| { | ||
| JsonSerializerOptions options = new JsonSerializerOptions | ||
| { | ||
| DefaultBufferSize = 1, | ||
| ReadCommentHandling = JsonCommentHandling.Skip, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add a test for "Allow comments" as well (especially for more than 1 leading/trailing comment).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you nest complex documents, JsonSerializer uses JsonDocument which throws: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. In that case, can we add a test for simple POCO (that doesn't have @steveharter, @JeremyKuhne, @bartonjs - this is something we should fix up. Either the serialize should disallow "allow comments" similar to @watfordgnf, this can be deferred for now (i.e. the PR doesn't need to block on this discussion).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. |
||
| }; | ||
|
|
||
| int i = await JsonSerializer.ReadAsync<int>(stream, options); | ||
| Assert.Equal(1, i); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,37 @@ public static void ReadPrimitives() | |
| Assert.Equal("Hello", s2); | ||
| } | ||
|
|
||
| [Fact] | ||
| public static void ReadPrimitivesWithWhitespace() | ||
| { | ||
| int i = JsonSerializer.Parse<int>(Encoding.UTF8.GetBytes(@" 1 ")); | ||
|
watfordgnf marked this conversation as resolved.
|
||
| Assert.Equal(1, i); | ||
|
|
||
| int i2 = JsonSerializer.Parse<int>("2\t"); | ||
| Assert.Equal(2, i2); | ||
|
|
||
| int? i3 = JsonSerializer.Parse<int?>("\r\nnull"); | ||
| Assert.Null(i3); | ||
|
|
||
| long l = JsonSerializer.Parse<long>(Encoding.UTF8.GetBytes("\t" + long.MaxValue.ToString())); | ||
| Assert.Equal(long.MaxValue, l); | ||
|
|
||
| long l2 = JsonSerializer.Parse<long>(long.MaxValue.ToString() + " \r\n"); | ||
| Assert.Equal(long.MaxValue, l2); | ||
|
|
||
| string s = JsonSerializer.Parse<string>(Encoding.UTF8.GetBytes(@"""Hello"" ")); | ||
| Assert.Equal("Hello", s); | ||
|
|
||
| string s2 = JsonSerializer.Parse<string>(@" ""Hello"" "); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking a bit more about using string s3 = JsonSerializer.Parse<string>(@"""Hello"" ""Hello""");
string s4 = JsonSerializer.Parse<string>(@"""Hello"" 42");
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The serializer should definitely throw when given a string like this. In Json.NET there is a setting on the JSON reader to allow multiple content. Additional content can optionally be delimited by a comma
It is a Useful Feature. People like it when streaming huge JSON content. You don't need it for 3.0.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added two extra cases to Value.ReadTests ReadPrimitiveExtraBytesFail to cover that scenari @ahsonkhan. |
||
| Assert.Equal("Hello", s2); | ||
|
|
||
| bool b = JsonSerializer.Parse<bool>(" \ttrue "); | ||
| Assert.Equal(true, b); | ||
|
|
||
| bool b2 = JsonSerializer.Parse<bool>(" false\n"); | ||
| Assert.Equal(false, b2); | ||
| } | ||
|
|
||
| [Fact] | ||
| public static void ReadPrimitivesFail() | ||
| { | ||
|
|
@@ -84,6 +115,8 @@ public static void ReadPrimitiveExtraBytesFail() | |
| { | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Parse<int[]>("[2] {3}")); | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Parse<int[]>(Encoding.UTF8.GetBytes(@"[2] {3}"))); | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Parse<string>(@"""Hello"" 42")); | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Parse<string>(Encoding.UTF8.GetBytes(@"""Hello"" 42"))); | ||
| } | ||
|
|
||
| [Fact] | ||
|
|
||
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.
As an aside, @steveharter, returning "true" in these
HandleXhelper methods to indicate there is no more data/we are done is the opposite of what I would have expected. We should consider inverting all these conditions so that "true" means success/keep reading, and false means "i'm done/return to the caller".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.
Provided I can address all of your other feedback, I'm up for making this change (unless it would be better done in a follow-up PR).
Uh oh!
There was an error while loading. Please reload this page.
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.
Great! cc @JeremyKuhne (in case you disagree with flipping the condition). I know that you were in favor of such a change, when we talked previously.
I think that would be best. Let's keep this PR isolated to the bug fix.
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.
Sounds good. I'll wait until the other PR's land that are affecting HandleXXX.
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.
One benefit of the direct return like the original is to support future cases (in the upcoming extensibility model) where the reader may be passed into a new static serializer
Parse()method. In that case we want to stop when we finish reading the current scope (object for example) and not throw an exception. There may be different ways to detect\support that but it is something to track when we add that feature.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.
Does JsonDocument support trailing trivia?
Uh oh!
There was an error while loading. Please reload this page.
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, it does. We have a few tests around that:
corefx/src/System.Text.Json/tests/JsonDocumentTests.cs
Line 783 in 10d0327
corefx/src/System.Text.Json/tests/JsonDocumentTests.cs
Lines 1443 to 1447 in 10d0327
cc @bartonjs