First things first…YamlDotNet is a great library and I’m thankful it exists and is so well maintained.

Also, this is just how I figured out a solution to my requirement. I’m fully open to and would welcome suggestions on an simpler way to do this.

I was recently trying to find a way to do some configuration definitions in as clean a manner as possible. Generally, I’m not a fan of YAML and agree with many of the complaints about YAML. But YAML is probably the most plain english and clutter free model for defining a configuration. XML and JSON are much preferred – in my developer brain – but JSON is harder to write by hand and XML seems to have fallen out of favor. (Personal note: I still have a love for XML and XML Schema.)

Okay, so I wanted to be able to read and write YAML from a .NET application. Thus, I turned to YamlDotNet. YDN is a great library that is continually updated and does a great job handling a complex specification. So, while it’s not perfect, I haven’t seen anyone do a better job and it does a really good job.

But I quickly ran into a problem. One of the requirements I had was the ability to have a list of different types in a YAML document that would load into a C# list as class objects that implement an interface, i.e., List<IInterface>. Following this type of code pattern:

C#
public class Root
{
	public List<IConcept> Concepts {get; set;}
}

public interface IConcept
{}

public class Concept1 : IConcept
{
	public string Name {get; set;}
	public List<IConcept> InnerConcepts {get; set;}
}

public class Concept2 : IConcept
{
	public string Descriptions { get; set; }
	public int? Value { get; set; }
	public IConcept SingleConcept {get; set;}
}

public class Concept3 : IConcept
{
	public string Descriptions { get; set; }
	public int Value { get; set; }
	public ConceptDetails Details {get; set;}
}

public class ConceptDetails
{
	public string Detail {get; set;}
	public int? Rating {get; set;}
}

And I very much wanted my YAML documents to look like this:

YAML
Concepts:
    - Concept1:
        Name: ConceptOne
        InnerConcepts: &ic0001
            - Concept2:
                Descriptions: Ow wow too much...
                Value: 92
            - Concept2:
                Descriptions: This is getting too big
                Value: 343
            - Concept1:
                Name: Building stuff deeper and deeper
    - Concept2:
        Descriptions: A description of Concept Two
        Value: 42
        SingleConcept:
            Concept2:
                Descriptions: Deep Descriptions
                Value: 2432
    - Concept1:
        Name: Another Concept One
        InnerConcepts: *ic0001
    - Concept2:
        Descriptions: Another description for Concept Two
        Value: 99
    - Concept3:
        Descriptions: description for Concept Three
        Value: 867
        Details:
            Detail: Some special details
            Rating: 874

The above YAML will deserialize without issue to a Dictionary<object, object> model using YamlDotNet.

var obj = (new Deserializer()).Deserialize<object>(yamlDocument)

But YamlDotNet won’t deserialize to the Root object graph because it can’t create an instance of an interface.

var root = (new Deserializer()).Deserialize<Root>(yamlDocument);

This line will throw an exception: Cannot dynamically create an instance of type ‘IConcept’. Reason: Cannot create an instance of an interface.

In order to successfully deserialize YDN needs to know the class to use for each item in the list. The YamlDotNet wiki describes a couple ways to do this with type discriminators. While both of these work they didn’t meet the requirements I had for my YAML document model.

I also found that I could do this by turning items into tags:

YAML
    - !Concept2
        Descriptions: Another description for Concept Two
        Value: 99

But I didn’t want to use tags, if I could avoid them, because it wasn’t the experience I wanted and would make the documents functionally different if in the future they were to be deserialized without the object graph. Mostly it was that I wanted the documents to be as natural to write, as possible, and tags introduced a complexity, that however minor, I was hoping to avoid.

Through a lot of web searches and a bunch of suggestions from ChatGPT and GitHub Copilot, I eventually landed on a working solution.

I will present my solution in pieces related specifically to the demonstration model I used for this article. Then I will present it as a generic model using a mapper object that to avoid hard coding interfaces and classes within the core code.

