【问题标题】:Can I specify a path in an attribute to map a property in my class to a child property in my JSON?我可以在属性中指定路径以将我的类中的属性映射到我的 JSON 中的子属性吗?
【发布时间】:2018-03-08 17:46:04
【问题描述】:

有一些代码(我无法更改)使用 Newtonsoft.Json 的 DeserializeObject<T>(strJSONData) 从 Web 请求中获取数据并将其转换为类对象(我可以更改类)。通过使用[DataMember(Name = "raw_property_name")] 装饰我的类属性,我可以将原始 JSON 数据映射到我的类中的正确属性。有没有办法可以将 JSON 复杂对象的子属性映射到简单属性?这是一个例子:

{
    "picture": 
    {
        "id": 123456,
        "data": 
        {
            "type": "jpg",
            "url": "http://www.someplace.com/mypicture.jpg"
        }
    }
}

除了 URL 之外,我不关心图片对象的任何其余部分,因此不想在我的 C# 类中设置复杂的对象。我真的只是想要这样的东西:

[DataMember(Name = "picture.data.url")]
public string ProfilePicture { get; set; }

这可能吗?

【问题讨论】:

标签: c# json json.net deserialization


【解决方案1】:

好吧,如果您只需要一个额外的属性,一种简单的方法是将您的 JSON 解析为 JObject,使用 ToObject()JObject 填充您的类,然后使用 SelectToken() 拉取在额外的属性中。

所以,假设你的班级看起来像这样:

class Person
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("age")]
    public string Age { get; set; }

    public string ProfilePicture { get; set; }
}

你可以这样做:

string json = @"
{
    ""name"" : ""Joe Shmoe"",
    ""age"" : 26,
    ""picture"":
    {
        ""id"": 123456,
        ""data"":
        {
            ""type"": ""jpg"",
            ""url"": ""http://www.someplace.com/mypicture.jpg""
        }
    }
}";

JObject jo = JObject.Parse(json);
Person p = jo.ToObject<Person>();
p.ProfilePicture = (string)jo.SelectToken("picture.data.url");

小提琴:https://dotnetfiddle.net/7gnJCK


如果您喜欢更花哨的解决方案,您可以自定义JsonConverter 以使JsonProperty 属性的行为与您描述的一样。转换器需要在类级别上运行,并结合上述技术使用一些反射来填充所有属性。以下是它在代码中的样子:

class JsonPathConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, 
                                    object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        object targetObj = Activator.CreateInstance(objectType);

        foreach (PropertyInfo prop in objectType.GetProperties()
                                                .Where(p => p.CanRead && p.CanWrite))
        {
            JsonPropertyAttribute att = prop.GetCustomAttributes(true)
                                            .OfType<JsonPropertyAttribute>()
                                            .FirstOrDefault();

            string jsonPath = (att != null ? att.PropertyName : prop.Name);
            JToken token = jo.SelectToken(jsonPath);

            if (token != null && token.Type != JTokenType.Null)
            {
                object value = token.ToObject(prop.PropertyType, serializer);
                prop.SetValue(targetObj, value, null);
            }
        }

        return targetObj;
    }

    public override bool CanConvert(Type objectType)
    {
        // CanConvert is not called when [JsonConverter] attribute is used
        return false;
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value,
                                   JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

为了演示,我们假设 JSON 现在如下所示:

{
  "name": "Joe Shmoe",
  "age": 26,
  "picture": {
    "id": 123456,
    "data": {
      "type": "jpg",
      "url": "http://www.someplace.com/mypicture.jpg"
    }
  },
  "favorites": {
    "movie": {
      "title": "The Godfather",
      "starring": "Marlon Brando",
      "year": 1972
    },
    "color": "purple"
  }
}

...除了之前的信息之外,您还对该人最喜欢的电影(标题和年份)和最喜欢的颜色感兴趣。您将首先使用[JsonConverter] 属性标记您的目标类以将其与自定义转换器关联,然后在每个属性上使用[JsonProperty] 属性,指定所需的属性路径(区分大小写)作为名称。目标属性也不必是原语——您可以像我在这里对Movie 所做的那样使用子类(注意不需要干预Favorites 类)。

[JsonConverter(typeof(JsonPathConverter))]
class Person
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("age")]
    public int Age { get; set; }

    [JsonProperty("picture.data.url")]
    public string ProfilePicture { get; set; }

    [JsonProperty("favorites.movie")]
    public Movie FavoriteMovie { get; set; }

    [JsonProperty("favorites.color")]
    public string FavoriteColor { get; set; }
}

