Skip to content
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 System.Text.Json converters for our Newtonsoft JSON converters + add tests #11840

Merged
merged 72 commits into from
Nov 15, 2023

Conversation

Szpoti
Copy link
Collaborator

@Szpoti Szpoti commented Oct 30, 2023

OP changed by Kimi:

The goal of this PR is to introduce STJ versions of NewtonSoft.JSON converters that behave the same (as much as possible). This PR adds tests to show behavior of STJ and NSJ converters. NSJ converters were not functionally changed.

STJ has the following properties:

  • Pro: Actively developed and written with performance, correctness, etc. in mind1.
  • Pro: STJ is IMO better documented than Newtonsoft.json at this point
  • Pro: Performance-wise it is about twice as fast. The precise speedup is not that important imo. Also it uses less memory, so less GCs.
  • Pro: STJ is a part of .NET SDK so no dependency on "external" code is needed. Though, Newtonsoft is now under Microsoft wings though I believe.
  • Pro: STJ is more AOT friendly than Newtonsoft.

The short term goal is to save some time in reading Client config JSON file.

Changes in behaviour

  1. InvalidCastException became JsonException at in:
    • NetworkJsonConverter
    • MoneySatoshiJsonConverter
    • FeeRateJsonConverter
  2. EndPointJsonConverterNg accepts null value for JsonWriter to write. The same is true for NetworkJsonConverterNg here. The reason being that EndPointJsonConverter and NetworkJsonConverter skips the WriteJson call when the value to write is null, so the exceptions here and here are not thrown.

Resources

Footnotes

  1. Newtonsoft.json was for a long time the number one library for JSON support but it certainly does not have the level of design work behind.

@Szpoti
Copy link
Collaborator Author

Szpoti commented Oct 31, 2023

By the latest commit, deserializing:

  • Constants.MaximumNumberOfBitcoins + 1 sat (209999999.97690001) -> Can deserialize✅
  • 21 million bitcoin -> Can deserialize✅
  • 1. (no digit after decimal point) -> Can deserialize (1.00000000)✅
  • 1e6 value -> Can't deserialize❌
  • 1,0 (decimal comma) -> Can't deserialize❌
  • 1,000.00 (thousands separator comma) -> Can't deserialize❌
  • 0.00000000000000000000000000000000000000000000001 -> Can deserialize (0.00000000)✅
  • 00000000000000000000000 -> Can deserialize (0.00000000)✅

In these cases, either both succeded or both failed, meaning their behaviour is the same.

@Szpoti
Copy link
Collaborator Author

Szpoti commented Nov 2, 2023

I moved AssertDeserializeJsonExceptionFailure(token) into it's own test, as the exception itself comes from the missing " quotes from around the value. It doesn't make sense to check it in every value case.

@Szpoti
Copy link
Collaborator Author

Szpoti commented Nov 2, 2023

@kiminuo I don't know if this "typo" was intentional or not, but the missing } that I fixed in
809538b caused the test to pass, as it was an invalid format after all, but not because of the numeric value.
Fixed the typo, failed the test.
Now Newtonsoft gives back null at

	public override Money? ReadJson(JsonReader reader, Type objectType, Money? existingValue, bool hasExistingValue, JsonSerializer serializer)
	{
		var stringValue = reader.Value as string;
		return Parse(stringValue);
	}

and System.Text.Json throws json exception at

	public override Money? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		if (reader.TokenType != JsonTokenType.String)
		{
			throw new JsonException("Expected a JSON number value.");
		}

@Szpoti
Copy link
Collaborator Author

Szpoti commented Nov 2, 2023

if (reader.TokenType != JsonTokenType.String)
		{
			throw new JsonException("Expected a JSON number value.");
		}

This is weird, based on the if, we don't expect a number value here, it's the opposite. We expect string value, and throw if it's not.

@Szpoti
Copy link
Collaborator Author

Szpoti commented Nov 2, 2023

Also FYI, there are big inconsistency in how we handle different values in json. Some numbers are numeric, some or strings, and I can't really see the pattern, why its like this. For example here, MoneyBtcJsonConverter works with string values like "Money":"10.0", but FeeRateJsonConverter works with numeric values like "Rate":10.0.

