2

I have a .NET Core 3.1 controller derived from Microsoft.AspNetCore.Mvc.ControllerBase, the custom json converter is not called on result output.

IProduct

public interface IProduct { ... }

Controller:

[HttpGet("{id}")]
public IProduct Get(string id)
{
   IProduct product = _data.GetProduct(id);
   return product;
}

[HttpPut]
public Task Save(IProduct product)
{
    return _data.Save(product);
}

JsonConverter:

public class ProductConverter : System.Text.Json.Serialization.JsonConverter<IProduct> 
{
    public override IProduct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
         // this code works from unit tests, trust me :)
    }

    public override void Write(Utf8JsonWriter writer, IProduct value, JsonSerializerOptions options) 
    {
        // this code works from unit tests, trust me :)
    }
}

Startup:

services
  .AddControllers()
  .AddJsonOptions(options => {
      options.JsonSerializerOptions.Converters.Add(new ProductConverter());
  });

Symptoms

  • When controller's action method Save is called, the ProductConverter.Read is called
  • When controller's action method Get is called, the ProductConverter.Write method is not called
  • I get json result of actual implementation of IProduct

Am I missing something or doing something wrong?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • 1
    Have you ran it under your debugger? Are your breakpoints actually getting hit? – mxmissile May 14 '21 at 13:35
  • breaking at controller in both methods, breaking in read method of converter, but not in write method – Kirill Shilin May 14 '21 at 13:37
  • This answer is old but I think it still applies: [Why when return interface in a web api method, I get the values of inherited class with interface values?](https://stackoverflow.com/a/19653031/3744182): *By the time it hits the Json serializer, it doesn't care what the return type of the method was. All it knows is "I have this object, let's see what I can do with it".* ... – dbc May 14 '21 at 13:43
  • Your converter isn't called because the actual, concrete type being returned isn't `IProduct`, so [`JsonConverter.CanConvert(objectType)`](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs#L53) returns `false`. – dbc May 14 '21 at 13:45
  • Try overriding `public override bool CanConvert(Type typeToConvert) => typeof(IProduct).IsAssignableFrom(typeToConvert);` – dbc May 14 '21 at 13:47
  • tried. but due to logic in ProductConverter that defines the actual type first and then calls JsonSerializer.Serialize(writer, actualTypeToConvert, actualProduct, OPTIONS) causes stackoverflow, unless i remove converter itself from the options) – Kirill Shilin May 14 '21 at 13:57
  • Thanks for the answers, everyone! Would appriciate other ideas – Kirill Shilin May 14 '21 at 13:57
  • 1
    @KirillShilin - Well that actually shows that overriding `CanConvert` answers your question. You didn't include implementations for `Read()` and `Write()` in your question, but to resolve the stack overflow, see [How to use default serialization in a custom System.Text.Json JsonConverter?](https://stackoverflow.com/q/65430420/3744182). Basically, cloning the `options` and removing the converter is the way to go. – dbc May 14 '21 at 14:00
  • _cloning the `options` and removing the converter..._ is the only solution i came with so far. Also considering creating custom output filter with ability to specify the converter explicitly. – Kirill Shilin May 14 '21 at 14:04
  • You could also try wrapping the `IProduct` in some wrapper with the converter applied to the property, e.g. `public class ProductContainer { [JsonConverter(typeof(ProductConverter))] public IProduct Product { get; set; } }` – dbc May 14 '21 at 14:08

1 Answers1

2

This answer by Damien_The_Unbeliever to Why when return interface in a web api method, I get the values of inherited class with interface values? is old but apparently still applies to ASP.NET Core 3.1:

By the time it hits the Json serializer, it doesn't care what the return type of the method was. All it knows is "I have this object, let's see what I can do with it".

Thus your converter isn't called because the actual, concrete type being returned isn't IProduct and so the base class implementation JsonConverter<IProduct>.CanConvert(objectType) returns false:

public override bool CanConvert(Type typeToConvert)
{
    return typeToConvert == typeof(T);
}

To make your ProductConverter apply to concrete implementations of IProduct as well, override CanConvert as follows:

public override bool CanConvert(Type typeToConvert) => 
    typeof(IProduct).IsAssignableFrom(typeToConvert);

Note that, if the implementations of Read() and Write() try to generate a "default" serialization of the incoming concrete product by calling the serializer recursively with the concrete type, you may get a stack overflow. To resolve that, you could:

dbc
  • 104,963
  • 20
  • 228
  • 340