Issue with Deserialization of Enum to string using System.Text.JSON - c#

I am currently using .NET 6 and System.Text.Json for serialization. I am having issues where system.text.json does not deserialize the enum to string properly when I return the response using OkObjectResult or ObjectResult
I have used the following on model
public class Customer
{
public string? id { get; set; }
public string? Name { get; set; }
public string Address { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public CustomerType Type {get; set;}
}
using System.Text.Json.Serialization;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CustomerType
{
NEW,
EXISTING
}
Now the API Code
public async Task<IActionResult> GetCustomerById(string Id)
{
var results = await _customerService.GetData(Id);
// The results have correct JSON data with enum as string but as soon as I pass this to OkObjectResult, it changes the enum back to int
return new OkObjectResult(results );
}
Service
public async Task<Customer> GetData(string Id)
{
var results = await _httpClient.SendAsync(Id); // Get Data
var jsonResults = await results.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions{ Converters ={
new JsonStringEnumConverter()};
return JsonSerializer.Deserialize<Customer>(jsonResults ,
options ) ; // This returns the data correctly
}
Now my question why OkObjectResult breaks the code and return the integer instead of enum string value

You need to introduce the enum converter to ASP.NET in your startup code, like this:
services.AddJsonOptions(options =>
{
var converter = new JsonStringEnumConverter();
options.JsonSerializerOptions.Converters.Add(converter);
});

Related

ASP.NET Web API [FromForm] custom binding for list of objects property does not work from Postman, but does with Swagger

I have a controller method to accept multipart/form-data request with primitive types, File and List of objects. I saw that I need custom model binder when I have complex data type using FromForm and I made one, but the problem is that binding works from Swagger, but not from Postman.
Here is my controller method first:
public async Task<IActionResult> Post([FromForm] AddRequest addRequest)
{
var result = await _service.AddAsync(addRequest);
if (result.Success)
{
return Ok(result);
}
return BadRequest();
}
My request looks like this:
public class AddRequest
{
public string? Name { get; set; }
public string? Description { get; set;}
public IFormFile? File { get; set; }
public IEnumerable<LocaleViewModel>? Locales { get; set; }
}
And here is my viewmodel:
[ModelBinder(BinderType = typeof(MetadataValueModelBinder))]
public class LocaleViewModel
{
public long Id { get; set; }
public string Code { get; set; }
}
And finally model binder:
public class MetadataValueModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (values.Length == 0)
return Task.CompletedTask;
var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
var deserialized = JsonSerializer.Deserialize(values.FirstValue, bindingContext.ModelType, options);
bindingContext.Result = ModelBindingResult.Success(deserialized);
return Task.CompletedTask;
}
}
So, when I fire request from Swagger, binding is done correctly and model name is Locales, but when I do it from Postman or my SPA, binding is not working and model name is Locales[0]. Am I missing something in my model binder or I'm doing everything completely wrong?
I was trying to understand how ValueProvider.GetValue works but with no success.
How do you pass parameters in postman? Please try like this:
IValueProvider.GetValue uses the specified key to retrieve the value object. When there are multiple Locales in the parameter, MetadataValueModelBinder will execute multiple times, each time obtaining the value of a Locales.
First execution:
Second exeution:
Test Result:

class property manipulation when parsing json data in c#