Also if you look at WabiSabiConfig.json (default state):

  • "ConfirmationTarget": 108
  • "Rate": 0.003
  • "MinInputCountByRoundMultiplier": 0.5
  • "DoSSeverity": "0.10"
  • "MaxSuggestedAmountBase": "0.10"
  • "MaxRegistrableAmount": "43000.00"

Do you know why?

@kiminuo
Copy link
Collaborator

kiminuo commented Nov 14, 2023

I don't know if change in behavior is a problem but I'd like to know the reason behind.

The goal is not to change behavior, if possible. Changes in behavior should be IMHO documented in the post here.

@Szpoti Szpoti requested a review from turbolay November 14, 2023 16:08
@kiminuo
Copy link
Collaborator

kiminuo commented Nov 15, 2023

@turbolay @adamPetho I think this is quite close to being done. Could you review it one more time please?

adamPetho
adamPetho previously approved these changes Nov 15, 2023
Copy link
Collaborator

@adamPetho adamPetho left a comment

Choose a reason for hiding this comment

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

Overally LGTM.

Questions and nitpicks below.

WalletWasabi/JsonConverters/ExtPubKeyJsonConverterNg.cs Outdated Show resolved Hide resolved
public override bool HandleNull => true;

/// <inheritdoc/>
public override Network? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can get rid of the nullability of the return type if at the end we do a check like this.

Network network = Network.GetNetwork(networkString);
if (network is null) => throw Exception
return network

Copy link
Collaborator

@kiminuo kiminuo Nov 15, 2023

Choose a reason for hiding this comment

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

I think that we can do:

networkString = reader.GetString()?.Trim();

if (networkString is null)
{
	throw new ArgumentNullException(nameof(networkString));
}

->

networkString = reader.GetString()!.Trim();

That makes sense to me because null is checked before.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I figured out that changing the nullability breaks the test.
If we don't allow to return null, I think we change the behavior, too. Which goes against the goal of this PR.

It's up to you guys, if you want to deal with this in this PR or in another PR or never.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I applied my patch and it does not break tests.

}

string? stringValue = reader.GetString();
return Parse(stringValue);
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's wrong with TimeSpan.Parse(stringValue)?
Does that behave so differently that we need our own parser function?

Copy link
Collaborator

Choose a reason for hiding this comment

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

What's wrong with TimeSpan.Parse(stringValue)?

Nothing IMO.

Does that behave so differently that we need our own parser function?

Dunno, it just looks like somebody prefered different format at that time. TimeSpan.Parse(s) parses different format: [ws][-]{ d | [d.]hh:mm[:ss[.ff]] }[ws] according to https://learn.microsoft.com/en-us/dotnet/api/system.timespan.parse?view=net-7.0#system-timespan-parse(system-string).

Copy link
Collaborator

Choose a reason for hiding this comment

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

we should look into this in another PR, it's weird

Copy link
Collaborator

@kiminuo kiminuo Nov 15, 2023

Choose a reason for hiding this comment

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

But what do you guys want to achieve really?

If people like the format <days>d <hours>h <minutes>m <seconds>s, then it does what it is supposed to do. It's implemented in an inefficient way but AFAIK there are not that many TimeSpans to convert, or not?

edit: I believe that a compiled regex would be pretty fast here.

Copy link
Collaborator

@turbolay turbolay left a comment

Choose a reason for hiding this comment

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

{
string? networkString;

if (reader.TokenType == JsonTokenType.Null)
Copy link
Collaborator

Choose a reason for hiding this comment

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

When is this the case?

Copy link
Collaborator

Choose a reason for hiding this comment

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

if you read null as a JSON token. Not "null" but simply null - e.g. { "Network": null }.

@kiminuo kiminuo merged commit 3765b0b into WalletWasabi:master Nov 15, 2023
7 checks passed
@kiminuo
Copy link
Collaborator

kiminuo commented Nov 15, 2023

I merged the PR. I think it's finished. Feel free to create any follow-up PR if you have an idea what to improve. But I think everything was addressed.

Thanks for the help guys.

@Szpoti Szpoti deleted the newtonsoftVSmicrosoft branch November 16, 2023 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants