runtime: System.Text.Json Deserialization throws NotSupportedException when implementing IEnumerable

JsonSerializer.Deserialize throws NotSupportedException when IEnumerable<T> is implemented.

The Category and Card records implement the interface IEnumerable<T>:

public record Item(string Title, string Text, decimal Price);
public record Category(string Title) : IEnumerable<Item>
{
    public IList<Item> Items { get; init; } = new List<Item>();

    public IEnumerator<Item> GetEnumerator() => Items.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Items).GetEnumerator();

    public void Add(Item item) => Items.Add(item);
}
public record Card(string Title) : IEnumerable<Category>
{
    public IList<Category> Categories { get; init; } = new List<Category>();

    public IEnumerator<Category> GetEnumerator() => Categories.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Categories).GetEnumerator();

    public void Add(Category category) => Categories.Add(category);
}

This allows creating objects with an collection initializer:

Card card = new("The Restaurant")
{
    new Category("Appetizers")
    {
        new Item("Dungeon Crab Cocktail", "Classic cocktail sauce", 27M),
        new Item("Almond Crusted Scallops", "Almonds, Parmesan, chive beurre blanc", 19M)
    },
    new Category("Dinner")
    {
        new Item("Grilled King Salmon", "Lemon chive beurre blanc", 49M)
    }
};

Using a Card object, serialization with the JsonSerializer succeeds. Deserialization throws the NotSupportedException because the type is abstract, an interface, or is read only which is not the case.

Call stack:

System.NotSupportedException: The collection type 'Card' is abstract, an interface, or is read only, and could not be instantiated and populated. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
 ---> System.NotSupportedException: The collection type 'Card' is abstract, an interface, or is read only, and could not be instantiated and populated.
   --- End of inner exception stack trace ---
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException(ReadStack& state, Utf8JsonReader& reader, NotSupportedException ex)
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(Type type, Utf8JsonReader& reader, ReadStack& state)
   at System.Text.Json.Serialization.Converters.IEnumerableOfTConverter`2.CreateCollection(Utf8JsonReader& reader, ReadStack& state, JsonSerializerOptions options)
   at System.Text.Json.Serialization.Converters.IEnumerableDefaultConverter`2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](Utf8JsonReader& reader, Type returnType, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, Type returnType, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at <Program>$.<<Main>$>g__DeserializeJson|0_1(String json) in C:\github\jsonserializerwithienumerable\JsonSerializerIssue\JsonSerializerIssue\Program.cs:line 48
   at <Program>$.<Main>$(String[] args) in C:\github\jsonserializerwithienumerable\JsonSerializerIssue\JsonSerializerIssue\Program.cs:line 31

If the implementation of the IEnumerable<T> interface is removed, and all other code stays the same, deserialization works as expected. The issue only occurs if the interface is implemented. Without this interface, the collection initializer cannot be used.

Sample project is available here: https://github.com/christiannagel/jsonserializerwithienumerable

Thanks!

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 4
  • Comments: 17 (5 by maintainers)

Most upvoted comments

@christiannagel - regarding the public vs internal part, I ended up pulling it from the JsonSerializerOptions, which may have been the intended route: I’m not sure I could have simplified this more if I had direct access to the internal converter.

My collection class is RecordArray<T>:

internal class RecordArrayConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert) 
        => typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(RecordArray<>);

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) 
        => (JsonConverter)Activator.CreateInstance(typeof(RecordArrayConverter<>).MakeGenericType(typeToConvert.GetGenericArguments()[0]));

    private class RecordArrayConverter<T> : JsonConverter<RecordArray<T>>
    {
        public override RecordArray<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 
            => new RecordArray<T>(((JsonConverter<T[]>)options.GetConverter(typeof(T[]))).Read(ref reader, typeof(T[]), options));

        public override void Write(Utf8JsonWriter writer, RecordArray<T> value, JsonSerializerOptions options) 
            => ((JsonConverter<IEnumerable<T>>)options.GetConverter(typeof(IEnumerable<T>))).Write(writer, value, options);
    }
}

I’m not sure how this can be handled besides a custom converter. You have an IEnumerable that you want to be treated as an object and not an IEnumerable. The IEnumerable convertor comes before the Object converter because all IEnumerables are also objects

actually, it looks like the serialization is also wrong here.

[
  [
    {
      "Title": "Dungeon Crab Cocktail",
      "Text": "Classic cocktail sauce",
      "Price": 27
    },
    {
      "Title": "Almond Crusted Scallops",
      "Text": "Almonds, Parmesan, chive beurre blanc",
      "Price": 19
    }
  ],
  [
    {
      "Title": "Grilled King Salmon",
      "Text": "Lemon chive beurre blanc",
      "Price": 49
    }
  ]
]

I ran into the very same problem. Deserializing to the concrete type throws an exception simply because that type implements IEnumerable. It seems the deserializer checks if the type implements the interface at all even when it shouldn’t matter. Removing the interface worked, but was not what I wanted (for similar reasons to the above).

@Symbai I think this is a different scenario. The exception says NotSupportedException because the type is abstract, an interface, or is read only. The Card type is neither abstract, nor an interface, and not read-only. Removing the implementation of IEnumerable<T>, deserialization is fully working.