We need to create a deserializer to specifically handle any item mapped to the IConcept interface. If it isn’t expecting an IConcept interface than we let the default deserializer handle the item (last line in the Deserialize method. For IConcept items we determine the class from the scalar value. Then we pass the class to the object deserializer for regular deserialization. If we can’t match we throw an exception. ChatGPT and I went back and forth several times figuring out this code and getting it to a working form.

C#
public class PolymorphicConceptDeserializer : YamlDotNet.Serialization.INodeDeserializer
{
		
	private readonly INodeDeserializer _innerDeserializer;

	public PolymorphicConceptDeserializer(INodeDeserializer innerDeserializer)
	{
		_innerDeserializer = innerDeserializer;
	}

	public bool Deserialize(
		IParser reader,
		Type expectedType,
		Func<IParser, Type, object> nestedObjectDeserializer,
		out object value, ObjectDeserializer rootDeserializer)
	{
		// Check if we are deserializing an IConcept type
		if (expectedType == typeof(IConcept))
		{
			var current = reader.Current as MappingStart;
			if (current != null)
			{
				// Read the key (e.g., Concept1, Concept2)
				reader.MoveNext();
				var scalar = reader.Current as Scalar;
				if (scalar != null)
				{
					Type concreteType = scalar.Value switch
					{
						"Concept1" => typeof(Concept1),
						"Concept2" => typeof(Concept2),
						_ => throw new InvalidOperationException($"Unknown concept type '{scalar.Value}'")
					};

					// Move to the object value
					reader.MoveNext();
					value = nestedObjectDeserializer(reader, concreteType);
					reader.MoveNext(); // Move past the end of the mapping node
					return true;
				}
			}
		}

		// If it isn't an IConcept type then use the default deserializer
		return _innerDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value, rootDeserializer);
	}

	
}

As you can see, we are using the scalar value from the list items to map to the class. For my purposes, as you’ll see in the complete solution, I’m happy to live with class names having to match with values in my YAML document, but you could build an additional mapping/translation layer in to drive off of class attributes or another source/transformation rule.

To use this class we have to tell YamlDotNet about it. We do this by building a deserializer object and then calling the Deserialize method.

C#
var deserializer = new DeserializerBuilder()
    .WithNodeDeserializer(inner => new PolymorphicConceptDeserializer(inner), syntax => syntax.InsteadOf<ObjectNodeDeserializer>())
    .Build();
    
var root = deserializer.Deserialize<Root>(yamlDocument);

The result will be an object graph. If you are working in LinqPad you can visualize it by calling root.Dump():

YAML
ConfigName: TestConfiguration
Version: 1
Tables:
    - Table1
    - Table2
    - Table3
Concepts:
    - Concept2:
          Descriptions: A description of Concept Two
          Value: 42
    - Concept1:
          Name: Another Concept One
    - Concept2:
          Descriptions: Another description for Concept Two
          Value: 99
The image shows the table output of calling LinqPad's Dump method on the Root instance created by deserializing the YAML document.

Now we can successfully deserialize but I also wanted to be able to serialize a Root object. My goal was to support full round trips (YAML doc to object graph back to valid YAML doc.) If we execute var rootDoc = (new Serializer()).Serialize(root); we’ll get a YAML document without the scalar values to indicate the class. This does not meet the requirement for our YAML document and won’t allow the serialized document to be deserialized, thus failing to meet the round trip goal.

YAML
ConfigName: TestConfiguration
Version: 1
Tables:
  - Table1
  - Table2
  - Table3
Concepts:
  - Descriptions: A description of Concept Two
    Value: 42
  - Name: Another Concept One
  - Descriptions: Another description for Concept Two
    Value: 99

This makes sense, we had to provide a handler to meet our deserialization requirement so we shouldn’t expect YamlDotNet to meet our serialization requirement without a handler.

To get the serialization to work, as desired, we have to implement a custom type converter, IYamlTypeConverter, to handle serializing the object as its class, otherwise it’s just serialized as properties on an object. I want to give credit to two Stackoverflow answers for getting me to the solution: https://stackoverflow.com/questions/64242023/yamldotnet-custom-serialization and https://stackoverflow.com/questions/78211029/.

