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

Anchor not found when referenced in double nested structure #399

Open
MortenChristiansen opened this issue May 5, 2019 · 6 comments
Open
Labels

Comments

@MortenChristiansen
Copy link

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>();
}
@aaubry
Copy link
Owner

aaubry commented May 5, 2019

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.

@MortenChristiansen
Copy link
Author

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

@aaubry
Copy link
Owner

aaubry commented May 5, 2019

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

@aaubry
Copy link
Owner

aaubry commented May 16, 2019

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; }
}

@MortenChristiansen
Copy link
Author

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; }
}

@aaubry
Copy link
Owner

aaubry commented Jun 10, 2019

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.

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

No branches or pull requests

2 participants