在 WebApi 中自动绑定来自 snake case JSON 的 pascal case c# 模型
Automatically bind pascal case c# model from snake case JSON in WebApi
我正在尝试从 snake_cased JSON 中的 WebApi v2(完整框架,而非点网核心)绑定我的 PascalCased c# 模型。
这是我的 api:
public class MyApi : ApiController
{
[HttpPost]
public IHttpActionResult DoSomething([FromBody]InputObjectDTO inputObject)
{
database.InsertData(inputObject.FullName, inputObject.TotalPrice)
return Ok();
}
}
这是我的输入对象:
public class InputObjectDTO
{
public string FullName { get; set; }
public int TotalPrice { get; set; }
...
}
我遇到的问题是 JSON 看起来像这样:
{
"full_name": "John Smith",
"total_price": "20.00"
}
我知道我可以使用 JsonProperty 属性:
public class InputObjectDTO
{
[JsonProperty(PropertyName = "full_name")]
public string FullName { get; set; }
[JsonProperty(PropertyName = "total_price")]
public int TotalPrice { get; set; }
}
但是我的 InputObjectDTO 是 巨大的,而且还有很多其他人也喜欢它。它有数百个属性都是蛇形的,如果不必为每个 属性 指定 JsonProperty 属性就好了。我可以让它工作 "automatically" 吗?也许使用自定义模型活页夹或自定义 json 转换器?
您可以添加自定义 json 转换器代码,如下所示。这应该允许您指定 属性 映射。
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;
}
}
然后在您的 class 上指定此属性。
这应该有效。
这篇博客解释了使用控制台应用程序的方法。 https://www.jerriepelser.com/blog/deserialize-different-json-object-same-class/
好吧,您应该可以使用自定义 JsonConverter
来读取您的数据。使用 中提供的反序列化,您可以创建一个 DefaultContractResolver
,它会在 class 具有上面指定的 SnakeCasedAttribute
时创建自定义反序列化。
ContractResolver 如下所示
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;
}
}
SnakeCaseConverter
会是这样的吗?
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();
}
}
然后您可以使用此属性(这只是一个占位符)
注释您的 dto class
[SnakeCased]
public class InputObjectDTO {
public string FullName { get; set; }
public int TotalPrice { get; set; }
}
作为参考,这是使用的属性
[AttributeUsage(AttributeTargets.Class)]
public class SnakeCasedAttribute : Attribute {
public SnakeCasedAttribute() {
// intended blank
}
}
还有一件事要注意,在您当前的形式中,JSON 转换器会抛出一个错误(“20.00”不是一个整数),但我猜您可以从这里处理它分开你自己:)
要获得完整参考,您可以在 this dotnetfiddle
中查看工作版本
无需重新发明轮子。 Json.Net 通过设置已经有一个 SnakeCaseNamingStrategy
class to do exactly what you want. You just need to set it as the NamingStrategy
on the DefaultContractResolver
。
将此行添加到 WebApiConfig
中的 Register
方法 class:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() };
这里有一个演示(控制台应用程序)来证明这个概念:https://dotnetfiddle.net/v5siz7
如果您想将蛇形外壳应用于某些 class 而不是其他,您可以通过应用指定命名策略的 [JsonObject]
属性来实现,如下所示:
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class InputObjectDTO
{
public string FullName { get; set; }
public decimal TotalPrice { get; set; }
}
通过属性设置的命名策略优先于通过解析器设置的命名策略,因此您可以在解析器中设置默认策略,然后在需要时使用属性覆盖它。 (Json.Net 包含三种命名策略:SnakeCaseNamingStrategy
, CamelCaseNamingStrategy
and DefaultNamingStrategy
。)
现在,如果您想要 反序列化 使用一种命名策略,而 序列化 使用不同的策略 class (es),那么以上两种解决方案都不适合您,因为命名策略将在 Web 中双向应用 API。因此,在那种情况下,您将需要一些自定义的东西,例如@icepickle 的 中显示的内容来控制何时应用每个。
我正在尝试从 snake_cased JSON 中的 WebApi v2(完整框架,而非点网核心)绑定我的 PascalCased c# 模型。
这是我的 api:
public class MyApi : ApiController
{
[HttpPost]
public IHttpActionResult DoSomething([FromBody]InputObjectDTO inputObject)
{
database.InsertData(inputObject.FullName, inputObject.TotalPrice)
return Ok();
}
}
这是我的输入对象:
public class InputObjectDTO
{
public string FullName { get; set; }
public int TotalPrice { get; set; }
...
}
我遇到的问题是 JSON 看起来像这样:
{
"full_name": "John Smith",
"total_price": "20.00"
}
我知道我可以使用 JsonProperty 属性:
public class InputObjectDTO
{
[JsonProperty(PropertyName = "full_name")]
public string FullName { get; set; }
[JsonProperty(PropertyName = "total_price")]
public int TotalPrice { get; set; }
}
但是我的 InputObjectDTO 是 巨大的,而且还有很多其他人也喜欢它。它有数百个属性都是蛇形的,如果不必为每个 属性 指定 JsonProperty 属性就好了。我可以让它工作 "automatically" 吗?也许使用自定义模型活页夹或自定义 json 转换器?
您可以添加自定义 json 转换器代码,如下所示。这应该允许您指定 属性 映射。
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;
}
}
然后在您的 class 上指定此属性。
这应该有效。
这篇博客解释了使用控制台应用程序的方法。 https://www.jerriepelser.com/blog/deserialize-different-json-object-same-class/
好吧,您应该可以使用自定义 JsonConverter
来读取您的数据。使用 DefaultContractResolver
,它会在 class 具有上面指定的 SnakeCasedAttribute
时创建自定义反序列化。
ContractResolver 如下所示
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;
}
}
SnakeCaseConverter
会是这样的吗?
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();
}
}
然后您可以使用此属性(这只是一个占位符)
注释您的 dto class[SnakeCased]
public class InputObjectDTO {
public string FullName { get; set; }
public int TotalPrice { get; set; }
}
作为参考,这是使用的属性
[AttributeUsage(AttributeTargets.Class)]
public class SnakeCasedAttribute : Attribute {
public SnakeCasedAttribute() {
// intended blank
}
}
还有一件事要注意,在您当前的形式中,JSON 转换器会抛出一个错误(“20.00”不是一个整数),但我猜您可以从这里处理它分开你自己:)
要获得完整参考,您可以在 this dotnetfiddle
中查看工作版本无需重新发明轮子。 Json.Net 通过设置已经有一个 SnakeCaseNamingStrategy
class to do exactly what you want. You just need to set it as the NamingStrategy
on the DefaultContractResolver
。
将此行添加到 WebApiConfig
中的 Register
方法 class:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() };
这里有一个演示(控制台应用程序)来证明这个概念:https://dotnetfiddle.net/v5siz7
如果您想将蛇形外壳应用于某些 class 而不是其他,您可以通过应用指定命名策略的 [JsonObject]
属性来实现,如下所示:
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class InputObjectDTO
{
public string FullName { get; set; }
public decimal TotalPrice { get; set; }
}
通过属性设置的命名策略优先于通过解析器设置的命名策略,因此您可以在解析器中设置默认策略,然后在需要时使用属性覆盖它。 (Json.Net 包含三种命名策略:SnakeCaseNamingStrategy
, CamelCaseNamingStrategy
and DefaultNamingStrategy
。)
现在,如果您想要 反序列化 使用一种命名策略,而 序列化 使用不同的策略 class (es),那么以上两种解决方案都不适合您,因为命名策略将在 Web 中双向应用 API。因此,在那种情况下,您将需要一些自定义的东西,例如@icepickle 的