Skip to content

Commit c96d42d

Browse files
authored
Apply UnknownDerivedTypeHandling.Fail to tag-based polymorphism (#133)
Previously, UnknownDerivedTypeHandling.Fail only applied to property-based discriminators. Unknown tags in tag-based polymorphism were silently ignored, falling through to the base type. This made it impossible to detect configuration errors when using tag-based type discrimination. This change applies the same Fail/FallBackToBase logic to the tag lookup path in both the reflection-based converter and the source generator. A new ThrowUnknownTypeTag helper is added to YamlThrowHelper for consistent error messages that include the unrecognized tag value.
1 parent 07592b2 commit c96d42d

4 files changed

Lines changed: 300 additions & 0 deletions

File tree

src/SharpYaml.SourceGenerator/YamlSerializerContextGenerator.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3316,6 +3316,19 @@ private static void EmitReadValue(
33163316
builder.Append(" return (").Append(typeName).Append(")ReadValue").Append(derivedIndex).AppendLine("(bufferedReader)!;");
33173317
builder.AppendLine(" }");
33183318
}
3319+
3320+
// When tag is present but unrecognized, try default type before failing
3321+
if (polymorphism.DefaultDerivedType is not null && indexByType.TryGetValue(polymorphism.DefaultDerivedType, out var defaultIndexForUnknownTag))
3322+
{
3323+
builder.Append(" return (").Append(typeName).Append(")ReadValue").Append(defaultIndexForUnknownTag).AppendLine("(bufferedReader)!;");
3324+
}
3325+
else
3326+
{
3327+
builder.AppendLine(" if (unknownDerivedTypeHandling == global::SharpYaml.YamlUnknownDerivedTypeHandling.Fail)");
3328+
builder.AppendLine(" {");
3329+
builder.Append(" throw global::SharpYaml.Serialization.YamlThrowHelper.ThrowUnknownTypeTag(bufferedReader, rootTag, typeof(").Append(typeName).AppendLine("));");
3330+
builder.AppendLine(" }");
3331+
}
33193332
builder.AppendLine(" }");
33203333

33213334
// Fallback: use default derived type if available
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
#nullable enable
2+
3+
using System.Collections.Generic;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using SharpYaml.Serialization;
6+
7+
namespace SharpYaml.Tests.Serialization;
8+
9+
[TestClass]
10+
public class YamlUnknownTagHandlingTests
11+
{
12+
// ---- Model types ----
13+
14+
[YamlPolymorphic(DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag)]
15+
[YamlDerivedType(typeof(AttributeDog), Tag = "!dog")]
16+
[YamlDerivedType(typeof(AttributeCat), Tag = "!cat")]
17+
private class AttributeAnimal
18+
{
19+
public string Name { get; set; } = string.Empty;
20+
}
21+
22+
private sealed class AttributeDog : AttributeAnimal
23+
{
24+
public int BarkVolume { get; set; }
25+
}
26+
27+
private sealed class AttributeCat : AttributeAnimal
28+
{
29+
public bool Indoor { get; set; }
30+
}
31+
32+
[YamlPolymorphic(DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag, UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.FallBackToBase)]
33+
[YamlDerivedType(typeof(FallbackDog), Tag = "!dog")]
34+
private class FallbackAnimal
35+
{
36+
public string Name { get; set; } = string.Empty;
37+
}
38+
39+
private sealed class FallbackDog : FallbackAnimal
40+
{
41+
public int BarkVolume { get; set; }
42+
}
43+
44+
private abstract class RuntimeAnimal
45+
{
46+
public string Name { get; set; } = string.Empty;
47+
}
48+
49+
private sealed class RuntimeDog : RuntimeAnimal
50+
{
51+
public int BarkVolume { get; set; }
52+
}
53+
54+
private sealed class RuntimeCat : RuntimeAnimal
55+
{
56+
public bool Indoor { get; set; }
57+
}
58+
59+
// ---- Attribute-based: unknown tag with default (Fail) handling ----
60+
61+
[TestMethod]
62+
public void UnknownTagFailsByDefaultWithAttributes()
63+
{
64+
var yaml = "!lizard\nName: Gecko\n";
65+
var ex = Assert.Throws<YamlException>(
66+
() => YamlSerializer.Deserialize<AttributeAnimal>(yaml));
67+
StringAssert.Contains(ex.Message, "!lizard");
68+
}
69+
70+
[TestMethod]
71+
public void KnownTagWorksWithAttributes()
72+
{
73+
var yaml = "!dog\nName: Rex\nBarkVolume: 5\n";
74+
var value = YamlSerializer.Deserialize<AttributeAnimal>(yaml);
75+
76+
Assert.IsNotNull(value);
77+
Assert.IsInstanceOfType<AttributeDog>(value);
78+
Assert.AreEqual("Rex", value.Name);
79+
Assert.AreEqual(5, ((AttributeDog)value).BarkVolume);
80+
}
81+
82+
[TestMethod]
83+
public void NoTagDeserializesToBaseTypeWithAttributes()
84+
{
85+
var yaml = "Name: Plain\n";
86+
var value = YamlSerializer.Deserialize<AttributeAnimal>(yaml);
87+
88+
Assert.IsNotNull(value);
89+
Assert.IsInstanceOfType<AttributeAnimal>(value);
90+
Assert.AreEqual("Plain", value.Name);
91+
}
92+
93+
// ---- Attribute-based: FallBackToBase ----
94+
95+
[TestMethod]
96+
public void UnknownTagFallsBackToBaseWhenConfigured()
97+
{
98+
var yaml = "!lizard\nName: Gecko\n";
99+
var value = YamlSerializer.Deserialize<FallbackAnimal>(yaml);
100+
101+
Assert.IsNotNull(value);
102+
Assert.IsInstanceOfType<FallbackAnimal>(value);
103+
Assert.AreEqual("Gecko", value.Name);
104+
}
105+
106+
// ---- Options-level: Fail ----
107+
108+
[TestMethod]
109+
public void UnknownTagFailsWithOptionsLevelFail()
110+
{
111+
var options = new YamlSerializerOptions
112+
{
113+
PolymorphismOptions = new YamlPolymorphismOptions
114+
{
115+
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
116+
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.Fail,
117+
DerivedTypeMappings =
118+
{
119+
[typeof(RuntimeAnimal)] = new List<YamlDerivedType>
120+
{
121+
new YamlDerivedType(typeof(RuntimeDog), "dog") { Tag = "!dog" },
122+
new YamlDerivedType(typeof(RuntimeCat), "cat") { Tag = "!cat" },
123+
}
124+
}
125+
}
126+
};
127+
128+
var yaml = "!parrot\nName: Polly\n";
129+
var ex = Assert.Throws<YamlException>(
130+
() => YamlSerializer.Deserialize<RuntimeAnimal>(yaml, options));
131+
StringAssert.Contains(ex.Message, "!parrot");
132+
}
133+
134+
[TestMethod]
135+
public void KnownTagWorksWithRuntime()
136+
{
137+
var options = new YamlSerializerOptions
138+
{
139+
PolymorphismOptions = new YamlPolymorphismOptions
140+
{
141+
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
142+
DerivedTypeMappings =
143+
{
144+
[typeof(RuntimeAnimal)] = new List<YamlDerivedType>
145+
{
146+
new YamlDerivedType(typeof(RuntimeDog), "dog") { Tag = "!dog" },
147+
}
148+
}
149+
}
150+
};
151+
152+
var yaml = "!dog\nName: Rex\nBarkVolume: 3\n";
153+
var value = YamlSerializer.Deserialize<RuntimeAnimal>(yaml, options);
154+
155+
Assert.IsNotNull(value);
156+
Assert.IsInstanceOfType<RuntimeDog>(value);
157+
Assert.AreEqual("Rex", value.Name);
158+
Assert.AreEqual(3, ((RuntimeDog)value).BarkVolume);
159+
}
160+
161+
// ---- Options-level: FallBackToBase ----
162+
163+
private class ConcreteAnimal
164+
{
165+
public string Name { get; set; } = string.Empty;
166+
}
167+
168+
private sealed class ConcreteDog : ConcreteAnimal
169+
{
170+
public int BarkVolume { get; set; }
171+
}
172+
173+
[TestMethod]
174+
public void UnknownTagFallsBackToBaseWithOptions()
175+
{
176+
var options = new YamlSerializerOptions
177+
{
178+
PolymorphismOptions = new YamlPolymorphismOptions
179+
{
180+
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
181+
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.FallBackToBase,
182+
DerivedTypeMappings =
183+
{
184+
[typeof(ConcreteAnimal)] = new List<YamlDerivedType>
185+
{
186+
new YamlDerivedType(typeof(ConcreteDog), "dog") { Tag = "!dog" },
187+
}
188+
}
189+
}
190+
};
191+
192+
var yaml = "!parrot\nName: Polly\n";
193+
var value = YamlSerializer.Deserialize<ConcreteAnimal>(yaml, options);
194+
195+
Assert.IsNotNull(value);
196+
Assert.IsInstanceOfType<ConcreteAnimal>(value);
197+
Assert.AreEqual("Polly", value.Name);
198+
}
199+
200+
// ---- Attribute-level override takes precedence over options ----
201+
202+
[TestMethod]
203+
public void AttributeUnknownHandlingOverridesOptions()
204+
{
205+
// FallbackAnimal has FallBackToBase in attribute; options say Fail — attribute wins
206+
var options = new YamlSerializerOptions
207+
{
208+
PolymorphismOptions = new YamlPolymorphismOptions
209+
{
210+
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.Fail,
211+
}
212+
};
213+
214+
var yaml = "!lizard\nName: Gecko\n";
215+
var value = YamlSerializer.Deserialize<FallbackAnimal>(yaml, options);
216+
217+
Assert.IsNotNull(value);
218+
Assert.IsInstanceOfType<FallbackAnimal>(value);
219+
Assert.AreEqual("Gecko", value.Name);
220+
}
221+
222+
// ---- Dictionary of polymorphic values with unknown tags ----
223+
224+
[TestMethod]
225+
public void UnknownTagInDictionaryValueFails()
226+
{
227+
var options = new YamlSerializerOptions
228+
{
229+
PolymorphismOptions = new YamlPolymorphismOptions
230+
{
231+
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
232+
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.Fail,
233+
DerivedTypeMappings =
234+
{
235+
[typeof(RuntimeAnimal)] = new List<YamlDerivedType>
236+
{
237+
new YamlDerivedType(typeof(RuntimeDog), "dog") { Tag = "!dog" },
238+
}
239+
}
240+
}
241+
};
242+
243+
var yaml = "rex: !dog\n Name: Rex\n BarkVolume: 3\npolly: !parrot\n Name: Polly\n";
244+
Assert.Throws<YamlException>(
245+
() => YamlSerializer.Deserialize<Dictionary<string, RuntimeAnimal>>(yaml, options));
246+
}
247+
248+
// ---- Tag-only entries (no discriminator) with unknown tags ----
249+
250+
[TestMethod]
251+
public void UnknownTagFailsWithTagOnlyEntries()
252+
{
253+
var options = new YamlSerializerOptions
254+
{
255+
PolymorphismOptions = new YamlPolymorphismOptions
256+
{
257+
DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag,
258+
UnknownDerivedTypeHandling = YamlUnknownDerivedTypeHandling.Fail,
259+
DerivedTypeMappings =
260+
{
261+
[typeof(RuntimeAnimal)] = new List<YamlDerivedType>
262+
{
263+
new YamlDerivedType(typeof(RuntimeDog)) { Tag = "!dog" },
264+
new YamlDerivedType(typeof(RuntimeCat)) { Tag = "!cat" },
265+
}
266+
}
267+
}
268+
};
269+
270+
var yaml = "!fish\nName: Nemo\n";
271+
var ex = Assert.Throws<YamlException>(
272+
() => YamlSerializer.Deserialize<RuntimeAnimal>(yaml, options));
273+
StringAssert.Contains(ex.Message, "!fish");
274+
}
275+
}

src/SharpYaml/Serialization/Converters/YamlObjectConverter.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,14 @@ private void WriteObjectCore(YamlWriter writer, object value, Contract contract)
13501350
{
13511351
targetType = derivedFromTag;
13521352
}
1353+
else if (polymorphism.DefaultDerivedType is not null)
1354+
{
1355+
targetType = polymorphism.DefaultDerivedType;
1356+
}
1357+
else if (polymorphism.UnknownDerivedTypeHandling == YamlUnknownDerivedTypeHandling.Fail)
1358+
{
1359+
throw YamlThrowHelper.ThrowUnknownTypeTag(reader, rootTag, typeof(T));
1360+
}
13531361
}
13541362

13551363
targetType ??= polymorphism.DefaultDerivedType ?? typeof(T);

src/SharpYaml/Serialization/YamlThrowHelper.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public static YamlException ThrowDuplicateMappingKey(YamlReader reader, string k
5252
public static YamlException ThrowUnknownTypeDiscriminator(YamlReader reader, string? discriminatorValue, Type baseType)
5353
=> new(reader.SourceName, reader.Start, reader.End, $"Unknown type discriminator '{discriminatorValue}' for '{baseType}'.");
5454

55+
/// <summary>Throws an exception for unknown Type Tag.</summary>
56+
public static YamlException ThrowUnknownTypeTag(YamlReader reader, string? tag, Type baseType)
57+
=> new(reader.SourceName, reader.Start, reader.End, $"Unknown type tag '{tag}' for '{baseType}'.");
58+
5559
/// <summary>Throws an exception for abstract Type Without Discriminator.</summary>
5660
public static YamlException ThrowAbstractTypeWithoutDiscriminator(YamlReader reader, Type type)
5761
=> new(reader.SourceName, reader.Start, reader.End, $"Cannot deserialize abstract type '{type}' without a known derived type discriminator.");

0 commit comments

Comments
 (0)