I am practicing with web api. My goal is to create a Get endpoint, which receive data from an external api, then return a different result. external api link: https://www.themealdb.com/api/json/v1/1/search.php?f=a, The external api data looks like:
{
"meals": [
{
"idMeal": "52768",
"strMeal": "Apple Frangipan Tart",
"strDrinkAlternate": null,
"strCategory": "Dessert",
.....
},
{
"idMeal": "52893",
"strMeal": "Apple & Blackberry Crumble",
....
}
]
}
I want my endpoint provide a different result like the following:
[
{
"idMeal": "52768",
"strMeal": "Apple Frangipan Tart",
"ingredients": ["Apple", "sugar"...]
},
{
"idMeal": "52893",
"strMeal": "Apple & Blackberry Crumble",
"ingredients": ["Apple", "sugar"...]
}
]
The following code is what I attempted so far, It's working, but the moment I changed property ingredient1 from public to private, that ingredient in list will become null, also, there are so many ingredients, some of them are null by default, I don't want to add them if they are null, how can I fix these two issues? Thanks a lot
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using RestSharp;
namespace testAPI.Controllers;
public class Content
{
[JsonPropertyName("meals")]
public List<Meal> Meals { get; set; }
}
public class Meal
{
[JsonPropertyName("idMeal")]
public string MealId { get; set; }
[JsonPropertyName("strMeal")]
public string Name { get; set; }
[JsonPropertyName("strIngredient1")]
public string Ingredient1 { get; set; }
[JsonPropertyName("strIngredient2")]
public string Ingredient2 { get; set; }
[JsonPropertyName("strIngredient20")]
public string Ingredient20 { get; set; }
public List<string> Ingredients
{
get { return new List<string>(){Ingredient1, Ingredient2, Ingredient20};}
}
}
[ApiController]
[Route("api/[controller]")]
public class DishesController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAllRecipes()
{
var client = new RestClient($"https://www.themealdb.com/api/json/v1/1/search.php?s=");
var request = new RestRequest();
var response = await client.ExecuteAsync(request);
var mealList = JsonSerializer.Deserialize<Content>(response.Content);
return Ok(mealList.Meals);
}
}
To address the problems one at a time...
the moment I changed property ingredient1 from public to private, that ingredient in list will become null
Changing the access modifier affects both deserialization and serialization, so this cannot be used to only stop it from serializing the property. You should split the data models up into what you want to receive and what you want to expose/return.
there are so many ingredients, some of them are null by default, I don't want to add them if they are null
Addition to splitting up the data models you can handle this when mapping from one model to the other.
The following code should fix both issues:
namespace TheMealDb.Models
{
// These are the models you receive from TheMealDb
// JSON converted to classes with https://json2csharp.com/
public class Root
{
public List<Meal> meals { get; set; }
}
public class Meal
{
public string idMeal { get; set; }
public string strMeal { get; set; }
public string strIngredient1 { get; set; }
public string strIngredient2 { get; set; }
public string strIngredient3 { get; set; }
// Other properties removed for brevity...
}
}
namespace Internal.Models
{
// This is the model you want to return from your controller action
public class Meal
{
[JsonPropertyName("id")] // No need to use the same name as from themealdb
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("ingredients")]
public List<string> Ingredients { get; set; }
}
}
Now, to fetch, map and return the data in your controller action:
[HttpGet]
public async Task<IActionResult> GetAllRecipes()
{
var client = new RestClient($"https://www.themealdb.com/api/json/v1/1/search.php?s=");
var request = new RestRequest();
var response = await client.ExecuteAsync(request);
// Deserialize to the "TheMealDb" models
var mealList = JsonSerializer.Deserialize<TheMealDb.Models.Root>(response.Content);
// Map to your own models
var myMealList = mealDbList.meals?.Select(MapToInternal);
return Ok(myMealList);
}
// Map "TheMealDb" model to your own model
private Internal.Models.Meal MapToInternal(TheMealDb.Models.Meal externalMeal)
{
return new Internal.Models.Meal
{
Id = externalMeal.idMeal,
Name = externalMeal.strMeal,
Ingredients = new []
{
externalMeal.strIngredient1,
externalMeal.strIngredient2,
externalMeal.strIngredient3,
// ...
}
// Remove empty/null ingredients
.Where(ingr => !string.IsNullOrEmpty(ingr))
.ToList()
};
}
See the code in action.

How do you deserialize a Geopoint using newtonsoft JSON deseralizer?

I'm storing customer position data using the following class:
public class CustomerData
{
public string CustomerName { get; set; }
public int CustomerNumber { get; set; }
public string CustomerAddress { get; set; }
public string CustomerCity { get; set; }
public string CustomerZip { get; set; }
public string CustomerState { get; set; }
public Geopoint CustomerGeopoint { get; set; }
}
inside a JSON file...and retrieving the data using a service like so:
public static async Task<ObservableCollection<CustomerData>> GetCustomerData()
{
var folder = ApplicationData.Current.LocalFolder;
var dataFile = await folder.TryGetItemAsync("CustomerData.json") as IStorageFile;
var stringResult = await FileIO.ReadTextAsync(dataFile);
ObservableCollection<CustomerData> CustomersRetrievedData = JsonConvert.DeserializeObject<ObservableCollection<CustomerData>>(stringResult, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});
Customers = CustomersRetrievedData;
return await Task.FromResult(CustomersRetrievedData);
}
as well as saving the data like this:
public static async void SaveCustomerData()
{
var folder = ApplicationData.Current.LocalFolder;
StorageFile newFile = await folder.CreateFileAsync("CustomerData.json", CreationCollisionOption.ReplaceExisting);
var stringData = JsonConvert.SerializeObject(Customers);
await FileIO.WriteTextAsync(newFile, stringData);
}
My problem is, after all the geopoint data is in there, when I try to read the data by deserializing it in the GetCustomerData() method, I get the following error:
Unable to find a constructor to use for type Windows.Devices.Geolocation.Geopoint. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute
I don't understand how to fix this, and I can't find anything on the newtonsoft documentation, anyone know how this is done?
Looking at the documentation for Geopoint we can see that it has 3 .ctors, none of which are parameterless. JSON.NET requires a parameterless .ctor for deserialization, as you can see from the error message you get.
So one thing you can do is change your class to include a property with a type (that you make) that mirrors the Geopoint structure (includes all its properties), but also includes a parameterless .ctor. You could even implement IGeoshape so that it could be used anywhere an IGeoshape was required.
Something along the lines of:
public class GeopointProxy : IGeoshape {
public GeopointProxy() { }
private AltitudeReferenceSystem _altitudeReferenceSystem;
// This is part of the IGeoshape interface, you can't change it's signature, and it's readonly. Fun.
[JsonIgnore]
public AltitudeReferenceSystem AltitudeReferenceSystem { get { return _altitudeReferenceSystem; } }
public AltitudeReferenceSystem SettableAltitudeReferenceSystem {
get {
return AltitudeReferenceSystem;
}
set {
_altitudeReferenceSystem = value;
}
}
// rinse and repeat as appropriate for the other interface members and class properties
// Then include a conversion function, or get fancy and add type conversion operators:
public Geopoint ToGeopoint() {
return new Geopoint(Position, AltitudeReferenceSystem, SpatialReferenceId);
}
}

