Search code examples
c#.netyamlyamldotnet

YamlDotNet - Custom Serialization


I have a .NET class which represents a RPC method call, like this:

class MethodCall
{
    public string MethodName { get; set; }
    public Collection<object> Arguments { get; set; }
}

I want to serialize a Collection<MethodCall> to YAML. I'm using YamlDotNet to achieve this.

By default, YamlDotNet will serialize these objects like this:

methodName: someName
arguments:
- arg1
- arg2
- ...

I would like to simplify the resulting YAML to:

someName:
- arg1
- arg2

Is there any easy way to achieve this? Please note that the arguments can be complex objects (i.e. not simple scalars).


Solution

  • You can achieve this by registering an implementation of IYamlTypeConverter that performs the conversion that you need.

    Here's a possible implementation:

    public sealed class MethodCallConverter : IYamlTypeConverter
    {
        // 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) => type == typeof(MethodCall);
    
        public object ReadYaml(IParser parser, Type type)
        {
            parser.Consume<MappingStart>();
    
            var call = new MethodCall
            {
                MethodName = (string)ValueDeserializer.DeserializeValue(parser, typeof(string), new SerializerState(), ValueDeserializer),
                Arguments = (Collection<object>)ValueDeserializer.DeserializeValue(parser, typeof(Collection<object>), new SerializerState(), ValueDeserializer),
            };
    
            parser.Consume<MappingEnd>();
            
            return call;
        }
        
        public void WriteYaml(IEmitter emitter, object value, Type type)
        {
            emitter.Emit(new MappingStart());
    
            var call = (MethodCall)value;
            ValueSerializer.SerializeValue(emitter, call.MethodName, typeof(string));
            ValueSerializer.SerializeValue(emitter, call.Arguments, typeof(Collection<object>));
    
            emitter.Emit(new MappingEnd());
        }
    }
    

    The converter needs to be registered into the SerializerBuilder and DeserializerBuilder through the WithTypeConverter method. Note that YamlDotNet does not provide us with a way to call the (de)serializer recursively, so we have to set some public properties as a workaround. This is not as clean as it could be, but still works:

    string SerializeMethodCall(MethodCall call)
    {
        var methodCallConverter = new MethodCallConverter();
        var serializerBuilder = new SerializerBuilder()
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
            .WithTypeConverter(methodCallConverter);
    
        methodCallConverter.ValueSerializer = serializerBuilder.BuildValueSerializer();
    
        var serializer = serializerBuilder.Build();
    
        var yaml = serializer.Serialize(call);
        return yaml;
    }
    
    MethodCall DeserializeMethodCall(string yaml)
    {
        var methodCallConverter = new MethodCallConverter();
        var deserializerBuilder = new DeserializerBuilder()
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
            .WithTypeConverter(methodCallConverter);
    
        methodCallConverter.ValueDeserializer = deserializerBuilder.BuildValueDeserializer();
    
        var deserializer = deserializerBuilder.Build();
        var call = deserializer.Deserialize<MethodCall>(yaml);
        return call;
    }