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