New to Telerik Reporting? Request free 30-day trial

Use Both 'System.Text.Json' and 'Newtonsoft.Json' for Serialization in ASP.NET Core

Environment

Product Progress® Telerik® Reporting

Description

A common requirement is to use System.Text.Json for serialization, instead of the required by the Reporting REST Service serialization package Newtonsoft.Json.

Suggested Workarounds

Global

The most suitable workarounds are explained in the feature request Migrate to System.Text.Json for serialization, instead of using Newtonsoft.Json.

In this article, you may find a link to a sample with the implementation suggested in the Stackoverflow thread How to configure two JSON serializers and select the correct one based on the route.

The sample project may be downloaded from our reporting-samples Github repository Two Json Serializers.

To ensure that the approach works, you may put break points in the conditional statements for the two formatters in the method ReadRequestBodyAsync or WriteResponseBodyAsync of the Controllers\CustomJsonFormatters.cs file. The Newtonsoft.Json formatter should be hit when the Reporting REST Service is called by the viewer or by calling manually the service, for example, at the ~/api/reports/version. The System.Text.Json formatter should be used when calling the Values controller, for example, at the end-point ~/api/values.

Here is the code of the class that is not implemented in the Stackoverflow thread:

internal class MySuperJsonOutputFormatter : TextOutputFormatter
{
    public MySuperJsonOutputFormatter()
    {
        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
        SupportedMediaTypes.Add("application/json");
    }

    public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var httpContext = context.HttpContext;
        var mvcOpt = httpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value;
        var formatters = mvcOpt.OutputFormatters;
        TextOutputFormatter formatter = null;

        Endpoint endpoint = httpContext.GetEndpoint();
        if (endpoint.Metadata.GetMetadata<UseSystemTextJsonAttribute>() != null)
        {
            formatter = formatters.OfType<SystemTextJsonOutputFormatter>().FirstOrDefault();
        }
        else if (endpoint.Metadata.GetMetadata<UseNewtonsoftJsonAttribute>() != null)
        {
            // don't use `Of<NewtonsoftJsonInputFormatter>` here because there's a NewtonsoftJsonPatchInputFormatter
            formatter = (NewtonsoftJsonOutputFormatter)(formatters
                .Where(f => typeof(NewtonsoftJsonOutputFormatter) == f.GetType())
                .FirstOrDefault());
        }
        else
        {
            throw new Exception("This formatter is only used for System.Text.Json InputFormatter or NewtonsoftJson InputFormatter");
        }

        await formatter.WriteResponseBodyAsync(context, selectedEncoding);
    }
}

Local

To use the above solutions solution you would need to configure the JSON settings/options and MvcOptions globally in the Startup class.

If you don't want to configure serialization options globally, you can create ServiceFilterAttribute, which you can attach to any controller that need to use a different JSON serialization.

It will take the result object of any endpoint, use the Newtonsoft serializer to produce a JSON string, and replace the endpoint result with that.

public class ToNewtonsoftActionFilter : IAsyncResultFilter 
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) 
    {
        if (context.Result is JsonResult jsonResult) 
        {
            string jsonStr = Newtonsoft.Json.JsonConvert.SerializeObject(jsonResult.Value, (Newtonsoft.Json.JsonSerializerSettings?)jsonResult.SerializerSettings ?? new Newtonsoft.Json.JsonSerializerSettings {
                ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver { NamingStrategy = new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy() }
            });

            context.Result = new ContentResult { Content = jsonStr, ContentType = "application/json" };
        }

        await next().ConfigureAwait(false);
    }
}
[ServiceFilter(typeof(ToNewtonsoftActionFilter))]
public class ReportDesignerControllerSTJBase : ReportDesignerControllerBase

You would also need to create ModelBinderAttribute attached which will cause the objects to be deserialized by Newtonsoft.

public class NewtonsoftJsonModelBinder : IModelBinder 
{
    public async Task BindModelAsync(ModelBindingContext bindingContext) 
    {
        if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));

        using var reader = new StreamReader(bindingContext.HttpContext.Request.Body);

        string body = await reader.ReadToEndAsync().ConfigureAwait(continueOnCapturedContext: false);
        object? value = Newtonsoft.Json.JsonConvert.DeserializeObject(body, bindingContext.ModelType);
        bindingContext.Result = ModelBindingResult.Success(value);
    }
}
public virtual new IActionResult GetMembers([ModelBinder(typeof(NewtonsoftJsonModelBinder))] TypeInfoWithFilter input) => base.GetMembers(input);

Finally, you would need to register ToNewtonsoftActionFilter as a Singleton service in the Program.cs but the usage is completely confined to the specific controllers.

builder.Services.AddSingleton<ToNewtonsoftActionFilter>();

The sample project may be downloaded from our reporting-samples Github repository Two Json Serializers Local.

See Also

In this article