Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Allow trailing trivia in JsonSerializer.Parse and Read #37500#37549

Merged
ahsonkhan merged 7 commits intodotnet:masterfrom
watfordgnf:bug-37500-jsonserializer-throws-trailing-whitespace
May 14, 2019
Merged

Allow trailing trivia in JsonSerializer.Parse and Read #37500#37549
ahsonkhan merged 7 commits intodotnet:masterfrom
watfordgnf:bug-37500-jsonserializer-throws-trailing-whitespace

Conversation

@watfordgnf
Copy link
Copy Markdown
Contributor

@watfordgnf watfordgnf commented May 9, 2019

This fixes #37500 by consuming all leading and trailing trivia in JsonSerializer.ReadCore. Comments are only consumed if JsonCommentHandling is not Disallow.

@dnfclas
Copy link
Copy Markdown

dnfclas commented May 9, 2019

CLA assistant check
All CLA requirements met.

@ahsonkhan ahsonkhan added this to the 3.0 milestone May 10, 2019
Comment thread src/System.Text.Json/tests/Serialization/Object.ReadTests.cs Outdated
Copy link
Copy Markdown

@ahsonkhan ahsonkhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the fix/implementation can be simplified. I would consider adjusting the main reader loop within ReadCore itself for the fix.

Comment thread src/System.Text.Json/tests/Serialization/Object.ReadTests.cs
Comment thread src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs Outdated
Comment thread src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs Outdated
Comment thread src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs Outdated
Comment thread src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs Outdated
Comment thread src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs Outdated
Comment thread src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs Outdated
@@ -33,7 +33,7 @@ private static void ReadCore(

if (HandleValue(tokenType, options, ref reader, ref state))
Copy link
Copy Markdown

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 HandleX helper 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".

Copy link
Copy Markdown
Contributor Author

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).

Copy link
Copy Markdown

@ahsonkhan ahsonkhan May 10, 2019

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

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.

unless it would be better done in a follow-up PR

I think that would be best. Let's keep this PR isolated to the bug fix.

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor

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?

Copy link
Copy Markdown

@ahsonkhan ahsonkhan May 14, 2019

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:

using (JsonDocument doc = JsonDocument.Parse(" " + value + " "))

[Theory]
[InlineData("\"hello\" ", "hello")]
[InlineData(" null ", (string)null)]
[InlineData("\"\\u0033\\u0031\"", "31")]
public static void ReadString(string json, string expectedValue)

cc @bartonjs

JsonSerializerOptions options = new JsonSerializerOptions
{
DefaultBufferSize = 1,
ReadCommentHandling = JsonCommentHandling.Skip,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you nest complex documents, JsonSerializer uses JsonDocument which throws:

System.Text.Json.Serialization.Tests.ObjectTests.ReadClassIgnoresLeadingOrTrailingTrivia(leadingTrivia: "/* Multi\nLine\nComment */ ", trailingTrivia: "\t// trailing comment\n ") [FAIL]
      System.ArgumentException : Comments cannot be stored in a JsonDocument, only the Skip and Disallow comment handling modes are supported.
      Parameter name: reader

Copy link
Copy Markdown

@ahsonkhan ahsonkhan May 10, 2019

Choose a reason for hiding this comment

The 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 System.Object as a property), with allow comments mode? The JsonDocument only comes into play for types with System.Object properties (or Dictionary<string, object>, etc.)

@steveharter, @JeremyKuhne, @bartonjs - this is something we should fix up. Either the serialize should disallow "allow comments" similar to JsonDocument OR convert it to skip before passing it to JsonDocument since the "allow" mode doesn't make much sense in this context. The inconsistency in behavior is unnecessary and will cause confusion.

@watfordgnf, this can be deferred for now (i.e. the PR doesn't need to block on this discussion).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

- Consolidates leading and trailing trivia tests into one
NOTE: throws NPE in state.PropertyPath if invalid JSON is in the trailer.
Copy link
Copy Markdown

@ahsonkhan ahsonkhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nits for more tests, but otherwise LGTM. Thanks for fixing the issue and the quick response :)

else
{
path.Append($"[{_previous[0].JsonClassInfo.Type.FullName}]");
path = new StringBuilder($"[{_previous[0].JsonClassInfo.Type.FullName}]");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can _previous[0].JsonClassInfo ever be null, similar to the if (Current.JsonClassInfo == null) case above?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. Good to know :)