// Don't need to mark up these properties because they are covered by the 
// property paths in the Person class
class Movie
{
    public string Title { get; set; }
    public int Year { get; set; }
}

有了所有的属性,你可以像往常一样反序列化,它应该“正常工作”:

Person p = JsonConvert.DeserializeObject<Person>(json);

小提琴:https://dotnetfiddle.net/Ljw32O

【讨论】:

  • 我真的很喜欢你的“花哨”解决方案,但你能让它与 .NET 4.0 兼容吗? prop.GetCustomAttributes 表示它不能与类型参数一起使用,而 token.ToObject 表示没有重载方法需要 2 个参数。
  • 呵呵,那是因为我刚刚更新它以兼容 4.0 ;-) 还更新了上面的代码。
  • 如何将其序列化回子属性
  • @ChrisMcGrath 我想你想要我添加的答案。
  • 此解决方案似乎破坏了应用于属性的其他 JsonConverterAttribute:它们不再自动使用:/
【解决方案2】:

标记的答案不是 100% 完整的,因为它忽略了任何可能注册的 IContractResolver,例如 CamelCasePropertyNamesContractResolver 等。

对于 can convert 也返回 false 将阻止其他用户案例,所以我将其更改为 return objectType.GetCustomAttributes(true).OfType&lt;JsonPathConverter&gt;().Any();

这是更新版本: https://dotnetfiddle.net/F8C8U8

我还消除了在属性上设置JsonProperty 的需要,如链接所示。

如果由于某种原因上面的链接失效或爆炸,我还包括以下代码:

public class JsonPathConverter : JsonConverter
    {
        /// <inheritdoc />
        public override object ReadJson(
            JsonReader reader,
            Type objectType,
            object existingValue,
            JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            object targetObj = Activator.CreateInstance(objectType);

            foreach (PropertyInfo prop in objectType.GetProperties().Where(p => p.CanRead && p.CanWrite))
            {
                JsonPropertyAttribute att = prop.GetCustomAttributes(true)
                                                .OfType<JsonPropertyAttribute>()
                                                .FirstOrDefault();

                string jsonPath = att != null ? att.PropertyName : prop.Name;

                if (serializer.ContractResolver is DefaultContractResolver)
                {
                    var resolver = (DefaultContractResolver)serializer.ContractResolver;
                    jsonPath = resolver.GetResolvedPropertyName(jsonPath);
                }

                if (!Regex.IsMatch(jsonPath, @"^[a-zA-Z0-9_.-]+$"))
                {
                    throw new InvalidOperationException($"JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots but name was ${jsonPath}."); // Array operations not permitted
                }

                JToken token = jo.SelectToken(jsonPath);
                if (token != null && token.Type != JTokenType.Null)
                {
                    object value = token.ToObject(prop.PropertyType, serializer);
                    prop.SetValue(targetObj, value, null);
                }
            }

            return targetObj;
        }

        /// <inheritdoc />
        public override bool CanConvert(Type objectType)
        {
            // CanConvert is not called when [JsonConverter] attribute is used
            return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
        }

        /// <inheritdoc />
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var properties = value.GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);
            JObject main = new JObject();
            foreach (PropertyInfo prop in properties)
            {
                JsonPropertyAttribute att = prop.GetCustomAttributes(true)
                    .OfType<JsonPropertyAttribute>()
                    .FirstOrDefault();

                string jsonPath = att != null ? att.PropertyName : prop.Name;

                if (serializer.ContractResolver is DefaultContractResolver)
                {
                    var resolver = (DefaultContractResolver)serializer.ContractResolver;
                    jsonPath = resolver.GetResolvedPropertyName(jsonPath);
                }

                var nesting = jsonPath.Split('.');
                JObject lastLevel = main;

                for (int i = 0; i < nesting.Length; i++)
                {
                    if (i == nesting.Length - 1)
                    {
                        lastLevel[nesting[i]] = new JValue(prop.GetValue(value));
                    }
                    else
                    {
                        if (lastLevel[nesting[i]] == null)
                        {
                            lastLevel[nesting[i]] = new JObject();
                        }

                        lastLevel = (JObject)lastLevel[nesting[i]];
                    }
                }
            }

            serializer.Serialize(writer, main);
        }
    }

