Different value of NullValueHandling for serialize and deserialize - c#

I have a .net core 3.1 application. I use the library json.net (newtonsoft) to serialize or deserialize the json . This is the app settings for newtonsoft :
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.SuppressAsyncSuffixInActionNames = false;
}).AddNewtonsoftJson(options =>
{
options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Local;
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
options.SerializerSettings.Converters.Add(new GuidJsonConverter());
});
I've put this line to ignore null json value on deserialization :
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
But I remark that it ignores also null value for serialization (when use Json method of the class Microsoft.AspNetCore.Mvc.Controller), but I don't want this behavior.
Is there a way to specify differents value of NullValueHandling for serialization and for deserialization ?

Finally I opt for this solution:
I made a BaseController class which inherits from Microsoft.AspNetCore.Mvc.Controller. I have inherited each of my controllers from this BaseController class.
In this class, I override the Microsoft.AspNetCore.Mvc.Controller.Json method :
public class BaseController : Controller
{
private readonly JsonSerializerSettings _jsonSerializerSettings;
public BaseController(IServiceProvider services)
{
IOptions<MvcNewtonsoftJsonOptions> newtonsoftOptions = services.GetService<IOptions<MvcNewtonsoftJsonOptions>>();
_jsonSerializerSettings = newtonsoftOptions.Value.SerializerSettings;
_jsonSerializerSettings.NullValueHandling = NullValueHandling.Include;
}
public override JsonResult Json(object data)
{
return Json(data, _jsonSerializerSettings);
}
Thanks to IOptions<MvcNewtonsoftJsonOptions> I was able to recover the serializer settings initialized in the startup.
EDIT
I remarks that the change of value _jsonSerializerSettings.NullValueHandling = NullValueHandling.Include; change also the init serializer settings.
So I've made an extensions method to copy all data of serializer settings in aim to update just the new settings :
public CustomerAccountController(IServiceProvider services)
{
IOptions<MvcNewtonsoftJsonOptions> newtonsoftOptions = services.GetService<IOptions<MvcNewtonsoftJsonOptions>>();
_jsonSerializerSettings = newtonsoftOptions.Value.SerializerSettings.CloneJsonSerializerSettings();
_jsonSerializerSettings.NullValueHandling = NullValueHandling.Include;
}
public static JsonSerializerSettings CloneJsonSerializerSettings(this JsonSerializerSettings settings)
{
JsonSerializerSettings cloneSettings = new JsonSerializerSettings();
cloneSettings.StringEscapeHandling = settings.StringEscapeHandling;
cloneSettings.FloatParseHandling = settings.FloatParseHandling;
cloneSettings.FloatFormatHandling = settings.FloatFormatHandling;
cloneSettings.DateParseHandling = settings.DateParseHandling;
cloneSettings.DateTimeZoneHandling = settings.DateTimeZoneHandling;
cloneSettings.DateFormatHandling = settings.DateFormatHandling;
cloneSettings.Formatting = settings.Formatting;
cloneSettings.MaxDepth = settings.MaxDepth;
cloneSettings.DateFormatString = settings.DateFormatString;
cloneSettings.Context = settings.Context;
cloneSettings.Error = settings.Error;
cloneSettings.SerializationBinder = settings.SerializationBinder;
cloneSettings.Binder = settings.Binder;
cloneSettings.TraceWriter = settings.TraceWriter;
cloneSettings.Culture = settings.Culture;
cloneSettings.ReferenceResolverProvider = settings.ReferenceResolverProvider;
cloneSettings.EqualityComparer = settings.EqualityComparer;
cloneSettings.ContractResolver = settings.ContractResolver;
cloneSettings.ConstructorHandling = settings.ConstructorHandling;
cloneSettings.TypeNameAssemblyFormatHandling = settings.TypeNameAssemblyFormatHandling;
cloneSettings.TypeNameAssemblyFormat = settings.TypeNameAssemblyFormat;
cloneSettings.MetadataPropertyHandling = settings.MetadataPropertyHandling;
cloneSettings.TypeNameHandling = settings.TypeNameHandling;
cloneSettings.PreserveReferencesHandling = settings.PreserveReferencesHandling;
cloneSettings.Converters = settings.Converters;
cloneSettings.DefaultValueHandling = settings.DefaultValueHandling;
cloneSettings.NullValueHandling = settings.NullValueHandling;
cloneSettings.ObjectCreationHandling = settings.ObjectCreationHandling;
cloneSettings.MissingMemberHandling = settings.MissingMemberHandling;
cloneSettings.ReferenceLoopHandling = settings.ReferenceLoopHandling;
cloneSettings.ReferenceResolver = settings.ReferenceResolver;
cloneSettings.CheckAdditionalContent = settings.CheckAdditionalContent;
return cloneSettings;
}

Related

AddJsonOptions doesn't work in Blazor Server

I want to add global JsonSerializer options to use ReferenceHandler.Preserve, i can't configure my blazor server App to use it as a global setting for all json Serializers.
i used
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
options.SerializerOptions.PropertyNameCaseInsensitive = true;
});
builder.Services.AddRazorPages().AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
builder.Services.Configure<JsonOptions>(o =>
{
o.SerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
o.SerializerOptions.PropertyNameCaseInsensitive = true;
});
none of them works as expected the options doesn't change from the defaults and i keep getting the same exception: "The JSON value could not be converted to"
using the same options at each request works
var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, PropertyNameCaseInsensitive = true };
var httpClient = _httpFactory.CreateClient("API");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _tokenProvider.JwtToken);
var result = await httpClient.GetFromJsonAsync<List<Manufacturer>>("manufacturer", options);
but i want to define the options for all requests without explicitly writing them each time.
Instead of Named Client you could use Typed Client (or even generated with NSwag or Refit) and handle JSON formatting options inside this typed API client.
E.g. NSwag API clients generator has an option to generate UpdateJsonSerializerSettings method which you can define in the base class for type dAPI client like:
internal class BaseClient
{
protected static void UpdateJsonSerializerSettings(JsonSerializerOptions settings) => settings.ConfigureDefaults();
}
// generated API client:
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.16.1.0 (NJsonSchema v10.7.2.0 (Newtonsoft.Json v13.0.0.0))")]
internal partial class SomeClient : BaseClient, ISomeClient
{
private System.Net.Http.HttpClient _httpClient;
private System.Lazy<System.Text.Json.JsonSerializerOptions> _settings;
public ActionsClient(System.Net.Http.HttpClient httpClient)
{
_httpClient = httpClient;
_settings = new System.Lazy<System.Text.Json.JsonSerializerOptions>(CreateSerializerSettings);
}
private System.Text.Json.JsonSerializerOptions CreateSerializerSettings()
{
var settings = new System.Text.Json.JsonSerializerOptions();
UpdateJsonSerializerSettings(settings);
return settings;
}
... the rest of generated code has omitted
}
// Extension which configures the default JSON settings (as an example):
public static class JsonExtensions
{
private static JsonSerializerOptions GetDefaultOptions() => new() { WriteIndented = true };
public static JsonSerializerOptions ConfigureDefaults(this JsonSerializerOptions? settings)
{
settings ??= GetDefaultOptions();
settings.PropertyNameCaseInsensitive = true;
settings.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
settings.NumberHandling = JsonNumberHandling.AllowReadingFromString;
settings.Converters.Add(new JsonStringEnumConverter());
return settings;
}
}
Then you register on DI your ISomeClient typed client like this:
builder.Services
.AddScoped<MyLovelyAuthorizationMessageHandler>()
.AddHttpClient<ISomeClient, SomeClient>(httpClient =>
{
httpClient.BaseAddress = new Uri(apiSettings.WebApiBaseAddress);
// ...etc.
}).AddHttpMessageHandler<MyLovelyAuthorizationMessageHandler>();
And then - inject ISomeClient where you need it, and call methods with typed DTOs keeping all JSON serialization/deserialization magic under the carpet.
[Inject] private ISomeClient Client {get; set;} = default!;
Documentation: about Typed Clients

Generate json key as snake_case with RestSharp

Any idea how to generate json key as snake_case with RestSharp?
With Newtonsoft.Json, I can set json output something like this
DefaultContractResolver contractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
};
string json = JsonConvert.SerializeObject(requestData, new JsonSerializerSettings
{
ContractResolver = contractResolver,
Formatting = Formatting.Indented
});
But I not sure how can be done with RestSharp
var client = new RestClient(getService.MstUrl);
client.AddDefaultHeader("Authorization", string.Format("Bearer {0}", token));
var request = new RestRequest(Method.POST).AddJsonBody(requestData);
var response = await client.ExecuteAsync(request);
It keep generate as camelCase. Is there any configuration like Newtonsoft.Json?
Using RestSharp, does not mean you can't use the Newtonsoft serializer as well.
From the restsharp documentation:
RestSharp support Json.Net serializer via a separate package. You can install it from NuGet.
client.UseNewtonsoftJson();
And keep on going with what you did:
DefaultContractResolver contractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
};
You should be able to create your own IRestSerializer implementation and supply that in client.UseSerializer
Given
public class SimpleJsonSerializer : IRestSerializer
{
private readonly DefaultContractResolver _contractResolver;
public SimpleJsonSerializer()
{
_contractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
};
}
public string Serialize(object obj) => JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
ContractResolver = _contractResolver,
Formatting = Formatting.Indented
});
public string Serialize(Parameter bodyParameter) => Serialize(bodyParameter.Value);
public T Deserialize<T>(IRestResponse response) => JsonConvert.DeserializeObject<T>(response.Content);
public string[] SupportedContentTypes { get; } =
{
"application/json", "text/json", "text/x-json", "text/javascript", "*+json"
};
public string ContentType { get; set; } = "application/json";
public DataFormat DataFormat { get; } = DataFormat.Json;
}
Usage
var client = new RestClient(getService.MstUrl);
client.AddDefaultHeader("Authorization", string.Format("Bearer {0}", token));
client.UseSerializer(() => new SimpleJsonSerializer(){});
var request = new RestRequest(Method.POST).AddJsonBody(requestData);
var response = await client.ExecuteAsync(request);
Disclaimer : I haven't used RestSharp in years and I never intend to use it again. Further more, I really do suggest to anyone thinking of using it to switch back to HttpClient, or even better IHttpClientFactory. More so, this is completely untested, and only given as an example. You may need to modify it to it your needs

