20

I am trying to bind my PascalCased c# model from snake_cased JSON in WebApi v2 (full framework, not dot net core).

Here's my api:

public class MyApi : ApiController
{
    [HttpPost]
    public IHttpActionResult DoSomething([FromBody]InputObjectDTO inputObject)
    {
        database.InsertData(inputObject.FullName, inputObject.TotalPrice)
        return Ok();
    }
}

And here's my input object:

public class InputObjectDTO
{
    public string FullName { get; set; }
    public int TotalPrice { get; set; }
    ...
}

The problem that I have is that the JSON looks like this:

{
    "full_name": "John Smith",
    "total_price": "20.00"
}

I am aware that I can use the JsonProperty attribute:

public class InputObjectDTO
{
    [JsonProperty(PropertyName = "full_name")]
    public string FullName { get; set; }

    [JsonProperty(PropertyName = "total_price")]
    public int TotalPrice { get; set; }
}

However my InputObjectDTO is huge, and there are many others like it too. It has hundreds of properties that are all snake cased, and it would be nice to not have to specify the JsonProperty attribute for each property. Can I make it to work "automatically"? Perhaps with a custom model binder or a custom json converter?

Rocklan
  • 7,888
  • 3
  • 34
  • 49

3 Answers3

32

No need to reinvent the wheel. Json.Net already has a SnakeCaseNamingStrategy class to do exactly what you want. You just need to set it as the NamingStrategy on the DefaultContractResolver via settings.

Add this line to the Register method in your WebApiConfig class:

config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
    new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() };

Here is a demo (console app) to prove the concept: https://dotnetfiddle.net/v5siz7


If you want to apply the snake casing to some classes but not others, you can do this by applying a [JsonObject] attribute specifying the naming strategy like so:

[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class InputObjectDTO
{
    public string FullName { get; set; }
    public decimal TotalPrice { get; set; }
}

The naming strategy set via attribute takes precedence over the naming strategy set via the resolver, so you can set your default strategy in the resolver and then use attributes to override it where needed. (There are three naming strategies included with Json.Net: SnakeCaseNamingStrategy, CamelCaseNamingStrategy and DefaultNamingStrategy.)


Now, if you want to deserialize using one naming strategy and serialize using a different strategy for the same class(es), then neither of the above solutions will work for you, because the naming strategies will be applied in both directions in Web API. So in in that case, you will need something custom like what is shown in @icepickle's answer to control when each is applied.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • 1
    @GonzaloLorieto In that situation, use `[JsonProperty]` for the properties that aren't snake cased. – Brian Rogers Feb 05 '19 at 20:16
  • Looks like an ideal solution, but can I apply it to just some classes? And can my output still be camel cased? – Rocklan Feb 05 '19 at 20:37
  • 1
    @Rocklan (1) Yes; (2) No. I've updated my answer to explain further. – Brian Rogers Feb 05 '19 at 21:25
  • Adding [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] is exactly what I needed. A small note, it was added in version 9.0.1 of Newtonsoft.json. Can't believe the answer was that simple! :) Thank you so much. It's always easy once you know how... but finding out how.. that's hard. – Rocklan Feb 06 '19 at 00:27
2

Well, you should be able to do it using a custom JsonConverter to read your data. Using the deserialization provided in Manojs' answer, you could create a DefaultContractResolver that would create a custom deserialization when the class has a SnakeCasedAttribute specified above.

The ContractResolver would look like the following

public class SnakeCaseContractResolver : DefaultContractResolver {
  public new static readonly SnakeCaseContractResolver Instance = new SnakeCaseContractResolver();

  protected override JsonContract CreateContract(Type objectType) {
    JsonContract contract = base.CreateContract(objectType);

    if (objectType?.GetCustomAttributes(true).OfType<SnakeCasedAttribute>().Any() == true) {
      contract.Converter = new SnakeCaseConverter();
    }

    return contract;
  }
}

The SnakeCaseConverter would be something like this?

public class SnakeCaseConverter : JsonConverter {
  public override bool CanConvert(Type objectType) => objectType.GetCustomAttributes(true).OfType<SnakeCasedAttribute>().Any() == true;
  private static string ConvertFromSnakeCase(string snakeCased) {
    return string.Join("", snakeCased.Split('_').Select(part => part.Substring(0, 1).ToUpper() + part.Substring(1)));
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
    var target = Activator.CreateInstance( objectType );
    var jobject = JObject.Load(reader);

    foreach (var property in jobject.Properties()) {
      var propName = ConvertFromSnakeCase(property.Name);
      var prop = objectType.GetProperty(propName);
      if (prop == null || !prop.CanWrite) {
        continue;
      }
      prop.SetValue(target, property.Value.ToObject(prop.PropertyType, serializer));
    }
    return target;
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
    throw new NotImplementedException();
  }
}

And then you could annotate your dto class using this attribute (which is just a placeholder)

[SnakeCased]
public class InputObjectDTO {
  public string FullName { get; set; }
  public int TotalPrice { get; set; }
}

and for reference, this is the used attribute

[AttributeUsage(AttributeTargets.Class)]
public class SnakeCasedAttribute : Attribute {
  public SnakeCasedAttribute() {
    // intended blank
  }
}

One more thing to notice is that in your current form the JSON converter would throw an error ("20.00" is not an int), but I am going to guess that from here you can handle that part yourself :)

And for a complete reference, you could see the working version in this dotnetfiddle

Icepickle
  • 12,689
  • 3
  • 34
  • 48
  • Looks like a great answer, thanks so much, I'll be able to try it out tomorrow. Pretty sure I can handle the decimal->int typo but I'll see how I go :) – Rocklan Feb 05 '19 at 07:03
1

You can add cusrom json converter code like below. This should allow you to specify property mapping.

public class ApiErrorConverter : JsonConverter
{
private readonly Dictionary<string, string>     _propertyMappings = new Dictionary<string, string>
{
    {"name", "error"},
    {"code", "errorCode"},
    {"description", "message"}
};

public override bool CanWrite => false;

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    throw new NotImplementedException();
}

public override bool CanConvert(Type objectType)
{
    return objectType.GetTypeInfo().IsClass;
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    object instance = Activator.CreateInstance(objectType);
    var props = objectType.GetTypeInfo().DeclaredProperties.ToList();

    JObject jo = JObject.Load(reader);
    foreach (JProperty jp in jo.Properties())
    {
        if (!_propertyMappings.TryGetValue(jp.Name, out var name))
            name = jp.Name;

        PropertyInfo prop = props.FirstOrDefault(pi =>
            pi.CanWrite && pi.GetCustomAttribute<JsonPropertyAttribute>().PropertyName == name);

        prop?.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
    }

    return instance;
    }
}

Then specify this attribute on your class.

This should work.

This blog explains the approach using console Application. https://www.jerriepelser.com/blog/deserialize-different-json-object-same-class/

Manoj Choudhari
  • 5,277
  • 2
  • 26
  • 37