diff --git a/src/SharpYaml.SourceGenerator/YamlSerializerContextGenerator.cs b/src/SharpYaml.SourceGenerator/YamlSerializerContextGenerator.cs
index d6f6100..03ee01f 100644
--- a/src/SharpYaml.SourceGenerator/YamlSerializerContextGenerator.cs
+++ b/src/SharpYaml.SourceGenerator/YamlSerializerContextGenerator.cs
@@ -407,10 +407,23 @@ private static void EmitContext(SourceProductionContext context, Compilation com
continue;
}
+ // Skip if the member itself has [YamlConverter(typeof(...))] — the converter handles serialization.
+ if (GetYamlConverterAttributeTypeName(member) is not null)
+ {
+ continue;
+ }
+
+ // Skip if the member type is handled by a converter (type-level attribute or context-level converter).
+ if (IsTypeHandledByConverter(memberType, model.SourceGenerationOptions.ConverterTypes, compilation))
+ {
+ continue;
+ }
+
if (TryGetArrayElementType(memberType, out var arrayElementType) ||
TryGetSequenceElementType(memberType, out arrayElementType, out _))
{
- if (IsKnownScalar(arrayElementType) || indexByType.ContainsKey(arrayElementType))
+ if (IsKnownScalar(arrayElementType) || indexByType.ContainsKey(arrayElementType) ||
+ IsTypeHandledByConverter(arrayElementType, model.SourceGenerationOptions.ConverterTypes, compilation))
{
continue;
}
@@ -437,7 +450,8 @@ private static void EmitContext(SourceProductionContext context, Compilation com
continue;
}
- if (IsKnownScalar(dictionaryValueType) || indexByType.ContainsKey(dictionaryValueType))
+ if (IsKnownScalar(dictionaryValueType) || indexByType.ContainsKey(dictionaryValueType) ||
+ IsTypeHandledByConverter(dictionaryValueType, model.SourceGenerationOptions.ConverterTypes, compilation))
{
continue;
}
@@ -5879,6 +5893,55 @@ private static string GetIgnoreConditionExpression(ISymbol member)
return null;
}
+ ///
+ /// Checks whether the given type is handled by a converter — either via a [YamlConverter] attribute
+ /// on the type itself, or via a context-level YamlConverter<T> registration.
+ /// Nullable<T> value types are unwrapped before checking.
+ ///
+ private static bool IsTypeHandledByConverter(
+ ITypeSymbol typeToCheck,
+ ImmutableArray converterTypes,
+ Compilation compilation)
+ {
+ // Unwrap Nullable for value types.
+ var unwrappedType = typeToCheck;
+ if (typeToCheck is INamedTypeSymbol nullable && nullable.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
+ {
+ unwrappedType = nullable.TypeArguments[0];
+ }
+
+ // Check if the type itself has [YamlConverter(typeof(...))].
+ if (GetYamlConverterAttributeTypeName(unwrappedType) is not null)
+ {
+ return true;
+ }
+
+ // Check if a context-level converter handles this type.
+ if (!converterTypes.IsDefaultOrEmpty)
+ {
+ var yamlConverterOfT = compilation.GetTypeByMetadataName("SharpYaml.Serialization.YamlConverter`1");
+ if (yamlConverterOfT is not null)
+ {
+ foreach (var converterType in converterTypes)
+ {
+ // Walk up the base type chain looking for YamlConverter.
+ for (var current = converterType as INamedTypeSymbol; current is not null; current = current.BaseType)
+ {
+ if (current.IsGenericType &&
+ SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, yamlConverterOfT) &&
+ current.TypeArguments.Length == 1 &&
+ SymbolEqualityComparer.Default.Equals(current.TypeArguments[0], unwrappedType))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
private static ImmutableArray GetSerializableMembers(INamedTypeSymbol type)
{
// Arrays/collections/dictionaries are handled by dedicated generated code paths, not as object graphs.
diff --git a/src/SharpYaml.Tests/Serialization/YamlSerializerContextGeneratorDiagnosticTests.cs b/src/SharpYaml.Tests/Serialization/YamlSerializerContextGeneratorDiagnosticTests.cs
index e0c5dba..a9a050c 100644
--- a/src/SharpYaml.Tests/Serialization/YamlSerializerContextGeneratorDiagnosticTests.cs
+++ b/src/SharpYaml.Tests/Serialization/YamlSerializerContextGeneratorDiagnosticTests.cs
@@ -147,6 +147,427 @@ internal partial class MappingContext : YamlSerializerContext
StringAssert.Contains(result.GeneratedSource, "value is global::Dog");
}
+ [TestMethod]
+ public void SHARPYAML002_IsSuppressed_WhenContextLevelConverterHandlesMemberType()
+ {
+ const string source = """
+ using SharpYaml.Serialization;
+
+ public sealed class CustomType
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public sealed class CustomTypeConverter : YamlConverter
+ {
+ public override CustomType Read(YamlReader reader)
+ {
+ var value = reader.GetScalarValue();
+ reader.Read();
+ return new CustomType { Value = value };
+ }
+
+ public override void Write(YamlWriter writer, CustomType value)
+ {
+ writer.WriteScalar(value.Value);
+ }
+ }
+
+ public sealed class ModelWithCustomType
+ {
+ public CustomType? Item { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithCustomType))]
+ [YamlSourceGenerationOptions(Converters = [typeof(CustomTypeConverter)])]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(0, diagnostics.Length, string.Join(Environment.NewLine, diagnostics.Select(static d => d.GetMessage())));
+ }
+
+ [TestMethod]
+ public void SHARPYAML002_IsSuppressed_WhenMemberHasYamlConverterAttribute()
+ {
+ const string source = """
+ using SharpYaml.Serialization;
+
+ public sealed class CustomType
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public sealed class CustomTypeConverter : YamlConverter
+ {
+ public override CustomType Read(YamlReader reader)
+ {
+ var value = reader.GetScalarValue();
+ reader.Read();
+ return new CustomType { Value = value };
+ }
+
+ public override void Write(YamlWriter writer, CustomType value)
+ {
+ writer.WriteScalar(value.Value);
+ }
+ }
+
+ public sealed class ModelWithConverterOnMember
+ {
+ [YamlConverter(typeof(CustomTypeConverter))]
+ public CustomType? Item { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithConverterOnMember))]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(0, diagnostics.Length, string.Join(Environment.NewLine, diagnostics.Select(static d => d.GetMessage())));
+ }
+
+ [TestMethod]
+ public void SHARPYAML002_IsSuppressed_WhenTypeHasYamlConverterAttribute()
+ {
+ const string source = """
+ using SharpYaml.Serialization;
+
+ public sealed class TypeLevelConverter : YamlConverter
+ {
+ public override ConverterDecoratedType Read(YamlReader reader)
+ {
+ var value = reader.GetScalarValue();
+ reader.Read();
+ return new ConverterDecoratedType { Value = value };
+ }
+
+ public override void Write(YamlWriter writer, ConverterDecoratedType value)
+ {
+ writer.WriteScalar(value.Value);
+ }
+ }
+
+ [YamlConverter(typeof(TypeLevelConverter))]
+ public sealed class ConverterDecoratedType
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public sealed class ModelWithTypeLevelConverter
+ {
+ public ConverterDecoratedType? Item { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithTypeLevelConverter))]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(0, diagnostics.Length, string.Join(Environment.NewLine, diagnostics.Select(static d => d.GetMessage())));
+ }
+
+ [TestMethod]
+ public void SHARPYAML002_IsSuppressed_WhenContextConverterHandlesArrayElementType()
+ {
+ const string source = """
+ using System.Collections.Generic;
+ using SharpYaml.Serialization;
+
+ public sealed class CustomType
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public sealed class CustomTypeConverter : YamlConverter
+ {
+ public override CustomType Read(YamlReader reader)
+ {
+ var value = reader.GetScalarValue();
+ reader.Read();
+ return new CustomType { Value = value };
+ }
+
+ public override void Write(YamlWriter writer, CustomType value)
+ {
+ writer.WriteScalar(value.Value);
+ }
+ }
+
+ public sealed class ModelWithList
+ {
+ public List? Items { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithList))]
+ [YamlSourceGenerationOptions(Converters = [typeof(CustomTypeConverter)])]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(0, diagnostics.Length, string.Join(Environment.NewLine, diagnostics.Select(static d => d.GetMessage())));
+ }
+
+ [TestMethod]
+ public void SHARPYAML002_IsSuppressed_WhenContextConverterHandlesDictionaryValueType()
+ {
+ const string source = """
+ using System.Collections.Generic;
+ using SharpYaml.Serialization;
+
+ public sealed class CustomType
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public sealed class CustomTypeConverter : YamlConverter
+ {
+ public override CustomType Read(YamlReader reader)
+ {
+ var value = reader.GetScalarValue();
+ reader.Read();
+ return new CustomType { Value = value };
+ }
+
+ public override void Write(YamlWriter writer, CustomType value)
+ {
+ writer.WriteScalar(value.Value);
+ }
+ }
+
+ public sealed class ModelWithDictionary
+ {
+ public Dictionary? Items { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithDictionary))]
+ [YamlSourceGenerationOptions(Converters = [typeof(CustomTypeConverter)])]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(0, diagnostics.Length, string.Join(Environment.NewLine, diagnostics.Select(static d => d.GetMessage())));
+ }
+
+ [TestMethod]
+ public void SHARPYAML002_StillFires_WhenNoConverterHandlesType()
+ {
+ const string source = """
+ using SharpYaml.Serialization;
+
+ public sealed class UnhandledType
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public sealed class ModelWithUnhandledType
+ {
+ public UnhandledType? Item { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithUnhandledType))]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(1, diagnostics.Length);
+ Assert.AreEqual(DiagnosticSeverity.Error, diagnostics[0].Severity);
+ StringAssert.Contains(diagnostics[0].GetMessage(), "UnhandledType");
+ }
+
+ [TestMethod]
+ public void SHARPYAML002_StillFires_WhenConverterHandlesDifferentType()
+ {
+ const string source = """
+ using SharpYaml.Serialization;
+
+ public sealed class TypeA
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public sealed class TypeB
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public sealed class TypeAConverter : YamlConverter
+ {
+ public override TypeA Read(YamlReader reader)
+ {
+ var value = reader.GetScalarValue();
+ reader.Read();
+ return new TypeA { Value = value };
+ }
+
+ public override void Write(YamlWriter writer, TypeA value)
+ {
+ writer.WriteScalar(value.Value);
+ }
+ }
+
+ public sealed class ModelWithTypeB
+ {
+ public TypeB? Item { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithTypeB))]
+ [YamlSourceGenerationOptions(Converters = [typeof(TypeAConverter)])]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(1, diagnostics.Length);
+ StringAssert.Contains(diagnostics[0].GetMessage(), "TypeB");
+ }
+
+ [TestMethod]
+ public void SHARPYAML002_IsSuppressed_WhenConverterInheritsFromAnotherConverter()
+ {
+ const string source = """
+ using SharpYaml.Serialization;
+
+ public sealed class CustomType
+ {
+ public string Value { get; set; } = string.Empty;
+ }
+
+ public class BaseCustomTypeConverter : YamlConverter
+ {
+ public override CustomType Read(YamlReader reader)
+ {
+ var value = reader.GetScalarValue();
+ reader.Read();
+ return new CustomType { Value = value };
+ }
+
+ public override void Write(YamlWriter writer, CustomType value)
+ {
+ writer.WriteScalar(value.Value);
+ }
+ }
+
+ public sealed class DerivedCustomTypeConverter : BaseCustomTypeConverter
+ {
+ }
+
+ public sealed class ModelWithCustomType
+ {
+ public CustomType? Item { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithCustomType))]
+ [YamlSourceGenerationOptions(Converters = [typeof(DerivedCustomTypeConverter)])]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(0, diagnostics.Length, string.Join(Environment.NewLine, diagnostics.Select(static d => d.GetMessage())));
+ }
+
+ [TestMethod]
+ public void SHARPYAML002_IsSuppressed_WhenContextConverterHandlesNullableValueType()
+ {
+ const string source = """
+ using SharpYaml.Serialization;
+
+ public struct CustomStruct
+ {
+ public int Value { get; set; }
+ }
+
+ public sealed class CustomStructConverter : YamlConverter
+ {
+ public override CustomStruct Read(YamlReader reader)
+ {
+ var value = reader.GetScalarValue();
+ reader.Read();
+ return new CustomStruct { Value = int.Parse(value) };
+ }
+
+ public override void Write(YamlWriter writer, CustomStruct value)
+ {
+ writer.WriteScalar(value.Value.ToString());
+ }
+ }
+
+ public sealed class ModelWithNullableStruct
+ {
+ public CustomStruct? Item { get; set; }
+ }
+
+ [YamlSerializable(typeof(ModelWithNullableStruct))]
+ [YamlSourceGenerationOptions(Converters = [typeof(CustomStructConverter)])]
+ internal partial class TestContext : YamlSerializerContext
+ {
+ }
+ """;
+
+ var result = RunGenerator(source);
+
+ var diagnostics = result.Diagnostics
+ .Where(static d => d.Id == "SHARPYAML002")
+ .ToArray();
+
+ Assert.AreEqual(0, diagnostics.Length, string.Join(Environment.NewLine, diagnostics.Select(static d => d.GetMessage())));
+ }
+
private static (Compilation OutputCompilation, Diagnostic[] Diagnostics, string GeneratedSource) RunGenerator(string source)
{
var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview);