Skip to content

Instantly share code, notes, and snippets.

@nicknow
Created December 28, 2024 17:22
Show Gist options
  • Save nicknow/c5320f8544118f5a5b4baea5ade56324 to your computer and use it in GitHub Desktop.
Save nicknow/c5320f8544118f5a5b4baea5ade56324 to your computer and use it in GitHub Desktop.
A model for doing YAML serialization/deserialization using YamlDotNet where the object graph list arrays that use an interface, i.e., List<IInterface>, that need to be converted to/from concrete classes that implement the interface.
/*
MIT License
Copyright (c) 2024 Nicolas A. Nowinski ([email protected])
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
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;}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment