Automatically bind pascal case c# model from snake case JSON in WebApi

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.


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