我如何支持对 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
的东西。这是我想要的:
- 我希望我的代码更改为对对象中的 属性 使用
List<string> Values
。将更新所有代码以使用此新类型。
- 如果我检测到“旧”API,我想尽可能透明地转换
string
-> List<string>
(通过字符串拆分)。基本上,我不希望我的代码知道这个对象的两种不同类型,或者这个对象的两个版本。
我尝试这样做的方法是从 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));
- 你需要为这些使用 Newtonsoft.Json.Schema nuget。
最后您需要做的就是执行一些验证:
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 实例
我依赖(我无法控制)的 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
的东西。这是我想要的:
- 我希望我的代码更改为对对象中的 属性 使用
List<string> Values
。将更新所有代码以使用此新类型。 - 如果我检测到“旧”API,我想尽可能透明地转换
string
->List<string>
(通过字符串拆分)。基本上,我不希望我的代码知道这个对象的两种不同类型,或者这个对象的两个版本。
我尝试这样做的方法是从 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));
- 你需要为这些使用 Newtonsoft.Json.Schema nuget。
最后您需要做的就是执行一些验证:
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 实例