MVC - How to deserialize a Dictionary without having brackets [0] added to the key?

my deserialize Dictionary's key results in "brand[0]" when I send in "brand" to the api.
I have a class like this:
public class SearchRequest
{
public bool Html { get; set; } = false;
public Dictionary<string, HashSet<string>> Tags { get; set; }
}
// MVC Controller
[HttpPost]
public ActionResult Index(SearchRequest searchRequest)
{
...
}
And a json request like this that I post to the controller:
{
"html": true,
"tags": {
"brand": [
"bareminerals"
]
}
}
The binding seams to work and the searchRequest object is created but the resulting dictionary dose not have the key "brand" in it but insted the key "brand[0]" how can I preserve the real values I send in?
Edit: I need tags to be able to contain multiple tags, with multiple options, this was a simpel example.
One soulution to my problem is to create a custom model bind, so this is what am using now, but I dont understand why I need to, and I feel like there should be a easyer way? But am gonna leve It here anyhow.
public class FromJsonBodyAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new JsonModelBinder();
}
private class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stream = controllerContext.HttpContext.Request.InputStream;
stream.Position = 0;
using (var reader = new StreamReader(stream))
{
var checkoutOrderDataStr = reader.ReadToEnd();
return JsonConvert.DeserializeObject(checkoutOrderDataStr, bindingContext.ModelType);
}
}
}
}
I'm not sure what is going on with your setup. You should not need a custom binder. I still think the problem is most likely with your calling code - whatever you're using as a client.
I'm using Asp.net Core 3.1. Here's what I threw together as a quick test.
Created Asp.net Core web application template with MVC. I declared two classes - a request POCO and a result POCO. The request was your class:
public class SearchRequest
{
public bool Html { get; set; } = false;
public Dictionary<string, HashSet<string>> Tags { get; set; }
}
The result was the same thing with a datetime field added just for the heck of it:
public class SearchResult : SearchRequest
{
public SearchResult(SearchRequest r)
{
this.Html = r.Html;
this.Tags = r.Tags;
}
public DateTime RequestedAt { get; set; } = DateTime.Now;
}
I Added a simple post method on the default HomeController.
[HttpPost]
public IActionResult Index([FromBody] SearchRequest searchRequest)
{
return new ObjectResult(new SearchResult(searchRequest));
}
I added a console Application to the solution to act as a client. I copied the two class definitions into that project.
I added this as the main method. Note you can either have the camel casing options on the request or not - asp.net accepted either.
static async Task Main(string[] _)
{
var tags = new[] { new { k = "brand", tags = new string[] { "bareminerals" } } }
.ToDictionary(x => x.k, v => new HashSet<string>(v.tags));
var request = new SearchRequest() { Html = true, Tags = tags };
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var json = JsonSerializer.Serialize(request, options);
Console.WriteLine(json);
using (var client = new HttpClient())
{
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("http://localhost:59276", content);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SearchResult>(data, options);
Console.WriteLine(data);
var keysSame = Enumerable.SequenceEqual(request.Tags.Keys, result.Tags.Keys);
var valuesSame = Enumerable.SequenceEqual(request.Tags.Values.SelectMany(x => x),
result.Tags.Values.SelectMany(x=>x));
Console.WriteLine($"Keys: {keysSame} Values: {valuesSame}");
}
}
This outputs:
{"html":true,"tags":{"brand":["bareminerals"]}}
{"requestedAt":"2020-10-30T19:22:17.8525982-04:00","html":true,"tags":{"brand":["bareminerals"]}}
Keys: True Values: True

