Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System.Text.Json doesn't handle multiple levels of polymorphism with abstract types #111188

Closed
vdachev-david opened this issue Jan 8, 2025 · 3 comments

Comments

@vdachev-david
Copy link

Description

I am trying to deserialize a JSON with polymorphism going down on multiple levels with base classes being abstract (similar to the diagram shown below). However, I keep getting an error such as:

The JSON payload for polymorphic interface or abstract type 'Shape1DBase' must specify a type discriminator.

classDiagram
    class ShapeBase
    <<abstract>> ShapeBase
    ShapeBase : +string Dimensions*

    class Shape1DBase
    <<abstract>> Shape1DBase
    ShapeBase <|-- Shape1DBase
    Shape1DBase: +string Type*
    Shape1DBase <|-- Dot

    class Dot

    class Shape2DBase
    <<abstract>> Shape2DBase
    ShapeBase <|-- Shape2DBase
    Shape2DBase : +string Type*

    class Circle
    Shape2DBase <|-- Circle
    Circle : +double Radius

    class Rectangle
    Shape2DBase <|-- Rectangle
    Rectangle : +double A
    Rectangle : +double B
Loading

Reproduction Steps

I run the following program:

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

string json = """
[
	{ "Dimensions": "1D", "Type": "DOT" },
	{ "Dimensions": "2D", "Type": "CIRCLE", "Radius": 5 },
	{ "Dimensions": "2D", "Type": "RECTANGLE", "A": 5, "B": 3 }
]
""";

ShapeBase[]? shapes = JsonSerializer.Deserialize<ShapeBase[]>(json) ?? throw new Exception("JSON deserialized to null.");

foreach (ShapeBase shape in shapes)
{
	Console.WriteLine($"- {shape}");
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = nameof(Dimensions), UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(Shape1DBase), "1D")]
[JsonDerivedType(typeof(Shape2DBase), "2D")]
abstract record class ShapeBase
{
	public abstract string Dimensions { get; set; }
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = nameof(Type), UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(Dot), "DOT")]
abstract record class Shape1DBase : ShapeBase
{
	public override string Dimensions { get => "1D"; set { } }
	public abstract string Type { get; set; }
}

record class Dot : Shape1DBase
{
	public override string Type { get => "DOT"; set { } }
}

[JsonPolymorphic(TypeDiscriminatorPropertyName = nameof(Type), UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
[JsonDerivedType(typeof(Circle), "CIRCLE")]
[JsonDerivedType(typeof(Rectangle), "RECTANGLE")]
abstract record class Shape2DBase : ShapeBase
{
	public override string Dimensions { get => "2D"; set { } }
	public abstract string Type { get; set; }
}

record class Circle : Shape2DBase
{
	public override string Type { get => "CIRCLE"; set { } }
	public double Radius { get; set; }
}

record class Rectangle : Shape2DBase
{
	public override string Type { get => "RECTANGLE"; set { } }
	public double A { get; set; }
	public double B { get; set; }
}

Expected behavior

I expect the program to output:

- Dot { Dimensions = 1D, Type = DOT }
- Circle { Dimensions = 2D, Type = CIRCLE, Radius = 5 }
- Rectangle { Dimensions = 2D, Type = RECTANGLE, A = 5, B = 3 }

Actual behavior

I get an exception:

System.NotSupportedException
  HResult=0x80131515
  Message=The JSON payload for polymorphic interface or abstract type 'Shape1DBase' must specify a type discriminator. Path: $[0] | LineNumber: 1 | BytePositionInLine: 30.
  Source=System.Text.Json
  StackTrace:
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException(ReadStack& state, Utf8JsonReader& reader, Exception innerException)
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(JsonTypeInfo typeInfo, Utf8JsonReader& reader, ReadStack& state)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.OnTryReadAsObject(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, Object& value)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
   at System.Text.Json.Serialization.JsonCollectionConverter`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, Boolean& isPopulatedValue)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, T& value, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Deserialize(Utf8JsonReader& reader, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo`1 jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo`1 jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at Program.<Main>$(String[] args) in ...\Program.cs:line 12

Inner Exception 1:
NotSupportedException: The JSON payload for polymorphic interface or abstract type 'Shape1DBase' must specify a type discriminator.

Regression?

Not really.

  • In .NET 7 and .NET 8 I get the following exception instead:

    System.NotSupportedException: 'Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'Shape1DBase'. Path: $[0] | LineNumber: 1 | BytePositionInLine: 30.'

  • In .NET 6 and earlier there are no [JsonPolimorphic] and [JsonDerivedType] attributes.

Known Workarounds

None.

Configuration

  • .NET Runtime: 9.0.0
  • OS: Windows 11 Enterprise, Version 24H2
  • Architecture: x64
  • I have also tried this code with the same result on a Windows Server 2022 Standard, Version 21H2.

Other information

  • I tried turning abstract classes to concrete, and any abstract properties to virtual and it seems only the first level of polymorphism is honored because I get the following output:
    - Shape1DBase { Dimensions = 1D, Type = DOT }
    - Shape2DBase { Dimensions = 2D, Type = CIRCLE }
    - Shape2DBase { Dimensions = 2D, Type = RECTANGLE }
    
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Jan 8, 2025
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

@eiriktsarpalis
Copy link
Member

Duplicate of #101286. Polymorphism configuration is not inherited either by supertypes or subtypes and needs to be specified independently for each type being serialized; this is due to constraints related to how the source generator works. The suggested workaround is to use reflection and the contract customization APIs to dynamically construct polymorphic configuration as desired.

@dotnet-policy-service dotnet-policy-service bot removed the untriaged New issue has not been triaged by the area owner label Jan 8, 2025
@vdachev-david
Copy link
Author

@eiriktsarpalis , thank you for the prompt clarification. I think this is worth pointing out in the documentation. It seems I'm not the first one coming across this... feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants