3

I have this JSON:

{
    "Variable1": "1",
    "Variable2": "50000",
    "ArrayObject": [null]
}

I have this stubs:

public class Class1
{
  public string Variable1 { get; set; }
  public string Variable2 { get; set; }
  [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
  public List<ArrayObject> ArrayObject { get; set; }
}

public class ArrayObject
{
  public string VariableArray1 { get; set; }
  public string VariableArray2 { get; set; }
}

I'd like to ignore the null elements inside array preferably using the json settings or some sort of converter. So the result should be an empty array in that case or null.

Here is the code I've been trying to make this work.

class Program
{
  static void Main(string[] args)
  {
    string json = @"{
      ""Variable1"": ""1"",
      ""Variable2"": ""50000"",
      ""ArrayObject"": [null]
    }";

    var settings = new JsonSerializerSettings()
    {
      ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
      NullValueHandling = NullValueHandling.Ignore,
    };

    Class1 class1 = JsonConvert.DeserializeObject<Class1>(json, settings);

    Console.WriteLine(class1.ArrayObject == null);
    Console.WriteLine(class1.ArrayObject.Count());

    foreach (var item in class1.ArrayObject)
    {
      Console.WriteLine(item.VariableArray1);
      Console.WriteLine(item.VariableArray2);
      Console.WriteLine("#######################");
    }
  }

  public class Class1
  {
    public string Variable1 { get; set; }
    public string Variable2 { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public List<ArrayObject> ArrayObject { get; set; }
  }

  public class ArrayObject
  {
    public string VariableArray1 { get; set; }
    public string VariableArray2 { get; set; }
  }
}

I thought that using NullValueHandling = NullValueHandling.Ignore would make it work. Apparently not. Any ideas?

Update: I need a global solution, I don't want to have to modify every viewmodel inside my project.

7
  • 1
    What issue you are facing with NullValueHandling = NullValueHandling.Ignore ? Commented Jul 16, 2020 at 17:57
  • well as I said, I want to ignore null elements inside my array on deserialization. Commented Jul 16, 2020 at 17:58
  • 1
    NullValueHandling is useful to during serialization to decide whether the property with null value should be deserialized or ignored. Based on value of NullValueHandling the resultant json will have property name with null set to it or the json will not have that property at all. So NullValueHandling will of little help during deserialization Commented Jul 16, 2020 at 18:24
  • Does this answer your question? How to ignore a property in class if null, using json.net Commented Jul 16, 2020 at 18:28
  • @DCCoder - that doesn't answer the question, NullValueHandling=NullValueHandling.Ignore doesn't filter out array items. Commented Jul 16, 2020 at 18:30

4 Answers 4

5

Setting NullValueHandling = NullValueHandling.Ignore will not filter null values from JSON arrays automatically during deserialization because doing so would cause the remaining items in the array to be re-indexed, rendering invalid any array indices that might have been stored elsewhere in the serialization graph.

If you don't care about preserving array indices and want to filter null values from the array during deserialization anyway, you will need to implement a custom JsonConverter such as the following:

public class NullFilteringListConverter<T> : JsonConverter<List<T>>
{
    public override List<T> ReadJson(JsonReader reader, Type objectType, List<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        var list = existingValue as List<T> ?? (List<T>)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
        serializer.Populate(reader, list);
        list.RemoveAll(i => i == null);
        return list;
    }

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

public static partial class JsonExtensions
{
    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;
    }
}

And apply it to your model as follows:

public class Class1
{
    public string Variable1 { get; set; }
    public string Variable2 { get; set; }
    [JsonConverter(typeof(NullFilteringListConverter<ArrayObject>))]
    public List<ArrayObject> ArrayObject { get; set; }
}

Or, add it in settings as follows:

var settings = new JsonSerializerSettings()
{
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
    NullValueHandling = NullValueHandling.Ignore,
    Converters = { new NullFilteringListConverter<ArrayObject>() }, 
};

Notes:

  • Since you didn't ask about filtering null values during serialization, I didn't implement it, however it would be easy to do by changing CanWrite => true; and replacing WriteJson() with:

     public override void WriteJson(JsonWriter writer, List<T> value, JsonSerializer serializer) => serializer.Serialize(writer, value.Where(i => i != null));
    

Demo fiddles here and here.

Update

I need a global solution. If you need to automatically filter all null values from all possible List<T> objects in every model, the following JsonConverter will do the job:

public class NullFilteringListConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsArray || objectType == typeof(string) || objectType.IsPrimitive)
            return false;
        var itemType = objectType.GetListItemType();
        return itemType != null && (!itemType.IsValueType || Nullable.GetUnderlyingType(itemType) != null);
    }

    object ReadJsonGeneric<T>(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var list = existingValue as List<T> ?? (List<T>)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
        serializer.Populate(reader, list);
        list.RemoveAll(i => i == null);
        return list;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        var itemType = objectType.GetListItemType();
        var method = typeof(NullFilteringListConverter).GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
        try
        {
            return method.MakeGenericMethod(new[] { itemType }).Invoke(this, new object[] { reader, objectType, existingValue, serializer });
        }
        catch (Exception ex)
        {
            // Wrap the TargetInvocationException in a JsonSerializerException
            throw new JsonSerializationException("Failed to deserialize " + objectType, ex);
        }
    }

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

