Home > Software engineering >  C# custom json serialization converter to set all default value of an object to null
C# custom json serialization converter to set all default value of an object to null

Time:11-09

I have a nested object that looks like:

public record Options
{
    public BatterySettings BatterySettings { get; init; } = new();
    public LogSettings LogSettings { get; init; } = new();
}

public record LogSettings 
{
    public string SourceName { get; init; } = "Default";
}

public record BatterySettings
{
    public int BatteryLevel { get; init; } = 5;
    public string BatteryHealth { get; init; } = "Normal";
    public BatteryLocations BatteryLocation { get; init; } = BatteryLocations.North;
}

public enum BatteryLocations
{
    North,
    South
}

After initializing the object and setting some properties i.e.:

var opt = new Options 
{
    BatterySettings = new BatterySettings {
        BatteryLevel = 10,
        BatteryHealth = "Low"
    }   
}

I would like to get a JSON string that represents this object opt while having all the default value set to null i.e. in this above example, the resulting opt JSON string would look like:

{
   "BatterySettings":{
      "BatteryLevel":10,
      "BatteryHealth":"Low",
      "BatteryLocation":null
   },
   "LogSettings":{
      "SourceName":null
   }
}

Is there a built-in way in .NET to do such a thing?

Edit 1: the built-in way of utilizing the null serialization settings would not work since the object Options has non-null default values for its properties and sub-object properties. It seems that a custom converter would need to be implemented here though I have trouble figuring out the correct approach to this due to having to compare default values with the object's current value for every given nodes

CodePudding user response:

In case of Json.Net there is a NullValueHandling settings under the JsonSerializerSettings where you can control the null values' serialization.

var settings new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Include
};
string json = JsonConvert.SerializeObject(opt, Formatting.Indented, settings);

In case of System.Text.Json there is a WhenWritingNull settings (please be aware that IgnoreNullValues is obsolete) under the JsonSerializerOptions where you can control the null values' serialization.

var options = new JsonSerializerOptions()
{
    DefaultIgnoreCondition = JsonIgnoreCondition.Never
};

string json = JsonSerializer.Serialize<Options>(opt,options);

CodePudding user response:

In this post I will show you how can you serialize the BatterySettings

  • if the property value is the same as the auto-generated property's default value then serialize it as null
  • if the property value is different than the auto-generated property's default value then serialize it as it is

In your particular case the default values of your auto-generated properties may or may not be the same as the run-time defaults. So, we can't use the default operator. To solve this problem I suggest the following "trick"

public record BatterySettings
{
    private const int BatteryLevelDefault = 5;
    public int BatteryLevel { get; init; } = BatteryLevelDefault;

    private const string BatteryHealthDefault = "Normal";
    public string BatteryHealth { get; init; } = BatteryHealthDefault;

    private const BatteryLocations BatteryLocationDefault = BatteryLocations.North;
    public BatteryLocations BatteryLocation { get; init; } = BatteryLocationDefault;
}

So, the "trick" is that we have a dedicated constant field for each property to store the default values. I've marked them as private so other class can't access them only via reflection.


Now let's see how the converter looks like for this data structure.
(Please note that this is not production-ready code. It is just for demonstration purposes.)

class BatterySettingsConverter : JsonConverter<BatterySettings>
{
    private readonly PropertyInfo[] Properties = typeof(BatterySettings).GetProperties();
    private readonly FieldInfo[] ConstFields = typeof(BatterySettings).GetFields(BindingFlags.NonPublic | BindingFlags.Static);

    public override BatterySettings? ReadJson(JsonReader reader, Type objectType, BatterySettings? existingValue, bool hasExistingValue, JsonSerializer serializer)
        => throw new NotImplementedException();

    public override void WriteJson(JsonWriter writer, BatterySettings? value, JsonSerializer serializer)
    {
        var result = new JObject();
        foreach (PropertyInfo prop in Properties)
        {
            var defaultValueField = ConstFields.FirstOrDefault(fi => fi.Name.StartsWith(prop.Name));
            if (!prop.CanRead || defaultValueField == null)
                continue;
            
            object propVal = prop.GetValue(value);
            object defaultVal = defaultValueField.GetValue(value);
            JToken serializeVal = !propVal.Equals(defaultVal) ? JToken.FromObject(propVal, serializer) : null;

            result.Add(prop.Name, serializeVal);           
        }
        result.WriteTo(writer);
    }
}
  • I've stored on the class-level the properties and the fields of the BatterySettings record
  • Inside the WriteJson, first I create an accumulator object (result)
  • Then I iterate through the properties and try to find the matching field
    • If the related field is not exist / the property does not have a getter then I simply skip it
    • This logic could be and should be tailored to your needs
  • I retrieve the property's actual value and the constant field's value
  • Based on the result of equality check I decide what to serialize
  • At the very end I ask json.net to perform the serialization of the accumulator object

After I have decorated the BatterySettings with the following attribute [JsonConverter(typeof(BatterySettingsConverter))] then I can perform some testing

var x = new BatterySettings();
var json = JsonConvert.SerializeObject(x); 
//{"BatteryLevel":null,"BatteryHealth":null,"BatteryLocation":null}

var y = new BatterySettings() { BatteryHealth = "A"}; 
json = JsonConvert.SerializeObject(y); 
//{"BatteryLevel":null,"BatteryHealth":"A","BatteryLocation":null}

var z = new BatterySettings() { BatteryLocation = BatteryLocation.South};
json = JsonConvert.SerializeObject(z); 
//{"BatteryLevel":null,"BatteryHealth":null,"BatteryLocation":1}

You can apply the same logic for the rest of your domain classes/records/sturcts.

  • Related