I've created simple model with JsonConverter attribute:
public class MyModel
{
[JsonProperty(PropertyName = "my_to")]
public string To { get; set; }
[JsonProperty(PropertyName = "my_from")]
public string From { get; set; }
[JsonProperty(PropertyName = "my_date")]
[JsonConverter(typeof(UnixDateConverter))]
public DateTime Date { get; set; }
}
and my converter:
public sealed class UnixDateConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!CanConvert(reader.ValueType))
{
throw new JsonSerializationException();
}
return DateTimeOffset.FromUnixTimeSeconds((long)reader.Value).ToUniversalTime().LocalDateTime;
}
public override bool CanConvert(Type objectType)
{
return Type.GetTypeCode(objectType) == TypeCode.Int64;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var datetime = (DateTime) value;
var dateTimeOffset = new DateTimeOffset(datetime.ToUniversalTime());
var unixDateTime = dateTimeOffset.ToUnixTimeSeconds();
writer.WriteValue(unixDateTime);
}
}
When I send request from Postman and I set content type as application/json everything works fine - my converter works fine, debugger stops at breakpoint in my converter, but I must use x-www-form-urlencoded.
Is there an option to use JsonConverter attribute inside model when sending data as x-www-form-urlencoded?
I managed to do this by creating custom model binder that implements IModelBinder
Here is generic version of my binder:
internal class GenericModelBinder<T> : IModelBinder where T : class, new()
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof (T))
{
return false;
}
var model = (T) bindingContext.Model ?? new T();
JObject #object = null;
var task = actionContext.Request.Content.ReadAsAsync<JObject>().ContinueWith(t => { #object = t.Result; });
task.Wait();
var jsonString = #object.ToString(Formatting.None);
JsonConvert.PopulateObject(jsonString, model);
bindingContext.Model = model;
return true;
}
}
And here is sample usage:
[Route("save")]
[HttpPost]
public async Task<IHttpActionResult> Save([ModelBinder(typeof (GenericModelBinder<MyModel>))] MyModel model)
{
try
{
//do some stuff with model (validate it, etc)
await Task.CompletedTask;
DbContext.SaveResult(model.my_to, model.my_from, model.my_date);
return Content(HttpStatusCode.OK, "OK", new TextMediaTypeFormatter(), "text/plain");
}
catch (Exception e)
{
Debug.WriteLine(e);
Logger.Error(e, "Error saving to DB");
return InternalServerError();
}
}
I'm not sure if JsonProperty and JsonConverter attributes worked, but they should.
I'm aware that this might not be the best way to do it, but this code worked for me. Any suggestion are more than welcome.
Related
I am stuck at a problem with this, my current base API class implements getting an object from an API like this:
HttpResponceMessage GetResponce(string path) => Task.Run(async() => await client.GetAsync(path));
T GetTFromRequest<T>(string path, JsonConverter[] converters) =>
Task.Run(async() => await GetResponce(path).Content.ReadAsAsync<T>(converters); // Converters gets put into a new JsonMediaTypeFormatter
These are used in other classes to make quick methods. Class Example:
public class ExampleAPI : BaseAPI
{
private static readonly JsonConverter[] converters = new[] { new RecordCoverter() };
public string APIKey { get; set; }
public ExampleData GetExampleData(/* params here such as string apiKey */) =>
GetTFromRequset<ExampleData>($"examplepath?key={APIKey}", converters);
}
Example data would be something like this:
{
"success":true,
"records":[
{
"Foo":"Bar",
"Data":"Dahta"
}
]
}
So the classes would look like this:
public class RecordConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(IRecord);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public interface IRecord {}
public class ExampleData
{
public bool success { get; set; } = true;
public IRecord[] records { get; set; } = new[] {};
}
public class DataRecord : IRecord
{
public string Foo { get; set; }
public string Data { get; set; }
}
public class FaltyRecord : IRecord
{
public string Bar { get; set; }
public string Dahta { get; set; }
}
The issue that keeps arising that I can't figure out if the objects in "records" is FaltyRecords or DataRecords without doing reader.Read() which would just throw off just being about to put the following into RecordConverter:
if(reader.NextProperties().All(x => DataRecordProps.Contains(x)))
return serializer.Deserialize<DataExample>(reader);
if(reader.NextProperties().All(x => FaltyRecordProps.Contains(x)))
return serializer.Deserialize<FaltyRecord>(reader);
if(...)
return ...;
I know that in XmlReader there's the method GetSubtree() so I can do var propReader = reader.GetSubtree(); then use propReader as much as I want and just do retrun (IRecord[])serializer.Deserialize(reader);. I also see using JObject a lot but I don't see a JObject(reader) method.
Use JObject.Load(reader) method.
var jObj = JObject.Load(reader);
var properties = jObj.Properties();
// code here to find the type
return jObj.ToObject(type);
I am trying to deserialize a JSON string to a concrete class, which inherits from an abstract class, but I just can't get it working. I have googled and tried some solutions but they don't seem to work either.
This is what I have now:
abstract class AbstractClass { }
class ConcreteClass { }
public AbstractClass Decode(string jsonString)
{
JsonSerializerSettings jss = new JsonSerializerSettings();
jss.TypeNameHandling = TypeNameHandling.All;
return (AbstractClass)JsonConvert.DeserializeObject(jsonString, null, jss);
}
However, if I try to cast the resulting object, it just doesn't work.
The reason why I don't use DeserializeObject is that I have many concrete classes.
Any suggestions?
I am using Newtonsoft.Json
One may not want to use TypeNameHandling (because one wants more compact json or wants to use a specific name for the type variable other than "$type"). Meanwhile, the customCreationConverter approach will not work if one wants to deserialize the base class into any of multiple derived classes without knowing which one to use in advance.
An alternative is to use an int or other type in the base class and define a JsonConverter.
[JsonConverter(typeof(BaseConverter))]
abstract class Base
{
public int ObjType { get; set; }
public int Id { get; set; }
}
class DerivedType1 : Base
{
public string Foo { get; set; }
}
class DerivedType2 : Base
{
public string Bar { get; set; }
}
The JsonConverter for the base class can then deserialize the object based on its type. The complication is that to avoid a stack overflow (where the JsonConverter repeatedly calls itself), a custom contract resolver must be used during this deserialization.
public class BaseSpecifiedConcreteClassConverter : DefaultContractResolver
{
protected override JsonConverter ResolveContractConverter(Type objectType)
{
if (typeof(Base).IsAssignableFrom(objectType) && !objectType.IsAbstract)
return null; // pretend TableSortRuleConvert is not specified (thus avoiding a stack overflow)
return base.ResolveContractConverter(objectType);
}
}
public class BaseConverter : JsonConverter
{
static JsonSerializerSettings SpecifiedSubclassConversion = new JsonSerializerSettings() { ContractResolver = new BaseSpecifiedConcreteClassConverter() };
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(Base));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
switch (jo["ObjType"].Value<int>())
{
case 1:
return JsonConvert.DeserializeObject<DerivedType1>(jo.ToString(), SpecifiedSubclassConversion);
case 2:
return JsonConvert.DeserializeObject<DerivedType2>(jo.ToString(), SpecifiedSubclassConversion);
default:
throw new Exception();
}
throw new NotImplementedException();
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException(); // won't be called because CanWrite returns false
}
}
That's it. Now you can use serialize/deserialize any derived class. You can also use the base class in other classes and serialize/deserialize those without any additional work:
class Holder
{
public List<Base> Objects { get; set; }
}
string json = #"
[
{
""Objects"" :
[
{ ""ObjType"": 1, ""Id"" : 1, ""Foo"" : ""One"" },
{ ""ObjType"": 1, ""Id"" : 2, ""Foo"" : ""Two"" },
]
},
{
""Objects"" :
[
{ ""ObjType"": 2, ""Id"" : 3, ""Bar"" : ""Three"" },
{ ""ObjType"": 2, ""Id"" : 4, ""Bar"" : ""Four"" },
]
},
]";
List<Holder> list = JsonConvert.DeserializeObject<List<Holder>>(json);
string serializedAgain = JsonConvert.SerializeObject(list);
Debug.WriteLine(serializedAgain);
I would suggest to use CustomCreationConverter in the following way:
public enum ClassDiscriminatorEnum
{
ChildClass1,
ChildClass2
}
public abstract class BaseClass
{
public abstract ClassDiscriminatorEnum Type { get; }
}
public class Child1 : BaseClass
{
public override ClassDiscriminatorEnum Type => ClassDiscriminatorEnum.ChildClass1;
public int ExtraProperty1 { get; set; }
}
public class Child2 : BaseClass
{
public override ClassDiscriminatorEnum Type => ClassDiscriminatorEnum.ChildClass2;
}
public class BaseClassConverter : CustomCreationConverter<BaseClass>
{
private ClassDiscriminatorEnum _currentObjectType;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jobj = JObject.ReadFrom(reader);
_currentObjectType = jobj["Type"].ToObject<ClassDiscriminatorEnum>();
return base.ReadJson(jobj.CreateReader(), objectType, existingValue, serializer);
}
public override BaseClass Create(Type objectType)
{
switch (_currentObjectType)
{
case ClassDiscriminatorEnum.ChildClass1:
return new Child1();
case ClassDiscriminatorEnum.ChildClass2:
return new Child2();
default:
throw new NotImplementedException();
}
}
}
try something like this
public AbstractClass Decode(string jsonString)
{
var jss = new JavaScriptSerializer();
return jss.Deserialize<ConcreteClass>(jsonString);
}
UPDATE
for this scenario methinks all work as you want
public abstract class Base
{
public abstract int GetInt();
}
public class Der:Base
{
int g = 5;
public override int GetInt()
{
return g+2;
}
}
public class Der2 : Base
{
int i = 10;
public override int GetInt()
{
return i+17;
}
}
....
var jset = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All };
Base b = new Der()
string json = JsonConvert.SerializeObject(b, jset);
....
Base c = (Base)JsonConvert.DeserializeObject(json, jset);
where c type is test.Base {test.Der}
UPDATE
#Gusman suggest use TypeNameHandling.Objects instead of TypeNameHandling.All. It is enough and it will produce a less verbose serialization.
Actually, as it has been stated in an update, the simplest way (in 2019) is to use a simple custom pre-defined JsonSerializerSettings, as explained here
string jsonTypeNameAll = JsonConvert.SerializeObject(priceModels, Formatting.Indented,new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});
And for deserializing :
TDSPriceModels models = JsonConvert.DeserializeObject<TDSPriceModels>(File.ReadAllText(jsonPath), new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});
public class CustomConverter : JsonConverter
{
private static readonly JsonSerializer Serializer = new JsonSerializer();
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObject = JObject.Load(reader);
var typeString = jObject.Value<string>("Kind"); //Kind is a property in json , from which we know type of child classes
var requiredType = RecoverType(typeString);
return Serializer.Deserialize(jObject.CreateReader(), requiredType);
}
private Type RecoverType(string typeString)
{
if (typeString.Equals(type of child class1, StringComparison.OrdinalIgnoreCase))
return typeof(childclass1);
if (typeString.Equals(type of child class2, StringComparison.OrdinalIgnoreCase))
return typeof(childclass2);
throw new ArgumentException("Unrecognized type");
}
public override bool CanConvert(Type objectType)
{
return typeof(Base class).IsAssignableFrom(objectType) || typeof((Base class) == objectType;
}
public override bool CanWrite { get { return false; } }
}
Now add this converter in JsonSerializerSettings as below
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
jsonSerializerSettings.Converters.Add(new CustomConverter());
After adding serialize or deserialize base class object as below
JsonConvert.DeserializeObject<Type>("json string", jsonSerializerSettings );
I had a similar issue, and I solved it with another way, maybe this would help someone:
I have json that contains in it several fields that are always the same, except for one field called "data" that can be a different type of class every time.
I would like to de-serialize it without analayzing every filed specific.
My solution is:
To define the main class (with 'Data' field) with , the field Data is type T.
Whenever that I de-serialize, I specify the type:
MainClass:
public class MainClass<T>
{
[JsonProperty("status")]
public Statuses Status { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("data")]
public T Data { get; set; }
public static MainClass<T> Parse(string mainClsTxt)
{
var response = JsonConvert.DeserializeObject<MainClass<T>>(mainClsTxt);
return response;
}
}
User
public class User
{
[JsonProperty("id")]
public int UserId { get; set; }
[JsonProperty("first_name")]
public string FirstName { get; set; }
[JsonProperty("last_name")]
public string LastName { get; set; }
}
Product
public class Product
{
[JsonProperty("product_id")]
public int ProductId { get; set; }
[JsonProperty("product_name")]
public string ProductName { get; set; }
[JsonProperty("stock")]
public int Stock { get; set; }
}
Using
var v = MainClass<User>.Parse(userJson);
var v2 = MainClass<Product>.Parse(productJson);
json example
userJson: "{"status":1,"description":"my description","data":{"id":12161347,"first_name":"my fname","last_name":"my lname"}}"
productJson: "{"status":1,"description":"my description","data":{"product_id":5,"product_name":"my product","stock":1000}}"
public abstract class JsonCreationConverter<T> : JsonConverter
{
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T) == objectType;
}
public override object ReadJson(JsonReader reader,Type objectType,
object existingValue, JsonSerializer serializer)
{
try
{
var jObject = JObject.Load(reader);
var target = Create(objectType, jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
catch (JsonReaderException)
{
return null;
}
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Now implement this interface
public class SportActivityConverter : JsonCreationConverter<BaseSportActivity>
{
protected override BaseSportActivity Create(Type objectType, JObject jObject)
{
BaseSportActivity result = null;
try
{
switch ((ESportActivityType)jObject["activityType"].Value<int>())
{
case ESportActivityType.Football:
result = jObject.ToObject<FootballActivity>();
break;
case ESportActivityType.Basketball:
result = jObject.ToObject<BasketballActivity>();
break;
}
}
catch(Exception ex)
{
Debug.WriteLine(ex);
}
return result;
}
}
I have an Action in Controller which has an argument of type Class1
[HttpPost]
public IActionResult Create(Class1 c)
{
}
I send data to it using JQuery's Ajax function.
I would like to write my own code to deserialize SampleProperty:
class Class1
{
public string SampleProperty { get; set; }
}
Is it possible? I would like to override default deserialization.
I've tried many things, for example writing converter:
public class SamplePropertyConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
if ((string)existingValue == "abc")
return "abc123";
else
return existingValue;
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanWrite => false;
public override bool CanRead => true;
}
and then using it like this:
class Class1
{
[JsonConverter(typeof(SamplePropertyConverter))]
public string SampleProperty { get; set; }
}
but in such case SamplePropertyConverter is not used at all.
I also tried to add it in Startup, but then I see that it enters CanConvert method, but only for some other requests, not sending Class1 to Create Action.
If you do not use json, you could use custom model binding to make it.
1.Assume you have ajax code:
var data = { "SampleProperty": "abc"};
$(document).ready(function () {
$.ajax({
url: '/Test/Create',
type: 'POST',
data: data,
success: function () {
}
});
});
2.Controller:
[HttpPost]
public IActionResult Create(Class1 c)
{
}
3.Class1.cs:
class Class1
{
[ModelBinder(BinderType = typeof(TestModelBinder))]
public string SampleProperty { get; set; }
}
4.TestModelBinder.cs
public class TestModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var values = bindingContext.ValueProvider.GetValue("SampleProperty");
string result = "";
if (values.FirstValue == "abc")
{
result = "abc123";
}else
{
result = values.FirstValue;
}
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
Suppose I have following model class:
public class Action
{
public enum Type
{
Open,
Close,
Remove,
Delete,
Reverse,
Alert,
ScaleInOut,
Nothing
}
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("active")]
[JsonConverter(typeof(IntToBoolConverter))]
public bool Active { get; set; }
[JsonProperty("type")]
[JsonConverter(typeof(ActionTypeConverter))]
public Type ActionType { get; set; }
[JsonProperty("result")]
[JsonConverter(typeof(ActionResultConverter))]
public ActionResult Result { get; set; }
}
and I want to deserialize following JSON into that class:
{
"name":"test1",
"id":"aa0832f0508bb580ce7f0506132c1c13",
"active":"1",
"type":"open",
"result":{
"property1":"buy",
"property2":"123.123",
"property3":"2016-07-16T23:00:00",
"property4":"768",
"property5":true
}
}
Result object can be different each time (one of 6 models) and its type depends on JSON property type.
I have created custom ActionResultConverter (JsonConverter annotation above Result property of Action class) that should be able to create specific result object based on string in type property of JSON.
My problem is that I don't know how to access that property from converter because only the result part of whole JSON is passed to JsonReader.
Any ideas or help will be appreciated.
Thanks!
Json.NET does not provide a method to access the value of a property of a parent object in the JSON hierarchy while deserializing a child object. Likely this is because a JSON object is defined to be an unordered set of name/value pairs, according to the standard, so there can be no guarantee the desired parent property occurs before the child in the JSON stream.
Thus, rather than handling the Type property in a converter for ActionResult, you'll need to do it in a converter for Action itself:
[JsonConverter(typeof(ActionConverter))]
public class Action
{
readonly static Dictionary<Type, System.Type> typeToSystemType;
readonly static Dictionary<System.Type, Type> systemTypeToType;
static Action()
{
typeToSystemType = new Dictionary<Type, System.Type>
{
{ Type.Open, typeof(OpenActionResult) },
};
systemTypeToType = typeToSystemType.ToDictionary(p => p.Value, p => p.Key);
}
public static Type SystemTypeToType(System.Type systemType)
{
return systemTypeToType[systemType];
}
public static System.Type TypeToSystemType(Type type)
{
return typeToSystemType[type];
}
public enum Type
{
Open,
Close,
Remove,
Delete,
Reverse,
Alert,
ScaleInOut,
Nothing
}
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("active")]
[JsonConverter(typeof(IntToBoolConverter))]
public bool Active { get; set; }
[JsonProperty("type")]
[JsonConverter(typeof(ActionTypeConverter))]
public Type ActionType { get; set; }
[JsonProperty("result")]
public ActionResult Result { get; set; }
}
class ActionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var obj = JObject.Load(reader);
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);
var action = existingValue as Action ?? (Action)contract.DefaultCreator();
// Remove the Result property for manual deserialization
var result = obj.GetValue("Result", StringComparison.OrdinalIgnoreCase).RemoveFromLowestPossibleParent();
// Populate the remaining properties.
using (var subReader = obj.CreateReader())
{
serializer.Populate(subReader, action);
}
// Process the Result property
if (result != null)
action.Result = (ActionResult)result.ToObject(Action.TypeToSystemType(action.ActionType));
return action;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public static class JsonExtensions
{
public static JToken RemoveFromLowestPossibleParent(this JToken node)
{
if (node == null)
return null;
var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
if (contained != null)
contained.Remove();
// Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
if (node.Parent is JProperty)
((JProperty)node.Parent).Value = null;
return node;
}
}
Notice the use of JsonSerializer.Populate() inside ReadJson(). This automatically fills in all properties of Action other than Result, avoiding the need for manual deserialization of each.
Inspired by http://json.codeplex.com/discussions/56031:
public sealed class ActionModelConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(ActionModel).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jObject = JObject.Load(reader);
ActionModel actionModel = new ActionModel();
// TODO: Manually populate properties
actionModel.Id = (string)jObject["id"].ToObject<string>();
var type = (ActionModel.Type)jObject["type"].ToObject<ActionModel.Type>();
switch (type)
{
case ActionModel.Type.Open:
var actionResult = jObject["result"].ToObject<ActionOpenResult>(jsonSerializer);
default:
throw new JsonSerializationException($"Unsupported action type: '{type}'");
}
actionModel.Result = actionResult;
return actionModel;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Coded in editor, so sorry for typos :)
This is a tough one. I have an issue with binding a model from JSON. I am attempting to resolve polymorphic-ally the record supplied with the type of record that it will resolve to (I want to be able to add many record types in the future). I have attempted to use the following example to resolve my model when calling the endpoint however this example only works for MVC and not Web API applications.
I have attempted to write it using IModelBinder and BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext). However I can't find the equivalent of ModelMetadataProviders in the System.Web.Http namespace.
Appreciate any help anyone can give.
I have a Web API 2 application which has the following object structure.
public abstract class ResourceRecord
{
public abstract string Type { get; }
}
public class ARecord : ResourceRecord
{
public override string Type
{
get { return "A"; }
}
public string AVal { get; set; }
}
public class BRecord : ResourceRecord
{
public override string Type
{
get { return "B"; }
}
public string BVal { get; set; }
}
public class RecordCollection
{
public string Id { get; set; }
public string Name { get; set; }
public List<ResourceRecord> Records { get; }
public RecordCollection()
{
Records = new List<ResourceRecord>();
}
}
JSON Structure
{
"Id": "1",
"Name": "myName",
"Records": [
{
"Type": "A",
"AValue": "AVal"
},
{
"Type": "B",
"BValue": "BVal"
}
]
}
After some research I discovered that metadata providers don't exist within WebAPI and in order to bind to complex abstract objects you have to write your own.
I started by writing a new model binding method, with the use of a custom type name JSon serializer and finally I updated my endpoint to use the custom binder. It's worth noting the following will only work with requests in the body, you will have to write something else for requests in the header. I would suggest a read of chapter 16 of Adam Freeman's Expert ASP.NET Web API 2 for MVC Developers and complex object binding.
I was able to serialize my object from the body of the request using the following code.
WebAPI configuration
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Services.Insert(typeof(ModelBinderProvider), 0,
new SimpleModelBinderProvider(typeof(RecordCollection), new JsonBodyModelBinder<RecordCollection>()));
}
}
Custom model binder
public class JsonBodyModelBinder<T> : IModelBinder
{
public bool BindModel(HttpActionContext actionContext,
ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(T))
{
return false;
}
try
{
var json = ExtractRequestJson(actionContext);
bindingContext.Model = DeserializeObjectFromJson(json);
return true;
}
catch (JsonException exception)
{
bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);
return false;
}
return false;
}
private static T DeserializeObjectFromJson(string json)
{
var binder = new TypeNameSerializationBinder("");
var obj = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
Binder = binder
});
return obj;
}
private static string ExtractRequestJson(HttpActionContext actionContext)
{
var content = actionContext.Request.Content;
string json = content.ReadAsStringAsync().Result;
return json;
}
}
Custom Serialization binding
public class TypeNameSerializationBinder : SerializationBinder
{
public string TypeFormat { get; private set; }
public TypeNameSerializationBinder(string typeFormat)
{
TypeFormat = typeFormat;
}
public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = null;
typeName = serializedType.Name;
}
public override Type BindToType(string assemblyName, string typeName)
{
string resolvedTypeName = string.Format(TypeFormat, typeName);
return Type.GetType(resolvedTypeName, true);
}
}
End point definition
[HttpPost]
public void Post([ModelBinder(BinderType = typeof(JsonBodyModelBinder<RecordCollection>))]RecordCollection recordCollection)
{
}
The TypeNameSerializationBinder class is not necessary anymore as well as the WebApiConfig configuration.
First, you need to create enum for record type:
public enum ResourceRecordTypeEnum
{
a,
b
}
Then, change your "Type" field in ResourceRecord to be the enum we just created:
public abstract class ResourceRecord
{
public abstract ResourceRecordTypeEnum Type { get; }
}
Now you should create these 2 classes:
Model Binder
public class ResourceRecordModelBinder<T> : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(T))
return false;
try
{
var json = ExtractRequestJson(actionContext);
bindingContext.Model = DeserializeObjectFromJson(json);
return true;
}
catch (JsonException exception)
{
bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);
return false;
}
}
private static T DeserializeObjectFromJson(string json)
{
// This is the main part of the conversion
var obj = JsonConvert.DeserializeObject<T>(json, new ResourceRecordConverter());
return obj;
}
private string ExtractRequestJson(HttpActionContext actionContext)
{
var content = actionContext.Request.Content;
string json = content.ReadAsStringAsync().Result;
return json;
}
}
Converter class
public class ResourceRecordConverter : CustomCreationConverter<ResourceRecord>
{
private ResourceRecordTypeEnum _currentObjectType;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jobj = JObject.ReadFrom(reader);
// jobj is the serialized json of the reuquest
// It pulls from each record the "type" field as it is in requested json,
// in order to identify which object to create in "Create" method
_currentObjectType = jobj["type"].ToObject<ResourceRecordTypeEnum>();
return base.ReadJson(jobj.CreateReader(), objectType, existingValue, serializer);
}
public override ResourceRecord Create(Type objectType)
{
switch (_currentObjectType)
{
case ResourceRecordTypeEnum.a:
return new ARecord();
case ResourceRecordTypeEnum.b:
return new BRecord();
default:
throw new NotImplementedException();
}
}
}
Controller
[HttpPost]
public void Post([ModelBinder(BinderType = typeof(ResourceRecordModelBinder<RecordCollection>))] RecordCollection recordCollection)
{
}
Another option if you don't want to create a generic binder.
Custom Binder
public class RecordCollectionModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(RecordCollection))
{
return false;
}
try
{
var json = ExtractRequestJson(actionContext);
bindingContext.Model = DeserializeObjectFromJson(json);
return true;
}
catch (JsonException exception)
{
bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);
return false;
}
}
private string ExtractRequestJson(HttpActionContext actionContext)
{
var content = actionContext.Request.Content;
string json = content.ReadAsStringAsync().Result;
return json;
}
private RecordCollection DeserializeObjectFromJson(string json)
{
var jObject = JObject.Parse(json);
var result = jObject.ToObject<RecordCollection>();
if (result.Restrictions == null)
{
return result;
}
int index = 0;
foreach (var record in result.Records.ToList())
{
switch (record.Type)
{
case "A":
result.Restrictions[index] = jObject["Records"][index].ToObject<ARecord>();
break;
case "B":
result.Restrictions[index] = jObject["Records"][index].ToObject<BRecord>();
break;
}
index++;
}
return result;
}
}
Controller
[HttpPost]
public void Post([ModelBinder(BinderType = typeof(RecordCollectionModelBinder))] RecordCollection recordCollection)
{
}