我可以在属性中指定一个路径来将我的类中的属性映射到JSON中的子属性吗?

69

我有一些代码(无法更改),它使用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; }

这是可能的吗?


我在这里找到了最好的答案:https://dev59.com/u1QK5IYBdhLWcg3wBrUK 如果有人想要查看,请自行前往! - letie
7个回答

85

如果您只需要一个额外的属性,一种简单的方法是将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);

Fiddle: https://dotnetfiddle.net/Ljw32O


1
我真的很喜欢你的“花哨”解决方案,但是你能让它兼容.NET 4.0吗?prop.GetCustomAttributes表示它不能与类型参数一起使用,而token.ToObject表示没有重载方法接受2个参数。 - David P
1
嘿,那是因为我刚刚更新了它以兼容4.0版本 ;-) 同时也更新了上面的代码。 - Brian Rogers
1
如何将其序列化回子属性? - Chris McGrath
1
@ChrisMcGrath,我认为你想要我添加的答案。 - Cristiano Santos
1
这个解决方案似乎会破坏应用于属性的其他JsonConverterAttribute:它们不再自动使用 :/ - Melvyn
显示剩余5条评论

19

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

同时,将can convert返回false将阻止其他用户用例,因此我将其更改为return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().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);
        }
    }

我很喜欢你添加了可写支持和<inheritdoc />--我可能需要从你那里借鉴一下,在我的实现中使用。虽然你可能想从我的那里借鉴读取支持,因为你的不支持没有设置器的属性(例如处理集合的最佳实践)。-- 我的代码位于:https://pastebin.com/4804DCzH - BrainSlugs83

10

与其做某事……

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://dev59.com/Y3M_5IYBdhLWcg3w1G6N#20769644
        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);
    }                               
}

对象 jValue = prop.GetValue(value); - Dragos Durlut
1
我发现似乎可以通过使用JToken.FromObject()来避免上面的条件代码。然而,总体方法中似乎存在一个致命缺陷,即FromObject()不会递归调用JsonConverter。因此,如果您有一个包含具有JSON路径名称的对象的数组,它将无法正确处理它们。 - Jon Miller

5

如果有人需要使用 @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);
[...]

1

提醒一下,我额外添加了一些内容来处理嵌套属性上的其他转换。例如,我们有一个嵌套的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);
                }

我尝试了您的解决方案,因为我在属性上使用了UnixDateTimeConverter属性,但是当解析该属性时,我遇到了JsonSerializationException:“解析日期时出现意外标记。期望整数或字符串,得到无。”异常发生在converter.ReadJson()处。 - teisnet
1
(解决方案:)在将其传递给converterType.ReadJson()之前,您需要在从token.CreateReader()返回的对象上调用Read()。 - teisnet

1

另一种解决方案(原始源代码来自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>());
...

0

在本帖的所有答案的帮助下,我想出了一个解决方案,即JsonPathConverter类(用作JsonConverter属性),它实现了ReadJsonWriteJson,可以使用正斜杠

该类的实现:

/// <summary>
/// Custom converter that allows mapping a JSON value according to a navigation path using forward slashes "/".
/// </summary>
public class JsonPathConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject data = JObject.Load(reader);
        object resultObject = Activator.CreateInstance(objectType);

        // Get all properties of a provided class
        PropertyInfo[] properties = objectType
            .GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);

        foreach (PropertyInfo propertyInfo in properties)
        {
            JsonPropertyAttribute propertyAttribute = propertyInfo
                .GetCustomAttributes(true)
                .OfType<JsonPropertyAttribute>()
                .FirstOrDefault();

            // Use either custom JSON property or regular property name
            string propertyJsonPath = propertyAttribute != null
                ? propertyAttribute.PropertyName
                : propertyInfo.Name;

            if (string.IsNullOrEmpty(propertyJsonPath))
            {
                continue;
            }

            // Split by the delimiter, and traverse recursively according to the path
            string[] nesting = propertyJsonPath.Split('/');
            object propertyValue = null;
            JToken token = null;
            for (int i = 0; i < nesting.Length; i++)
            {
                string name = nesting[i];
                bool isLast = i == nesting.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(resultObject, propertyValue);
        }

        return resultObject;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject resultJson = new();

        // Get all properties of a provided class
        IEnumerable<PropertyInfo> properties = value
            .GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);

        foreach (PropertyInfo propertyInfo in properties)
        {
            JsonPropertyAttribute propertyAttribute = propertyInfo
                .GetCustomAttributes(true)
                .OfType<JsonPropertyAttribute>()
                .FirstOrDefault();

            // Use either custom JSON property or regular property name
            string propertyJsonPath = propertyAttribute != null
                ? propertyAttribute.PropertyName
                : propertyInfo.Name;

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

            if (string.IsNullOrEmpty(propertyJsonPath))
            {
                continue;
            }

            // Split by the delimiter, and traverse according to the path
            string[] nesting = propertyJsonPath.Split('/');
            JObject lastJsonLevel = resultJson;
            for (int i = 0; i < nesting.Length; i++)
            {
                if (i == nesting.Length - 1)
                {
                    lastJsonLevel[nesting[i]] = JToken.FromObject(propertyInfo.GetValue(value));
                }
                else
                {
                    if (lastJsonLevel[nesting[i]] == null)
                    {
                        lastJsonLevel[nesting[i]] = new JObject();
                    }

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

        serializer.Serialize(writer, resultJson);
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
    }
}

请记得你还需要使用以下命名空间:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Reflection;

使用这个自定义的JsonConverter非常简单。假设我们有OP的JSON:

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

根据这个,我们可以创建一个对象来保存JSON数据:
[JsonConverter(typeof(JsonPathConverter))]
public class Picture
{
    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("data/type")]
    public int Type { get; set; }

    [JsonProperty("data/url")]
    public string Url { get; set; }
}

注意:不要忘记使用JsonConverter属性标记您的目标类,并像上面所示指定新创建的JsonPathConverter转换器。

然后,只需像平常一样将JSON反序列化为我们的对象即可:

var picture = JsonConvert.DeserializeObject<Picture>(json);

这个代码几乎完美,只是在序列化对象时,如果字段的值为null,会出错。稍微修改一下代码来处理这种边缘情况非常简单。if (i == nesting.Length - 1) { var val = propertyInfo.GetValue(value) ?? JValue.CreateNull(); lastJsonLevel[nesting[i]] = JToken.FromObject(val); } - Flojomojo

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