反序列化JSON为字符串或值
我喜欢使用Refit以类型安全的方式调用Web API。但有时,API并不配合你的强类型期望。例如,你可能会遇到由动态类型爱好者编写的API。
例如,我遇到一个返回如下值的API:
1
2
3
|
{
"important": true
}
|
没问题,我定义了这样一个类来反序列化:
1
2
3
4
|
public class ImportantResponse
{
public bool Important { get; set; }
}
|
生活很美好。直到有一天,API返回了这个:
1
2
3
|
{
"important": "What is important is subjective to the viewer."
}
|
糟糕!这个哲学课程破坏了我的客户端。一个解决方法是:
1
2
3
4
|
public class ImportantResponse
{
public JsonElement Important { get; set; }
}
|
这可行,但不够好。它没有向消费者传达这个值只能是字符串或布尔值。这时我想起了过去的一篇博客文章。
四月愚人节笑话来救援
当我担任ASP.NET MVC的项目经理时,我的同事兼首席开发人员Eilon写了一篇博客文章"The String or the Cat: A New .NET Framework Library",介绍了StringOr<TOther>
类。这个类可以表示双重状态值,要么是字符串,要么是另一种类型。
结果他的博客文章是个四月愚人节笑话。但这个想法一直留在我心中。现在,我需要它的真实实现。但我将命名为StringOrValue<T>
。
现代StringOrValue
今天实现这个的一个好处是我们可以利用现代C#功能。这是起始实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
[JsonConverter(typeof(StringOrValueConverter))]
public readonly struct StringOrValue<T> : IStringOrObject {
public StringOrValue(string stringValue) {
StringValue = stringValue;
IsString = true;
}
public StringOrValue(T value) {
Value = value;
IsValue = true;
}
public T? Value { get; }
public string? StringValue { get; }
[MemberNotNullWhen(true, nameof(StringValue))]
public bool IsString { get; }
[MemberNotNullWhen(true, nameof(Value))]
public bool IsValue { get; }
}
|
我们可以使用MemberNotNullWhen
属性告诉编译器,当IsString
为true时,StringValue
不为null。当IsValue
为true时,Value
不为null。
它还用JsonConverter
属性修饰,告诉JSON序列化器使用StringOrValueConverter
类来序列化和反序列化此类型。
转换器实现
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
|
internal class StringOrValueConverter : JsonConverter<IStringOrObject>
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsGenericType
&& typeToConvert.GetGenericTypeDefinition() == typeof(StringOrValue<>);
public override IStringOrObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var targetType = typeToConvert.GetGenericArguments()[0];
if (reader.TokenType == JsonTokenType.String)
{
var stringValue = reader.GetString();
return stringValue is null
? CreateEmptyInstance(targetType)
: CreateStringInstance(targetType, stringValue);
}
var value = JsonSerializer.Deserialize(ref reader, targetType, options);
return value is null
? CreateEmptyInstance(targetType)
: CreateValueInstance(targetType, value);
}
// 其他辅助方法...
}
|
在实际的StringOrValue<T>
实现中,我实现了IEquatable<T>
、IEquatable<StringOrValue<T>>
并重写了隐式运算符:
1
2
|
public static implicit operator StringOrValue<T>(string stringValue) => new(stringValue);
public static implicit operator StringOrValue<T>(T value) => new(value);
|
这允许你编写这样的代码:
1
2
|
StringOrValue<int> valueAsString = "Hello";
StringOrValue<int> valueAsNumber = 42;
|
实际应用
有了这个实现,我可以回到原始示例并这样写:
1
2
3
4
|
public class ImportantResponse
{
public StringOrValue<bool> Important { get; set; }
}
|
现在我可以处理两种情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var response = JsonSerializer.Deserialize<ImportantResponse>(json)
?? throw new InvalidOperationException("Deserialization failed.");
if (response.Important.IsValue) {
if (response.Important.Value) {
Console.WriteLine("It's important!");
}
else {
Console.WriteLine("It's not important.");
}
}
else {
Console.WriteLine(response.Important.StringValue);
}
|
完整实现
以下是完整的实现代码:
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
|
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Haack.Json;
[JsonConverter(typeof(StringOrValueConverter))]
public readonly struct StringOrValue<T> : IStringOrObject, IEquatable<T>, IEquatable<StringOrValue<T>>
{
public StringOrValue(T value)
{
Value = value;
IsValue = true;
}
public StringOrValue(string stringValue)
{
StringValue = stringValue;
IsString = true;
}
public string? StringValue { get; }
public T? Value { get; }
object? IStringOrObject.ObjectValue => Value;
[MemberNotNullWhen(true, nameof(StringValue))]
public bool IsString { get; }
[MemberNotNullWhen(true, nameof(Value))]
public bool IsValue { get; }
public static implicit operator StringOrValue<T>(string stringValue) => new(stringValue);
public static implicit operator StringOrValue<T>(T value) => new(value);
public override string ToString() => (IsString ? StringValue : Value?.ToString()) ?? string.Empty;
public bool Equals(T? obj) => IsValue && EqualityComparer<T>.Default.Equals(Value, obj);
public bool Equals(StringOrValue<T> other)
=> other.IsValue && IsValue && EqualityComparer<T>.Default.Equals(Value, other.Value)
|| other.IsString && IsString && StringComparer.Ordinal.Equals(StringValue, other.StringValue);
public override bool Equals([NotNullWhen(true)] object? obj)
=> obj is StringOrValue<T> value && Equals(value.Value);
public override int GetHashCode() => IsValue
? Value?.GetHashCode() ?? 0
: StringValue?.GetHashCode(StringComparison.Ordinal) ?? 0;
public static bool operator ==(StringOrValue<T>? left, T right)
=> left is not null && left.Equals(right);
public static bool operator !=(StringOrValue<T>? left, T right)
=> left is null || !left.Equals(right);
}
|
这个实现提供了一个优雅的解决方案,用于处理JSON反序列化中字段类型不确定的常见问题。