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

Json Serialiser doesn't handle structs well and ignores type converters #622

Open
AlexeyRaga opened this issue Feb 26, 2024 · 11 comments
Open

Comments

@AlexeyRaga
Copy link

AlexeyRaga commented Feb 26, 2024

Hello,

I have some wrapper types that I want to use with GraphQL, which look like this:

public struct ProductId(Guid value) {
    public Guid Value => value;

    public override string ToString() => value.ToString();
    public override bool Equals(object? obj) => obj is ProductId other && other.Value == value;
    public override int GetHashCode() => value.GetHashCode();
    public static bool operator ==(ProductId left, ProductId right) => left.Value == right.Value;
    public static bool operator !=(ProductId left, ProductId right) => !(left == right);
}

I also provide JsonConverters for these types, which look similar to:

class ProductIdSystemTextJsonConverter : JsonConverter<ProductId>
{
    public override ProductId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        new Foo(Guid.Parse(reader.GetString()!));

    public override void Write(Utf8JsonWriter writer, ProductId value, JsonSerializerOptions options) =>
        writer.WriteStringValue(value.Value);
}

I register the converters in the client:

var serialiser = new SystemTextJsonSerializer
        {
            Options =
            {
                PropertyNameCaseInsensitive = true
            }
        };
serialiser.Options.Converter.Add(new ProductIdTypeConverter());

But when deserialising the GraphQL response I get this error:

System.InvalidOperationException
Sequence contains no matching element
   at System.Linq.ThrowHelper.ThrowNoMatchException()
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)
   at GraphQL.Client.Serializer.SystemTextJson.ImmutableConverter.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options) in /_/src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs:line 80

I do not quite understand why ImmutableConverter was chosen and not the specific one that I registered...

I validated the approach, and it works if I create my wrapper types as records somehow:

public readonly record struct ProductId(Guid Value)
{
    public override string ToString() => Value.ToString();
}

But the goal is to make it work with plain structs and have libraries like https://github.com/lucasteles/Strongly to generate wrapper types, Json converters, and all the required boilerplate around them.

I believe that fixing the "plain structs" misbehavior would allow it to work rather smoothly.

@AlexeyRaga
Copy link
Author

AlexeyRaga commented Feb 26, 2024

Even for the records, this doesn't work:

public readonly partial record struct ProductId
{
    public System.Guid Value { get; }

    public ProductId(System.Guid value)
    {
        Value = value;
    }
}

But this does work:

public readonly partial record struct ProductId(Guid Value) {}

@AlexeyRaga
Copy link
Author

Interestingly, it works if I use Newtonsoft.Json but not System.Text.Json

@rose-a
Copy link
Collaborator

rose-a commented Mar 2, 2024

Hi, sorry for the late reply...

I do not quite understand why ImmutableConverter was chosen and not the specific one that I registered...

You could try to insert your custom converter at the beginning of the converters list (like here), this had to be done for the ErrorPathConverter and MapConverter, too.

@AlexeyRaga
Copy link
Author

AlexeyRaga commented Mar 3, 2024

@rose-a Thanks for the suggestion!
Indeed, it works with the insert method!

But I still think that it is a bug because annotating the type with

[System.Text.Json.Serialization.JsonConverter(typeof(ProductIdSystemTextJsonConverter))]

doesn't work.

But it does work for the Newtonsoft serialiser. So, something is still odd here.

@rose-a
Copy link
Collaborator

rose-a commented Mar 3, 2024

Yeah, I'd expect that to work, too.

But I don't know how to fine-tune the System.Text.Json serializer for that... PRs welcome 😉

@AlexeyRaga
Copy link
Author

@rose-a I may try to find some time to look into it...
As a super quick and easy fix, would you accept this logic?

If the type is marked with JsonConverterAttribute (there is a converter for this type), ImmutableConverter should return false from CanConvert so that the registered converter can have its turn.

Also, could you please elaborate on the ImmutableConverter, why is it needed?

@rose-a
Copy link
Collaborator

rose-a commented Mar 4, 2024

Also, could you please elaborate on the ImmutableConverter, why is it needed?

If I remember correctly it is needed for deserialization of anonymous types... If you're playing around, you could check what happens if you remove it (some tests should fail)

If the type is marked with JsonConverterAttribute (there is a converter for this type), ImmutableConverter should return false from CanConvert so that the registered converter can have its turn.

Sounds reasonable, although I have no idea on how this will impact performance... @sungam3r @Shane32 do you have an opinion on this?

@AlexeyRaga
Copy link
Author

If you're playing around, you could check what happens if you remove it (some tests should fail)

Yeah, that's the thing.
I already checked it out, all the tests still pass if we remove ImmutableConverter.

I tried commenting out this line

https://github.com/graphql-dotnet/graphql-client/blob/master/src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs#L9

and even removing the whole ImmutableConverter.cs file, all the tests are happy.

Hence the question: why do we need it?

@rose-a
Copy link
Collaborator

rose-a commented Mar 4, 2024

Hm... trying to read up on this, it seems like this might have become obsolete wit .NET 5.0...

@AlexeyRaga
Copy link
Author

If so, then it may fix the issue in a nicest way: fixing bugs by removing code :)

@rose-a
Copy link
Collaborator

rose-a commented May 21, 2024

@AlexeyRaga Could you please post a complete combination of class/struct and JsonConverter which shows the described behavior?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants