YamlDotNet
YamlDotNet copied to clipboard
Anchor not found when referenced in double nested structure
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>();
}
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.
I've tried both adding an empty line as well as a new property at the bottom, but neither had any effect.
Ok, thanks for the feedback. I'll look into this issue as soon as possible.
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; }
}
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; }
}
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.