aaubry / YamlDotNet

YamlDotNet is a .NET library for YAML
MIT License
2.58k stars 484 forks source link

Using default deserialization in a custom type converter #1005

Closed mikhail-barg closed 2 weeks ago

mikhail-barg commented 2 weeks ago

I am working on a flexible yaml deserialization where I'd like to have "shortcuts" for some classes.

For example for a class like this

public sealed class HttpSource
{
  [Required] public string url { get; set; };
  public Auth? auth { get; set; }
}

public sealed class Sample 
{
  public HttpSource http_source { get;set; }
}

I'd like to be able to not only write a regular detailed yaml:

http_source: 
  url: "http://sample.com"
  auth: ..

but also a simplified one (when only a url matters):

http_source: "http://sample.com"

This looks like a nice feature in my case, because most of these yamls are going to be written manually by people and I'd like to save them from whiting boilerplate.

So I've come to the following solution:

  1. I'm adding an IMaybeSimpleValue interface and a SimpleValueAttribute attribute (just a marker intreface and attributes):
    
    public interface IMaybeSimpleValue{ }

[AttributeUsage(AttributeTargets.Property] public sealed class SimpleValueAttribute: Attribute {}


2. I mark my class with the interface and the "main" property of the class with an attribute:
```c#
public sealed class HttpSource: IMaybeSimpleValue
{
  [Required, SimpleValue] public string url { get; set; };
  public Auth? auth { get; set; }
}
  1. I implement a custom type converter:

    public sealed class MaybeSimpleValueConverter : IYamlTypeConverter
    {
    private readonly IObjectFactory m_objectFactory;
    
    internal MaybeSimpleValueConverter(IObjectFactory objectFactory)
    {
    this.m_objectFactory = objectFactory;
    }
    
    bool IYamlTypeConverter.Accepts(Type type)
    {
    return typeof(IMaybeSimpleValue).IsAssignableFrom(type);
    }
    
    object? IYamlTypeConverter.ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
    {
    if (parser.Current is YamlDotNet.Core.Events.Scalar)
    {
      object value = this.m_objectFactory.Create(type);
      List<PropertyInfo> valueProperties = type.GetProperties().Where(pi => pi.IsDefined(typeof(SimpleValueAttribute))).ToList();
      PropertyInfo pi = valueProperties[0];  //lots of validation skipped
      Scalar scalar = parser.Consume<Scalar>();
      pi.SetValue(value, scalar.Value);
      return value;
    }
    else
    {
      ???? default deserialization goes here!! //return rootDeserializer.Invoke(type);
    }
    }
    
    void IYamlTypeConverter.WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
    {
    throw new NotSupportedException();
    }
    }

So the general idea is that all the classes implementing IMaybeSimpleValue intreface are handled by the type converter. The converter checks if yaml contains a scalar for the object, and if it does then it creates an instance of the object and initializes it's sole required property. (And this does work).

But if the yaml contains a proper object description, it should fall back to the default deserialization routine. And this is where it does not work because I seem to have no way to call a "default" deserialization for a class, without my custom converter. (If I call rootDeserializer.Invoke(type) it just goes stack overflow because it goes to my type converter again).

So, is there a way to have a "default" deserialization working for my type, without repeating code from ObjectNodeDeserializer

Or is there a simpler way to achieve what I'm aiming for?

mikhail-barg commented 2 weeks ago

Okay, I managed to find a way to do this, by using not a type converter, but implementing a custom NodeSerializer and replacing default ScalarNodeDeserializer (where: syntax => syntax.InsteadOf<ScalarNodeDeserializer>()).

Will close the issue then

mikhail-barg commented 2 weeks ago

And another, even simpler solution would be just to implement a public static ClassName Parse(string v) method in each class requiring such behavior