Converting JsonValue to domain object

I have an API call as listed below:
JsonValue result = api.GET("/chart/" + problemList.PatientMRN.ToString() + "/problems", problemInfo);
string resultString = result.ToString();
Note: I am referring to System.Json.JsonValue
Alternative Approach (using JavaScriptSerializer )
Rootobject_Labresult objGResponse = new JavaScriptSerializer().Deserialize<Rootobject_Labresult>(resultString);
From the string in Json, I created corresponding classes (using Paste Special in Visual Studio edit menu).
public class Rootobject_Labresult
{
public Labresult[] labresults { get; set; }
public int totalcount { get; set; }
}
public class Labresult
{
public string createddate { get; set; }
public DateTime createddatetime { get; set; }
public string departmentid { get; set; }
public string description { get; set; }
}
But when I create an array, I am getting following error.
Labresult[] labresults = result[0];
////Error: cannot implicitly convert type System.Json.JsonValue to Labresult
What is the best way to convert JsonValue to the domain object (Labresult) ?
This could have also been done simpler using Json.Net
JsonConvert.DeserializeObject<T> Method (String)
//...code removed for brevity
string json = result.ToString();
Rootobject_Labresult rootObject = JsonConvert.DeserializeObject<Rootobject_Labresult>(json);
Labresult[] labresults = rootObject.labresults;
From there you should be able to extract the desired domain values.
And as simple as that was you could have created an extension
public static class JsonValueExtensions {
public static T ToObject<T>(this JsonValue value) {
return JsonConvert.DeserializeObject<T>(value.ToString());
}
}
which reduces the original code even further
//...code removed for brevity
Rootobject_Labresult rootObject = result.ToObject<Rootobject_Labresult>();
Labresult[] labresults = rootObject.labresults;
The assumption being that result in the above snippet example is an instance of JsonValue
I believe the best way to convert from string/System.Json.JsonValue to DotNet Object using Json.NET
All you need is the Newtonsoft.Json.Linq namespace and you can do all the conversation with single line of code
using Newtonsoft.Json.Linq;
var result = JToken.Parse(jsonVal.ToString()).ToObject<Rootobject_Labresult>();
I have created a example here.
https://dotnetfiddle.net/N2VfKl
Here is another example for object conversation using Json.NET
https://dotnetfiddle.net/rAkx7m
Json.Net also allow you to work with the json object without declare the class.
https://dotnetfiddle.net/ZIA8BV

How to bind Json parameters to Web Api parameters in ASP.NET?

When I have this method in an MVC Controller
[HttpPost]
public async Task<ActionResult> MyMethod(int param1, string param2)
{
//....
}
I can send a Json object {param1:1, param2:"str"} it works just fine and parameters are resolved. However when I do this for a WebApi 2 it doesn't work. Because [FromBody] can only be used by 1 parameter according to following example on documentation.
At most one parameter is allowed to read from the message body
// Caution: Will not work!
public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }
How can we obtain the same behavior of MVC controller from WebApi controller?
Edit: Creating corresponding classes and replacing parameters is not an option, because a messaging tool checks these methods for maintenance. Signatures should remain the same.
Try to compose one object from these values:
public class Foo
{
public int id {get;set;}
public int name {get;set;}
}
public HttpResponseMessage Post([FromBody] Foo foo)
{
//some stuff...
}
If signature should remain the same, you can try to specify params in url, like that: myurl?id=1&name=Tom still via POST verb.
You can try like this:
public HttpResponseMessage Post([FromBody]dynamic value)
{
int id= value.id.ToString();
string name = value.name.ToString();
}
And pass json like following
{
"id":"1",
"name":"abc"
}
If you have to pass multiple parameter please use class object:
public class PortalClass
{
public ApplicationModel applicationModel { get; set; }
public string user_id { get; set; }
public string id { get; set; }
public object pageCollection { get; set; }
}
public object GetApplication(PortalClass data)
{
JsonSerializerSettings settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, PreserveReferencesHandling = PreserveReferencesHandling.None };
var myObject=JsonConvert.DeserializeObject<PageCollection>(data.pageCollection.ToString(), settings)
return null;
}
Client Side:
var data = {
user_id: userId,
id: id
};
http.post(url, data).then(
function (response) {
}, function (err) {
callback.reject(err);
});

Categories

Resources