Skip to content

JSON source generator emitting source for types unexpectedly #108682

Open
@stephentoub

Description

@stephentoub

Description

In the repo below, Exception should not be serialized as it's defined, but rather only via the ExceptionConverter that treats it as a string. However, the source generator is emitting code to handle all of the properties on Exception. While this is unnecessary code, it also results in trimmer warnings about members of Exception that aren't safe to serialized.

Reproduction Steps

using System.Text.Json;
using System.Text.Json.Serialization;

Console.WriteLine(JsonSerializer.Serialize(new MyClass { Error = new Exception("error") } ));

sealed class MyClass
{
    [JsonConverter(typeof(ExceptionConverter))]
    public Exception? Error { get; set; }
}

sealed class ExceptionConverter : JsonConverter<Exception>
{
    public override Exception Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        new Exception(reader.GetString());

    public override void Write(Utf8JsonWriter writer, Exception? value, JsonSerializerOptions options) =>
        writer.WriteStringValue(value?.Message ?? string.Empty);
}

[JsonSerializable(typeof(MyClass))]
partial class MyContext : JsonSerializerContext;

Expected behavior

Code emitted for Exception related to its converter.

Actual behavior

Emits this:

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

// Suppress warnings about [Obsolete] member usage in generated code.
#pragma warning disable CS0612, CS0618

partial class MyContext
{
    private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Exception>? _Exception;
    
    /// <summary>
    /// Defines the source generated JSON serialization contract metadata for a given type.
    /// </summary>
    #nullable disable annotations // Marking the property type as nullable-oblivious.
    public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Exception> Exception
    #nullable enable annotations
    {
        get => _Exception ??= (global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Exception>)Options.GetTypeInfo(typeof(global::System.Exception));
    }
    
    private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Exception> Create_Exception(global::System.Text.Json.JsonSerializerOptions options)
    {
        if (!TryGetTypeInfoForRuntimeCustomConverter<global::System.Exception>(options, out global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Exception> jsonTypeInfo))
        {
            var objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::System.Exception>
            {
                ObjectCreator = () => new global::System.Exception(),
                ObjectWithParameterizedConstructorCreator = null,
                PropertyMetadataInitializer = _ => ExceptionPropInit(options),
                ConstructorParameterMetadataInitializer = null,
                ConstructorAttributeProviderFactory = static () => typeof(global::System.Exception).GetConstructor(InstanceMemberBindingFlags, binder: null, global::System.Array.Empty<global::System.Type>(), modifiers: null),
                SerializeHandler = ExceptionSerializeHandler,
            };
            
            jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::System.Exception>(options, objectInfo);
            jsonTypeInfo.NumberHandling = null;
        }
    
        jsonTypeInfo.OriginatingResolver = this;
        return jsonTypeInfo;
    }

