JSON反序列化难题的优雅解决方案:StringOrValue<T>类型

本文介绍了处理JSON反序列化中字段类型不确定问题的创新解决方案,通过实现StringOrValue<T>类型,优雅处理既可以是字符串又可以是其他类型值的字段,包含完整C#代码实现。

反序列化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反序列化中字段类型不确定的常见问题。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计