5

I have the following action signature

    [ValidateInput(false)]
    public HttpResponseMessage PostParam(Param param)

With Param looking something like this:

public class Param {
  public int Id { get; set;}
  public string Name { get; set; }
  public string Choices { get; set; }
}

Here's the hitch - what comes over the wire is something like this

{
  Id: 2,
  Name: "blah",
  Choices: [
    {
      foo: "bar"
    },
    {
      blah: "blo"
      something: 123
    }
  ]
}

I don't want "Choices" to deserialize - I want it stored as a string (yes, I understand the security implications). Understandably, I get an error because since the default binder does not know this.

Now with Asp Mvc creating a specific ModelBinder would be fairly simple. I'd

  • inherit DefaultModelBinder
  • override the property deserialization with my own
  • set the binder in my Application_Start using Binders.Add

Seems like with Web Api this is a different process - the System.Web.DefaultModelBinder doesn't have anything to override and that I can't hook things up using Binders.Add. I've tried looking around but couldn't find much on how to actually do what I want. This is further complicated since apparently the ModelBinders api changed quite a bit over Beta and RTM so there's a lot of outdated information out there.

George Mauer
  • 117,483
  • 131
  • 382
  • 612
  • Can't you change your client to send the `Choices` array already serializes as a string to your Web.API method? So `{ Id: 2, Name: "blah", Choices: "[ { foo: \"bar\" }, { blah: \"blo\" something: 123 } ]" }` – nemesv Feb 01 '13 at 20:06
  • @nemesv - I would prefer to not do any pre-processing on the client. I don't want to force all api consumers to worry about my server implementation. – George Mauer Feb 01 '13 at 20:20
  • 1
    Do you plan to support XML clients or JSON is enough? Because if XML is not needed you can change your `Choices` property to use `JArray` `public JArray Choices { get; set; }` – nemesv Feb 01 '13 at 20:32
  • @nemesv JSON is sufficient and it's really unlikely to be more than one client to be honest. Really there's a dozen different ways to hack around the issue, but I'm looking for specifically how to do this with ModelBinders (or if that's not the right tool for the job then why and what is). – George Mauer Feb 01 '13 at 20:34

1 Answers1

21

In Web API you have to distinguish three concepts - ModelBinding, Formatters and ParameterBinding. That is quite confusing to people moving from/used to MVC, where we only talk about ModelBinding.

ModelBinding, contrary to MVC, is responsible only for pulling data out of URI. Formatters deal with reading the body, and ParameterBinding (HttpParameterBinding) encompasses both of the former concepts.

ParameterBinding is really only useful when you want to revolutionize the whole mechanism (i.e. allow two objects to be bound from body, implement MVC-style binding and so on) - for simpler tasks modifying binders (for URI specific data) or formatters (for body data) is almost always more than enough.

Anyway, to the point - what you want to achieve can very easily be done with a custom JSON.NET converter (JSON.NET is the default serialization library behind Web API JSON formatting engine).

All you need to do is:

public class Param
{
    public int Id { get; set; }
    public string Name { get; set; }

    [JsonConverter(typeof(CustomArrayConverter))]
    public string Choices { get; set; }
}

And then add the converter:

internal class CustomArrayConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
                                    JsonSerializer serializer)
    {
        var array = JArray.Load(reader);
        return JsonConvert.SerializeObject(array);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, JArray.Parse(value as string));
    }
}

In this case we are telling JSON.NET in the converter to store Choices as string (in the read method), and when you return the Param object with the Choices property to the client (in the write method) we take the string and serialize to an array so that the output JSON looks identical to the input one.

You can test it now like this:

    public Param PostParam(Param param)
    {
        return param;
    }

And verify that the data coming in is like you wished, and the data coming out is identical to the original JSON.

Filip W
  • 27,097
  • 6
  • 95
  • 82
  • 2
    Is there a way to specify this globally without adding attributes? I.e. register somehow a converter which will always apply for a property of a certain type. My use case is trying to convert properties of type (mongo) ObjectId. I added a ModelBinder (implementing IModelBinder) and this works for controller methods having direct parameters of type ObjectId, but for methods having parameters complex types which in turn have properties of type ObjectId it doesn't. – adrian h. Apr 02 '15 at 17:07
  • @adrianhara: did you ever figure this one out? Having to specify the attribute everywhere is annoying. – Joshua Frank Nov 04 '15 at 18:50
  • Unfortunately I didn't and yes it's annoying. Hopefully someone will answer here with a better solution. – adrian h. Nov 05 '15 at 07:11