There are three methods for a type converter but only two are relevant to our solution. The Accept method tells YamlDotNet’s serializer/deserializer process whether this converter will handle a particular type. In our case we will handle types that implement the IConcept interface and the WriteYaml will handle the serialization for those objects. The ReadYaml isn’t used, although I do wonder if it could do what I’m doing in the INodeDeserializer implementation (left to be wondered about some other day), so we simply pass along to the root deserializer. I do wonder if I should throw a NotImplemented exception here. It doesn’t matter to me because I only register this on a SerializerBuilder so it won’t be called for deserialization processes (until I or someone forgets and registers it wrong!)

C#
public sealed class PolymorphicConceptConverter : IYamlTypeConverter
{
	//Credit where credit is due:
	//		https://stackoverflow.com/questions/64242023/yamldotnet-custom-serialization 
	//		and https://stackoverflow.com/questions/78211029/convert-list-to-objects-with-custom-names-in-yamldotnet
	//These two Stackoverflow posts got me to this solution.
	
	// Unfortunately the API does not provide those in the ReadYaml and WriteYaml
	// methods, so we are forced to set them after creation.
	public IValueSerializer ValueSerializer { get; set; }
	public IValueDeserializer ValueDeserializer { get; set; }

	public bool Accepts(Type type)=> typeof(IConcept).IsAssignableFrom(type);

	public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
	{
		return rootDeserializer(typeof(IConcept));
	}

	public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
	{
		emitter.Emit(new MappingStart());
		
		var component = value;
		var otherPropertisMap = type
			.GetProperties()
			.ToDictionary(p => p.Name, p => p.GetValue(component));
		ValueSerializer.SerializeValue(emitter, type.Name, typeof(string));
		ValueSerializer.SerializeValue(emitter, otherPropertisMap, typeof(Dictionary<string, object>));
		emitter.Emit(new MappingEnd());		
	}


}

Our method starts and ends with calls to MappingStart() and MappingEnd(). These tell the emitter that we are starting a new YAML object and that we done with that YAML object, it’s how the emitter knows that whatever comes before and after what we emit is part of a different object.

To serialize supported classes we use the type to get a list of properties and then build a dictionary of the property names and their values from the object. Note, that this doesn’t respect YamlDotNet code attributes – if you need that you would need to implement it.

The first value we serialize is the name of the class, type.Name. This is what writes Concept1 as the list’s value.

Then we pass this dictionary to the value serializer to be written to the YAML document. We have to do this as a dictionary as if we pass the object itself to the value serializer we’ll create a stackoverflow exception as the WriteYaml method would be called recursively.

Now we need to tell the serializer to use our custom type converter. This is a bit more complex than with the deserializer because we have to pass the ValueSerializer to our class, since it doesn’t have access to an object from which to access it.

C#
var conceptObjectConverter = new PolymorphicConceptConverter();

var rootSerializerBuilder = new SerializerBuilder()
	.WithTypeConverter(conceptObjectConverter);
		
conceptObjectConverter.ValueSerializer = rootSerializerBuilder.BuildValueSerializer();
	
var serializer = rootSerializerBuilder.Build();

As you can see, we create an instance of the type converter, PolymorphicConceptConverter, and pass it to the SerializerBuilder, and then we set the value of ValueSerializer from the SerializerBuilder instance. Finally, we call Build() to return the Serializer instance that we’ll call to serialize the Root object.

A call to serializer.Serialize() will return a string object with our YAML document: var rootDoc = serializer.Serialize(root);.

YAML
ConfigName: TestConfiguration
Version: 1
Tables:
- Table1
- Table2
- Table3
Concepts:
- Concept2:
    Descriptions: A description of Concept Two
    Value: 42
- Concept1:
    Name: Another Concept One
- Concept2:
    Descriptions: Another description for Concept Two
    Value: 99

