Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Zinvoke opened this issue Aug 28, 2023 · 4 comments

Comments

@Zinvoke
Copy link

Zinvoke commented Aug 28, 2023

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
Copy link
Collaborator

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
Copy link
Author

Zinvoke commented Aug 30, 2023

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?

@Zinvoke Zinvoke closed this as completed Aug 30, 2023
@Zinvoke Zinvoke reopened this Aug 30, 2023
@EdwardCooke
Copy link
Collaborator

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
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants