An API I depend on (that I have no control over) previously took a JSON structure like so:
{
"values": "one, two, three"
}
The API made a backward-breaking change to this payload, and converted the comma-separated values in a string to a JSON array:
{
"values": ["one", "two", "three"]
}
Using Flurl, the way I deserialized this was:
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>();
}
I can no longer use string Values
but use something like List<string> Values
. Here is what I want:
List<string> Values
for the property in the object. All code will be updated to use this new type.string
-> List<string>
(via string split) as transparently as reasonably possible. Basically, I don't want my code knowing about two different types for this object, or two versions of this object.My path to trying to do this was to inherit a class from JsonConverter<string>
(in the Newtonsoft.Json
package) and apply it to my 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();
}
First, I don't know if I'm headed in the right direction on this. What gave me pause was that it didn't make sense to create a converter for a string
type, because I feel like that only works when I implement JsonConverter<string>.ReadJson()
. For WriteJson()
, it gives me a string
but really what I need to do is alter how I write List<string>
(it either goes out normally as itself, or I "box" it back into a comma separated list of values as a string.
How can I properly do this? The reason why I liked the JsonConverter
approach is that it gives me that "conversion transparency" I really want (my code won't know it's happening since the conversion logic happens behind the scenes).
You can solve this problem in the following way:
Let's suppose you have defined the different payload classes like this:
public abstract class Payload<T>
{
[JsonProperty("values")]
public T Values { get; set; }
}
public class PayloadV1: Payload<string>
{
}
public class PayloadV2 : Payload<List<string>>
{
}
Then you can take advantage of Json schema to describe how a json should look like for each version:
private readonly JSchema schemaV1;
private readonly JSchema schemaV2;
...
var generator = new JSchemaGenerator();
schemaV1 = generator.Generate(typeof(PayloadV1));
schemaV2 = generator.Generate(typeof(PayloadV2));
And finally all you need to do is to perform some validation:
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("...");
}
UPDATE #1: Use V2 as primary and V1 as fallback
In order to be able to use the V2 as the primary version you need some converters. I've implemented them via extension methods:
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) };
}
I've used here C# 9's new expression to avoid repeating the type name
With these in your hand you can amend the PutPayload
method like this:
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("...");
}
PutJsonAsync
receives either a V1 or a V2 instance depending on the url.ToV2()
to return a V2 instance