Can Newtonsoft Json.Net Skip Serializing Empty Lists

Skip empty collections when serializing to JSON

Removing data from a JSON string post-serialization is nearly always the wrong approach. You should always handle it during serialization.

In addition, you most likely should be checking if items is an empty collection:

[Serializable]
public class Item
{
public String Type { get; set; }
public int Order { get; set; }
public int id { get; set; }
public string text { get; set; }
public IList<Item> items { get; set; }

public bool ShouldSerializeitems()
{
return items.Any();
}

public Item()
{
items = new List<Item>();
}
}

This correctly outputs:

[
{
"Type": "Category",
"Order": 1,
"id": 24,
"text": "abc",
"items": [
{
"Type": "Product",
"Order": 0,
"id": 1900,
"text": "abc product"
}
]
},
{
"Type": "Category",
"Order": 1,
"id": 6,
"text": "efg",
"items": [
{
"Type": "Product",
"Order": 0,
"id": 2446,
"text": "efg Product"
},
{
"Type": "Product",
"Order": 0,
"id": 2447,
"text": "efg1 Product"
}
]
}
]

How to make Json.Net skip serialization of empty collections

I have implemented this feature in the custom contract resolver of my personal framework (link to the specific commit in case the file will be moved later). It uses some helper methods and includes some unrelated code for custom references syntax. Without them, the code will be:

public class SkipEmptyContractResolver : DefaultContractResolver
{
public SkipEmptyContractResolver (bool shareCache = false) : base(shareCache) { }

protected override JsonProperty CreateProperty (MemberInfo member,
MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
bool isDefaultValueIgnored =
((property.DefaultValueHandling ?? DefaultValueHandling.Ignore)
& DefaultValueHandling.Ignore) != 0;
if (isDefaultValueIgnored
&& !typeof(string).IsAssignableFrom(property.PropertyType)
&& typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) {
Predicate<object> newShouldSerialize = obj => {
var collection = property.ValueProvider.GetValue(obj) as ICollection;
return collection == null || collection.Count != 0;
};
Predicate<object> oldShouldSerialize = property.ShouldSerialize;
property.ShouldSerialize = oldShouldSerialize != null
? o => oldShouldSerialize(o) && newShouldSerialize(o)
: newShouldSerialize;
}
return property;
}
}

This contract resolver will skip serialization of all empty collections (all types implementing ICollection and having Length == 0), unless DefaultValueHandling.Include is specified for the property or the field.

Newtonsoft Json.Net - How to conditionally add (or skip) items in an array when deserializing?

You can create the following JsonConverter<T []> that will skip array entries beyond a certain count:

public class MaxLengthArrayConverter<T> : JsonConverter<T []>
{
public MaxLengthArrayConverter(int maxLength) => this.MaxLength = maxLength >= 0 ? maxLength : throw new ArgumentException(nameof(maxLength));

public int MaxLength { get; }

public override T [] ReadJson(JsonReader reader, Type objectType, T [] existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
reader.AssertTokenType(JsonToken.StartArray);
var list = new List<T>();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
{
if (list.Count < MaxLength)
list.Add(serializer.Deserialize<T>(reader));
else
reader.Skip();
}
return list.ToArray();
}

public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, T [] value, JsonSerializer serializer) => throw new NotImplementedException();
}

public static partial class JsonExtensions
{
public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) =>
reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));

public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();

public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}

public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}

Then, assuming your financial market orderbook model looks something like this:

public class OrderBook
{
public Order [] Orders { get; set; }
}

You can deserialize as follows:

int maxLength = 20;  // Or whatever you want.
var settings = new JsonSerializerSettings
{
Converters = { new MaxLengthArrayConverter<Order>(maxLength) },
};
var model = JsonConvert.DeserializeObject<OrderBook>(json, settings);

Assert.IsTrue(model.Orders.Length <= maxLength);

Notes:

  • In your question you mention only arrays, but if your model is actually using lists rather than arrays, use the following converter instead:

    public class MaxLengthListConverter<T> : JsonConverter<List<T>>
    {
    public MaxLengthListConverter(int maxLength) => this.MaxLength = maxLength >= 0 ? maxLength : throw new ArgumentException(nameof(maxLength));

    public int MaxLength { get; }

    public override List<T> ReadJson(JsonReader reader, Type objectType, List<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
    if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
    return null;
    reader.AssertTokenType(JsonToken.StartArray);
    existingValue ??= (List<T>)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
    existingValue.Clear();
    while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
    {
    if (existingValue.Count < MaxLength)
    existingValue.Add(serializer.Deserialize<T>(reader));
    else
    reader.Skip();
    }
    return existingValue;
    }

    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, List<T> value, JsonSerializer serializer) => throw new NotImplementedException();
    }
  • This answer assumes that you want all arrays of type T [] in your model to be truncated to some specific length in runtime. If this is not true, and you need different max lengths for different arrays to be individually specified in runtime, you will need a more complex solution, probably involving a custom contract resolver.

Demo fiddle here.

How to omit empty collections when serializing with Json.NET

If you're looking for a solution which can be used generically across different types and does not require any modification (attributes, etc), then the best solution that I can think if would be a custom DefaultContractResolver class. It would use reflection to determine if any IEnumerables for a given type are empty.