You can copy that YAML and pass it back through the deserializer to get an instance of Root. I’m able to meet my requirements.

You’ll notice in the beginning of this document I posted some C# code defining a Root object graph more complicated than what I used in the examples. I used a simplified YAML document to keep the examples shorter. I built the more complicated object graph so I could test nesting and ensure the solution would work with nested lists and objects – it does.

Of course, the solution has a problem as it is very strongly linked to a specific interface and implementing classes. You can’t do a Concept3 or implement a new interface and classes without changing the core code. That won’t work.

To overcome that issue I implemented a mapping class that makes it easy to configure this information and pass it to the serializer and deserializer.

C#
public class PolymorphicObjectMapper
{
	private Dictionary<string, InterfaceMap> interfaces = new Dictionary<string, InterfaceMap>();
	private HashSet<string> handledTypes = new HashSet<string>();
	
	public void Add(Type InterfaceClass, Type ConcreteClass)
	{
		if (handledTypes.Contains(ConcreteClass.Name)) return;
		
		if (!interfaces.ContainsKey(InterfaceClass.Name)) interfaces.Add(InterfaceClass.Name, new InterfaceMap());
		
		handledTypes.Add(ConcreteClass.Name);
		
		interfaces[InterfaceClass.Name].classMappings.Add(ConcreteClass.Name, ConcreteClass);
	}

	public bool CanHandleInterface(Type interfaceType)
	{
		return interfaces.ContainsKey(interfaceType.Name);
	}

	public bool CanHandleType(Type type)
	{
		return handledTypes.Contains(type.Name);
	}

	public Type GetConcreteType(string typeName, Type interfaceType)
	{
		if (interfaces.ContainsKey(interfaceType.Name) && interfaces[interfaceType.Name].classMappings.ContainsKey(typeName))
			return interfaces[interfaceType.Name].classMappings[typeName];
		else throw new Exception("Cannot Find Type for Specificed Type Name and Interface");
	}
}

public class InterfaceMap
{
	public string interfaceName { get; set;}
	public Dictionary<string, Type> classMappings { get; set;}
	
	public InterfaceMap()
	{
		classMappings = new Dictionary<string, Type>();
	}
}

We then update our node deserializer and type converter classes to have a constructor that takes a mapping object:

public PolymorphicObjectDeserializer(INodeDeserializer innerDeserializer, PolymorphicObjectMapper polymorphicObjectMapper)

public PolymorphicObjectConverter (PolymorphicObjectMapper polymorphicObjectMapper)

And we configure the mapper in our main code before passing it to constructors:

C#
//This will configure a mapper to let the Polymorhpic serializers handle interfaces and concrete classes
var mapper = new PolymorphicObjectMapper();
mapper.Add(typeof(IConcept), typeof(Concept1));
mapper.Add(typeof(IConcept), typeof(Concept2));
mapper.Add(typeof(IConcept), typeof(Concept3));
	 
var deserializer = new DeserializerBuilder()
				.WithNodeDeserializer(inner => new PolymorphicObjectDeserializer(inner, mapper), syntax => syntax.InsteadOf<ObjectNodeDeserializer>())
				.Build();
					

var polymorhpicObjectConverter = new PolymorphicObjectConverter(mapper);
	
var rootSerializerBuilder = new SerializerBuilder()
	.WithTypeConverter(polymorhpicObjectConverter);

The addition of the mapper logic opens up additional possibilities for more complex scenarios. For example, you could have a builder that uses attributes to populate a mapper with relevant interfaces and classes. If you wanted to handle logic where the YAML document values don’t map to class names, the mapper class can be customized to handle it. etc.

Below is the complete solution. You can drop this into a LinqPad query, add a reference to the YamlDotNet NuGet package, and run it.

