nozzlegear / ShopifySharp

ShopifySharp is a .NET library that helps developers easily authenticate with and manage Shopify stores.
https://nozzlegear.com/shopify-development-handbook
MIT License
742 stars 309 forks source link

GraphQL Mutation Result Deserialization Issues (When Using System.Text.Json.JsonSerializer) #1089

Open mike-a-stephens opened 1 month ago

mike-a-stephens commented 1 month ago

When deserializing the response from a GraphQL mutation, System.Text.Json.JsonSerializer is throwing an exception, whereas NetwtonSoft can deserialize the response successfully.

For example, using the following code:

public async Task<ProductCreatePayload> UploadProduct(Article article)
{
    var service = GetGraphService();

    var mutation = @"mutation productCreate($title: String!, $descriptionHtml: String) {
    productCreate(input: {
            title: $title
            descriptionHtml: $descriptionHtml
        }) {
            product {
                id
            }
        }
    }";

    var variables = new
    {
        title = article.Name.Value,
        description = article.FullDescription.Value
    };

    var response = await service.SendAsync(new GraphRequest { query = mutation, variables = variables });
    var ptyElt = response.EnumerateObject().Single().Value;

    var result = JsonConvert.DeserializeObject<ProductCreatePayload>(ptyElt.GetRawText());
    var test = System.Text.Json.JsonSerializer.Deserialize<ProductCreatePayload>(ptyElt.GetRawText());

    return result;
}

The NewtonSoft deserializer succeeds and returns the deserialized object, however System.Text.Json deserializer fails with the following exception:

System.InvalidOperationException HResult=0x80131509 Message=The polymorphic type 'ShopifySharp.GraphQL.INode' has already specified derived type 'ShopifySharp.GraphQL.ReturnLineItem'. Source=System.Text.Json StackTrace: at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_DerivedTypeIsAlreadySpecified(Type baseType, Type derivedType) at System.Text.Json.Serialization.Metadata.PolymorphicTypeResolver..ctor(JsonSerializerOptions options, JsonPolymorphismOptions polymorphismOptions, Type baseType, Boolean converterCanHaveMetadata) at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureSynchronized 172_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ConfigureProperties() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureSynchronized 172_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ConfigureProperties() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureSynchronized 172_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType) at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureSynchronized 172_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ConfigureProperties() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureSynchronized 172_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ConfigureProperties() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureSynchronized 172_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ConfigureProperties() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureSynchronized 172_0() --- End of stack trace from previous location --- at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureSynchronized 172_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType) at System.Text.Json.JsonSerializerOptions.GetTypeInfoForRootType(Type type, Boolean fallBackToNearestAncestorType) at System.Text.Json.JsonSerializer.GetTypeInfo[T](JsonSerializerOptions options) at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)

This appears to be causing the generic ShopifySharp SendAsync methods to fail when processing the response of a mutation.

For example, in my original attempt at implementing the productCreate call, I used code similar to the following:

        return await service.SendAsync<ProductCreatePayload>(new GraphRequest { query = mutation, variables = variables });

but was getting the exception thrown above, which lead me to try deserializing manually with NewtonSoft.

Am I doing something wrong, or is there indeed an issue when using System.Text.Json.JsonSerializer.Deserialize<>() ?

clement911 commented 1 month ago

To deserialize automatically, try using the generic overload SendAsync<ProductCreatePayload>.

mike-a-stephens commented 1 month ago

Referring to my original report (near the bottom), you will see that I originally tried the generic method you mentioned. It was the exception being thrown from this generic method which lead lead me to investigating the issue further. As mentioned, out of the box using the automatic deserialization fails with the exception I posted.

mike-a-stephens commented 1 month ago

Further to this, it appears that I am only having issues with SendAsync<ProductCreatePayload>(). I just created another API call using SendAsync<ProductVariantsBulkCreatePayload>() and that deserializes correctly and without an exception being thrown.

nozzlegear commented 1 month ago

Thanks for the detailed investigation so far @mike-a-stephens. I'm going to be working on the GraphService in the next day or so, especially picking up an old PR I never finished in #1051. I will be sure to get this fixed and add test cases for this deserialization issue.