json string deserialization to custom object - c#

I am posting this question after reading the posts available. I have an ASP.NET web api controller with following methods.
[DataContract]
public class CustomPerson
{
[DataMember]
public ulong LongId { get; set; }
}
[DataContract]
public class Employee : CustomPerson
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string Address { get; set; }
}
Then in the controller
public class CustomController : ApiController
{
[HttpPost]
[ActionName("AddEmployee")]
public bool AddEmployee(Employee empInfo)
{
bool bIsSuccess = false;
// Code loginc here
bIsSuccess = true;
return bIsSuccess;
}
[HttpPost]
[ActionName("AddEmployeeCustom")]
public async Task<bool> AddEmployeeCustom()
{
string rawRequest = await Request.Content.ReadAsStringAsync();
bool bIsSuccess = false;
// Code loginc here
try
{
Employee emp = JsonConvert.DeserializeObject<Employee>(rawRequest);
}
catch (Exception ex)
{ }
return bIsSuccess;
}
}
When I call the following request to AddEmployee via soap ui the custom object is received without error i.e the empty value for LongId is ignored
{
"Name": "test1",
"Address": "Street 1",
"LongId": ""
}
When I call the AddEmployeeCustom method the runtime throws exception:
Error converting value "" to type 'System.UInt64'. Path 'LongId', line 4, position 14.
One option I read is to convert the incoming string to JObject and then create object of Employee class but I am trying to understand and mimic the behavior of default request handling mechanism when the incoming request is automatically handled by the controller and deserialized to Employee object

The problem is that your JSON is not valid for your model.
In the first method AddEmployee the process called Model Binding happens. MVC does the job of converting post content into the object. It seems its tolerant to type mismatch and forgives you the empty string.
In the second case, you try to do it yourself, and try simply run deserialization without validating input data. Newtonsoft JSON does not understand empty string and crashes.
If you still need to accept invalid JSON you may want to overwrite default deserialization process by implementing custom converter
public class NumberConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return typeof(ulong) == objectType;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = reader.Value.ToString();
ulong result;
if (string.IsNullOrEmpty(value) || !ulong.TryParse(value, out result))
{
return default(ulong);
}
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
And then specify your custom converter instance when calling deserialize
return JsonConvert.DeserializeObject<Employee>(doc.ToJson(), new NumberConverter());

Related

How to deserialize a nested json string?

Suppose I have this classes:
internal class Test
{
public string value { get; set; }
public Test(string value)
{
this.value = value;
}
}
public class Response<T>
{
private string errorMessage { get; }
private T returnValue { get; }
public Response(string errorMessage, T returnValue)
{
this.errorMessage = errorMessage;
this.returnValue = returnValue;
}
public bool HasError() { return errorMessage != ""; }
public bool AssertEqualsReturnValue(object obj) { return returnValue.Equals(obj); }
}
and I have this Main function:
string json =
#"{
""errorMessage"": ""This is an error"",
""returnValue"": {
""value"": ""this is what returns""
}
}
";
Response<Test> r = JsonSerializer.Deserialize<Response<Test>>(json); ;
Console.WriteLine(r.AssertEqualsReturnValue("this is what returns"));
Console.WriteLine(r.HasError());
I get the next error message:
System.InvalidOperationException: 'Each parameter in the deserialization constructor on type 'IntroSE.Kanban.Backend.Response`1[ConsoleApp1.Test]' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. The match can be case-insensitive.'
It won't desirialize because I need to somehow tell the JsonSerializer I have a Test object in returnValue key.
I saw some solutions online but the problem is they all use JsonConverter which is no in use anymore in .Net 6.0.
You have defined your errorMessage and returnValue properties with private visibility. If you change them to public after that the exception will not be thrown.
Please be aware that the JsonInclude attribute can be used only in case of non-public property accessors, like private set; or private get;.
FYI
you can do this in VS
copy the json
{
"errorMessage": "This is an error",
"returnValue": {
"value": "this is what returns"
}
}
Choose EDIT < Paste Special > Paste JSON As Classes
Check and compare with your classes

Custom model binder return null object in web api

I am using custom model binder in web api, but my model value is null.
It is unable to get the value from ValueProvider.
Please look at my code below.
bindingContext.ValueProvider.GetValue("Report") is null
Here is my code.
public class TestReportDto
{
public ReportFormatType ExportFormat { get; set; }
public string Report { get; set; }
public Dictionary<string, object> ParameterValues { get; set; }
}
public enum ReportFormatType
{
PDF,
XLS
}
My Model Binder class.
public class TestModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var testReportDto = new TestReportDto();
bool isSuccess = true;
if (bindingContext.ValueProvider.GetValue("Report") != null)
{
testReportDto.Report = Convert.ToString(bindingContext.ValueProvider.GetValue("Report").RawValue);
}
else
{
isSuccess = false;
bindingContext.ModelState.AddModelError("request.Report", "Report");
}
bindingContext.Model = testReportDto;
return isSuccess;
}
}
API Code:
public async Task<string> Post([ModelBinder(typeof(TestModelBinder))]TestReportDto request)
{
return "Hello";
}
You can get the value from the Request object of the HttpActionContext object inside your custom model binder. The example I used is below.
var bodyContent = actionContext.Request.Content.ReadAsStringAsync().Result;
Please note that this is quick and dirty solution for the problem you're facing. If you would play by the rules then you should create provider class for handling that kind of data (body content), and then a factory class to engage the whole process correctly. Just like it's described here.

ApiExplorer does not recognize route attributes with custom type

I have a project where I want to use route attributes with a custom type.
The following code where I have the custom type as a query parameter works fine and the help page displays the custom type.
// GET api/values?5,6
[Route("api/values")]
public string Get(IntegerListParameter ids)
{
return "value";
}
WebApi.HelpPage gives the following documentation
Help:Page
If I change the code to use route attributes, the result is that I get an empty help page.
// GET api/values/5,6
[Route("api/values/{ids}")]
public string Get(IntegerListParameter ids)
{
return "value";
}
When I inspect the code I observe in HelpController.cs that ApiExplorer.ApiDescriptions returns an empty collection of ApiDescriptions
public ActionResult Index()
{
ViewBag.DocumentationProvider = Configuration.Services.GetDocumentationProvider();
Collection<ApiDescription> apiDescriptions = Configuration.Services.GetApiExplorer().ApiDescriptions;
return View(apiDescriptions);
}
Is there any way to get ApiExplorer to recognize my custom class IntegerListParameter as attribute routing?
You need to:
add HttpParameterBinding for your IntegerListParameter type
mark the binding as IValueProviderParameterBinding and implement ValueProviderFactories
add a converter for IntegerListParameter and override CanConvertFrom method for typeof(string) parameter
After these actions, route with custom type IntegerListParameter must be recognized in ApiExplorer.
See my example for type ObjectId:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
//...
config.ParameterBindingRules.Insert(0, GetCustomParameterBinding);
TypeDescriptor.AddAttributes(typeof(ObjectId), new TypeConverterAttribute(typeof(ObjectIdConverter)));
//...
}
public static HttpParameterBinding GetCustomParameterBinding(HttpParameterDescriptor descriptor)
{
if (descriptor.ParameterType == typeof(ObjectId))
{
return new ObjectIdParameterBinding(descriptor);
}
// any other types, let the default parameter binding handle
return null;
}
}
public class ObjectIdParameterBinding : HttpParameterBinding, IValueProviderParameterBinding
{
public ObjectIdParameterBinding(HttpParameterDescriptor desc)
: base(desc)
{
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
{
try
{
SetValue(actionContext, new ObjectId(actionContext.ControllerContext.RouteData.Values[Descriptor.ParameterName] as string));
return Task.CompletedTask;
}
catch (FormatException)
{
throw new BadRequestException("Invalid id format");
}
}
public IEnumerable<ValueProviderFactory> ValueProviderFactories { get; } = new[] { new QueryStringValueProviderFactory() };
}
public class ObjectIdConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
}
Not exactly sure what data structure IntegerListParameter list is but if you need to send a comma delimited list of integers in the query(e.g. ~api/products?ids=1,2,3,4) you can use filter attributes. An example implementation of this can be found here: Convert custom action filter for Web API use?

WebApi return custom ServiceResult

I have normal WebApi controller that with methods:
public class ModelController : ApiController
{
public IEnumerable<Model> Get()
{
return service.Get();
}
public string Get(int id)
{
return service.Get(id);
}
public void Post([FromBody]MyViewModel value)
{
return service.Save(value);
}
public void Put(int id, [FromBody]MyViewModel value)
{
return service.Save(value);
}
public void Delete(int id)
{
return service.Delete(value);
}
}
I want to wrap return results in to custom object ServiceResult
public class ServiceResult<T>
{
public bool IsError{ get; set; }
public T ObjectModel {get; set}
}
I want to change the return type on all my controller methods to ServiceResult
Here is an example:
public ServiceResult Post([FromBody]MyViewModel value)
{
service.Save(value);
//getting error here:
return new ServiceResult() { IsError= true, ObjectModel = value };
}
But getting the error
The 'ObjectContent`1' type failed to serialize the response body for
content type 'application/xml; charset=utf-8'.
Type 'Models.View.MyViewModel' with
data contract name
'MyViewModel://schemas.datacontract.org/2004/07/MyViewModel'
is not expected. Consider using a DataContractResolver or add any
types not known statically to the list of known types - for example,
by using the KnownTypeAttribute attribute or by adding them to the
list of known types passed to DataContractSerializer.
Do you have any idea how can I fix that?
Your ServiceResult object requires a type argument, Try this
return new ServiceResult<MyViewModel>() { IsError= true, ObjectModel = value };

