YamlDotNet icon indicating copy to clipboard operation
YamlDotNet copied to clipboard

DefaultValuesHandling not working when serialising dynamic properties

Open dsparkplug opened this issue 6 years ago • 2 comments

The DefaultValuesHandling behaviour is not being applied to dynamic properties.

To reproduce, run the following code on version 8.0.0

private static void Main(string[] args)
{

	dynamic exportModel = new ExpandoObject();

	exportModel.TestStringWithValue = "TestValue";
	exportModel.TestNullString = default(string);
	exportModel.TestBooleanTrue = true;
	exportModel.TestBooleanFalse = default(bool);
	exportModel.TestIntegerWithValue = 99;
	exportModel.TestZeroInteger = default(int);

	string result1;
	var serializerBuilder = new SerializerBuilder();
	var serializer = serializerBuilder.Build();
	using (var sw = new StringWriter())
	{
		serializer.Serialize(sw, exportModel);
		result1 = sw.ToString();
	}

	string result2;
	serializerBuilder = new SerializerBuilder().ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults);
	serializer = serializerBuilder.Build();
	using (var sw = new StringWriter())
	{
		serializer.Serialize(sw, exportModel);
		result2 = sw.ToString();
	}

	Console.WriteLine(result1);
	Console.WriteLine(result2);

	Console.WriteLine($"Equal={(result1 == result2)}");
}

The output is:

TestStringWithValue: TestValue
TestNullString:
TestBooleanTrue: true
TestBooleanFalse: false
TestIntegerWithValue: 99
TestZeroInteger: 0

TestStringWithValue: TestValue
TestNullString:
TestBooleanTrue: true
TestBooleanFalse: false
TestIntegerWithValue: 99
TestZeroInteger: 0

Equal=True

We could achieve the expected results in version 7.0.0 using the following code:

private static void Main(string[] args)
{

	dynamic exportModel = new ExpandoObject();

	exportModel.TestStringWithValue = "TestValue";
	exportModel.TestNullString = default(string);
	exportModel.TestBooleanTrue = true;
	exportModel.TestBooleanFalse = default(bool);
	exportModel.TestIntegerWithValue = 99;
	exportModel.TestZeroInteger = default(int);

	string result1;
	var serializerBuilder = new SerializerBuilder().EmitDefaults();
	var serializer = serializerBuilder.Build();
	using (var sw = new StringWriter())
	{
		serializer.Serialize(sw, exportModel);
		result1 = sw.ToString();
	}

	string result2;
	serializerBuilder = new SerializerBuilder();
	serializer = serializerBuilder.Build();
	using (var sw = new StringWriter())
	{
		serializer.Serialize(sw, exportModel);
		result2 = sw.ToString();
	}

	Console.WriteLine(result1);
	Console.WriteLine(result2);

	Console.WriteLine($"Equal={(result1 == result2)}");

}

The output (as we would expect) is:

TestStringWithValue: TestValue
TestNullString:
TestBooleanTrue: true
TestBooleanFalse: false
TestIntegerWithValue: 99
TestZeroInteger: 0

TestStringWithValue: TestValue
TestBooleanTrue: true
TestIntegerWithValue: 99

Equal=False

Note that replacing the ExpandoObject in the first code sample with a class with typed properties produces the correct output (same as the output the second code sample).

dsparkplug avatar Oct 29 '19 02:10 dsparkplug

I have also tried deriving from ChainedObjectGraphVisitor and adding it to the builder using WithEmissionPhaseObjectGraphVisitor. The overridden EnterMapping method does to appear to get called for dynamic properties.

dsparkplug avatar Oct 29 '19 02:10 dsparkplug

It seems to work correctly if I override EnterMapping(IObjectDescriptor key, IObjectDescriptor value, IEmitter context) instead of EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) and compare the value with the default for value.Type rather than key.Type. e.g.

public class TestDefaultValuesObjectGraphVisitor : ChainedObjectGraphVisitor
{
	public TestDefaultValuesObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor)
		: base(nextVisitor)
	{
	}

	public override bool EnterMapping(IObjectDescriptor key, IObjectDescriptor value, IEmitter context)
	{
		var defaultValue = value.Type.IsValueType ? Activator.CreateInstance(value.Type) : null; 
		if (Equals(value.Value, defaultValue))
			return false;

		return base.EnterMapping(key, value, context);
	}
}
var serializerBuilder = new SerializerBuilder().WithEmissionPhaseObjectGraphVisitor(x => new TestDefaultValuesObjectGraphVisitor(x.InnerVisitor));

So maybe DefaultValuesObjectGraphVisitor can be modified similarly?

dsparkplug avatar Oct 29 '19 04:10 dsparkplug