我如何支持对 Flurl / Json.net 中的 JSON 负载的向后突破性更改?

How do I support a backward breaking change to JSON payload in Flurl / Json.net?

我依赖(我无法控制)的 API 之前采用了 JSON 结构,如下所示:

{
  "values": "one, two, three"
}

API 对此有效负载进行了向后突破性更改,并将字符串中的逗号分隔值转换为 JSON 数组:

{
  "values": ["one", "two", "three"]
}

使用 Flurl,我反序列化的方式是:

public class Payload
{
    public string Values { get; set; } = "";
}

// somewhere else where I make the API call
public async Task<Payload> PutPayload(Payload data)
{
    return await "https://theapi.com"
        .PutJsonAsync(data)
        .ReceiveJson<Payload>();
}

我不能再使用 string Values,而是使用类似 List<string> Values 的东西。这是我想要的:

我尝试这样做的方法是从 JsonConverter<string>(在 Newtonsoft.Json 包中)继承 class 并将其应用到我的 Payload class:

internal class ConvertCsvStringToArray : JsonConverter<string>
{
    // Implement WriteJson() and ReadJson() somehow
}

public class Payload
{
    [JsonConverter(typeof(ConvertCsvStringToArray))]
    public List<string> Values { get; set; } = new();
}

首先,我不知道我在这方面的方向是否正确。让我犹豫的是,为 string 类型创建转换器没有意义,因为我觉得只有在实现 JsonConverter<string>.ReadJson() 时才有效。对于 WriteJson(),它给了我一个 string 但实际上我需要做的是改变我写 List<string> 的方式(它要么像它自己一样正常输出,要么我将它“装箱”回作为字符串的逗号分隔值列表。

我怎样才能正确地做到这一点?我喜欢 JsonConverter 方法的原因是它给了我真正想要的“转换透明度”(我的代码不会知道它正在发生,因为转换逻辑发生在幕后)。

您可以通过以下方式解决此问题:

假设您已经像这样定义了不同的负载 类:

public abstract class Payload<T>
{
    [JsonProperty("values")]
    public T Values { get; set; }
}

public class PayloadV1: Payload<string>
{
}

public class PayloadV2 : Payload<List<string>>
{
}

然后您可以利用 Json 架构来描述每个版本的 json 应该是什么样子:

private readonly JSchema schemaV1;
private readonly JSchema schemaV2;
...
var generator = new JSchemaGenerator();
schemaV1 = generator.Generate(typeof(PayloadV1));
schemaV2 = generator.Generate(typeof(PayloadV2));

最后您需要做的就是执行一些验证:

public async Task<Payload> PutPayload(Payload data)
{
    var json = await "https://theapi.com"
        .PutJsonAsync(data)
        .ReceiveString();
    
    var semiParsed = JObject.Parse(json);
    if(semiParsed.IsValid(schemaV1))
    {
       return JsonConvert.DeserializeObject<PayloadV1>(json);
    }
    else if(semiParsed.IsValid(schemaV2))
    {
       return JsonConvert.DeserializeObject<PayloadV2>(json);
    }
    throw new NotSupportedException("...");
}

更新 #1:使用 V2 作为主要版本,使用 V1 作为后备版本

为了能够使用 V2 作为主要版本,您需要一些转换器。我已经通过扩展方法实现了它们:

public static class PayloadExtensions
{
    private const string separator = ",";

    public static PayloadV2 ToV2(this PayloadV1 v1)
        => new() { Values = v1.Values.Split(separator).ToList() };

    public static PayloadV1 ToV1(this PayloadV2 v2)
        => new() { Values = string.Join(separator, v2.Values) };
}

我在这里使用了 C# 9 的 new expression 以避免重复类型名称

有了这些,您可以像这样修改 PutPayload 方法:

public async Task<PayloadV2> PutPayload(PayloadV2 data, string url = "https://theapi.com")
{
    var json = await url
        .PutJsonAsync(isV1Url(url) ? data.ToV1() : data)
        .ReceiveString();

    var semiParsed = JObject.Parse(json);
    if (semiParsed.IsValid(schemaV1))
    {
        return JsonConvert.DeserializeObject<PayloadV1>(json).ToV2();
    }
    else if (semiParsed.IsValid(schemaV2))
    {
        return JsonConvert.DeserializeObject<PayloadV2>(json);
    }
    throw new NotSupportedException("...");
}
  • 该方法接收一个 V2 实例,return接收一个 V2 实例
  • url作为参数被接收,可以多次引用
  • PutJsonAsync 根据 url
  • 接收 V1 或 V2 实例
  • 如果响应是 v1 格式,那么您需要调用 .ToV2() 到 return V2 实例