【讨论】:

  • 我喜欢你添加了可写支持和 -- 我可能不得不在我自己的实现中向你借用它。尽管您可能想借用我的阅读支持,因为您的不支持没有设置器的属性(例如,使用集合的最佳实践)。 -- 我的地址是:pastebin.com/4804DCzH
【解决方案3】:

不做

lastLevel [nesting [i]] = new JValue(prop.GetValue (value));

你必须这样做

lastLevel[nesting[i]] = JValue.FromObject(jValue);

否则我们有一个

无法确定类型的 JSON 对象类型 ...

异常

一段完整的代码是这样的:

object jValue = prop.GetValue(value);
if (prop.PropertyType.IsArray)
{
    if(jValue != null)
        //https://stackoverflow.com/a/20769644/249895
        lastLevel[nesting[i]] = JArray.FromObject(jValue);
}
else
{
    if (prop.PropertyType.IsClass && prop.PropertyType != typeof(System.String))
    {
        if (jValue != null)
            lastLevel[nesting[i]] = JValue.FromObject(jValue);
    }
    else
    {
        lastLevel[nesting[i]] = new JValue(jValue);
    }                               
}

【讨论】:

  • object jValue = prop.GetValue(value);
  • 我发现您似乎可以通过使用JToken.FromObject() 来避免上面的条件代码。然而,在整个方法中似乎也存在一个致命缺陷,即FromObject() 不会递归调用JsonConverter。因此,如果您有一个包含对象的数组,该对象的名称也为 JSON 路径,它将无法正确处理它们。
【解决方案4】:

如果有人需要使用 @BrianRogers 的 JsonPathConverter 和 WriteJson 选项,这里有一个解决方案(仅适用于具有仅点的路径):

删除CanWrite 属性,使其再次默认变为true

WriteJson 代码替换为以下内容:

public override void WriteJson(JsonWriter writer, object value,
    JsonSerializer serializer)
{
    var properties = value.GetType().GetRuntimeProperties ().Where(p => p.CanRead && p.CanWrite);
    JObject main = new JObject ();
    foreach (PropertyInfo prop in properties) {
        JsonPropertyAttribute att = prop.GetCustomAttributes(true)
            .OfType<JsonPropertyAttribute>()
            .FirstOrDefault();

        string jsonPath = (att != null ? att.PropertyName : prop.Name);
        var nesting=jsonPath.Split(new[] { '.' });
        JObject lastLevel = main;
        for (int i = 0; i < nesting.Length; i++) {
            if (i == nesting.Length - 1) {
                lastLevel [nesting [i]] = new JValue(prop.GetValue (value));
            } else {
                if (lastLevel [nesting [i]] == null) {
                    lastLevel [nesting [i]] = new JObject ();
                }
                lastLevel = (JObject)lastLevel [nesting [i]];
            }
        }

    }
    serializer.Serialize (writer, main);
}

如上所述,这只适用于包含的路径。鉴于此,您应该将以下代码添加到 ReadJson 以防止其他情况:

[...]
string jsonPath = (att != null ? att.PropertyName : prop.Name);
if (!Regex.IsMatch(jsonPath, @"^[a-zA-Z0-9_.-]+$")) {
    throw new InvalidOperationException("JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots."); //Array operations not permitted
}
JToken token = jo.SelectToken(jsonPath);
[...]

