3

How would I deserialize YAML to a immutable data structure?

e.g. I have this YAML:

Value: SomeString
Number: 99

And this data structure:

public class MyData
{
    public MyData(string value, int number)
    {
        Value = value;
        Number = number;
    }

    public string Value { get; }
    public int Number { get; }
}

For this I'd to use the constructor. So somehow I'd need to first retrieve a Dictionary<string, object> parsed from the YAML respecting my class (so 99 would be int, not string), then scan my type for an appropriate constructor,

YamlUser
  • 39
  • 2

3 Answers3

1

Although the question doesn't mention it, I'm assuming you are using YamlDotNet (or SharpYaml which is a fork of YamlDotNet)

YamlDotNet doesnt support deserializing into classes that do not have a default constructor - but one option to achieve what you want is to deserialize into an intermediate Builder type that is mutable which can produce the final type. e.g.

public class MyDataBuilder
{
    public string Value { get; set; }
    public int Number { get; set; }
    public MyData Build() => new MyData(Value, Number);
}

And then use something like:

deserializer.Deserialize<MyDataBuilder>(yaml).Build();

You would end up having to create a parallel set of builders for your whole model however, e.g. if MyData had a third parameter of type MyOtherData (I've changed the example to use records instead of classes to make it concise):

public record MyOtherData(string OtherValue);
public record MyData(string Value, int Number, MyOtherData otherData);

In which case we would need another Builder:

public class MyOtherDataBuilder 
{
    public string OtherValue { get; set; }
}

And MyDataBuilder would look like:

public class MyDataBuilder
{
    public string Value { get; set; }
    public int Number { get; set; }
    public MyOtherDataBuilder MyOtherData { get; set; }
    public MyData Build() => new MyData(Value, Number, MyOtherData.Build());
}
Brian Flynn
  • 269
  • 6
  • 13
0

It's an old but surprisingly relevant question. Now, with records in C#, immutable collections in .net, lack of ability to deserialize immutable data is a blocker - there is no way we need to change all our data types just to be able to deserialize. One practical workaround that I found - is to convert yaml to json first, then deal with json your preferred way - System.Text.Json, Newtonsoft, etc.

Here is how to do is easiest way:

static string ConvertToJson(string yaml) {
    object DeserializeYaml() =>
        new DeserializerBuilder()
            .Build()
            .Deserialize(new StringReader(yaml))
        ?? throw new InvalidOperationException("Cannot deserialize yaml string:" + Environment.NewLine + yaml);

    string SerializeYamlObjectToJson(object yamlObject) =>
        new SerializerBuilder()
            .JsonCompatible()
            .Build()
            .Serialize(yamlObject);

    return SerializeYamlObjectToJson(DeserializeYaml());
}

The only disadvantage, potentially big, is performance. I feel, however, that it's rarely an important requirement for yaml.

Alex Butenko
  • 3,664
  • 3
  • 35
  • 54
-1

use the FormatterServices.GetUninitializedObject API (this will NOT invoke any constructors at all) and then use reflection to set fields.

Code example:

 var instance = FormatterServices.GetUninitializedObject(typeof(MyData));
 var flags = BindingFlags.NonPublic | BindingFlags.Instance;
 var type = typeof(MyData);
 var stringField = type.GetField("_value", flags);

 stringField.SetValue(instance, "SomeString");

 var numberField = type.GetField("_number", flags);
 numberField.SetValue(instance, 99);

 MyData data = (MyData)instance;
George Lica
  • 1,798
  • 1
  • 12
  • 23
  • I'm sorry, I would like to use the constructor for this. Also your answer would not help me in how I'd get the appropriate values from the YAML. I adjusted my question accordingly. – YamlUser Aug 08 '15 at 05:54
  • I thought that this is a general question about how to deserialize data to immutable types. Your question now has no meaning. Just call the constructor :) what is your problem? Iterate the dictionary and for each item, create a new MyData. – George Lica Aug 08 '15 at 06:06
  • the whole ideea in deserialization is to NOT call the constructor that might manipulate incomming data before setting the private fields. Generally you need to bypass constructors. – George Lica Aug 08 '15 at 06:27
  • I suspect the OP is using a library like YamlDotNet - which doesn't support deserializing into classes with no default constructor. – Brian Flynn Jun 09 '21 at 07:49