C#
void Main()
{
	var yamlDocument = 
@"
ConfigName: TestConfiguration
Version: 1
Tables:
    - Table1
    - Table2
    - Table3
Concepts:
    - Concept1:
        Name: ConceptOne
        InnerConcepts: &ic0001
            - Concept2:
                Descriptions: Ow wow too much...
                Value: 92
            - Concept2:
                Descriptions: This is getting too big
                Value: 343
            - Concept1:
                Name: Building stuff deeper and deeper
    - Concept2:
        Descriptions: A description of Concept Two
        Value: 42
        SingleConcept:
            Concept2:
                Descriptions: Deep Descriptions
                Value: 2432
    - Concept1:
        Name: Another Concept One
        InnerConcepts: *ic0001
    - Concept2:
        Descriptions: Another description for Concept Two
        Value: 99
    - Concept3:
        Descriptions: description for Concept Three
        Value: 867
        Details:
            Detail: Some special details
            Rating: 874
";


	//This will configure a mapper to let the Polymorhpic serializers handle interfaces and concrete classes
	var mapper = new PolymorphicObjectMapper();
	mapper.Add(typeof(IConcept), typeof(Concept1));
	mapper.Add(typeof(IConcept), typeof(Concept2));
	mapper.Add(typeof(IConcept), typeof(Concept3));
	 
	var deserializer = new DeserializerBuilder()
					.WithNodeDeserializer(inner => new PolymorphicObjectDeserializer(inner, mapper), syntax => syntax.InsteadOf<ObjectNodeDeserializer>())
					.Build();
					
	var root = deserializer.Deserialize<Root>(yamlDocument);

	var polymorhpicObjectConverter = new PolymorphicObjectConverter(mapper);
	
	var rootSerializerBuilder = new SerializerBuilder()
		.WithTypeConverter(polymorhpicObjectConverter);
		
	polymorhpicObjectConverter.ValueSerializer = rootSerializerBuilder.BuildValueSerializer();
	
	var rootSerializer = rootSerializerBuilder.Build();
	var rootDoc = rootSerializer.Serialize(root);
	
	string seperator = "------------------------------";
	
	Console.WriteLine("Original YAML Document");
	Console.WriteLine(seperator);
	yamlDocument.Dump();
	Console.WriteLine(seperator);
	Console.WriteLine("Deserialized Object Graph");
	root.Dump();
	Console.WriteLine(seperator);
	Console.WriteLine("Serialized Document from Object Graph");
	Console.WriteLine(seperator);
	rootDoc.Dump();
	Console.WriteLine(seperator);

}

public class PolymorphicObjectDeserializer : YamlDotNet.Serialization.INodeDeserializer
{
		
	private readonly INodeDeserializer _innerDeserializer;
	private PolymorphicObjectMapper _map;

	public PolymorphicObjectDeserializer(INodeDeserializer innerDeserializer, PolymorphicObjectMapper polymorphicObjectMapper)
	{
		_innerDeserializer = innerDeserializer;
		_map = polymorphicObjectMapper;
	}

	public bool Deserialize(
		IParser reader,
		Type expectedType,
		Func<IParser, Type, object> nestedObjectDeserializer,
		out object value, ObjectDeserializer rootDeserializer)
	{
		// Check if we are deserializing an IConcept type
		if (_map.CanHandleInterface(expectedType))
		{
			var current = reader.Current as MappingStart;
			if (current != null)
			{
				// Read the key (e.g., Concept1, Concept2)
				reader.MoveNext();
				var scalar = reader.Current as Scalar;
				if (scalar != null)
				{
					Type concreteType = _map.GetConcreteType(scalar.Value, expectedType);

					// Move to the object value
					reader.MoveNext();
					value = nestedObjectDeserializer(reader, concreteType);
					reader.MoveNext(); // Move past the end of the mapping node
					return true;
				}
			}
		}

		// Fallback to the default deserializer
		return _innerDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value, rootDeserializer);
	}

	
}

public sealed class PolymorphicObjectConverter : IYamlTypeConverter
{
	//Credit where credit is due:
	//		https://stackoverflow.com/questions/64242023/yamldotnet-custom-serialization 
	//		and https://stackoverflow.com/questions/78211029/convert-list-to-objects-with-custom-names-in-yamldotnet
	//These two Stackoverflow posts got me to this solution.
	
