Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/SharpYaml.SourceGenerator/YamlSerializerContextGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3316,6 +3316,19 @@ private static void EmitReadValue(
builder.Append(" return (").Append(typeName).Append(")ReadValue").Append(derivedIndex).AppendLine("(bufferedReader)!;");
builder.AppendLine(" }");
}

// When tag is present but unrecognized, try default type before failing
if (polymorphism.DefaultDerivedType is not null && indexByType.TryGetValue(polymorphism.DefaultDerivedType, out var defaultIndexForUnknownTag))
{
builder.Append(" return (").Append(typeName).Append(")ReadValue").Append(defaultIndexForUnknownTag).AppendLine("(bufferedReader)!;");
}
else
{
builder.AppendLine(" if (unknownDerivedTypeHandling == global::SharpYaml.YamlUnknownDerivedTypeHandling.Fail)");
builder.AppendLine(" {");
builder.Append(" throw global::SharpYaml.Serialization.YamlThrowHelper.ThrowUnknownTypeTag(bufferedReader, rootTag, typeof(").Append(typeName).AppendLine("));");
builder.AppendLine(" }");
}
builder.AppendLine(" }");

// Fallback: use default derived type if available
Expand Down
275 changes: 275 additions & 0 deletions src/SharpYaml.Tests/Serialization/YamlUnknownTagHandlingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
#nullable enable

using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SharpYaml.Serialization;

namespace SharpYaml.Tests.Serialization;

