Skip to content

Type disciminator is missing with OkResult #58832

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

Open
1 task done
vanillajonathan opened this issue Nov 7, 2024 · 8 comments
Open
1 task done

Type disciminator is missing with OkResult #58832

vanillajonathan opened this issue Nov 7, 2024 · 8 comments
Assignees
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates
Milestone

Comments

@vanillajonathan
Copy link
Contributor

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I am using polymorphic JSON which works when returning the class but not when returning Ok(data) because then the $type type discriminator is missing.

Works:

[HttpGet(Name = "Pet")]
public ActionResult<BaseModel> Get()
{
    return new Cat() { CatName = "Mizzy" };
}

Does not work:

[HttpGet(Name = "Pet")]
public ActionResult<BaseModel> Get()
{
    return Ok(new Cat() { CatName = "Mizzy" });
}

Also does not work:

[HttpGet(Name = "Pet")]
public ActionResult<BaseModel> Get()
{
    return Ok(new Cat() { CatName = "Mizzy" } as BaseModel);
}

Expected Behavior

When using the Ok method to return a OkObjectResult it should output a type discriminator.

Steps To Reproduce

using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ExampleController : ControllerBase
    {
        [HttpGet(Name = "GetPet")]
        public ActionResult<BaseModel> Get()
        {
            return Ok(new Cat() { CatName = "Mizzy" } as BaseModel);
        }
    }

    [JsonPolymorphic]
    [JsonDerivedType(typeof(Cat), "cat")]
    [JsonDerivedType(typeof(Dog), "dog")]
    public class BaseModel
    {
        public string Name { get; set; }
    }

    public class Cat : BaseModel
    {
        public string CatName { get; set; }
    }

    class Dog : BaseModel
    {
        public string DogName { get; set; }
    }
}

Exceptions (if any)

No response

.NET Version

8.0.10

Anything else?

No response

@ghost ghost added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Nov 7, 2024
@martincostello martincostello added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates and removed old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Nov 7, 2024
@mikekistler mikekistler self-assigned this Nov 9, 2024
@mikekistler
Copy link
Contributor

Thank you for filing this issue and providing clear and complete information on how to reproduce it. I have reproduced it and done some investigation. I believe that this is happening because when the result value is wrapped in "Ok" the type information is lost / not conveyed to System.Text.Json, which treats is as just a regular object rather than a Cat.

I will continue investigating and post here when I have something concrete.

@mikekistler
Copy link
Contributor

I've spoken with the engineering team and learned a bit more. In particular, there seem to be a couple workarounds for this problem. The first is to explicitly set the DeclaredType on the ObjectResult returned from Ok, like this:

     [HttpGet("fido")]
    public ActionResult<BaseModel> GetFido()
    {
        var result = Ok(new Dog() { DogName = "Fido" });
        result.DeclaredType = typeof(BaseModel);
        return result;
    }

Another workaround is to change the return type of the method to an IResult and use TypedResults to generate the response, like this:

    [HttpGet("snoopy")]
    public Ok<BaseModel> GetSnoopy()
    {
        return TypedResults.Ok(new Dog() { DogName = "Snoopy" } as BaseModel);
    }

Both of these approaches generate a response body with the $type property.

Would one of these approaches work for you?

@vanillajonathan
Copy link
Contributor Author

None of them are pretty, it would be great if it "just worked" without having to use any workarounds. The workarounds are not intuitive if users have to talk to the engineering team to figure it out.

But until then I could do:

[HttpGet("mizzy")]
public ActionResult<BaseModel> GetMizzy()
{
    var pet = new Cat() { CatName = "Mizzy" };
    return Ok(pet) { DeclaredType = typeof(BaseModel) };
}

Maybe the Ok method could have generic overload so I could do return Ok<Cat>(pet);.

@BrennanConroy
Copy link
Member

The root cause is that the T from ActionResult<T> isn't being used when returning an ActionResult (non-T), Ok is an ActionResult.

The following code is where we convert the ActionResult<T> into an IActionResult for processing, and you'll notice that the first thing it does it just return the ActionResult directly if it has one, otherwise it'll create an ObjectResult and use typeof(TValue) for the DeclaredType field.

IActionResult IConvertToActionResult.Convert()

So in the case of returning an ActionResult, the T isn't being used anywhere which means when we write the Json all we see if object.GetType() which will be Cat or Dog so will be written as a non-polymorphic object.

@vanillajonathan
Copy link
Contributor Author

Is it feasible for ASP.NET to somehow change this in a way that polymorphism just works, or is setting the DeclaredType property the recommended way for users do accomplish polymorphism?

@mihtjel
Copy link

mihtjel commented May 12, 2025

Any updates on this? Or at least, whether it's something that might be addressed, or is unlikely to change :-)

@mihtjel
Copy link

mihtjel commented May 15, 2025

Maybe the Ok method could have generic overload so I could do return Ok<Cat>(pet);.

I thought I would try out this solution for my instance of running into this issue, and I put this method in a derived base class for controllers (Please excuse the code quality :D):

    [NonAction]
    public virtual OkObjectResult Ok<TValue>([ActionResultObjectValue] TValue value)
    {
        var r = base.Ok(value);
        r.DeclaredType = typeof(TValue);
        return r;
    }

What I had not realized was that this did not require me to use Ok<BaseType>, if the variable I passed as parameter already had the base type declared; that is, it simply made my code work without any other changes. 🤷‍♂️ I don't know if this causes issues in other cases, but it looks like it may be a fairly simple fix otherwise?

@mikekistler mikekistler added this to the Backlog milestone May 15, 2025
@mikekistler
Copy link
Contributor

Thanks for the gentle poke on this. It had slipped off the radar. I added it to our Backlog milestone as something to consider as resources become available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates
Projects
None yet
Development

No branches or pull requests

5 participants