4

I am using Newtonsoft JSON in my MVC based off the example cited here: Setting the Default JSON Serializer in ASP.NET MVC.

My data contract class looks something like:

[DataContract]
public MyContractClass
{
    public MyContractClass()
    {
        this.ThisPropertyFails = new List<ClassDefinedInAnotherAssembly>();
    }

    [DataMember(EmitDefaultValue = false, Name = "thisPropertyIsFine")]
    public string ThisPropertyIsFine { get; set; }

    [DataMember(EmitDefaultValue = false, Name = "thisPropertyFails")]
    public IList<ClassDefinedInAnotherAssembly> ThisPropertyFails { get; internal set; }
}

The code I'm specifically using to de-serialize looks like this:

public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
    if (controllerContext == null)
    {
        throw new ArgumentNullException("controllerContext");                
    }

    if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
    {
        return null;                
    }

    var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
    var bodyText = reader.ReadToEnd();

    if (String.IsNullOrEmpty(bodyText))
    {
        return null;
    }
    else
    {
        JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
        serializerSettings.Converters.Add(new StringEnumConverter());
        serializerSettings.Converters.Add(new ExpandoObjectConverter());

        DictionaryValueProvider<object> result = new DictionaryValueProvider<object>(JsonConvert.DeserializeObject<ExpandoObject>(bodyText, serializerSettings), CultureInfo.CurrentCulture);
        return result;
    }

    //return String.IsNullOrEmpty(bodyText) ? null : new DictionaryValueProvider<object>(JsonConvert.DeserializeObject<ExpandoObject>(bodyText, new ExpandoObjectConverter(), new StringEnumConverter()), CultureInfo.InvariantCulture);
}

However, in the MVC action, ModelState.IsValid is false, and looking at the errors, I see this:

{"The parameter conversion from type 'System.Collections.Generic.List`1[[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]' to type 'OtherAssembly.ClassDefinedInAnotherAssembly' failed because no type converter can convert between these types."}

Does anyone have any idea what is going on here? This same class works fine with my WebApi project (which is 'OtherAssembly' in this example).

Edit #1