public static partial class JsonExtensions
{
    internal static Type GetListItemType(this 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];
            }
            type = type.BaseType;
        }
        return null;
    }
}

And add it to settings as follows:

var settings = new JsonSerializerSettings()
{
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
    NullValueHandling = NullValueHandling.Ignore,
    Converters = { new NullFilteringListConverter() },
};

Class1 class1 = JsonConvert.DeserializeObject<Class1>(json, settings);

With this converter, adding [JsonConverter(typeof(NullFilteringListConverter<ArrayObject>))] to ArrayObject is no longer required. Do note that all List<T> instances in your deserialization graph may get re-indexed whenever these settings are used! Make sure you really want this as the side-effects of changing indices of items referred to elsewhere by index may include data corruption (incorrect references) rather than an outright ArgumentOutOfRangeException.

Demo fiddle #3 here.

Sign up to request clarification or add additional context in comments.

2 Comments

Can I add this converter to a json setting so it works globally on every list? Or do I need to go in every model and add this attribute?
You can add a NullFilteringListConverter<T> to JsonSerializerSettings.Converters for every relevant T and the filtering will be done for all List<T> collections in your model. If you need a single converter for all possible List<T> collections then that requires some additional work. If that is what you need, please edit your question and add details. Your current question shows JsonProperty(NullValueHandling = NullValueHandling.Ignore)] applied to the property so I assumed a property-specific solution would be sufficient.
2

You could also have a custom setter that filters out null values.

private List<ArrayObject> _arrayObject;
public List<ArrayObject> ArrayObject
{
    get => _arrayObject;
    set
    {
        _arrayObject = value.Where(x => x != null).ToList();
    }
}

Fiddle working here https://dotnetfiddle.net/ePp0A2

Comments

1

NullValueHandling.Ignore does not filter null values from an array. But you can do this easily by adding a deserialization callback method in your class to filter out the nulls:

public class Class1
{
    public string Variable1 { get; set; }
    public string Variable2 { get; set; }
    public List<ArrayObject> ArrayObject { get; set; }

    [OnDeserialized]
    internal void OnDeserialized(StreamingContext context)
    {
        ArrayObject?.RemoveAll(o => o == null);
    }
}

Working demo: https://dotnetfiddle.net/v9yn7j

2 Comments

I'd have to put this in every viewmodel inside my project. I need a global solution.
I need a global solution - That's a pretty important detail you left out of your question. You'll need to use a generic JsonConverter then. See @dbc's updated answer.
0

Perhaps a global solution for System.Text.Json serializer will be useful to someone:


public class CollectionNullElementsIgnoreConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        if (typeToConvert.Name == nameof(String) || typeToConvert.IsPrimitive ||
            typeToConvert.GetInterface(nameof(IEnumerable)) == null) return false;

        var genericType = GetGenericType(typeToConvert);

        if (genericType == null) return false;
        if (genericType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(genericType)) return false;
        if (genericType.IsValueType || !genericType.IsClass) return false;

        return true;
    }

    private static Type GetGenericType(Type type) => type.GetGenericArguments().FirstOrDefault();

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var genericType = GetGenericType(typeToConvert);
        var converterType = typeof(CollectionNullElementsIgnoreConverter<>).MakeGenericType(genericType);
        return (JsonConverter)Activator.CreateInstance(converterType);
    }
}

public class CollectionNullElementsIgnoreConverter<TItem> : JsonConverter<IEnumerable<TItem?>>
{
    public override IEnumerable<TItem> Read(ref Utf8JsonReader reader, Type typeToConvert,
        JsonSerializerOptions options)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.Null: return null;
            case JsonTokenType.StartArray:
                var collection = new HashSet<TItem>();
                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndArray)
                    {
                        break;
                    }

                    if (reader.TokenType is JsonTokenType.Null or JsonTokenType.None)
                    {
                        continue;
                    }

                    var item = JsonSerializer.Deserialize<TItem>(ref reader, options);
                    if (item != null)
                    {
                        collection.Add(item);
                    }
                }

                return collection.AsEnumerable();
            default:
                return [JsonSerializer.Deserialize<TItem>(ref reader, options)];
        }
    }

    public override void Write(Utf8JsonWriter writer, IEnumerable<TItem> value, JsonSerializerOptions options)
    {
        writer.WriteStartArray();

        foreach (var item in value)
        {
            if (item == null)
            {
                continue;
            }

            JsonSerializer.Serialize(writer, item, options);
        }

        writer.WriteEndArray();
    }
}

3 Comments

Please don’t post the same answer to multiple questions.
It looks like I didn't do it, please provide a link
I see. The code is indeed different. I was thrown off by the very similar explanation. I apologize.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.