[TestClass]
public class YamlUnknownTagHandlingTests
{
// ---- Model types ----

[YamlPolymorphic(DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag)]
[YamlDerivedType(typeof(AttributeDog), Tag = "!dog")]
[YamlDerivedType(typeof(AttributeCat), Tag = "!cat")]
private class AttributeAnimal
{
public string Name { get; set; } = string.Empty;
}

private sealed class AttributeDog : AttributeAnimal
{
public int BarkVolume { get; set; }
}

private sealed class AttributeCat : AttributeAnimal
{
public bool Indoor { get; set; }
}

[YamlPolymorphic(DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag, UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.FallBackToBase)]
[YamlDerivedType(typeof(FallbackDog), Tag = "!dog")]
private class FallbackAnimal
{
public string Name { get; set; } = string.Empty;
}

private sealed class FallbackDog : FallbackAnimal
{
public int BarkVolume { get; set; }
}

private abstract class RuntimeAnimal
{
public string Name { get; set; } = string.Empty;
}

private sealed class RuntimeDog : RuntimeAnimal
{
public int BarkVolume { get; set; }
}

private sealed class RuntimeCat : RuntimeAnimal
{
public bool Indoor { get; set; }
}

// ---- Attribute-based: unknown tag with default (Fail) handling ----

[TestMethod]
public void UnknownTagFailsByDefaultWithAttributes()
{
var yaml = "!lizard\nName: Gecko\n";
var ex = Assert.Throws<YamlException>(
() => YamlSerializer.Deserialize<AttributeAnimal>(yaml));
StringAssert.Contains(ex.Message, "!lizard");
}

[TestMethod]
public void KnownTagWorksWithAttributes()
{
var yaml = "!dog\nName: Rex\nBarkVolume: 5\n";
var value = YamlSerializer.Deserialize<AttributeAnimal>(yaml);

Assert.IsNotNull(value);
Assert.IsInstanceOfType<AttributeDog>(value);
Assert.AreEqual("Rex", value.Name);
Assert.AreEqual(5, ((AttributeDog)value).BarkVolume);
}

[TestMethod]
public void NoTagDeserializesToBaseTypeWithAttributes()
{
var yaml = "Name: Plain\n";
var value = YamlSerializer.Deserialize<AttributeAnimal>(yaml);

Assert.IsNotNull(value);
Assert.IsInstanceOfType<AttributeAnimal>(value);
Assert.AreEqual("Plain", value.Name);
}

// ---- Attribute-based: FallBackToBase ----

[TestMethod]
public void UnknownTagFallsBackToBaseWhenConfigured()
{
var yaml = "!lizard\nName: Gecko\n";
var value = YamlSerializer.Deserialize<FallbackAnimal>(yaml);

Assert.IsNotNull(value);
Assert.IsInstanceOfType<FallbackAnimal>(value);
Assert.AreEqual("Gecko", value.Name);
}

// ---- Options-level: Fail ----

[TestMethod]
public void UnknownTagFailsWithOptionsLevelFail()
{
var options = new YamlSerializerOptions
{
PolymorphismOptions = new YamlPolymorphismOptions
{
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.Fail,
DerivedTypeMappings =
{
[typeof(RuntimeAnimal)] = new List<YamlDerivedType>
{
new YamlDerivedType(typeof(RuntimeDog), "dog") { Tag = "!dog" },
new YamlDerivedType(typeof(RuntimeCat), "cat") { Tag = "!cat" },
}
}
}
};

var yaml = "!parrot\nName: Polly\n";
var ex = Assert.Throws<YamlException>(
() => YamlSerializer.Deserialize<RuntimeAnimal>(yaml, options));
StringAssert.Contains(ex.Message, "!parrot");
}

[TestMethod]
public void KnownTagWorksWithRuntime()
{
var options = new YamlSerializerOptions
{
PolymorphismOptions = new YamlPolymorphismOptions
{
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
DerivedTypeMappings =
{
[typeof(RuntimeAnimal)] = new List<YamlDerivedType>
{
new YamlDerivedType(typeof(RuntimeDog), "dog") { Tag = "!dog" },
}
}
}
};

var yaml = "!dog\nName: Rex\nBarkVolume: 3\n";
var value = YamlSerializer.Deserialize<RuntimeAnimal>(yaml, options);

Assert.IsNotNull(value);
Assert.IsInstanceOfType<RuntimeDog>(value);
Assert.AreEqual("Rex", value.Name);
Assert.AreEqual(3, ((RuntimeDog)value).BarkVolume);
}

// ---- Options-level: FallBackToBase ----

private class ConcreteAnimal
{
public string Name { get; set; } = string.Empty;
}

private sealed class ConcreteDog : ConcreteAnimal
{
public int BarkVolume { get; set; }
}

[TestMethod]
public void UnknownTagFallsBackToBaseWithOptions()
{
var options = new YamlSerializerOptions
{
PolymorphismOptions = new YamlPolymorphismOptions
{
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.FallBackToBase,
DerivedTypeMappings =
{
[typeof(ConcreteAnimal)] = new List<YamlDerivedType>
{
new YamlDerivedType(typeof(ConcreteDog), "dog") { Tag = "!dog" },
}
}
}
};

var yaml = "!parrot\nName: Polly\n";
var value = YamlSerializer.Deserialize<ConcreteAnimal>(yaml, options);

Assert.IsNotNull(value);
Assert.IsInstanceOfType<ConcreteAnimal>(value);
Assert.AreEqual("Polly", value.Name);
}

// ---- Attribute-level override takes precedence over options ----

[TestMethod]
public void AttributeUnknownHandlingOverridesOptions()
{
// FallbackAnimal has FallBackToBase in attribute; options say Fail — attribute wins
var options = new YamlSerializerOptions
{
PolymorphismOptions = new YamlPolymorphismOptions
{
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.Fail,
}
};

var yaml = "!lizard\nName: Gecko\n";
var value = YamlSerializer.Deserialize<FallbackAnimal>(yaml, options);

Assert.IsNotNull(value);
Assert.IsInstanceOfType<FallbackAnimal>(value);
Assert.AreEqual("Gecko", value.Name);
}

// ---- Dictionary of polymorphic values with unknown tags ----

[TestMethod]
public void UnknownTagInDictionaryValueFails()
{
var options = new YamlSerializerOptions
{
PolymorphismOptions = new YamlPolymorphismOptions
{
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.Fail,
DerivedTypeMappings =
{
[typeof(RuntimeAnimal)] = new List<YamlDerivedType>
{
new YamlDerivedType(typeof(RuntimeDog), "dog") { Tag = "!dog" },
}
}
}
};

var yaml = "rex: !dog\n Name: Rex\n BarkVolume: 3\npolly: !parrot\n Name: Polly\n";
Assert.Throws<YamlException>(
() => YamlSerializer.Deserialize<Dictionary<string, RuntimeAnimal>>(yaml, options));
}

// ---- Tag-only entries (no discriminator) with unknown tags ----

[TestMethod]
public void UnknownTagFailsWithTagOnlyEntries()
{
var options = new YamlSerializerOptions
{
PolymorphismOptions = new YamlPolymorphismOptions
{
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.Fail,
DerivedTypeMappings =
{
[typeof(RuntimeAnimal)] = new List<YamlDerivedType>
{
new YamlDerivedType(typeof(RuntimeDog)) { Tag = "!dog" },
new YamlDerivedType(typeof(RuntimeCat)) { Tag = "!cat" },
}
}
}
};

var yaml = "!fish\nName: Nemo\n";
var ex = Assert.Throws<YamlException>(
() => YamlSerializer.Deserialize<RuntimeAnimal>(yaml, options));
StringAssert.Contains(ex.Message, "!fish");
}
}
8 changes: 8 additions & 0 deletions src/SharpYaml/Serialization/Converters/YamlObjectConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,14 @@ private void WriteObjectCore(YamlWriter writer, object value, Contract contract)
{
targetType = derivedFromTag;
}
else if (polymorphism.DefaultDerivedType is not null)
{
targetType = polymorphism.DefaultDerivedType;
}
else if (polymorphism.UnknownDerivedTypeHandling == YamlUnknownDerivedTypeHandling.Fail)
{
throw YamlThrowHelper.ThrowUnknownTypeTag(reader, rootTag, typeof(T));
}
}

targetType ??= polymorphism.DefaultDerivedType ?? typeof(T);
Expand Down
4 changes: 4 additions & 0 deletions src/SharpYaml/Serialization/YamlThrowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public static YamlException ThrowDuplicateMappingKey(YamlReader reader, string k
public static YamlException ThrowUnknownTypeDiscriminator(YamlReader reader, string? discriminatorValue, Type baseType)
=> new(reader.SourceName, reader.Start, reader.End, $"Unknown type discriminator '{discriminatorValue}' for '{baseType}'.");

/// <summary>Throws an exception for unknown Type Tag.</summary>
public static YamlException ThrowUnknownTypeTag(YamlReader reader, string? tag, Type baseType)
=> new(reader.SourceName, reader.Start, reader.End, $"Unknown type tag '{tag}' for '{baseType}'.");

/// <summary>Throws an exception for abstract Type Without Discriminator.</summary>
public static YamlException ThrowAbstractTypeWithoutDiscriminator(YamlReader reader, Type type)
=> new(reader.SourceName, reader.Start, reader.End, $"Cannot deserialize abstract type '{type}' without a known derived type discriminator.");
Expand Down