Building on the answer from already linked SO post, we could write custom converter for the property with small tweaks.
First, here is example class that i have used:
public class TestDto
{
[JsonConverter(typeof(SingleOrArrayConverter<Person>))]
public List<Person> person { get; set; }
}
public class Person
{
public string firstName { get; set; }
}
As you can see I annotated the property with [JsonConverter(typeof(SingleOrArrayConverter<Person>))] attribute for our converter to kick in.
Then I have adjusted mentioned converter - i added handling of unexcpected object while reading and also corrected wrtie method to just output an array no matter what:
public class SingleOrArrayConverter<TItem> : SingleOrArrayConverter<List<TItem>, TItem>
{
public SingleOrArrayConverter() : this(true) { }
public SingleOrArrayConverter(bool canWrite) : base(canWrite) { }
}
public class SingleOrArrayConverterFactory : JsonConverterFactory
{
public bool CanWrite { get; }
public SingleOrArrayConverterFactory() : this(true) { }
public SingleOrArrayConverterFactory(bool canWrite) => CanWrite = canWrite;
public override bool CanConvert(Type typeToConvert)
{
var itemType = GetItemType(typeToConvert);
if (itemType == null)
return false;
if (itemType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(itemType))
return false;
if (typeToConvert.GetConstructor(Type.EmptyTypes) == null || typeToConvert.IsValueType)
return false;
return true;
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var itemType = GetItemType(typeToConvert);
var converterType = typeof(SingleOrArrayConverter<,>).MakeGenericType(typeToConvert, itemType);
return (JsonConverter)Activator.CreateInstance(converterType, new object[] { CanWrite });
}
static Type GetItemType(Type type)
{
// Quick reject for performance
if (type.IsPrimitive || type.IsArray || type == typeof(string))
return null;
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
// Add here other generic collection types as required, e.g. HashSet<> or ObservableCollection<> or etc.
}
type = type.BaseType;
}
return null;
}
}
public class SingleOrArrayConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
{
public SingleOrArrayConverter() : this(true) { }
public SingleOrArrayConverter(bool canWrite) => CanWrite = canWrite;
public bool CanWrite { get; }
public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return null;
case JsonTokenType.StartObject:
// Consume and ignore unexpected object, when array is expected
_ = JsonSerializer.Deserialize<TItem>(ref reader, options);
return null;
case JsonTokenType.StartArray:
var list = new TCollection();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
list.Add(JsonSerializer.Deserialize<TItem>(ref reader, options));
}
return list;
default:
throw new InvalidOperationException();
}
}
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var item in value)
JsonSerializer.Serialize(writer, item, options);
writer.WriteEndArray();
}
}
At last, following code worked seamlessly:
var restClient = new RestClient(configureSerialization: s => s.UseSystemTextJson());
var result = await restClient.GetAsync<TestDto>(url);
personshould benull, missing entirely or an empty array. If you own the service, fix it. If not, you'll have to customize deserialization eg using a custom type converter that checks the contents of the"person"attribute before actually deserializing it."person":[{....}]or"person":{...}? Does the service have an Open API schema that specifies what"person"should contain? Perhaps the service author decides to use a single object for 0-1 values by design. A bad design, but they probably won't change it, so you'll have to create a custom converter