Skip to content

Commit bdb99e6

Browse files
committed
Replace polymorphic json attributes with a custom GraphUnionTypeConverter json converter
This change fixes a bug which prevented the deserialization of union types into their union cases due to the internal types used by ShopifySharp to hold the union case values (e.g. `CommentEventEmbedCustomer`, `CommentEventEmbedOrder`). The polymorphic attributes weren't able to determine how to deserialize into and out of those internal types (due to the `.Value` property on those internal types holding the actual value of the union case). To fix this, the GraphUnionTypeConverter has been introduced and replaces the `JsonDerivedType` and `JsonPolymorphic` attributes. It handles deserialization and serialization of the union type, moving values into and out of the internal wrappers behind the scenes. It may even stand to reason that we don't need the internal wrappers at all with this converter – something to explore in the future. Fixes #1213 type: fix scope: generated-graphql
1 parent 23697db commit bdb99e6

File tree

2 files changed

+110
-8
lines changed

2 files changed

+110
-8
lines changed

ShopifySharp.GraphQL.Parser/Writer.fs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ let private writeNamespaceAndUsings (writer: Writer) : ValueTask =
220220
do! NewLine
221221
do! "using System.Collections.Generic;"
222222
do! NewLine
223+
do! "using ShopifySharp.Infrastructure.Serialization.Json;"
224+
do! NewLine
223225
}
224226

225227
let private writeSummary indentation (summary: string[]) writer : ValueTask =
@@ -271,15 +273,10 @@ let private writeDateOnlyJsonConverterAttribute (fieldType: FieldType) writer: V
271273
}
272274

273275

274-
let private writeJsonDerivedTypeAttributes unionTypeName (typeNames: string[]) writer: ValueTask =
276+
let private writeGraphUnionTypeConverterAttribute unionTypeName writer: ValueTask =
275277
pipeWriter writer {
276-
do! "[JsonPolymorphic(TypeDiscriminatorPropertyName = \"__typename\")]"
278+
do! $"[JsonConverter(typeof(GraphUnionTypeConverter<{unionTypeName}>))]"
277279
do! NewLine
278-
279-
for typeName in typeNames do
280-
let wrapperTypeName = toUnionCaseWrapperName unionTypeName typeName
281-
do! $"""[JsonDerivedType(typeof({wrapperTypeName}), typeDiscriminator: "{typeName}")]"""
282-
do! NewLine
283280
}
284281

285282
let private writeJsonDerivedTypeAttributes2 interfaceName (classNames: string[]) writer: ValueTask =
@@ -559,7 +556,7 @@ let private writeUnionType (unionType: UnionType) (_: IParsedContext) (writer: W
559556
pipeWriter writer {
560557
yield! writeSummary Outdented unionType.XmlSummary
561558
yield! writeDeprecationAttribute Outdented unionType.Deprecation
562-
yield! writeJsonDerivedTypeAttributes unionType.Name unionType.Types
559+
yield! writeGraphUnionTypeConverterAttribute unionType.Name
563560

564561
do! $"public record {unionType.Name}: GraphQLObject<{unionType.Name}>, IGraphQLUnionType"
565562

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#nullable enable
2+
using System;
3+
using System.Linq;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
namespace ShopifySharp.Infrastructure.Serialization.Json;
8+
9+
/// <summary>
10+
/// A JSON converter for GraphQL union base types that handles polymorphic deserialization
11+
/// by directly deserializing to concrete types that inherit from the union base.
12+
/// </summary>
13+
/// <typeparam name="TUnion">The union base type (e.g., CustomerPaymentInstrument)</typeparam>
14+
internal class GraphUnionTypeConverter<TUnion> : JsonConverter<TUnion>
15+
where TUnion : class
16+
{
17+
private static IJsonSerializer ResolveSerializer(JsonSerializerOptions options, IServiceProvider? serviceProvider = null)
18+
{
19+
return InternalServiceResolver.GetServiceOrDefault<IJsonSerializer>(serviceProvider, () => new SystemJsonSerializer(options));
20+
}
21+
22+
public override TUnion? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
23+
{
24+
using var doc = JsonDocument.ParseValue(ref reader);
25+
var rootElement = new SystemJsonElement(doc.RootElement, doc);
26+
27+
// Get __typename to determine the correct concrete type
28+
if (!rootElement.TryGetProperty("__typename", out var typenameElement))
29+
return null;
30+
31+
if (typenameElement.ValueType != JsonValueType.String)
32+
return null;
33+
34+
var typename = ((JsonElement) typenameElement.GetRawObject()).GetString();
35+
if (string.IsNullOrEmpty(typename))
36+
return null;
37+
38+
// Find the wrapper type for this union type and typename
39+
var wrapperTypeName = $"{typeToConvert.Name}{typename}";
40+
var wrapperType = typeToConvert.Assembly.GetTypes()
41+
.FirstOrDefault(t => t.Name == wrapperTypeName && t.IsSubclassOf(typeToConvert));
42+
43+
if (wrapperType == null)
44+
return null;
45+
46+
// Get the Value property type (the concrete type we need to deserialize to)
47+
var valueProperty = wrapperType.GetProperty("Value");
48+
if (valueProperty == null)
49+
return null;
50+
51+
var concreteType = valueProperty.PropertyType;
52+
53+
// Create a new options instance without this converter to avoid infinite recursion
54+
var newOptions = new JsonSerializerOptions(options);
55+
var convertersToRemove = newOptions.Converters
56+
.Where(c => c.GetType().IsGenericType &&
57+
c.GetType().GetGenericTypeDefinition() == typeof(GraphUnionTypeConverter<>))
58+
.ToList();
59+
60+
foreach (var converter in convertersToRemove)
61+
newOptions.Converters.Remove(converter);
62+
63+
// Deserialize to the concrete type
64+
var serializer = ResolveSerializer(newOptions);
65+
var concreteObject = serializer.Deserialize(rootElement, concreteType);
66+
//JsonSerializer.Deserialize(rootElement.GetRawText(), concreteType, newOptions);
67+
if (concreteObject == null)
68+
return null;
69+
70+
// Create the wrapper using its constructor
71+
var constructor = wrapperType.GetConstructor([concreteType]);
72+
if (constructor == null)
73+
return null;
74+
75+
return (TUnion)constructor.Invoke([concreteObject]);
76+
}
77+
78+
public override void Write(Utf8JsonWriter writer, TUnion? value, JsonSerializerOptions options)
79+
{
80+
if (value == null)
81+
{
82+
writer.WriteNullValue();
83+
return;
84+
}
85+
86+
var serializer = ResolveSerializer(options);
87+
88+
// Get the Value property from the wrapper and serialize it
89+
var valueProperty = value.GetType().GetProperty("Value");
90+
if (valueProperty != null)
91+
{
92+
var wrappedValue = valueProperty.GetValue(value);
93+
if (wrappedValue != null)
94+
{
95+
serializer.Serialize(wrappedValue);
96+
//JsonSerializer.Serialize(writer, wrappedValue, options);
97+
return;
98+
}
99+
}
100+
101+
// Fallback to direct serialization
102+
//JsonSerializer.Serialize(writer, value, value.GetType(), options);
103+
serializer.Serialize(value);
104+
}
105+
}

0 commit comments

Comments
 (0)