自动绑定WebApi中从蛇形JSON转化为帕斯卡风格的C#模型

20

我正在尝试将使用PascalCase的c#模型从snake_case JSON绑定到WebApi v2(完整框架,而不是dot net core)。

这是我的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属性,那将是一个很好的选择。我是否可以让它“自动”工作?也许通过自定义模型绑定器或自定义JSON转换器实现?


你无法控制创建JSON的API吗? - Icepickle
没有对调用API的控制。 - Rocklan
JSON中的单词是否总是用下划线分隔? - Manoj Choudhari
对于这个问题,假设我们可以安全地将下划线转换为大写字母的下一个字母(以及第一个字母)。 - Rocklan
@Rocklan,你有查看过https://dev59.com/514b5IYBdhLWcg3w5VT9#28553455吗? - Dmitresky
3个回答

33
不需要重复造轮子。Json.Net已经有一个SnakeCaseNamingStrategy类来实现你想要的功能。你只需要通过设置将其作为NamingStrategy添加到DefaultContractResolver中即可。在你的WebApiConfig类的Register方法中添加以下代码:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
    new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() };

这里有一个演示(控制台应用程序)来证明这个概念: https://dotnetfiddle.net/v5siz7


如果你想对某些类应用蛇形命名法而对其他类不做更改,可以通过应用[JsonObject]属性来指定命名策略,如下所示:

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

通过属性设置的命名策略优先于通过解析器设置的命名策略,因此您可以在解析器中设置默认策略,然后在需要时使用属性覆盖它。 (Json.Net 包含三种命名策略:SnakeCaseNamingStrategyCamelCaseNamingStrategyDefaultNamingStrategy。)
现在,如果您想对同一类使用不同的命名策略进行反序列化和序列化,那么以上两种解决方案都无法满足您的需求,因为Web API中的命名策略将在两个方向上应用。 因此,在这种情况下,您需要像@icepickle的answer中所示的自定义内容来控制何时应用每个策略。

1
在这种情况下,对于那些不是蛇形命名的属性,请使用[JsonProperty] - Brian Rogers
看起来是一个理想的解决方案,但我能把它应用到某些类上吗?我的输出还可以使用驼峰命名吗? - Rocklan
1
@Rocklan (1) 是的;(2) 不是。我已经更新了我的答案以进一步解释。 - Brian Rogers
添加 [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] 正是我所需要的。一个小提示,它是在 Newtonsoft.json 的 9.0.1 版本中添加的。简直不敢相信答案如此简单! :) 非常感谢。一旦你知道了方法,事情总是变得容易...但找到方法却很难。 - Rocklan

2
好的,您可以使用自定义JsonConverter来读取数据。使用Manojs' answer中提供的反序列化方法,您可以创建一个DefaultContractResolver,当类具有指定的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类。
[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上的工作版本。

看起来是个很好的答案,非常感谢,我明天就可以试一下了。我相信我能处理十进制->整数的打字错误,但我会看看自己能否成功 :) - Rocklan

1

您可以像下面这样添加自定义的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;
    }
}

然后在你的类上指定该属性。

这应该有效。

这篇博客解释了使用控制台应用程序的方法。https://www.jerriepelser.com/blog/deserialize-different-json-object-same-class/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接