【问题标题】:Pascal case dynamic properties with Json.NET使用 Json.NET 的 Pascal 案例动态属性
【发布时间】:2012-02-12 08:34:25
【问题描述】:

这就是我所拥有的:

using Newtonsoft.Json;

var json = "{\"someProperty\":\"some value\"}";
dynamic deserialized = JsonConvert.DeserializeObject(json);

这很好用:

Assert.That(deserialized.someProperty.ToString(), Is.EqualTo("some value"));

我希望它在不更改 json 的情况下工作(属性的第一个字母大写):

Assert.That(deserialized.SomeProperty.ToString(), Is.EqualTo("some value"));

【问题讨论】:

    标签: c# .net json.net


    【解决方案1】:

    我同意 Avner Shahar-Kashtan 的观点。 您不应该这样做,尤其是在您无法控制 JSON 的情况下。

    也就是说,可以使用ExpandoObject 和自定义ExpandoObjectConverter 来完成。 JSON.NET 已经提供了ExpandoObjectConverter,因此只需稍作调整,您就可以得到您想要的。

    注意代码 sn-p 中的 //CHANGED cmets 向您显示我在哪里更改了它。

    public class CamelCaseToPascalCaseExpandoObjectConverter : JsonConverter
    {
      //CHANGED
      //the ExpandoObjectConverter needs this internal method so we have to copy it
      //from JsonReader.cs
      internal static bool IsPrimitiveToken(JsonToken token) 
      {
          switch (token)
          {
              case JsonToken.Integer:
              case JsonToken.Float:
              case JsonToken.String:
              case JsonToken.Boolean:
              case JsonToken.Null:
              case JsonToken.Undefined:
              case JsonToken.Date:
              case JsonToken.Bytes:
                  return true;
              default:
                  return false;
          }
      }
    
    /// <summary>
    /// Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
      // can write is set to false
    }
    
    /// <summary>
    /// Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
      return ReadValue(reader);
    }
    
    private object ReadValue(JsonReader reader)
    {
      while (reader.TokenType == JsonToken.Comment)
      {
        if (!reader.Read())
          throw new Exception("Unexpected end.");
      }
    
      switch (reader.TokenType)
      {
        case JsonToken.StartObject:
          return ReadObject(reader);
        case JsonToken.StartArray:
          return ReadList(reader);
        default:
          //CHANGED
          //call to static method declared inside this class
          if (IsPrimitiveToken(reader.TokenType))
            return reader.Value;
    
          //CHANGED
          //Use string.format instead of some util function declared inside JSON.NET
          throw new Exception(string.Format(CultureInfo.InvariantCulture, "Unexpected token when converting ExpandoObject: {0}", reader.TokenType));
      }
    }
    
    private object ReadList(JsonReader reader)
    {
      IList<object> list = new List<object>();
    
      while (reader.Read())
      {
        switch (reader.TokenType)
        {
          case JsonToken.Comment:
            break;
          default:
            object v = ReadValue(reader);
    
            list.Add(v);
            break;
          case JsonToken.EndArray:
            return list;
        }
      }
    
      throw new Exception("Unexpected end.");
    }
    
    private object ReadObject(JsonReader reader)
    {
      IDictionary<string, object> expandoObject = new ExpandoObject();
    
      while (reader.Read())
      {
        switch (reader.TokenType)
        {
          case JsonToken.PropertyName:
            //CHANGED
            //added call to ToPascalCase extension method       
            string propertyName = reader.Value.ToString().ToPascalCase();
    
            if (!reader.Read())
              throw new Exception("Unexpected end.");
    
            object v = ReadValue(reader);
    
            expandoObject[propertyName] = v;
            break;
          case JsonToken.Comment:
            break;
          case JsonToken.EndObject:
            return expandoObject;
        }
      }
    
      throw new Exception("Unexpected end.");
    }
    
    /// <summary>
    /// Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override bool CanConvert(Type objectType)
    {
      return (objectType == typeof (ExpandoObject));
    }
    
    /// <summary>
    /// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override bool CanWrite
    {
      get { return false; }
    }
    }
    

    一个简单的字符串到 Pascal 大小写转换器。如果需要,让它更智能。

    public static class StringExtensions
    {
        public static string ToPascalCase(this string s)
        {
            if (string.IsNullOrEmpty(s) || !char.IsLower(s[0]))
                return s;
    
            string str = char.ToUpper(s[0], CultureInfo.InvariantCulture).ToString((IFormatProvider)CultureInfo.InvariantCulture);
    
            if (s.Length > 1)
                str = str + s.Substring(1);
    
            return str;
        }
    }
    

    现在你可以像这样使用它了。

    var settings = new JsonSerializerSettings()
                       {
                           ContractResolver = new CamelCasePropertyNamesContractResolver(),
                           Converters = new List<JsonConverter> { new CamelCaseToPascalCaseExpandoObjectConverter() }
                       };
    
    var json = "{\"someProperty\":\"some value\"}";
    
    dynamic deserialized = JsonConvert.DeserializeObject<ExpandoObject>(json, settings);
    
    Console.WriteLine(deserialized.SomeProperty); //some value
    
    var json2 = JsonConvert.SerializeObject(deserialized, Formatting.None, settings);
    
    Console.WriteLine(json == json2); //true
    

    ContractResolverCamelCasePropertyNamesContractResolver 用于将对象序列化回 JSON 并再次使其成为 Camel 大小写。这也由 JSON.NET 提供。如果不需要,可以省略。

    【讨论】:

    • 好的,看起来这就是答案!我希望会有比这更好的扩展点......我现在会投票并在我尝试过时标记为已回答。谢谢!
    • 优秀的代码。虽然我完全不同意“你不应该这样做”的评论。如果 json 来自 javascript 客户端,则可能会发生这种约定不匹配。 Javascript 的属性约定是 camelCase。属性的 C# 约定是 Pascal。总得换个时间吧。
    • 这是一个很好的解决方案。我相信在某些用例中转换为 Pascal 案例是可以接受的做法。在我的例子中,一个类的一个属性是动态的。结果是非动态类型的帕斯卡大小写,动态及其子属性的骆驼大小写。我还修改了“CanConvert”方法以使用“return (objectType == typeof(Object));”,从而可以映射到动态属性。
    • .NET Core 1.0 的默认设置现在是将 PascalCase C# 属性转换为 camelCase json 属性,这为这个答案提供了新的相关性,我认为它非常相关。
    【解决方案2】:

    我不禁觉得这不是一个好主意。似乎您正在尝试保留编码约定,但以维护线路格式(JSON 结构)和逻辑类之间的保真度为代价。这可能会导致希望保留 JSON 类的开发人员感到困惑,并且如果您还需要或将来需要将此数据重新序列化为相同的 JSON 格式,则可能会导致问题。

    也就是说,这可能可以通过提前创建 .NET 类,然后使用 DeserializeObject(string value, JsonSerializerSettings settings) 重载,将 JsonSerializerSettingsBinder 属性集传递给它来实现。您还需要编写自定义 SerializationBinder,在其中手动定义 JSON 类和预定义的 .NET 类之间的关系。

    可以在运行时生成 Pascal 大小写的类而不预先定义它们,但我还没有深入研究 JSON.NET 的实现。也许是其他 JsonSerializerSettings 设置之一,例如传递 CustomCreationConverter,但我不确定详细信息。

    【讨论】:

    • 虽然我没有问这样做是否是一个好主意,但你和Martjin都提到它不是......所以是因为它是dynamic?如果我反序列化为public class { public string SomeProperty { get; set; } },您是否仍然觉得这是一个坏主意并更喜欢属性的小驼峰式大小写 (someProperty)?
    • 当您决定反序列化为合约时,编码约定已经存在。然而,今天的默认 MVC 绑定器不区分大小写。
    • .NET 中属性的大写约定是 PascalCase,而我从中获取 JSON 的系统的约定恰好是 camelCase。我认为转换为 PascalCase 是最好的方法,我很惊讶你们中的这么多人反对它。
    【解决方案3】:

    对于 newtonsoft,将此属性添加到您的属性中:

    [JsonProperty("schwabFirmId")]
    

    如果您准备包含 MongoDB,一个更简单的选项(因为您只需要在每个课程中执行一次):尝试添加对 MongoDB.Bson.Serialization.Conventions 的引用。

    然后在你的模型构造函数中添加这个:

    var pack = new ConventionPack { new CamelCaseElementNameConvention(), new IgnoreIfDefaultConvention(true) };
                ConventionRegistry.Register("CamelCaseIgnoreDefault", pack, t => true);
    

    任何一个都会保留您最喜欢的 C# 属性 PascalCased 和您的 json camelCased。

    反序列化会将入站数据视为 PascalCased,序列化会将其更改为 camelCase。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-04-20
      • 2020-02-03
      • 2023-03-15
      • 2014-05-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多