Using the code directly, with the known type, does indeed work. So it's something to do with properties under ExpandoObject. For example, this code:

    JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
    serializerSettings.Converters.Add(new StringEnumConverter());
    serializerSettings.Converters.Add(new ExpandoObjectConverter());

    JsonSerializer serializer = JsonSerializer.Create(serializerSettings);

    using (StreamReader streamReader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
    {
        using (JsonReader jsonReader = new JsonTextReader(streamReader))
        {
            var resultAbc = serializer.Deserialize(jsonReader, typeof(MyContractClass));
        }
    }

Works just fine.

Edit #2

It appears I'm not the only person to have this issue. Anyone using MVC and using the oft-cited source code to use Newtonsoft makes it impossible to de-serialize complex sub-properties: http://tech.pro/q/34/using-jsonnet-to-deserialize-incoming-json-in-aspnet-mvc

No idea why that code is so popular if it doesn't even work after 1 level in the contract?

Community
  • 1
  • 1
automaton
  • 1,972
  • 5
  • 25
  • 40
  • Maybe you should extend your own `JsonConverter` to make explicit convertion of you OtherAssembly.ClassDefinedInAnotherAssembly ? – Perfect28 May 21 '14 at 17:59
  • @Ksven I probably could do that but there are hundreds, if not thousands of classes, so it seems stupid to have to do that for all of them? That makes no sense. – automaton May 21 '14 at 18:06
  • You have _thousands_ of classes to serialize? – Bob. May 21 '14 at 19:08
  • @Bob. Haha I guess thousands was an exaggeration, but I do have over 300 right now, and it's growing (enterprise application). It's not practical to write specific converters for all of them, and I shouldn't even have to, as this exact same stuff works in WebApi with Newtonsoft with no issues. – automaton May 21 '14 at 19:10
  • Hopefully, you are only serializing relevant data and not entire classes because data is in those classes... I've always been using straight deserialization of objects with `MyObjectType myObj = JsonConvert.DeserializeObject(temp);` – Bob. May 21 '14 at 19:14
  • @Bob. I'm not sure what you mean, the classes are specifically data contract classes, hand-rolled only to have the properties I require. But there are many, and they all exist in JS as my own class structure as well. Every property exists to communicate information between the client and server. As for your example, that example means you know the type up-front, but as per my example, I'm trying to make a generic way to use Newtonsoft, with many properties/sub-properties of various types that are all required. – automaton May 21 '14 at 20:05
  • @automaton Any luck on this? I've run into the same issue and I can't understand why it's not more widespread. Surely lots of people are using JSON.Net as a value provider in MVC 4? – dan Aug 04 '14 at 04:25
  • @dan haha no, no luck. what I ended up doing is just using 'object' type instead on the contract, and then in the code, casting to a JObject, and then reading the properties off of that. It's pretty nasty, but it was the only way I could find that was reasonable... – automaton Aug 27 '14 at 14:59

2 Answers2

0

Too much overhead. Try :

public MyContractClass
{

    [JsonProperty("thisPropertyIsFine")]
    public string ThisPropertyIsFine { get; set; }

    [JsonProperty("thisPropertyFails")]
    public ClassDefinedInAnotherAssembly[] ThisPropertyFails { get; set; }
}

Serialize and deserialize this way

/* as Bob mentioned */
var deserialized = JsonCovert.DeserializeObject<MyContractClass>(jsonString);
var serialized = JsonCovert.SerializeObject(myContractClassInstance);

Json.NET does not serialize non-public members. See this Answer to change this behavior.

Community
  • 1
  • 1
Robert
  • 724
  • 5
  • 7
  • Having an IList or an array with an internal set is normal, as per CA/SA practices in C#. Furthermore, JSON.NET works just fine with it, as it doesn't need to *set* it, all it needs to do is call IEnumerator.Add, so you're wrong. And the issue is not about that at all. The issue is de-serializing complex objects from a different assembly automatically, which is still not possible. – automaton Aug 27 '14 at 15:01
0

The error message "The parameter conversion from type 'X' to type 'Y' failed because no type converter can convert between these types." is coming from the ASP.Net Model Binding process, not from Json.Net's deserialization process.

To fix this error you just need to create a custom model binder for whatever type is giving you the problem (ClassDefinedInAnotherAssembly). Here is an example I'm currently using for one of my custom types called Money:

public class MoneyModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // get the result
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        Money value;
        // if the result is null OR we cannot successfully parse the value as our custom type then let the default model binding process attempt it using whatever the default for Money is
        return valueProviderResult == null || !Money.TryParse(valueProviderResult.AttemptedValue, out value) ? base.BindModel(controllerContext, bindingContext) : value;
    }
}

TryParse from Money type:

public static bool TryParse(string value, out Money money)
{
    money = null;

    if (string.IsNullOrWhiteSpace(value))
        return false;

    decimal parsedValue;
    if (decimal.TryParse(value, out parsedValue) == false)
        return false;

    money = parsedValue;

    return true;
}

You wire up model binders in Global.asax.cs under Application_Start():

protected void Application_Start()
{
    // custom model binders
    ModelBinders.Binders.Add(typeof(Money), new MoneyModelBinder());
}

Any time the Model Binding process runs across a type of Money it will call this class and attempt to do the conversion.


You may want to remove your Json.Net ValueProviderFactory temporarily so its a non-factor while you resolve the model binding issue. There are lots of Json.Net ValueProviderFactorys out there. Not all are equal and may be causing an issue like not serializing dictionaries/arrays or not fully iterating an objects entire hierarchy before giving up.

TugboatCaptain
  • 4,150
  • 3
  • 47
  • 79