【讨论】:

    【解决方案5】:

    另一种解决方案(原始源代码取自https://gist.github.com/lucd/cdd57a2602bd975ec0a6)。我已经清理了源代码并添加了类/类数组支持。需要 C# 7

    /// <summary>
    /// Custom converter that allows mapping a JSON value according to a navigation path.
    /// </summary>
    /// <typeparam name="T">Class which contains nested properties.</typeparam>
    public class NestedJsonConverter<T> : JsonConverter
        where T : new()
    {
        /// <inheritdoc />
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(T);
        }
    
        /// <inheritdoc />
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var result = new T();
            var data = JObject.Load(reader);
    
            // Get all properties of a provided class
            var properties = result
                .GetType()
                .GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
    
            foreach (var propertyInfo in properties)
            {
                var jsonPropertyAttribute = propertyInfo
                    .GetCustomAttributes(false)
                    .FirstOrDefault(attribute => attribute is JsonPropertyAttribute);
    
                // Use either custom JSON property or regular property name
                var propertyName = jsonPropertyAttribute != null
                    ? ((JsonPropertyAttribute)jsonPropertyAttribute).PropertyName
                    : propertyInfo.Name;
    
                if (string.IsNullOrEmpty(propertyName))
                {
                    continue;
                }
    
                // Split by the delimiter, and traverse recursively according to the path
                var names = propertyName.Split('/');
                object propertyValue = null;
                JToken token = null;
                for (int i = 0; i < names.Length; i++)
                {
                    var name = names[i];
                    var isLast = i == names.Length - 1;
    
                    token = token == null
                        ? data.GetValue(name, StringComparison.OrdinalIgnoreCase)
                        : ((JObject)token).GetValue(name, StringComparison.OrdinalIgnoreCase);
    
                    if (token == null)
                    {
                        // Silent fail: exit the loop if the specified path was not found
                        break;
                    }
    
                    if (token is JValue || token is JArray || (token is JObject && isLast))
                    {
                        // simple value / array of items / complex object (only if the last chain)
                        propertyValue = token.ToObject(propertyInfo.PropertyType, serializer);
                    }
                }
    
                if (propertyValue == null)
                {
                    continue;
                }
    
                propertyInfo.SetValue(result, propertyValue);
            }
    
            return result;
        }
    
        /// <inheritdoc />
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
        }
    }
    

    样本模型

    public class SomeModel
    {
        public List<string> Records { get; set; }
    
        [JsonProperty("level1/level2/level3")]
        public string SomeValue{ get; set; }
    }
    

    示例 json:

    {
        "records": ["some value1", "somevalue 2"],
        "level1":
        {
             "level2":
             {
                 "level3": "gotcha!"
             }
        }
    }
    

    添加 JsonConverter 后,您可以像这样使用它:

    var json = "{}"; // input json string
    var settings = new JsonSerializerSettings();
    settings.Converters.Add(new NestedJsonConverter<SomeModel>());
    var result = JsonConvert.DeserializeObject<SomeModel>(json , settings);
    

    小提琴:https://dotnetfiddle.net/pBK9dj

    请记住,如果您在不同的类中有多个嵌套属性,那么您需要添加与您拥有的类一样多的转换器:

    settings.Converters.Add(new NestedJsonConverter<Model1>());
    settings.Converters.Add(new NestedJsonConverter<Model2>());
    ...
    

    【讨论】:

      【解决方案6】:

      仅供参考,我添加了一些额外的内容来说明嵌套属性上的任何其他转换。例如,我们有一个嵌套的 DateTime? 属性,但结果有时以空字符串的形式提供,所以我们必须有 另一个 JsonConverter 来适应这个。

      我们的班级是这样结束的:

      [JsonConverter(typeof(JsonPathConverter))] // Reference the nesting class
      public class Timesheet {
      
          [JsonConverter(typeof(InvalidDateConverter))]
          [JsonProperty("time.start")]
          public DateTime? StartTime { get; set; }
      
      }
      
      

      JSON 是:

      
      {
          time: {
              start: " "
          }
      }
      

      上面JsonConverter的最后更新是:

      var token = jo.SelectToken(jsonPath);
                      if (token != null && token.Type != JTokenType.Null)
                      {
                          object value = null;
      
                          // Apply custom converters
                          var converters = prop.GetCustomAttributes<JsonConverterAttribute>(); //(true).OfType<JsonPropertyAttribute>().FirstOrDefault();
                          if (converters != null && converters.Any())
                          {
                              foreach (var converter in converters)
                              {
                                  var converterType = (JsonConverter)Activator.CreateInstance(converter.ConverterType);
                                  if (!converterType.CanRead) continue;
                                  value = converterType.ReadJson(token.CreateReader(), prop.PropertyType, value, serializer);
                              }
                          }
                          else
                          {
                              value = token.ToObject(prop.PropertyType, serializer);
                          }
      
      
                          prop.SetValue(targetObj, value, null);
                      }
      

      【讨论】:

        猜你喜欢
        • 2013-04-03
        • 1970-01-01
        • 1970-01-01
        • 2012-10-01
        • 2012-10-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多