System.Text.Jsonを使っていて、空のコレクションを返すプロパティをシリアライズしたくない(出力したくないというか無視したいというか)ときがあったので、対応を考えてみましたという内容です。

シリアライズするクラスを編集できるのであれば、JsonIgnoreCondition.WhenWritingNullを使って実現します。編集できないクラスであれば、JSONコントラクトというメタデータ(JsonTypeInfo)を使って実現します。

nullやデフォルト値の場合にシリアライズしない方法であれば、JsonIgnoreConditionを使うとあっさりできます。下記を参照ください。

既存の動き

まず既存の動きを確認しましょう。 空のコレクションは当然ですがJSON上で空配列([])としてシリアライズされます。

using System.Text.Json;

var options = new JsonSerializerOptions {
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var json = JsonSerializer.Serialize(new Sample(), options);
Console.WriteLine(json);
/*
{"values":[]}
*/

public class Sample {
    // 空のコレクションを返すプロパティ
    public IEnumerable<int> Values { get; init; } = Enumerable.Empty<int>();
}

シリアライズするクラスを編集できるとき

シリアライズ対象を編集できるのであれば、空のコレクションのプロパティに対して、

  1. JsonIgnoreCondition.WhenWritingNullJsonIgnoreAttributeを指定する
  2. getアクセサーをコレクションが空であればnullを返すようにする

という実装で少しトリッキーな気もしますがいけるかなと。

using System.Text.Json.Serialization;
using System.Text.Json;

var options = new JsonSerializerOptions {
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var json = JsonSerializer.Serialize(new Sample(), options);
Console.WriteLine(json);
/*
{}
*/

public class Sample {
    private IEnumerable<int> _values = new List<int>();

    // 取得できる値がnullのときはシリアライズしない
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public IEnumerable<int>? Values {
        get {
            // 空であればnullを返す
            return _values.Any() ? _values : null;
        }
        init {
            _values = value ?? throw new InvalidOperationException();
        }
    }
}

シリアライズするクラスを編集できないとき

コレクションが空であればnullを返すプロパティという上記実装ができない場合、次にような実装で実現できます。

  1. JsonSerializerOptions.DefaultIgnoreConditionJsonIgnoreCondition.WhenWritingNullを指定する
  2. JSONコントラクトというメタデータをカスタマイズして、コレクションが空ならnullを返す

以下サンプルコードです。

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

// 空のコレクションを返すプロパティのJSONコントラクトをカスタマイズする
var modifier = (JsonTypeInfo typeInfo) => {
    if (typeInfo.Kind is not JsonTypeInfoKind.Object) {
        return;
    }
    var properties = typeInfo.Properties.Where(property => property.PropertyType.IsAssignableTo(typeof(IEnumerable)));
    foreach (var property in properties) {
        var getter = property.Get;
        // プリパティの値が空のコレクションであればnullを返す
        property.Get = (obj) => {
            if (getter?.Invoke(obj) is not IEnumerable values) {
                return null;
            }

            return values.Cast<object>().Any()
                ? values
                : null;
        };
    }
};

var options = new JsonSerializerOptions {
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    // nullをシリアライズしない
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    TypeInfoResolver = new DefaultJsonTypeInfoResolver {
        // JSONコントラクトをカスタマイズする
        Modifiers = { modifier }
    }
};

var json = JsonSerializer.Serialize(new Sample(), options);
Console.WriteLine(json);
/*
{}
*/

public class Sample {
    public IEnumerable<int> Values { get; init; } = Enumerable.Empty<int>();
}

JSONコントラクトのカスタマイズについては下記ドキュメントが参考になります。