I created a custom converter extending from JsonConverter, which would be used for an ASP.NET MVC and a Web API.
public class MyCustomConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
....
}
}
And I created this CustomObject that uses that converter:
[JsonConverter(typeof(MyCustomJsonConverter))]
public class CustomObject
{
...
}
This converter work correctly for second application (WebApi), that means method of ReadJson are running after calling it in TestOfUsingJson. And in this case I didn't have to set up anything.
For the first application (ASP.NET MVC) I have a trouble, object are converted from json, but this object are not created from my custom converter. Method of ReadJson are not running.
Method which who use custom converter are looks same on the every application
public HttpResponseMessage TestOfUsingJson([FromBody] CustomObject objs)
{
...
}
Some settings for Json Serializer in the ASP.NET MVC's Global.asax.cs:
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new MyCustomJsonConverter() }
};
What am I doing wrong?
I have prepared some solution that may be useful for someone
Create binding model
public class BindJson : IModelBinder
{
public BindJson()
{
}
public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
throw new SysException("Missing controller context");
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return null;
controllerContext.HttpContext.Request.InputStream.Position = 0;
using (var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
{
var data = reader.ReadToEnd();
if (!String.IsNullOrEmpty(bodyText))
{
return JsonConvert.DeserializeObject(data, bindingContext.ModelType);
}
}
return null;
}
}
Assign binder in Global.asax.cs
..
ModelBinders.Binders.Add(typeof(BindJson), new BindJson());
..
Assign binder into class
[ModelBinder(typeof(BindJson))]
public class CustomObject
{
...
}
Call binder in method
public HttpResponseMessage TestOfUsingJson(CustomObject objs)
{
...
}
Related
Since I had to search forever for an answer, that would just work as is, I will provide an example here, when you serialize and deserialize an object with a composite pattern structure.
My problem was deserializing this kind of class structure (https://en.wikipedia.org/wiki/Composite_pattern):
abstract class BaseClass
{
public int Id { get; set; }
}
class Leaf : BaseClass
{
public string Foo { get; set; }
}
class Composite : BaseClass
{
public List<BaseClass> ClassList = new List<BaseClass>();
}
Serialization worked by using:
var composite = new Composite();
JsonConvert.SerializeObject(composite, Formatting.Indented);
Deserialziation didn't work out of box.
Solution for deserialization is to build a CustomConverter:
public class BaseClassJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(BaseClass) == objectType;
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
try
{
var jObject = JObject.Load(reader);
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.Converters.Add(new BaseClassJsonConverter());
if (jObject.ContainsKey("Foo"))
return JsonConvert.DeserializeObject<Leaf>(jObject.ToString(), jsonSerializerSettings);
else if (jObject.ContainsKey("ClassList"))
return JsonConvert.DeserializeObject<Composite>(jObject.ToString(), jsonSerializerSettings);
else
throw new System.SystemException("Class not implemented");
return null;
}
catch (JsonReaderException)
{
return null;
}
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Key difference to other answers I found is that they were missing to put the settings in the ReadJson into the SeserialzieObject again, that way it couldn't handle the deserialization of a recursive structure (Deserializing JSON to abstract class).
Now you either decorate your BaseClass with: [JsonConverter(typeof(BaseConverter))]
Or you call the deserialization with the converter in the settings:
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.Converters.Add(new BaseClassJsonConverter());
JsonConvert.DeserializeObject<BaseClass>(jsonString, jsonSerializerSettings);
I have an ApiController class with a method accepting a complex object.
public class SampleController : ApiController
{
[Route("api/Sample/")]
[HttpPost]
public HttpResponseMessage GetSampleInfo([FromBody]SampleClass sampleClassObject)
{
// Some code
}
}
When debugging, this Controller class method is called, but what was passed as a Guid in the object, shows a new Guid (as 00000000-0000-0000-0000-000000000000). I'm first checking this method with Postman. I tried passing the object using x-www-form-urlencoded and also as application/json.
What I'm passing in Postman:
{
"sampleID": "A9A999AA-AA99-9AA9-A999-9999999999AA",
"otherValue": 1
}
I checked other issues like mine but I have tried the solutions but still I'm getting the passed Guid as a new Guid.
P.S.
The SampleClass looks like as follows:
public class SampleClass
{
public Guid sampleID { get; set; }
public int otherValue { get; set; }
}
Update:
I have used the following JsonConverter.
public class GuidConverterCustom : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Guid) == objectType;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.Null:
return Guid.Empty;
case JsonToken.String:
string str = reader.Value as string;
if (string.IsNullOrEmpty(str))
{
return Guid.Empty;
}
else
{
return new Guid(str);
}
default:
throw new ArgumentException("Invalid token type");
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (Guid.Empty.Equals(value))
{
writer.WriteValue("");
}
else
{
writer.WriteValue((Guid)value);
}
}
}
And included the following in the Global.asax.cs:
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings();
settings.Converters.Add(new GuidConverterCustom());
return settings;
};
Your method should look like this, no need for custom JsonConverter.
[Route("sample")]
[HttpPost]
public IHttpActionResult PostGuid(SampleClass id)
{
return Ok();
}
It then works with both x-www-form-urlencoded and application/json. If you use json do not forget the header Content-Type: application/json.
Base class: AbstractBuildBlock, Derived: TextBlock, ImageBlock, EmptyBlock, ... .
Blocks here: Site -> Pages[someIndex] -> Rows[someIndex] -> BuildBlocks
Fields BuildBlocks is of type AbstractBuildBlock, so when I'm saving Site to DB in BuildBlocks each record has descriminator AbstractBuildBlock. I try to do the next in BuildBlockRepository:
switch(obj.ContentType)
{
case "text":
obj = obj as TextBlock;
context.BuildBlocks.Add(obj);
break;
}
After obj = obj as TextBlock obj is null. The reason is that obj is of type AbstractBuildBlock. I found at msdn that this code should work:
BaseClass a = new DerivedClass()
DerivedClass b = a as DerivedClass
So I need reproduce this code at model binding. This is ajax request:
$('.saveSite').click(function () {
$.ajax({
url: '/Site/Update',
data: { site: getSite() },
method: 'POST',
success: function (data) {
console.log('Success save');
},
error: function (data) {
debugBox(data);
}
});
});
and mvc action for this request
public string Update(Site site)
{
siteRepository.Add(site);
return "Success";
}
So I send Site in json form, BuildBlocks that are in this site in form of json too, but of course their(blocks) type is not AbstractBuildBlock, they are all of TextBlock, ImageBlock, etc. and have fields with values.
The problem: Site has field BuildBlocks which type is AbstractBuildBlock and model binder do something like this:
buildBlock = new AbstractBuildBlock(); //loose derived classes fields and posibility to convert it in DerivedClass
buildBlocks.push(buildBlock)
but I need someting like this
switch(buildBlock.contenType) {
case "text" : buildBlock = new TextBlock();buidlBlocks.push(buildBlock);
}
JSON NET Custom deserializer not work at all
ASP MVC 5 How to read object from request with the help of JSON NET
look at answers in two links above, the correct ajax call is described
And the server code below
mvc action
public string Update(Site site)
{
TextBlock block = site.Pages[0].Rows[0].BuildBlocks[0] as TextBlock;
siteRepository.Add(site);
return "Success";
}
AbstractJsonCreationConverter I've created it in Infrastructure folder
public abstract class AbstractJsonCreationConverter<T> : JsonConverter
{
protected abstract T Create(Type objectType, JObject jsonObject);
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
var target = Create(objectType, jsonObject);
serializer.Populate(jsonObject.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
And in the same folder concrete class
public class JsonBuildBlockConverter : AbstractJsonCreationConverter<AbstractBuildBlock>
{
protected override AbstractBuildBlock Create(Type objectType, JObject jsonObject)
{
var type = jsonObject["contentType"].ToString();
switch(type)
{
case "text":
return new TextBlock();
default:
return null;
}
}
}
and one more class in Infrastructure
internal class SiteModelBinder : System.Web.Mvc.IModelBinder
{
public object BindModel(ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext)
{
// use Json.NET to deserialize the incoming Position
controllerContext.HttpContext.Request.InputStream.Position = 0; // see: https://stackoverflow.com/a/3468653/331281
Stream stream = controllerContext.RequestContext.HttpContext.Request.InputStream;
var readStream = new StreamReader(stream, Encoding.UTF8);
string json = readStream.ReadToEnd();
return JsonConvert.DeserializeObject<Site>(json, new JsonBuildBlockConverter());
}
}
The last class is ModelBinder that will be called to parse variables of type Site, to make it work you need to register it in Global.asax.cs in ApplicationStart()
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ModelBinders.Binders.Add(typeof(Site), new SiteModelBinder()); //RegisterModelBinder for Site
}
This is all.
I try to add a variable of "custom data type" in my HttpGet Route.
I have this code:
[HttpGet("{idObject}")]
public ResponseSchema Get(ObjectId idObject)
{
if (idObject == null) {
throw new BodyParseException();
}
var user = _usersLogic.GetById(idObject);
if (user == null) {
_response.Success = false;
_response.ErrorCode = "UserDoesNotExist";
}
else {
_response.Objects.Add(user);
}
return _response;
}
ObjectId is a Datatype defined in using MongoDB.Bson.
For the Json Serialization and Deserialization we already have the code to automatically convert on both sides. But can this be similarly done in the Url itself.
We are right now using this Mvc version:
"Microsoft.AspNet.Mvc": "6.0.0-beta8"
So the URL looks like this:
GET Users/55b795827572761a08d735ac
The code to parse it from "string" to "ObjectId" is:
ObjectId.TryParse(idString, out idObject);
The question is where to put that TryParse code. Because I need to tell ASP.NET how it should parse the idObject from String to ObjectId. Since the URL basically is a string.
For Post or Put JSON Payload I already found a solution. I know that this is something different. But Probably it is helpful to understand the scenario, or find a solution to this scenario:
public class EntityBaseDocument
{
[JsonConverter(typeof(ObjectIdConverter))]
public ObjectId Id { get; set; }
}
// Since we have this value converter. We can use ObjectId everywhere
public class ObjectIdConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
return new ObjectId(token.ToObject<string>());
}
public override bool CanConvert(Type objectType)
{
return typeof(ObjectId).IsAssignableFrom(objectType);
}
}
This will bind it from Uri objects:
public ResponseSchema Get([FromUri]ObjectId idObject)
So: ?param1=something¶m2=sometingelse
This will bind it from the body (e.g. a JSon object)
public ResponseSchema Get([FromBody]ObjectId idObject)
Or you can roll your own:
public ResponseSchema Get([ModelBinder(typeof(MyObjectBinder))]ObjectId idObject)
The example on asp.net of a model binder is:
public class GeoPointModelBinder : IModelBinder
{
// List of known locations.
private static ConcurrentDictionary<string, GeoPoint> _locations
= new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);
static GeoPointModelBinder()
{
_locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
_locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
_locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(GeoPoint))
{
return false;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return false;
}
string key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Wrong value type");
return false;
}
GeoPoint result;
if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
{
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
}
}
I believe NikoliaDante's answer works if you have a route such as /api/users?id={{idHere}}. However, if you are looking to have more RESTful routes, the solution below will do the trick for you. I just tested this out in a Web API 2 application and it works well. This will handle the use case where you may have a route such as /api/users/{{userId}}/something/{{somethingId}}.
//Http Parameter Binding Magic
public class ObjectIdParameterBinding : HttpParameterBinding
{
public ObjectIdParameterBinding(HttpParameterDescriptor p) : base(p){ }
public override Task ExecuteBindingAsync(System.Web.Http.Metadata.ModelMetadataProvider metadataProvider, HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
{
var value = actionContext.ControllerContext.RouteData.Values[Descriptor.ParameterName].ToString();
SetValue(actionContext, ObjectId.Parse(value));
var tsc = new TaskCompletionSource<object>();
tsc.SetResult(null);
return tsc.Task;
}
}
//Binding Attribute
public class ObjectIdRouteBinderAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
return new ObjectIdParameterBinding(parameter);
}
}
//Controller Example
[Route("api/users/{id}")]
public async Task<User> Get([ObjectIdRouteBinder] ObjectId id)
{
//Yay!
}
ASP.NET Web API provides several approaches for do that. Take a look for
Parameter Binding in Web API documentation.
Summary:
FromUriAttribute - for simple DTO classes
TypeConverter - to help Web API treat your class as simple type
HttpParameterBinding - allow to create behaviour attribute
ValueProvider - for more complex case
IActionValueBinder - to write own parameter-binding process at all
Trying to return a correct error message instead of the WebAPI default one {"Message":"The request is invalid.","ModelState" when Json deserialzation fails.
I implemented my custom ActionFilterAttribute:
internal class ValidateModelAttribute : ActionFilterAttribute {
public override void OnActionExecuting(HttpActionContext actionContext) {
if (!actionContext.ModelState.IsValid) {
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
}
I decorated my Controller method with this attribute:
[ValidateModelAttribute]
public async Task<HttpResponseMessage> Put(string username, string serviceId, [FromBody] Dictionary<string, string> jsonData)
{
// code
}
If I set a breakpoint in the OnActionExecuting it only breaks when the jsonData is parsed succesfully as a json. If the json is invalid it never gets into the filter, and the same error message is returned. So looks like this is done somewhere before but all the posts I found say that this should be the place to handle this.
Any idea what's wrong?
The attribute will never get called since deserialization is failing before the method is ever called, meaning that the attributes decorating the method are not called. You need a custom converter (the technique for handling cultures was borrowed from this answer).
public class Testee {}
public class Tester
{
[JsonConverter(typeof(CustomMesssageConverter<Testee>), "Custom Error Message")]
public Testee Testee { get; set; }
}
public class CustomMesssageConverter<T> : JsonConverter where T : new()
{
private string _customErrorMessage;
public CustomMesssageConverter(string customErrorMessage)
{
_customErrorMessage = customErrorMessage;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
if (reader.TokenType == JsonToken.Null)
return null;
// Load JObject from stream
JObject jObject = JObject.Load(reader);
// Create target object based on type
var target = new T();
//Create a new reader for this jObject, and set all properties to match the original reader.
JsonReader jObjectReader = jObject.CreateReader();
jObjectReader.Culture = reader.Culture;
jObjectReader.DateParseHandling = reader.DateParseHandling;
jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jObjectReader.FloatParseHandling = reader.FloatParseHandling;
// Populate the object properties
serializer.Populate(jObjectReader, target);
return target;
}
catch(Exception ex)
{
// log ex here
throw new Exception(_customErrorMessage);
}
}
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
}