Search code examples
c#json.net-8.0system.text.json

JsonConverterAttribute is not used by JsonSerializer


I am trying to serialize a property of Encoding type. I wrote a converter class for that purpose. When I use JsonSerializerOptions everything works, but when I use JsonConverterAttribute I get an exception. I am not sure what I am doing wrong to use JsonConverterAttribute so I hope for your help.

Example code and exceptions:

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

namespace TestJsonConverterBug
{
    internal class Program
    {
        static readonly JsonSerializerOptions _options = new();

        static void Main()
        {
            _options.Converters.Add(new JsonEncodingConverter());

            TestOptions();
            TestAttributeSerialize();
            TestAttributeDeserialize();
        }

        // WORKS WITH OPTIONS
        static void TestOptions()
        {
            var obj = new SerializableClass1();

            var json = JsonSerializer.Serialize(obj, _options);

            var enc = JsonSerializer.Deserialize<SerializableClass1>(json, _options);
        }

        // DOESN'T WORK WITH CONVERTER ATTRIBUTE
        static void TestAttributeSerialize()
        {
            var obj = new SerializableClass2();

            var json = JsonSerializer.Serialize(obj);
        }

        // DOESN'T WORK WITH CONVERTER ATTRIBUTE
        static void TestAttributeDeserialize()
        {
            var obj = new SerializableClass2();

            var json = JsonSerializer.Serialize(obj, _options);

            var enc = JsonSerializer.Deserialize<SerializableClass2>(json);
        }
    }

    internal class SerializableClass1
    {
        public SerializableClass1()
        {
            Encoding = Encoding.ASCII;
        }

        public Encoding Encoding { get; set; }
    }

    internal class SerializableClass2
    {
        public SerializableClass2()
        {
            Encoding = Encoding.ASCII;
        }

        [JsonConverter(typeof(JsonEncodingConverter))]
        public Encoding Encoding { get; set; }
    }

    internal class JsonEncodingConverter : JsonConverter<Encoding>
    {
        public override bool CanConvert(Type typeToConvert)
            => typeToConvert.IsAssignableTo(typeof(Encoding));

        public override Encoding? Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options
        ) => Encoding.GetEncoding(reader.GetString()!);

        public override void Write(
            Utf8JsonWriter writer,
            Encoding value,
            JsonSerializerOptions options
        ) => writer.WriteStringValue(value.EncodingName);
    }
}

TestAttributeSerialize exception:

System.InvalidOperationException
  HResult=0x80131509
  Message=The type 'System.ReadOnlySpan`1[System.Byte]' of property 'Preamble' on type 'System.Text.Encoding' is invalid for serialization or deserialization because it is a pointer type, is a ref struct, or contains generic parameters that have not been replaced by specific types.
  Source=System.Text.Json
  StackTrace:
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_CannotSerializeInvalidType(Type typeToConvert, Type declaringType, MemberInfo memberInfo)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.CreatePropertyInfo(JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, JsonSerializerOptions options, Boolean shouldCheckForRequiredKeyword, Boolean hasJsonIncludeAttribute)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.AddMembersDeclaredBySuperType(JsonTypeInfo typeInfo, Type currentType, Boolean constructorHasSetsRequiredMembersAttribute, PropertyHierarchyResolutionState& state)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.PopulateProperties(JsonTypeInfo typeInfo)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.CreateTypeInfoCore(Type type, JsonConverter converter, JsonSerializerOptions options)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.GetTypeInfo(Type type, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoNoCaching(Type type)
   at System.Text.Json.JsonSerializerOptions.CachingContext.CreateCacheEntry(Type type, CachingContext context)
--- End of stack trace from previous location ---
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ConfigureProperties()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.<EnsureConfigured>g__ConfigureSynchronized|172_0()
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType)
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoForRootType(Type type, Boolean fallBackToNearestAncestorType)
   at System.Text.Json.JsonSerializer.GetTypeInfo[T](JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
   at TestJsonConverterBug.Program.TestAttributeSerialize() in ...\TestJsonConverterBug\Program.cs:line 35
   at TestJsonConverterBug.Program.Main() in ...\TestJsonConverterBug\Program.cs:line 16

TestAttributeDeserialize exception:

System.InvalidOperationException
  HResult=0x80131509
  Message=The type 'System.ReadOnlySpan`1[System.Byte]' of property 'Preamble' on type 'System.Text.Encoding' is invalid for serialization or deserialization because it is a pointer type, is a ref struct, or contains generic parameters that have not been replaced by specific types.
  Source=System.Text.Json
  StackTrace:
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_CannotSerializeInvalidType(Type typeToConvert, Type declaringType, MemberInfo memberInfo)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.CreatePropertyInfo(JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, JsonSerializerOptions options, Boolean shouldCheckForRequiredKeyword, Boolean hasJsonIncludeAttribute)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.AddMembersDeclaredBySuperType(JsonTypeInfo typeInfo, Type currentType, Boolean constructorHasSetsRequiredMembersAttribute, PropertyHierarchyResolutionState& state)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.PopulateProperties(JsonTypeInfo typeInfo)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.CreateTypeInfoCore(Type type, JsonConverter converter, JsonSerializerOptions options)
   at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.GetTypeInfo(Type type, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoNoCaching(Type type)
   at System.Text.Json.JsonSerializerOptions.CachingContext.CreateCacheEntry(Type type, CachingContext context)
--- End of stack trace from previous location ---
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ConfigureProperties()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.<EnsureConfigured>g__ConfigureSynchronized|172_0()
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Nullable`1 ensureNotNull, Boolean resolveIfMutable, Boolean fallBackToNearestAncestorType)
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoForRootType(Type type, Boolean fallBackToNearestAncestorType)
   at System.Text.Json.JsonSerializer.GetTypeInfo[T](JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at TestJsonConverterBug.Program.TestAttributeDeserialize() in ...\TestJsonConverterBug\Program.cs:line 45
   at TestJsonConverterBug.Program.Main() in ...\TestJsonConverterBug\Program.cs:line 17

Solution

  • This is a known regression in .NET 8 which has already been reported. See:

    It seems to be an unintended(?) side effect of the intentional breaking change noted in [Breaking change]: The System.Text.Json reflection-based deserializer is resolving all property metadata eagerly #37041. In that issue, MSFT's Eirik Tsarpalis recommends:

    Removing the unsupported property, authoring a custom converter for the unsupported type, or adding the JsonIgnoreAttribute like so...

    Since adding a converter does not work, you can instead mark the Encoding property with [JsonIgnore] and add a surrogate string property with the encoding name:

    internal class SerializableClass2
    {
        public SerializableClass2()
        {
            Encoding = Encoding.ASCII;
        }
    
        [JsonInclude, JsonPropertyName("Encoding")]
        string EncodingName { get => Encoding?.EncodingName!; set => Encoding = Encoding.GetEncoding(value); }
    
        [JsonIgnore]
        public Encoding Encoding { get; set; }
    }
    

    The surrogate can be private as long as it is marked with [JsonInclude].

    Demo fiddle here.