Automatically selecting parameterized constructor while JsonCovertor Deserialization

I using json .NET to deserialize my json string to my model.
below is what I am trying to achieve, please advice what is best way ..
When there is no data my response looks like below
json string = "{\"message\":\"SUCCESS\",\"result\":null}"
the result eventually is tied to a view. So when the response is null I would like to intialize my view with default model values. And so would like to call Default constructor on deserialization. Default constructor looks like below.
public ProfileModel()
{
this.DefaultTab = DefaultTabOption.PROFILE;
this.DataLoadPosition = new DataLoadPositionOptionsModel();
this.DataLayout = new DataLayoutOptionsModel();
this.NAData = new NADataOptionsModel();
this.DataTable = new DataDisplayOptionsModel();
}
But when there is data, the reponse looks like below.
{"message":"SUCCESS","result":{"dataLayout":{"vertical":false},"dataLoadPosition":{"cell":"B2","cursorLocation":false},"dataTable":{"decimalPts":1},"defaultTab":"BROWSE","naData":{"custom":"","naDataOption":"FORWARDFILL"}}}
In this case, I would like to call my parameterized constructor so that models are initialized correctly.
Deserialization code:
using (StreamReader reader = new StreamReader(responseStream))
{
var t = JsonConvert.DeserializeObject<T>(reader.ReadToEnd());
return t;
}
Where T is my main Model which initilialises multiple models. Below is the parameterized contructor.
public ProfileModel(DefaultTabOption defaultTabModel,
DataLoadPositionOptionsModel dataLoadPositionOption ,
DataLayoutOptionsModel dataLayoutOptios ,
NADataOptionsModel naDataOptions ,
DataDisplayOptionsModel dataTableOptions)
{
this.DefaultTab = defaultTabModel;
this.DataLoadPosition = dataLoadPositionOption;
this.DataLayout = dataLayoutOptios;
this.NAData = naDataOptions;
this.DataTable = dataTableOptions;
}
What is they best way to deserialize so that default constructor is called when null and parameterized is call when not null. I tried the ConstructorHandling, NullValueHandling but I am not able to achieve desired results.
I've a little simplified your model
public sealed class ProfileModel
{
public ProfileModel()
{
DataLayout = new DataLayoutOptionsModel();
}
public ProfileModel(DataLayoutOptionsModel dataLayout)
{
DataLayout = dataLayout;
}
public DataLayoutOptionsModel DataLayout { get; private set; }
}
public sealed class DataLayoutOptionsModel
{
public bool Vertical { get; set; }
}
public class ResultModel
{
public string Message { get; set; }
public ProfileModel Result { get; set; }
}
To choose concrete constructor, you have to implement custom JsonConverter, for example
public sealed class MyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(ProfileModel).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
ProfileModel target;
JObject jObject = JObject.Load(reader);
JToken resultToken = jObject["Result"];
//This is result null check
if (resultToken.Type == JTokenType.Null)
{
target = new ProfileModel();
}
else
{
var optionsModel = resultToken["DataLayout"].ToObject<DataLayoutOptionsModel>();
target = new ProfileModel(optionsModel);
}
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer,
object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
So, result serialization looks like:
[Fact]
public void Test()
{
string tinyJsonl = "{\"Message\":\"SUCCESS\",\"Result\":null}";
var defaultProfile = JsonConvert.DeserializeObject<ProfileModel>(tinyJsonl, new MyJsonConverter());
Assert.False(defaultProfile.DataLayout.Vertical);
string fullJson = "{\"Message\":\"SUCCESS\",\"Result\":{\"DataLayout\":{\"Vertical\":true}}}";
var customProfile = JsonConvert.DeserializeObject<ProfileModel>(fullJson, new MyJsonConverter());
Assert.True(customProfile.DataLayout.Vertical);
}

Categories

Resources