Poy Chang's Blog

處理型別為介面的 JSON 序列化行為

前陣子我的套件在 GitHub 收到一個 Issue,在使用裡面 ToJson() 這個方法的時候,因為目標屬性是個介面型別,造成原物件的屬性值不會被序列化出來,所以就造成產生出來的 Json 字串無法正確使用了。這裡試著還原當時遇到的情境。

這邊假設我有以下的物件結構,People 類別中有個 Card 是使用 ITag 介面作為屬型型別,這個 ITag 有兩種實作,分別有不同的”樣貌”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class People
{
public ITag Card { get; set; }
}
public interface ITag
{
public string Name { get; set; }
}
public class ATag : ITag
{
public string Name { get; set; }
public int Age { get; set; }
}
public class BTag : ITag
{
public string Name { get; set; }
public string Gender { get; set; }
}

當我建立了兩個分別使用不同 ITag 實作的物件 JohnMary,並使用 System.Text.JsonJsonSerializer 序列化輸出時,實際上輸出的結果和希望得到的有了落差,因為序列化時,會使用 ITag 做處理,造成使用 ATag 實作的輸出少了 Age 屬性,使用 BTag 實作的輸出少了 Gender 屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var john = new People();
john.Card = new ATag
{
Name = "John",
Age = 20,
};
var mary = new People();
mary.Card = new BTag
{
Name = "Mary",
Gender = "Female",
};

JsonSerializer.Serialize(john);
// 希望得到的輸出:{"Card":{"Name":"John","Age":20}}
// 但實際上是輸出:{"Card":{"Name":"John"}}
JsonSerializer.Serialize(mary);
// 希望得到的輸出:{"Card":{"Name":"Mary","Gender":"Female"}}
// 但實際上是輸出:{"Card":{"Name":"Mary"}}

要怎麼處理呢?

其實我沒有完美的處理方案(如果你以想法請告訴我),我想到我能做的就只是增加一個客製的 JsonConverter,讓 JsonSerializer 序列化到 ITag 這個型別的時候,用我客製的規則來處理。

客製的規則其實是靠 Pattern Matching 型別模式比對來達成,在序列化時若發現進來的型別是 ATag 則使用 ATag 來做序列化,反之亦然。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ITagConverter : JsonConverter<ITag>
{
public override ITag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, ITag value, JsonSerializerOptions options)
{
switch (value)
{
case ATag tag:
JsonSerializer.Serialize(writer, tag, options);
break;
case BTag tag:
JsonSerializer.Serialize(writer, tag, options);
break;
default:
throw new ArgumentException(message: "It is not a recognized type.", paramName: nameof(value));
}
}
}

寫出來的程式碼感覺精簡,但是是靠寫死的方式來做,無法做到通用處理,畢竟會有那些實作該介面的型別,只有自己才知道。

完整的程式碼

這裡提供用 LinqPad 寫的完整、可執行程式碼,提供給想要玩玩看的人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
void Main()
{
var john = new People();
john.Card = new ATag
{
Name = "John",
Age = 20,
};
var mary = new People();
mary.Card = new BTag
{
Name = "Mary",
Gender = "Female",
};
var option = new JsonSerializerOptions();
option.Converters.Add(new ITagConverter());

JsonSerializer.Serialize(john, option).Dump();
JsonSerializer.Serialize(mary, option).Dump();
}

public class ITagConverter : JsonConverter<ITag>
{
public override ITag Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, ITag value, JsonSerializerOptions options)
{
switch (value)
{
case ATag tag:
JsonSerializer.Serialize(writer, tag, options);
break;
case BTag tag:
JsonSerializer.Serialize(writer, tag, options);
break;
default:
throw new ArgumentException(message: "It is not a recognized type.", paramName: nameof(value));
}
}
}

public class People
{
public ITag Card { get; set; }
}
public interface ITag
{
public string Name { get; set; }
}
public class ATag : ITag
{
public string Name { get; set; }
public int Age { get; set; }
}
public class BTag : ITag
{
public string Name { get; set; }
public string Gender { get; set; }
}

參考資料: