YamlDotNet icon indicating copy to clipboard operation
YamlDotNet copied to clipboard

Anchor not found when referenced in double nested structure

Open MortenChristiansen opened this issue 6 years ago • 6 comments
trafficstars

There seems to be a problem with the way anchors are resolved. I get the following exception when running the test shown below: YamlDotNet.Core.AnchorNotFoundException: '(Line: 18, Col: 14, Idx: 264) - (Line: 18, Col: 21, Idx: 271): Anchor 'goblin' not found'. If I remove the last line of the yaml definition, it does not throw any exception, which means that the two outermost anchors are resolved correctly. It seems that for some reason, the third level of nesting breaks the ability for the deserializer to resolve the anchor.

[Fact]
public void Test()
{
    var yml = @"
monsters:
- monster: &goblin
  name: Goblin
  attackType: Piercing

monster: *goblin

name: Forest
levels:
- level:
  name: Clearing
  monster: *goblin
  locations:
  - location:
    name: Location 1
    description: A nice location
    monster: *goblin";

    var deserializer = new DeserializerBuilder()
                .IgnoreUnmatchedProperties()
                .WithNamingConvention(new CamelCaseNamingConvention())
                .Build();

    var areaRecord = deserializer.Deserialize<AreaRecord>(yml);
    Assert.NotNull(areaRecord );
}

public class AreaRecord
{
    public string Name { get; set; }
    public List<LevelRecord> Levels { get; set; } = new List<LevelRecord>();
}

public class LevelRecord
{
    public string Name { get; set; }
    public List<dynamic> Locations { get; set; } = new List<dynamic>();
}

MortenChristiansen avatar May 05 '19 11:05 MortenChristiansen

Looking at your code, I would expect it to work. I'll take a look. Have you tried adding a new line at the end of the yml string ? Maybe there's an issue when parsing in that case.

aaubry avatar May 05 '19 14:05 aaubry

I've tried both adding an empty line as well as a new property at the bottom, but neither had any effect.

MortenChristiansen avatar May 05 '19 15:05 MortenChristiansen

Ok, thanks for the feedback. I'll look into this issue as soon as possible.

aaubry avatar May 05 '19 15:05 aaubry

I've checked this again, and the error happens because the monsters key is being ignored, since it is not present in the AreaRecord type. Because of that, the &goblin object is never parsed and thus not added to the lookup table that is used to resolve aliases. What you are doing would work if the anchor resolution was done on the parser level, but this would have the consequence that all references to *goblin would be parsed independently, so they would be different instances. So, to fix this you will need to add a Monsters property of the appropriate type to AreaRecord. Something like this:

public class AreaRecord
{
    public string Name { get; set; }
    public List<LevelRecord> Levels { get; set; } = new List<LevelRecord>();
    public List<dynamic> Monsters { get: set; }
}

aaubry avatar May 16 '19 08:05 aaubry

This does indeed solve my anchor problem, but not in the way that I had hoped. I expected that area record would contain the full goblin instance in the monsters list. Rather, the dynamic monster object in the list contains two properties:

"monster": null "type": "Monster"

I've added an updated test below, but it fails at the indicated line. I don't know if I have incorrect assumptions about how it is supposed to work, but I can't even see an indication that the monster matches the goblin anchor.

[Fact]
public void Test()
{
    var yml = @"


name: Forest
monsters:
- monster: &goblin
  name: Goblin
  attackType: Piercing
levels:
- level:
  name: Clearing
  monster: *goblin
  locations:
  - location:
    name: Location 1
    description: A nice location
    monster: *goblin";

    var deserializer = new DeserializerBuilder()
                .IgnoreUnmatchedProperties()
                .WithNamingConvention(new CamelCaseNamingConvention())
                .Build();

    var areaRecord = deserializer.Deserialize<AreaRecord>(yml);
    Assert.NotNull(areaRecord);
    Assert.NotNull(areaRecord.Levels[0].Locations[0]["monster"]); // Fails here
    Assert.Equal("Goblin", areaRecord.Levels[0].Locations[0]["monster"]["name"]);
}

public class AreaRecord
{
    public string Name { get; set; }
    public List<LevelRecord> Levels { get; set; } = new List<LevelRecord>();
    public List<dynamic> Monsters { get; set; } = new List<dynamic>();
}

public class LevelRecord
{
    public string Name { get; set; }
    public List<dynamic> Locations { get; set; } = new List<dynamic>();
}

If I use a concrete type LocationRecord instead of the dynamic one, the Monster property is just null.

public class LocationRecord
{
    public string Name { get; set; }
    public string Description { get; set; }
    public dynamic Monster { get; set; }
}

MortenChristiansen avatar May 17 '19 14:05 MortenChristiansen

That's because you are using dynamic for the Monsters list. If you want a concrete type, you should use that type in the list. When you use an alias, the value is not deserialized again. Instead, the same instance is used.

aaubry avatar Jun 10 '19 15:06 aaubry