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.