dotnet / dotnet/aspnetcore

ObjectResult - missing discriminator from System.Text.Json polymorphism

Open
#57,482 7 comments 7 reactions 0 assignees View on GitHub
area-mvc
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

### Is there an existing issue for this?

- [X] I have searched the existing issues

### 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.

```csharp
[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; }
}
```

```csharp
[HttpGet("[action]")]
public ActionResult GetRandomPerson()
{
Person person = GetPerson(); // returns either a PrivatePerson or LegalPerson
return Ok(person);
}
```

JSON output:
```json
{
"firstName": "Test",
"id": 1
}
```

Note how there is **no discriminator**

Expected JSON output:
```json
{
"$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:
```csharp
[HttpGet("[action]")]
public ActionResult Ok1()
{
Person person = GetPerson();
return new OkObjectResult(person) { DeclaredType = typeof(Person) };
}

[HttpGet("[action]")]
public ActionResult 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_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.