Leela Krishna Devalla
Leela Krishna Devalla

Reputation: 185

C# .Net Core 3.1 System.Text.Json Ignore empty collection in serialization

Using Newtonsoft we had a custom resolver for ignoring empty collections. Is there any equivalent configuration for the new system.text.json in .Net core 3.1

Upvotes: 16

Views: 8433

Answers (3)

Roxerio
Roxerio

Reputation: 891

TypeInfoResolver added in .NET 7(accessible in older runtimes via the system.text.json nuget package, pre-release as of time of this answer) allows this.

Example:

using System.Collections;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

public class TestObject {
    public List<int> Ints { get; } = new() {3, 4, 5};
    public List<int> EmptyInts { get; } = new();
    public List<int> NullInts { get; }
}

public static class Program {
    public static void Main() {
        var options = new JsonSerializerOptions {
            TypeInfoResolver = new DefaultJsonTypeInfoResolver {
                Modifiers = {DefaultValueModifier}
            },
        };
        var obj = new TestObject();
        var text = JsonSerializer.Serialize(obj, options);
        Console.WriteLine(text);
    }

    private static void DefaultValueModifier(JsonTypeInfo type_info) {
        foreach (var property in type_info.Properties) {
            if (typeof(ICollection).IsAssignableFrom(property.PropertyType)) {
                property.ShouldSerialize = (_, val) => val is ICollection collection && collection.Count > 0;
            }
        }
    }
}

output:

{"Ints":[3,4,5]}

Upvotes: 13

tmaj
tmaj

Reputation: 35017

An attempt

I know that this is not what you're after but maybe someone could build on this or there is a very very slim chance that it may suit some scenarios.

What I managed to do

An object new A() of

public class A
{
   public List<int> NullList {get;set;}
   public List<int> EmptyList {get;set;} = new List<int>();
};

becomes

{
  "EmptyList": null
}

Documentation

How to write custom converters for JSON serialization (marshalling) in .NET from 2021 Feb 25 in Custom converter patterns says:

There are two patterns for creating a custom converter: the basic pattern and the factory pattern. The factory pattern is for converters that handle type Enum or open generics. The basic pattern is for non-generic and closed generic types. For example, converters for the following types require the factory pattern:

  • Dictionary<TKey,TValue>
  • Enum
  • List

Some examples of types that can be handled by the basic pattern include:

  • Dictionary<int, string>
  • WeekdaysEnum
  • List
  • DateTime
  • Int32

The basic pattern creates a class that can handle one type. The factory pattern creates a class that determines, at run time, which specific type is required and dynamically creates the appropriate converter.

What I did

Demo

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

#nullable disable

namespace Sandbox4
{
 
    public class A
    {
        public List<int> NullList {get;set;}
        public List<int> EmptyList {get;set;} = new List<int>();
     };

    public class Program
    {
        public static void Main()
        {
            A a = new ();

            JsonSerializerOptions options = new ()
            {
                DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault,
                WriteIndented = true,
                Converters =
                {  
                     new IEnumerableTConverter()
                }
            };

            string aJson =
                JsonSerializer.Serialize<A>(a, options);
            
            Console.WriteLine(aJson);
        }
    }
}

Converter

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Sandbox4
{
    // Modified DictionaryTEnumTValueConverter 
    // from https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0#custom-converter-patterns
    public class IEnumerableTConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            var realType = typeToConvert.GetGenericTypeDefinition();
            if (realType.IsAssignableTo(typeof(IEnumerable<>)))
            {
                return false;
            }
            return true;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type generictype = type.GetGenericArguments()[0];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(ICollectionTConverterInner<,>).MakeGenericType(
                    new Type[] { type, generictype }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { type, options },
                culture: null);

            return converter;
        }

        private class ICollectionTConverterInner<T,U> :
            JsonConverter<T> where T: IEnumerable<U>
        {
            private readonly JsonConverter<T> _normalConverter;

            public ICollectionTConverterInner(Type type,JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                var existing = new JsonSerializerOptions().GetConverter(type);
                if( existing == null ) throw new ApplicationException($"Standard converter for {type} not found.");

                _normalConverter = (JsonConverter<T>) existing;
            }

            public override T Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                // Untested
                return _normalConverter.Read(ref reader, typeToConvert, options);
            }

            public override void Write(
                Utf8JsonWriter writer,
                T collection,
                JsonSerializerOptions options)
            {
                if(!collection.Any()) 
                {
                    writer.WriteNullValue();
                    return;
                }

                _normalConverter.Write(writer, collection, options);
            }
        }
    }
}

Upvotes: 0

tmaj
tmaj

Reputation: 35017

Conditionally ignore a property from .NET5's official How to migrate from Newtonsoft.Json to System.Text.Json from 2020 Dec 14 states:

System.Text.Json provides the following ways to ignore properties or fields while serializing:

  • The [JsonIgnore] attribute (...).
  • The IgnoreReadOnlyProperties global option (...).
  • (...) JsonSerializerOptions.IgnoreReadOnlyFields global (...)
  • The DefaultIgnoreCondition global option lets you ignore all value type properties that have default values, or ignore all reference type properties that have null values.

These options don't let you:

  • Ignore selected properties based on arbitrary criteria evaluated at run time.

For that functionality, you can write a custom converter. Here's a sample POCO and a custom converter for it that illustrates this approach:

(An example of a custom converter for a type follows)

Please note that this converter needs to be a converter for the type that contains the property that is a collection, it's not a converter for the collection type (for that see my 2nd answer).

Upvotes: 2

Related Questions