    private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] ExceptionPropInit(global::System.Text.Json.JsonSerializerOptions options)
    {
        var properties = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[8];

        var info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Collections.IDictionary>
        {
            IsProperty = true,
            IsPublic = true,
            IsVirtual = true,
            DeclaringType = typeof(global::System.Exception),
            Converter = null,
            Getter = static obj => ((global::System.Exception)obj).Data,
            Setter = null,
            IgnoreCondition = null,
            HasJsonInclude = false,
            IsExtensionData = false,
            NumberHandling = null,
            PropertyName = "Data",
            JsonPropertyName = null,
            AttributeProviderFactory = static () => typeof(global::System.Exception).GetProperty("Data", InstanceMemberBindingFlags, null, typeof(global::System.Collections.IDictionary), global::System.Array.Empty<global::System.Type>(), null),
        };
        
        properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Collections.IDictionary>(options, info0);
        properties[0].IsGetNullable = false;

        var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<string>
        {
            IsProperty = true,
            IsPublic = true,
            IsVirtual = true,
            DeclaringType = typeof(global::System.Exception),
            Converter = null,
            Getter = static obj => ((global::System.Exception)obj).HelpLink,
            Setter = static (obj, value) => ((global::System.Exception)obj).HelpLink = value!,
            IgnoreCondition = null,
            HasJsonInclude = false,
            IsExtensionData = false,
            NumberHandling = null,
            PropertyName = "HelpLink",
            JsonPropertyName = null,
            AttributeProviderFactory = static () => typeof(global::System.Exception).GetProperty("HelpLink", InstanceMemberBindingFlags, null, typeof(string), global::System.Array.Empty<global::System.Type>(), null),
        };
        
        properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<string>(options, info1);

        var info2 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>
        {
            IsProperty = true,
            IsPublic = true,
            IsVirtual = false,
            DeclaringType = typeof(global::System.Exception),
            Converter = null,
            Getter = static obj => ((global::System.Exception)obj).HResult,
            Setter = static (obj, value) => ((global::System.Exception)obj).HResult = value!,
            IgnoreCondition = null,
            HasJsonInclude = false,
            IsExtensionData = false,
            NumberHandling = null,
            PropertyName = "HResult",
            JsonPropertyName = null,
            AttributeProviderFactory = static () => typeof(global::System.Exception).GetProperty("HResult", InstanceMemberBindingFlags, null, typeof(int), global::System.Array.Empty<global::System.Type>(), null),
        };
        
        properties[2] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info2);

        var info3 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Exception>
        {
            IsProperty = true,
            IsPublic = true,
            IsVirtual = false,
            DeclaringType = typeof(global::System.Exception),
            Converter = null,
            Getter = static obj => ((global::System.Exception)obj).InnerException,
            Setter = null,
            IgnoreCondition = null,
            HasJsonInclude = false,
            IsExtensionData = false,
            NumberHandling = null,
            PropertyName = "InnerException",
            JsonPropertyName = null,
            AttributeProviderFactory = static () => typeof(global::System.Exception).GetProperty("InnerException", InstanceMemberBindingFlags, null, typeof(global::System.Exception), global::System.Array.Empty<global::System.Type>(), null),
        };
        
        properties[3] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Exception>(options, info3);

        var info4 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<string>
        {
            IsProperty = true,
            IsPublic = true,
            IsVirtual = true,
            DeclaringType = typeof(global::System.Exception),
            Converter = null,
            Getter = static obj => ((global::System.Exception)obj).Message,
            Setter = null,
            IgnoreCondition = null,
            HasJsonInclude = false,
            IsExtensionData = false,
            NumberHandling = null,
            PropertyName = "Message",
            JsonPropertyName = null,
            AttributeProviderFactory = static () => typeof(global::System.Exception).GetProperty("Message", InstanceMemberBindingFlags, null, typeof(string), global::System.Array.Empty<global::System.Type>(), null),
        };
        
        properties[4] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<string>(options, info4);
        properties[4].IsGetNullable = false;

        var info5 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<string>
        {
            IsProperty = true,
            IsPublic = true,
            IsVirtual = true,
            DeclaringType = typeof(global::System.Exception),
            Converter = null,
            Getter = static obj => ((global::System.Exception)obj).Source,
            Setter = static (obj, value) => ((global::System.Exception)obj).Source = value!,
            IgnoreCondition = null,
            HasJsonInclude = false,
            IsExtensionData = false,
            NumberHandling = null,
            PropertyName = "Source",
            JsonPropertyName = null,
            AttributeProviderFactory = static () => typeof(global::System.Exception).GetProperty("Source", InstanceMemberBindingFlags, null, typeof(string), global::System.Array.Empty<global::System.Type>(), null),
        };
        
        properties[5] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<string>(options, info5);

        var info6 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<string>
        {
            IsProperty = true,
            IsPublic = true,
            IsVirtual = true,
            DeclaringType = typeof(global::System.Exception),
            Converter = null,
            Getter = static obj => ((global::System.Exception)obj).StackTrace,
            Setter = null,
            IgnoreCondition = null,
            HasJsonInclude = false,
            IsExtensionData = false,
            NumberHandling = null,
            PropertyName = "StackTrace",
            JsonPropertyName = null,
            AttributeProviderFactory = static () => typeof(global::System.Exception).GetProperty("StackTrace", InstanceMemberBindingFlags, null, typeof(string), global::System.Array.Empty<global::System.Type>(), null),
        };
        
        properties[6] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<string>(options, info6);

        var info7 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Reflection.MethodBase>
        {
            IsProperty = true,
            IsPublic = true,
            IsVirtual = false,
            DeclaringType = typeof(global::System.Exception),
            Converter = null,
            Getter = static obj => ((global::System.Exception)obj).TargetSite,
            Setter = null,
            IgnoreCondition = null,
            HasJsonInclude = false,
            IsExtensionData = false,
            NumberHandling = null,
            PropertyName = "TargetSite",
            JsonPropertyName = null,
            AttributeProviderFactory = static () => typeof(global::System.Exception).GetProperty("TargetSite", InstanceMemberBindingFlags, null, typeof(global::System.Reflection.MethodBase), global::System.Array.Empty<global::System.Type>(), null),
        };
        
        properties[7] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Reflection.MethodBase>(options, info7);

        return properties;
    }

    // Intentionally not a static method because we create a delegate to it. Invoking delegates to instance
    // methods is almost as fast as virtual calls. Static methods need to go through a shuffle thunk.
    private void ExceptionSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::System.Exception? value)
    {
        if (value is null)
        {
            writer.WriteNullValue();
            return;
        }
        
        writer.WriteStartObject();

        writer.WritePropertyName(PropName_Data);
        global::System.Text.Json.JsonSerializer.Serialize(writer, ((global::System.Exception)value).Data, IDictionary);
        writer.WriteString(PropName_HelpLink, ((global::System.Exception)value).HelpLink);
        writer.WriteNumber(PropName_HResult, ((global::System.Exception)value).HResult);
        writer.WritePropertyName(PropName_InnerException);
        ExceptionSerializeHandler(writer, ((global::System.Exception)value).InnerException);
        writer.WriteString(PropName_Message, ((global::System.Exception)value).Message);
        writer.WriteString(PropName_Source, ((global::System.Exception)value).Source);
        writer.WriteString(PropName_StackTrace, ((global::System.Exception)value).StackTrace);
        writer.WritePropertyName(PropName_TargetSite);
        global::System.Text.Json.JsonSerializer.Serialize(writer, ((global::System.Exception)value).TargetSite, MethodBase);

        writer.WriteEndObject();
    }
}

Regression?

n/a

Known Workarounds

n/a

Configuration

.NET 9

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.Text.JsonenhancementProduct code improvement that does NOT require public API changes/additionssource-generatorIndicates an issue with a source generator feature

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions