我可以在属性中指定路径,将类中的属性映射到JSON中的子属性吗?
本文关键字:属性 映射 JSON 路径 我可以 | 更新日期: 2023-09-27 18:17:15
有一些代码(我不能改变)使用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; }
这可能吗?
如果您只需要一个额外的属性,一个简单的方法是将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
标记的答案不是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);
}
}
不做
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);
}
}
如果有人需要使用@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);
[...]
另一个解决方案(原始源代码取自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>());
...
仅供参考,我添加了一点额外的内容来考虑嵌套属性上的任何其他转换。例如,我们有一个嵌套的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);
}
在这个线程的所有答案的帮助下,我提出了JsonPathConverter
类的解决方案(用作JsonConverter
属性),它实现了ReadJson
和WriteJson
与正斜杠一起工作。
类实现:
/// <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);