Lowercase Json result in ASP.NET MVC 5

When my json gets returned from the server, it is in CamelCase, but I need lowercase. I have seen a lot of solutions for ASP.NET Web API and Core, but nothing for ASP.NET MVC 5.
[HttpGet]
public JsonResult Method()
{
var vms = new List<MyViewModel>()
{
new MyViewModel()
{
Name = "John Smith",
}
};
return Json(new { results = vms }, JsonRequestBehavior.AllowGet);
}
I want "Names" to be lowercase.
The best solution I have for this is to override the default Json method to use Newtonsoft.Json and set it to use camelcase by default.
First thing is you need to make a base controller if you don't have one already and make your controllers inherit that.
public class BaseController : Controller {
}
Next you create a JsonResult class that will use Newtonsoft.Json :
public class JsonCamelcaseResult : JsonResult
{
private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new List<JsonConverter> { new StringEnumConverter() }
};
public override void ExecuteResult(ControllerContext context)
{
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = !String.IsNullOrEmpty(this.ContentType) ? this.ContentType : "application/json";
response.ContentEncoding = this.ContentEncoding ?? response.ContentEncoding;
if (this.Data == null)
return;
response.Write(JsonConvert.SerializeObject(this.Data, _settings));
}
}
Then in your BaseController you override the Json method :
protected new JsonResult Json(object data)
{
return new JsonCamelcaseResult
{
Data = data,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
So in the end in your original action returning JSON you just keep it the same and the properties will be camelcased (propertyName) instead of pascalcased (PropertyName) :
[HttpGet]
public JsonResult Method()
{
var vms = new List<MyViewModel>()
{
new MyViewModel()
{
Name = "John Smith",
}
};
return Json(new { results = vms });
}

Force SnakeCase Deserializing on WebAPI Per Controller

i want to change the incoming requests deserializing format just for one of my controllers. so i added this in to my Global.asax and it works just fine:
HttpConfiguration config = GlobalConfiguration.Configuration;
config.Formatters.JsonFormatter.SerializerSettings = new Newtonsoft.Json.JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
};
but it apply the changes to all of the controllers. i just want apply it for one of my controllers. i also found this answer and i wrote this code according to that:
public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
{
var formatter = controllerSettings.Formatters.OfType<JsonMediaTypeFormatter>().Single();
controllerSettings.Formatters.Remove(formatter);
formatter = new JsonMediaTypeFormatter
{
SerializerSettings = { ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() } }
};
controllerSettings.Formatters.Add(formatter);
}
but unfortunately it works just for serializing the outputs. is there a way to define it for deserializing inputs?
You can do what you need with a tricky media type formatter. Usually custom formatter overrides methods CanReadType() / CanWriteType() and ReadFromStreamAsync() / WriteToStreamAsync(). CanWriteType() in your case should always return false since you are not intersted in customizing serialization. As regards deserialization you could use standard JsonMediaTypeFormatter (through inheritance or aggregation) and set its SerializerSettings to use SnakeCaseNamingStrategy:
public class SnakeCaseJsonFormatter : JsonMediaTypeFormatter
{
public SnakeCaseJsonFormatter()
{
SerializerSettings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new SnakeCaseNamingStrategy()
}
};
}
public override bool CanWriteType(Type type)
{
return false;
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
throw new NotImplementedException();
}
}
Remaining part is applying of such custom formatter on controller level. You could do this with a custom attribute implementing IControllerConfiguration interface. In Initialize() method set your custom formatter at first position so that it takes precedence over standard JsonMediaTypeFormatter. You should not remove standard JsonMediaTypeFormatter because it will handle data serializing:
public sealed class SnakeCaseNamingAttribute : Attribute, IControllerConfiguration
{
public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
{
controllerSettings.Formatters.Insert(0, new SnakeCaseJsonFormatter());
}
}
Now just apply this attribute on controller you want and voila:
[SnakeCaseNaming]
public class ValuesController : ApiController

Categories