12

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 ?

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
thug_
  • 893
  • 2
  • 11
  • 31

1 Answers1

16

As mentioned in the docs:

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

But there are workarounds.

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
harvzor
  • 2,832
  • 1
  • 22
  • 40
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Guru Stron: does this mean that `services.AddControllers().AddNewtonsoftJson(..)` would still work for MVC (Web Api) controllers when they're used along with minimal apis ? i.e. can we use controller types as an escape hatch to use newtonsoft json as an alternative to adding methods to types or using wrappers? – GrumpyRodriguez Sep 03 '22 at 17:14
  • 1
    @GrumpyRodriguez yes, for controllers and MVC nothing has changed, AFAIK – Guru Stron Sep 03 '22 at 23:21
  • Thanks a lot. I thought I'd have to stick to using the previous hosting model (GenericHost) and jump through hoops to not to lose the option to use Newtonsoft json, then I realised new hosting model is not the same as minimal api only. – GrumpyRodriguez Sep 04 '22 at 09:12
  • 1
    @GrumpyRodriguez yes, exactly. Minimal hosting model nowadays is default both for Minimal APIs and ordinary ASP.NET Core and it should cover most of the scenarios allowed by generic hosting model. Was glad to help! – Guru Stron Sep 04 '22 at 09:24
  • Thank you @GuruStron, its best solution I came across for using NewtonsoftJson with MinimalApi in .net core 6. – Tejasvi Hegde Oct 17 '22 at 08:38