public class IgnoreEmptyEnumerablesResolver : DefaultContractResolver
{
public static readonly IgnoreEmptyEnumerablesResolver Instance = new IgnoreEmptyEnumerablesResolver();

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);

if (property.PropertyType != typeof(string) &&
typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
property.ShouldSerialize = instance =>
{
IEnumerable enumerable = null;

// this value could be in a public field or public property
switch (member.MemberType)
{
case MemberTypes.Property:
enumerable = instance
.GetType()
.GetProperty(member.Name)
.GetValue(instance, null) as IEnumerable;
break;
case MemberTypes.Field:
enumerable = instance
.GetType()
.GetField(member.Name)
.GetValue(instance) as IEnumerable;
break;
default:
break;

}

if (enumerable != null)
{
// check to see if there is at least one item in the Enumerable
return enumerable.GetEnumerator().MoveNext();
}
else
{
// if the list is null, we defer the decision to NullValueHandling
return true;
}

};
}

return property;
}
}

How to ignore empty arrays using JsonConvert.DeserializeObject?

Your problem is not that you need to ignore empty arrays. If the "items" array were empty, there would be no problem:

"items":  [],

Instead your problem is as follows. The JSON standard supports two types of container:

  • The array, which is an ordered collection of values. An array begins with [ (left bracket) and ends with ] (right bracket). Values are separated by , (comma).

  • The object, which is an unordered set of name/value pairs. An object begins with { (left brace) and ends with } (right brace).

For some reason the server is returning an empty array in place of a null object. If Json.NET expects to encounter a JSON object but instead encounters a JSON array, it will throw the Cannot deserialize the current JSON array exception you are seeing.

You might consider asking whoever generated the JSON to fix their JSON output, but in the meantime, you can use the following converters to skip unexpected arrays when deserializing objects:

public class IgnoreUnexpectedArraysConverter<T> : IgnoreUnexpectedArraysConverterBase
{
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
}

public class IgnoreUnexpectedArraysConverter : IgnoreUnexpectedArraysConverterBase
{
readonly IContractResolver resolver;

public IgnoreUnexpectedArraysConverter(IContractResolver resolver)
{
if (resolver == null)
throw new ArgumentNullException();
this.resolver = resolver;
}

public override bool CanConvert(Type objectType)
{
if (objectType.IsPrimitive || objectType == typeof(string))
return false;
return resolver.ResolveContract(objectType) is JsonObjectContract;
}
}

public abstract class IgnoreUnexpectedArraysConverterBase : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var contract = serializer.ContractResolver.ResolveContract(objectType);
if (!(contract is JsonObjectContract))
{
throw new JsonSerializationException(string.Format("{0} is not a JSON object", objectType));
}

do
{
if (reader.TokenType == JsonToken.Null)
return null;
else if (reader.TokenType == JsonToken.Comment)
continue;
else if (reader.TokenType == JsonToken.StartArray)
{
var array = JArray.Load(reader);
if (array.Count > 0)
throw new JsonSerializationException(string.Format("Array was not empty."));
return null;
}
else if (reader.TokenType == JsonToken.StartObject)
{
// Prevent infinite recursion by using Populate()
existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue);
return existingValue;
}
else
{
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
}
}
while (reader.Read());
throw new JsonSerializationException("Unexpected end of JSON.");
}

public override bool CanWrite { get { return false; } }

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

Then, if empty arrays can appear in only one place in the object graph, you can add the converter to your model as follows:

public class Rootobject
{
[JsonConverter(typeof(IgnoreUnexpectedArraysConverter<Venue>))]
public Venue venue { get; set; }
}

But if, as you say, any object might be replaced with an empty array, you can use the non-generic IgnoreUnexpectedArraysConverter for all object types:

var resolver = new DefaultContractResolver(); // Cache for performance
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
Converters = { new IgnoreUnexpectedArraysConverter(resolver) },
};
var userInfo = JsonConvert.DeserializeObject<Rootobject>(jsonString, settings);

Notes:

  • The converter does not work with the TypeNameHandling or PreserveReferencesHandling settings.

  • The converter assumes that the object being deserialized has a default constructor. It the object has a parameterized constructor you will need to create a hardcoded converter to allocate and populate the object.

  • The converter throws an exception if the array is not empty, to ensure there is no data loss in the event of incorrect assumptions about the structure of the JSON. Sometimes servers will write a single object in place of a one-object array, and an array when there are zero, two or more objects. If you are also in that situation (e.g. for the "items" array) see How to handle both a single item and an array for the same property using JSON.net.

  • If you want the converter to return a default object instead of null when encountering an array, change it as follows:

    else if (reader.TokenType == JsonToken.StartArray)
    {
    var array = JArray.Load(reader);
    if (array.Count > 0)
    throw new JsonSerializationException(string.Format("Array was not empty."));
    return existingValue ?? contract.DefaultCreator();
    }

Working sample .Net fiddle.

Skip serializing a list element if a condition is satisfied (Newtonsoft)

Instead of relying on Newtonsoft to do this why would you not filter your list before serializing your list?

code:

[HttpGet("cars", Name = "GetCars")]
[ProducesResponseType(typeof(IEnumerable<Car>), 200)]
public async Task<IActionResult> GetCars()
{
var cars = _repo.GetCars().Where(c => c.ShouldSerializeCar );
retur Ok(cars);
}


Related Topics



Leave a reply



Submit