How to handle both a single item and an array for the same property using JSON.net
How to handle both a single item and an array for the same property using System.Text.Json?
As inspired by this answer by Brian Rogers and other answers to How to handle both a single item and an array for the same property using JSON.net, you can create a generic JsonConverter<List<T>>
that checks whether the incoming JSON value is an array, and if not, deserializes an item of type T
and returns the item wrapped in an appropriate list. Even better, you can create a JsonConverterFactory
that manufactures such a converter for all list types List<T>
encountered in your serialization graph.
First, define the following converter and converter factory:
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.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:
return new TCollection { JsonSerializer.Deserialize<TItem>(ref reader, options) };
}
}
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
if (CanWrite && value.Count == 1)
{
JsonSerializer.Serialize(writer, value.First(), options);
}
else
{
writer.WriteStartArray();
foreach (var item in value)
JsonSerializer.Serialize(writer, item, options);
writer.WriteEndArray();
}
}
}
Then add the the converter factory to JsonSerializerOptions.Converters
before deserialization:
var options = new JsonSerializerOptions
{
Converters = { new SingleOrArrayConverterFactory() },
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var list = JsonSerializer.Deserialize<List<Item>>(json, options);
Or add a specific converter either to options or to your data model directly using JsonConverterAttribute
:
class Item
{
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
[JsonConverter(typeof(SingleOrArrayConverter<string>))]
public List<string> Category { get; set; }
}
If your data model uses some other type of collection, say ObservableCollection<string>
, you can apply a lower level converter SingleOrArrayConverter<TCollection, TItem>
as follows:
[JsonConverter(typeof(SingleOrArrayConverter<ObservableCollection<string>, string>))]
public ObservableCollection<string> Category { get; set; }
Notes:
If you want the converter(s) to apply only during deserialization, pass
canWrite: false
to the parameterized constructor:Converters = { new SingleOrArrayConverterFactory(canWrite: false) }
The converter will still get used, but will unconditionally generate a default serialization.
The converter is not implemented for jagged
2d
ornD
collections such asList<List<string>>
. It is also not implemented for arrays and read-only collections.According to Serializer support for easier object and collection converters #1562, because
JsonConverter<T>
lacks an asyncRead()
method,A limitation of the existing [JsonConverter] model is that it must "read-ahead" during deserialization to fully populate the buffer up to the end up the current JSON level. This read-ahead only occurs when the async+stream
JsonSerializer
deserialize methods are called and only when the current JSON for that converter starts with a StartArray or StartObject token.Thus using this converter to deserialize potentially very large arrays may have a negative performance impact.
As discussed in the same thread, the converter API may get redesigned in System.Text.Json - 5.0 to fully support
async
deserialization by converters for arrays and object, implying that this converter may benefit from being rewritten when .NET 5 (no longer labeled with "Core") is eventually released.
Demo fiddle here.
Handling JSON single object and array
Your code is fine, it just needs a few type tweaks.
This line
List<TestResponse> list = JsonConvert.DeserializeObject<List<TestResponse>>(response.Content);
needs to be like this, because your response is an object
, not a List
.
TestResponse list = JsonConvert.DeserializeObject<TestResponse>(response);
Then your custom deserializer attribute:
[JsonConverter(typeof(SingleOrArrayConverter<string>))]
needs to become:
[JsonConverter(typeof(SingleOrArrayConverter<DeserializedResult>))]
because your Result
object is not a string
or an array of string
s, it's either an array of DeserializedResult
s or a DeserializedResult
.
.NET custom Json converter for list or single item
One option is to create a custom converter (inherit from JsonConverter)
Though with this case I would've probably just wrote
var isList = content.StartsWith("[");
var response = isList ? JsonSeralizer.Deseralize<IList<ExampleResponse>>(content)
: new IList<ExampleResponse> { JsonSeralizer.Deseralize<ExampleResponse>(content) };
C# NewtonSoft Single Object or Array JsonConverter not working, no errors
Your a
property is get-only, so in your ReadJson()
method you need to populate the incoming List<T> existingValue
list (which will be the current property value) rather than creating a new list:
internal class SingleObjectOrArrayJsonConverter<T> : JsonConverter<List<T>> where T : class, new()
{
public override void WriteJson(JsonWriter writer, List<T> value, JsonSerializer serializer) =>
// avoid possibility of infinite recursion by wrapping the List<T> with AsReadOnly()
serializer.Serialize(writer, value.Count == 1 ? (object)value.Single() : value.AsReadOnly());
public override List<T> ReadJson(JsonReader reader, Type objectType, List<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
existingValue ??= new ();
switch (reader.TokenType)
{
case JsonToken.StartObject: existingValue.Add(serializer.Deserialize<T>(reader)); break;
case JsonToken.StartArray: serializer.Populate(reader, existingValue); break;
default: throw new ArgumentOutOfRangeException($"Converter does not support JSON token type {reader.TokenType}.");
};
return existingValue;
}
}
See also as this answer to How to handle both a single item and an array for the same property using JSON.net which shows a similar but more general converter.
Demo fiddle here.
Related Topics
How to Check If a File Is in Use
Parsing CSV Files in C#, With Header
How to Get the Application'S Path in a .Net Console Application
Why Is Floating Point Arithmetic in C# Imprecise
Do Httpclient and Httpclienthandler Have to Be Disposed Between Requests
Using Cookiecontainer With Webclient Class
How to Convert Utf-8 Byte[] to String
Difference Between New and Override
How to Read a CSV File into a .Net Datatable
How to Simulate Mouse Click in C#
How To: Execute Command Line in C#, Get Std Out Results
How to Convert Json Object to Custom C# Object
Input String Was Not in a Correct Format
Observablecollection Not Noticing When Item in It Changes (Even With Inotifypropertychanged)