{
path.Append($"[{Current.JsonClassInfo.Type.FullName}]");
// No path if we've walked beyond the end of our JSON document
if (Current.JsonClassInfo == null)
Copy link
Copy Markdown

@ahsonkhan ahsonkhan May 10, 2019

Choose a reason for hiding this comment

The 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 "<none>".

JsonSerializerOptions options = new JsonSerializerOptions
{
DefaultBufferSize = 1,
ReadCommentHandling = JsonCommentHandling.Skip,
Copy link
Copy Markdown

@ahsonkhan ahsonkhan May 10, 2019

Choose a reason for hiding this comment

The 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 System.Object as a property), with allow comments mode? The JsonDocument only comes into play for types with System.Object properties (or Dictionary<string, object>, etc.)

@steveharter, @JeremyKuhne, @bartonjs - this is something we should fix up. Either the serialize should disallow "allow comments" similar to JsonDocument OR convert it to skip before passing it to JsonDocument since the "allow" mode doesn't make much sense in this context. The inconsistency in behavior is unnecessary and will cause confusion.

@watfordgnf, this can be deferred for now (i.e. the PR doesn't need to block on this discussion).

Comment thread src/System.Text.Json/tests/Serialization/Value.ReadTests.cs
Comment thread src/System.Text.Json/tests/Serialization/Object.ReadTests.cs
var options = new JsonSerializerOptions();
options.ReadCommentHandling = JsonCommentHandling.Skip;

ClassWithComplexObjects obj = JsonSerializer.Parse<ClassWithComplexObjects>(leadingTrivia + ClassWithComplexObjects.s_json + trailingTrivia, options);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

string s = JsonSerializer.Parse<string>(Encoding.UTF8.GetBytes(@"""Hello"" "));
Assert.Equal("Hello", s);

string s2 = JsonSerializer.Parse<string>(@" ""Hello"" ");
Copy link
Copy Markdown

@ahsonkhan ahsonkhan May 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking a bit more about using continue within the main loop (rather than breaking and doing one final Read with validation), I wonder what would happen if we had 2 primitive tokens next to each other. This should continue to throw (presumably the underlying reader will catch such cases), so this is probably fine, but just making sure.

string s3 = JsonSerializer.Parse<string>(@"""Hello"" ""Hello""");
string s4 = JsonSerializer.Parse<string>(@"""Hello"" 42");

Copy link
Copy Markdown
Member

@JamesNK JamesNK May 11, 2019

Choose a reason for hiding this comment

The 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

{'hi':true}{'bye':true} and {'hi':true},{'bye':true} could be read by the reader when the flag was true.

It is a Useful Feature. People like it when streaming huge JSON content. You don't need it for 3.0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

//var options = new JsonSerializerOptions();
//options.ReadCommentHandling = JsonCommentHandling.Allow;
//
//obj = JsonSerializer.Parse<ClassWithComplexObjects>(leadingTrivia + ClassWithComplexObjects.s_json + trailingTrivia, options);
Copy link
Copy Markdown

@ahsonkhan ahsonkhan May 13, 2019

Choose a reason for hiding this comment

The 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 Allow comments, which is why I think we should remove the commented out test code like this. I have filed an issue instead: https://github.com/dotnet/corefx/issues/37634

@ahsonkhan ahsonkhan merged commit 2b8126e into dotnet:master May 14, 2019
@ahsonkhan
Copy link
Copy Markdown

@watfordgnf, thanks for the contribution. I am looking forward to working with you more, in the future! :)

@watfordgnf
Copy link
Copy Markdown
Contributor Author

I'm glad I could help, and thank you for working with me to get this into the right shape.

@watfordgnf watfordgnf deleted the bug-37500-jsonserializer-throws-trailing-whitespace branch May 14, 2019 15:18
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…37500 (dotnet/corefx#37549)

* Add tests for trailing trivia to JsonSerializer

Contributes to dotnet/corefx#37500

* Consume trailing trivia in JsonSerializer.ReadCore

Fixes dotnet/corefx#37500

* Add tests for leading trivia too

- Consolidates leading and trailing trivia tests into one

* Simplify ReadCore loop per @ahsonkhan review

NOTE: throws NPE in state.PropertyPath if invalid JSON is in the trailer.

* Do not compute PropertyPath if outside document

* Add additional test cases for primitives

* Clean up tests and add negative test for leading/trailing comments


Commit migrated from dotnet/corefx@2b8126e
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JsonSerializer throws with trailing whitespace or comments

5 participants