dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.42k stars 10.01k forks source link

ObjectResult - missing discriminator from System.Text.Json polymorphism #57482

Open yesmey opened 2 months ago

yesmey commented 2 months ago

Is there an existing issue for this?

Describe the bug

We have encountered some odd behaviour when migrating our code from Newtonsoft.Json to System.Text.Json, and the scenario doesn't seem to be well documented.

[JsonDerivedType(typeof(LegalPerson), nameof(LegalPerson))]
[JsonDerivedType(typeof(PrivatePerson), nameof(PrivatePerson))]
public abstract class Person
{
    public int Id { get; set; }
}

public class PrivatePerson : Person
{
    public string? FirstName { get; set; }
}

public class LegalPerson : Person
{
    public string? ContractName { get; set; }
}
[HttpGet("[action]")]
public ActionResult<Person> GetRandomPerson()
{
    Person person = GetPerson(); // returns either a PrivatePerson or LegalPerson
    return Ok(person);
}

JSON output:

{
  "firstName": "Test",
  "id": 1
}

Note how there is no discriminator

Expected JSON output:

{
  "$type": "PrivatePerson",
  "firstName": "Test",
  "id": 1
}

The issue is that Ok(object?) becomes new OkObjectResult(object?) { DeclaredType = null }. Later during the serialization, DeclaredType = person.GetType(), which in this case ends up being either typeof(PrivatePerson) or typeof(LegalPerson).

Since we are not serializing a Person, but the underlying type, the discriminator is not included. Our contract clearly state that this endpoint returns a Person, but there is no way for the consumer to know how to deserialize the result without any discriminator.

The same issue applies for other ObjectResults such as Created, CreatedAtResult etc.

The solution is resolved if we write the endpoints like this:

[HttpGet("[action]")]
public ActionResult<Person> Ok1()
{
    Person person = GetPerson();
    return new OkObjectResult(person) { DeclaredType = typeof(Person) };
}

[HttpGet("[action]")]
public ActionResult<Person> Ok2()
{
    Person person = GetPerson();
    return person;
}

However, the documentation found in https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-8.0 doesn't seem to mention this.

Expected Behavior

The expected behaviour is for the discriminator to be included. Newtonsoft always seems to include the discriminator, but there is no way for us to enable that in System.Text.Json. Another potential solution would be for ObjectResult to take a generic type argument to resolve DeclaredType = typeof(T).

Steps To Reproduce

https://github.com/yesmey/PolymorphismBug

Exceptions (if any)

No response

.NET Version

8.0.400 and 9.0.100-preview.7

Anything else?

No response

yesmey commented 2 months ago

I found that this issue has been raised before in different context such as #44852 but from what I can tell, the comments implies that this should have been resolved already.