	// Unfortunately the API does not provide those in the ReadYaml and WriteYaml
	// methods, so we are forced to set them after creation.
	public IValueSerializer ValueSerializer { get; set; }
	public IValueDeserializer ValueDeserializer { get; set; }
	
	private PolymorphicObjectMapper _mapper;
	
	public PolymorphicObjectConverter (PolymorphicObjectMapper polymorphicObjectMapper)
	{
		_mapper = polymorphicObjectMapper;
	}

	public bool Accepts(Type type)
	{
		//return typeof(IConcept).IsAssignableFrom(type);
		
		return _mapper.CanHandleType(type);
	}

	public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
	{
		return rootDeserializer(type);
	}

	public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
	{
		emitter.Emit(new MappingStart());
		
		var component = value;
		var otherPropertisMap = type
			.GetProperties()
			.Where(p => p.GetValue(component) != null)
			.ToDictionary(p => p.Name, p => p.GetValue(component));
		ValueSerializer.SerializeValue(emitter, type.Name, typeof(string));
		ValueSerializer.SerializeValue(emitter, otherPropertisMap, typeof(Dictionary<string, object>));
		emitter.Emit(new MappingEnd());		
	}

}

public class PolymorphicObjectMapper
{
	private Dictionary<string, InterfaceMap> interfaces = new Dictionary<string, InterfaceMap>();
	private HashSet<string> handledTypes = new HashSet<string>();
	
	public void Add(Type InterfaceClass, Type ConcreteClass)
	{
		if (handledTypes.Contains(ConcreteClass.Name)) return;
		
		if (!interfaces.ContainsKey(InterfaceClass.Name)) interfaces.Add(InterfaceClass.Name, new InterfaceMap());
		
		handledTypes.Add(ConcreteClass.Name);
		
		interfaces[InterfaceClass.Name].classMappings.Add(ConcreteClass.Name, ConcreteClass);
	}

	public bool CanHandleInterface(Type interfaceType)
	{
		return interfaces.ContainsKey(interfaceType.Name);
	}

	public bool CanHandleType(Type type)
	{
		return handledTypes.Contains(type.Name);
	}

	public Type GetConcreteType(string typeName, Type interfaceType)
	{
		if (interfaces.ContainsKey(interfaceType.Name) && interfaces[interfaceType.Name].classMappings.ContainsKey(typeName))
			return interfaces[interfaceType.Name].classMappings[typeName];
		else throw new Exception("Cannot Find Type for Specificed Type Name and Interface");
	}
}

public class InterfaceMap
{
	public string interfaceName { get; set;}
	public Dictionary<string, Type> classMappings { get; set;}
	
	public InterfaceMap()
	{
		classMappings = new Dictionary<string, Type>();
	}
}

public class Root
{
	public string ConfigName {get; set;}
	public int Version {get; set;}
	public List<string> Tables {get; set;}
	public List<IConcept> Concepts {get; set;}
}

public interface IConcept
{}

public class Concept1 : IConcept
{
	public string Name {get; set;}
	public List<IConcept> InnerConcepts {get; set;}
}

public class Concept2 : IConcept
{
	public string Descriptions { get; set; }
	public int? Value { get; set; }
	public IConcept SingleConcept {get; set;}
}

public class Concept3 : IConcept
{
	public string Descriptions { get; set; }
	public int Value { get; set; }
	public ConceptDetails Details {get; set;}
}

public class ConceptDetails
{
	public string Detail {get; set;}
	public int? Rating {get; set;}
}

So that’s about it. I hope if you wondered upon this page you found it useful. If you have suggestions or figure out a better way to meet this requirement I would appreciate you leaving a comment.

Also…this is not production ready code so please don’t use it without taking the time to put some basic error handling into place…and, as always, use it at your own risk.

I’ve made this code available under an MIT License at the following Gist: https://gist.github.com/nicknow/c5320f8544118f5a5b4baea5ade56324

Categories: C#LINQPadTechnology