Search code examples
c#asp.net-corejson.net.net-6.0minimal-apis

How to configure NewtonsoftJson with Minimal API in .NET 6.0


I have net6.0 project with minimal api and I would like to use NetwtonsoftJson instead of built in System.Text.Json library for serialization and deserialization.

At the moment I have this configurations for JsonOptions and that works as expected

builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    options.SerializerOptions.WriteIndented = true;    
    options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
    options.SerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
});

If I try to change to something equivalent that uses Newtonsoft.Json.JsonSerializerSettings like below I am not getting same behavior. Instead it looks like it uses default System.Text.Json configuration.

builder.Services.Configure<JsonSerializerSettings>(options =>
{
    options.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
    options.Converters.Add(
        new StringEnumConverter
        {
            NamingStrategy = new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy()
        });
});

In net5.0 I know I could use this

services.AddControllers().AddNewtonsoftJson((options) => //options); // OR
services.AddMvc().AddNewtonsoftJson((options) => //options);

However, if I use it like above in my net6.0 project then I am not using anymore MinimalApi ?


Solution

  • As mentioned in the docs:

    The body binding source uses System.Text.Json for deserialization. It is not possible to change this default

    So I would recommend to just use System.Text.Json. But if really needed - there are workarounds you can try.

    From my understanding Minimal APIs rely on some conventions regarding type binding. From what I can see they search for method with next signature - ValueTask<TModel?> BindAsync(HttpContext context, ParameterInfo parameter) on the type otherwise will try to use httpContext.Request.ReadFromJsonAsync which internally uses System.Text.Json and that can't be changed, so services.Add...().AddNewtonsoftJson((options) => //options); approach will not work.

    To use Newtonsoft.Json you can try next (other than directly handling request via app.MapPost("/pst", (HttpContext c) => c.Request...)):

    If you have control over all your classes which needs to be deserialized using it you can inherit them all from some generic base class which will have the method with needed signature (also you can use interface with implemented static method):

    public class BaseModel<TModel>
    {
        public static async ValueTask<TModel?> BindAsync(HttpContext context, ParameterInfo parameter)
        {
            if (!context.Request.HasJsonContentType())
            {
                throw new BadHttpRequestException(
                    "Request content type was not a recognized JSON content type.",
                    StatusCodes.Status415UnsupportedMediaType);
            }
    
            using var sr = new StreamReader(context.Request.Body);
            var str = await sr.ReadToEndAsync();
            
            return JsonConvert.DeserializeObject<TModel>(str);
        }
    }
    

    And usage:

    class PostParams : BaseModel<PostParams>
    {
        [JsonProperty("prop")]
        public int MyProperty { get; set; }
    }
    
    // accepts json body {"prop": 2}
    app.MapPost("/pst", (PostParams po) => po.MyProperty);
    

    Note that BaseModel<TModel> implemenation in this example is quite naive and possibly can be improved (check out HttpRequestJsonExtensions.ReadFromJsonAsync at least).

    If you don't have control over the models or don't want to inherit them from some base you can look into creating wrappers:

    public class Wrapper<TModel>
    {
        public Wrapper(TModel? value)
        {
            Value = value;
        }
    
        public TModel? Value { get; }
    
        public static async ValueTask<Wrapper<TModel>?> BindAsync(HttpContext context, ParameterInfo parameter)
        {
            if (!context.Request.HasJsonContentType())
            {
                throw new BadHttpRequestException(
                    "Request content type was not a recognized JSON content type.",
                    StatusCodes.Status415UnsupportedMediaType);
            }
    
            using var sr = new StreamReader(context.Request.Body);
            var str = await sr.ReadToEndAsync();
    
            return new Wrapper<TModel>(JsonConvert.DeserializeObject<TModel>(str));
        }
    }
    

    And usage changes to:

    class PostParams
    {
        [JsonProperty("prop")]
        public int MyProperty { get; set; }
    }
    
    // accepts json body {"prop": 2}
    app.MapPost("/pst", (Wrapper<PostParams> po) => po.Value.MyProperty);
    

    Some extra useful links:

    • MVC model binders - by David Fowler. Though I was not able to make it work for services.AddControllers().AddNewtonsoftJson((options) => //options);
    • ParameterBinder - similar approach by Damian Edwards