aaubry / YamlDotNet

YamlDotNet is a .NET library for YAML
MIT License
2.48k stars 466 forks source link

Polymorphic deserialization - Alias $example_word cannot precede anchor declaration #842

Closed Zinvoke closed 10 months ago

Zinvoke commented 10 months ago

Ok so here is the situation i'm attempting to deserialize a list of interfaces using:

            var deserializer = new DeserializerBuilder()
                               .WithNamingConvention(UnderscoredNamingConvention.Instance)
                               .WithTypeDiscriminatingNodeDeserializer(options =>
                               {
                                   options.AddKeyValueTypeDiscriminator<IExample>("type",
                                       ("Word", typeof(WordExample)),
                                       ("Number", typeof(NumberExample)));
                               })
                               .IgnoreUnmatchedProperties()
                               .Build();

            //var deserializedConfig = deserializer.Deserialize<ExampleConfig>(yaml);

into:

    public sealed class ExampleConfig
    {
        public List<IExample> Examples { get; set; }
    }

    public interface IExample
    {
        public string Type { get; set; }
    }

    public sealed class WordExample : IExample
    {
        public string Type { get; set; } = "Word";

        public string Value { get; set; } = "Word Example";
    }

    public sealed class NumberExample : IExample
    {
        public string Type { get; set; } = "Number";

        public int Value { get; set; } = 1337;
    }

and everything works fine normally but when I do something like:

example_word: &example_word 'AN EXAMPLE WORD'

examples:
  - type: Number
    value: 1337
  - type: Word
    value: *example_word

using a alias/anchor I keep getting the exception: YamlDotNet.Core.AnchorNotFoundException : Alias $example_word cannot precede anchor declaration

am I doing something wrong or does the library not support this?

EdwardCooke commented 10 months ago

Anchors are supported by the library. The type discriminators are new and may be the culprit. I’ll try and take a look in a bit. Can you try doing it without the type discriminator and see if you get the same error?

Zinvoke commented 10 months ago

Anchors are supported by the library. The type discriminators are new and may be the culprit. I’ll try and take a look in a bit. Can you try doing it without the type discriminator and see if you get the same error?

What do you mean without the type discriminator do you mean without the extension method here:

.WithTypeDiscriminatingNodeDeserializer(options =>
                               {
                                   options.AddKeyValueTypeDiscriminator<IExample>("type",
                                       ("Word", typeof(WordExample)),
                                       ("Number", typeof(NumberExample)));
                               })

because if so no I don't get the same exception: **(Line: 5, Col: 5, Idx: 66) - (Line: 5, Col: 5, Idx: 66): Exception during deserialization

System.InvalidOperationException: Failed to create an instance of type 'Experiments.Tests.Models.IExample'. ---> System.MissingMethodException: Cannot dynamically create an instance of type 'Experiments.Tests.Models.IExample'. Reason: Cannot create an instance of an interface. at System.RuntimeType.ActivatorCache..ctor(RuntimeType rt) at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions) at YamlDotNet.Serialization.ObjectFactories.DefaultObjectFactory.Create(Type type) --- End of inner exception stack trace --- at YamlDotNet.Serialization.ObjectFactories.DefaultObjectFactory.Create(Type type) at YamlDotNet.Serialization.NodeDeserializers.ObjectNodeDeserializer.Deserialize(IParser parser, Type expectedType, Func`3 nestedObjectDeserializer, Object& value) at YamlDotNet.Serialization.ValueDeserializers.NodeValueDeserializer.DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer)**

Which I'm assuming that means it is the type discriminator?

EdwardCooke commented 10 months ago

What I was meaning was this, which results in the same exception, skipping the TypeDescriminator

var yaml = @"example_word: &example_word 'AN EXAMPLE WORD'

examples:
  - type: Number
    value: 1337
  - type: Word
    value: *example_word
";
var deserializer = new DeserializerBuilder()
    .WithNamingConvention(UnderscoredNamingConvention.Instance)
    .IgnoreUnmatchedProperties()
    .Build();
var o = deserializer.Deserialize<ExampleConfig>(yaml);

Console.WriteLine(o);

public class ExampleConfig
{
    public List<Example> Examples { get; set; }
}

public class Example
{
    public string Type { get; set; }
    public string Value { get; set; }
}

When I change ExampleConfig to include public string ExampleWord { get; set; } the above code works as well as your code. With that, I believe anchors/aliases only work when the anchor is assigned to an object.

I'm going to look into why it needs to be assigned to something and see if I can resolve that issue. I'll keep you updated.

EdwardCooke commented 10 months ago

Ok, I think I figured out why it needs to be a property in a class, it needs to know the type of object that it needs to deserialize to.

You will need to have ExampleWord as a property in whatever class your anchor is in and ignoring that field just isn't going to work, not without a major refactoring of the deserializers. At least, that's what I was able to tell from my testing and looking at the code.

If you don't want that property public, you can mark it as private and use the IncludeNonPublicProperties() method on the builder. Like this

var deserializer = new DeserializerBuilder()
                               .WithNamingConvention(UnderscoredNamingConvention.Instance)
                               .WithTypeDiscriminatingNodeDeserializer(options =>
                               {
                                   options.AddKeyValueTypeDiscriminator<IExample>("type",
                                       new Dictionary<string, Type> {
                                           { "Word", typeof(WordExample) },
                                           { "Number", typeof(NumberExample) }
                                       });
                               })
                               .IncludeNonPublicProperties()
                               .Build();
var o = deserializer.Deserialize<ExampleConfig>(yaml);

Console.WriteLine(o);

public sealed class ExampleConfig
{
#pragma warning disable IDE0051 // Remove unused private members
    private string? ExampleWord { get; set; }
#pragma warning restore IDE0051 // Remove unused private members

    public List<IExample> Examples { get; set; } = new List<IExample>();
}

I'm going to close this issue since I've given you the answer to the problem.