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.