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

Why does System.Text.Json throw an exception when deserializing polymorphic types in case $type is not the first property #96088

Closed
armanossiloko opened this issue Dec 16, 2023 · 3 comments

Comments

@armanossiloko
Copy link

I have noticed some behavior that got me surprised. I have an abstract BaseClass and a DerivedClass.

[JsonPolymorphic]
[JsonDerivedType(typeof(DerivedClass), "derived")]
public abstract class BaseClass
{
    public BaseClass() { }
}
public class DerivedClass : BaseClass
{
    public string? Whatever { get; set; }
}

And now I have two JSON strings: the first JSON has the type discriminator ($type) as the very first property within the JSON - the second JSON string does not. When I perform JsonSerializer.Deserialize<BaseClass>(), an exception is thrown on the second JSON string.

var jsonWorks = "{\"$type\": \"derived\", \"whatever\": \"Bar\"}";
var jsonBreaks = "{\"whatever\": \"Bar\", \"$type\": \"derived\"}";

var obj1 = JsonSerializer.Deserialize<BaseClass>(jsonWorks);
var obj2 = JsonSerializer.Deserialize<BaseClass>(jsonBreaks); // This one will throw an exception

The exception that is thrown is of type System.NotSupportedException with the following message:

System.NotSupportedException: 'Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyApp.BaseClass'. Path: $ | LineNumber: 0 | BytePositionInLine: 12.'

It also has an inner exception:

NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyApp.BaseClass'.

At first glance I thought this would be a bug until someone pointed out this particular area in the documentation.

The type discriminator must be placed at the start of the JSON object, grouped together with other metadata properties like $id and $ref.

Since on the top level of any JSON string, you can't really have duplicate properties (e.g the property username cannot really appear twice on the same level within a JSON), why is System.Text.Json designed in such a way to throw this exception?

For anyone interested, I came across this while I was trying to map a property to jsonb using EntityFrameworkCore and within Npgsql. I did have to wrap the JsonSerializer methods into a new class which for the sake of example is called DatabaseJsonConverter below. Also, during the "serialization" part of an object, the $type does get written as the first property within the resulting JSON, but for some reason, after it's saved, the order of properties in my PostgreSQL instance is not the same it was in the metadata => DatabaseJsonConverter.Serialize(metadata) resulting string.

Image taken from within the database in Datagrip:
image

builder.Entity<MyEntity>()
    .Property(e => e.MyProperty)
    .HasColumnType("jsonb")
    .HasConversion(
        metadata => DatabaseJsonConverter.Serialize(metadata),
        json => DatabaseJsonConverter.Deserialize<Metadata>(json)
    );
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Dec 16, 2023
@ghost
Copy link

ghost commented Dec 16, 2023

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

Issue Details

I have noticed some behavior that got me surprised. I have an abstract BaseClass and a DerivedClass.

[JsonPolymorphic]
[JsonDerivedType(typeof(DerivedClass), "derived")]
public abstract class BaseClass
{
    public BaseClass() { }
}
public class DerivedClass : BaseClass
{
    public string? Whatever { get; set; }
}

And now I have two JSON strings: the first JSON has the type discriminator ($type) as the very first property within the JSON - the second JSON string does not. When I perform JsonSerializer.Deserialize<BaseClass>(), an exception is thrown on the second JSON string.

var jsonWorks = "{\"$type\": \"derived\", \"whatever\": \"Bar\"}";
var jsonBreaks = "{\"whatever\": \"Bar\", \"$type\": \"derived\"}";

var obj1 = JsonSerializer.Deserialize<BaseClass>(jsonWorks);
var obj2 = JsonSerializer.Deserialize<BaseClass>(jsonBreaks); // This one will throw an exception

The exception that is thrown is of type System.NotSupportedException with the following message:

System.NotSupportedException: 'Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyApp.BaseClass'. Path: $ | LineNumber: 0 | BytePositionInLine: 12.'

It also has an inner exception:

NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyApp.BaseClass'.

At first glance I thought this would be a bug until someone pointed out this particular area in the documentation.

The type discriminator must be placed at the start of the JSON object, grouped together with other metadata properties like $id and $ref.

Since on the top level of any JSON string, you can't really have duplicate properties (e.g the property username cannot really appear twice on the same level within a JSON), why is System.Text.Json designed in such a way to throw this exception?

For anyone interested, I came across this while I was trying to map a property to jsonb using EntityFrameworkCore and within Npgsql. I did have to wrap the JsonSerializer methods into a new class which for the sake of example is called DatabaseJsonConverter below. Also, during the "serialization" part of an object, the $type does get written as the first property within the resulting JSON, but for some reason, after it's saved, the order of properties in my PostgreSQL instance is not the same it was in the metadata => DatabaseJsonConverter.Serialize(metadata) resulting string.

Image taken from within the database in Datagrip:
image

builder.Entity<MyEntity>()
    .Property(e => e.MyProperty)
    .HasColumnType("jsonb")
    .HasConversion(
        metadata => DatabaseJsonConverter.Serialize(metadata),
        json => DatabaseJsonConverter.Deserialize<Metadata>(json)
    );
Author: armanossiloko
Assignees: -
Labels:

area-System.Text.Json

Milestone: -

@elgonzo
Copy link

elgonzo commented Dec 16, 2023

See this comment and the comment(s) it refers to: #72604 (comment)

@eiriktsarpalis
Copy link
Member

Closing as duplicate of #72604

@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Dec 16, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Jan 16, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants