如何使用JSON.net处理同一属性的单个项和数组
我正试图修复我的SendGridPlus库以处理SendGrid事件,但由于API中对类别的处理不一致,我遇到了一些问题 在以下来自的有效负载示例中,您会注意到每个项目的如何使用JSON.net处理同一属性的单个项和数组,json.net,sendgrid,Json.net,Sendgrid,我正试图修复我的SendGridPlus库以处理SendGrid事件,但由于API中对类别的处理不一致,我遇到了一些问题 在以下来自的有效负载示例中,您会注意到每个项目的category属性可以是单个字符串或字符串数组 [ { "email": "john.doe@sendgrid.com", "timestamp": 1337966815, "category": [ "newuser", "transactional" ],
category
属性可以是单个字符串或字符串数组
[
{
"email": "john.doe@sendgrid.com",
"timestamp": 1337966815,
"category": [
"newuser",
"transactional"
],
"event": "open"
},
{
"email": "jane.doe@sendgrid.com",
"timestamp": 1337966815,
"category": "olduser",
"event": "open"
}
]
让JSON.NET变成这样,我的选择似乎是在字符串出现之前修复它,或者配置JSON.NET以接受不正确的数据。如果我能侥幸逃脱,我宁愿不做任何字符串解析
使用Json.Net是否还有其他方法可以处理此问题?您可以使用
JSONConverterAttribute
,如下所示:
假设你有一个看起来像
公共类根对象
{
公共字符串电子邮件{get;set;}
公共int时间戳{get;set;}
公共字符串smtpid{get;set;}
公共字符串@event{get;set;}
公共字符串类别[]{get;set;}
}
您将装饰类别属性,如下所示:
[JsonConverter(typeof(SendGridCategoryConverter))]
公共字符串类别{get;set;}
公共类SendGridCategoryConverter:JsonConverter
{
公共覆盖布尔CanConvert(类型objectType)
{
返回true;//添加您自己的逻辑
}
公共重写对象ReadJson(JsonReader阅读器,类型objectType,对象existingValue,JsonSerializer序列化程序)
{
//请在此处执行操作,以处理返回数组的问题,而不考虑数组中的对象数
}
公共重写void WriteJson(JsonWriter编写器、对象值、JsonSerializer序列化器)
{
//留给读者作为练习:)
抛出新的NotImplementedException();
}
}
处理这种情况的最佳方法是使用自定义的
在进入转换器之前,我们需要定义一个类来反序列化数据。对于单个项和数组之间可能不同的类别
属性,请将其定义为列表
,并使用[JsonConverter]
属性对其进行标记,以便JSON.Net知道如何为该属性使用自定义转换器。我还建议使用[JsonProperty]
属性,这样就可以为成员属性指定有意义的名称,而不依赖于JSON中定义的内容
类项目
{
[JsonProperty(“电子邮件”)]
公共字符串电子邮件{get;set;}
[JsonProperty(“时间戳”)]
公共int时间戳{get;set;}
[JsonProperty(“事件”)]
公共字符串事件{get;set;}
[JsonProperty(“类别”)]
[JsonConverter(类型(SingleOrArrayConverter))]
公共列表类别{get;set;}
}
下面是我将如何实现转换器。请注意,我已将转换器设置为通用的,以便可以根据需要与字符串或其他类型的对象一起使用
class SingleOrArrayConverter:JsonConverter
{
公共覆盖布尔CanConvert(类型objectType)
{
返回(objectType==typeof(List));
}
公共重写对象ReadJson(JsonReader阅读器,类型objectType,对象existingValue,JsonSerializer序列化程序)
{
JToken令牌=JToken.Load(读卡器);
if(token.Type==JTokenType.Array)
{
返回token.ToObject();
}
返回新列表{token.ToObject()};
}
公共覆盖布尔可写
{
获取{return false;}
}
公共重写void WriteJson(JsonWriter编写器、对象值、JsonSerializer序列化器)
{
抛出新的NotImplementedException();
}
}
下面是一个简短的程序,用示例数据演示转换器的工作:
类程序
{
静态void Main(字符串[]参数)
{
字符串json=@”
[
{
“电子邮件:”约翰。doe@sendgrid.com"",
“时间戳”:133796815,
“类别”:[
“新用户”,
“事务性”
],
“事件”:“打开”
},
{
“电子邮件:”简。doe@sendgrid.com"",
“时间戳”:133796815,
“”类别“”:“”旧用户“”,
“事件”:“打开”
}
]";
List List=JsonConvert.DeserializeObject(json);
foreach(列表中的项目obj)
{
Console.WriteLine(“电子邮件:+obj.email”);
Console.WriteLine(“时间戳:+obj.timestamp”);
控制台写入线(“事件:+obj.event”);
Console.WriteLine(“categories:+string.Join(“,”,obj.categories));
Console.WriteLine();
}
}
}
最后,这是上面的输出:
email: john.doe@sendgrid.com
timestamp: 1337966815
event: open
categories: newuser, transactional
email: jane.doe@sendgrid.com
timestamp: 1337966815
event: open
categories: olduser
小提琴:
编辑
如果需要采用另一种方式,即序列化,同时保持相同的格式,则可以实现转换器的WriteJson()
方法,如下所示。(请确保删除CanWrite
覆盖或将其更改为返回true
,否则将永远不会调用WriteJson()
public override void WriteJson(JsonWriter编写器、对象值、JsonSerializer序列化器)
{
列表=(列表)值;
如果(list.Count==1)
{
值=列表[0];
}
serializer.Serialize(writer,value);
}
Fiddle:我找到了另一个解决方案,可以使用object将类别处理为字符串或数组。这样我就不需要搞乱json序列化程序
如果你有时间,请看一下,告诉我你的想法
它基于当时的解决方案,但我还添加了时间戳的日期转换,升级了变量以反映当前的SendGrid模型(并使类别起作用)
我还创建了一个带有basicauth选项的处理程序。请参阅ashx文件和示例
谢谢大家! 我在这方面工作了很久,感谢布赖恩的回答。
A.
public class SingleOrArrayListConverter : JsonConverter
{
// Adapted from this answer https://stackoverflow.com/a/18997172
// to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
// by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
readonly bool canWrite;
readonly IContractResolver resolver;
public SingleOrArrayListConverter() : this(false) { }
public SingleOrArrayListConverter(bool canWrite) : this(canWrite, null) { }
public SingleOrArrayListConverter(bool canWrite, IContractResolver resolver)
{
this.canWrite = canWrite;
// Use the global default resolver if none is passed in.
this.resolver = resolver ?? new JsonSerializer().ContractResolver;
}
static bool CanConvert(Type objectType, IContractResolver resolver)
{
Type itemType;
JsonArrayContract contract;
return CanConvert(objectType, resolver, out itemType, out contract);
}
static bool CanConvert(Type objectType, IContractResolver resolver, out Type itemType, out JsonArrayContract contract)
{
if ((itemType = objectType.GetListItemType()) == null)
{
itemType = null;
contract = null;
return false;
}
// Ensure that [JsonObject] is not applied to the type.
if ((contract = resolver.ResolveContract(objectType) as JsonArrayContract) == null)
return false;
var itemContract = resolver.ResolveContract(itemType);
// Not implemented for jagged arrays.
if (itemContract is JsonArrayContract)
return false;
return true;
}
public override bool CanConvert(Type objectType) { return CanConvert(objectType, resolver); }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Type itemType;
JsonArrayContract contract;
if (!CanConvert(objectType, serializer.ContractResolver, out itemType, out contract))
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), objectType));
if (reader.MoveToContent().TokenType == JsonToken.Null)
return null;
var list = (IList)(existingValue ?? contract.DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
else
// Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Add<T> method.
list.Add(serializer.Deserialize(reader, itemType));
return list;
}
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var list = value as ICollection;
if (list == null)
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
// Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Count method.
if (list.Count == 1)
{
foreach (var item in list)
{
serializer.Serialize(writer, item);
break;
}
}
else
{
writer.WriteStartArray();
foreach (var item in list)
serializer.Serialize(writer, item);
writer.WriteEndArray();
}
}
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContent(this JsonReader reader)
{
while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
;
return reader;
}
internal static Type GetListItemType(this Type type)
{
// Quick reject for performance
if (type.IsPrimitive || type.IsArray || type == typeof(string))
return null;
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType;
}
return null;
}
}
var settings = new JsonSerializerSettings
{
// Pass true if you want single-item lists to be reserialized as single items
Converters = { new SingleOrArrayListConverter(true) },
};
var list = JsonConvert.DeserializeObject<List<Item>>(json, settings);
public class SingleOrArrayCollectionConverter<TCollection, TItem> : JsonConverter
where TCollection : ICollection<TItem>
{
// Adapted from this answer https://stackoverflow.com/a/18997172
// to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
// by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
readonly bool canWrite;
public SingleOrArrayCollectionConverter() : this(false) { }
public SingleOrArrayCollectionConverter(bool canWrite) { this.canWrite = canWrite; }
public override bool CanConvert(Type objectType)
{
return typeof(TCollection).IsAssignableFrom(objectType);
}
static void ValidateItemContract(IContractResolver resolver)
{
var itemContract = resolver.ResolveContract(typeof(TItem));
if (itemContract is JsonArrayContract)
throw new JsonSerializationException(string.Format("Item contract type {0} not supported.", itemContract));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
ValidateItemContract(serializer.ContractResolver);
if (reader.MoveToContent().TokenType == JsonToken.Null)
return null;
var list = (ICollection<TItem>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
else
list.Add(serializer.Deserialize<TItem>(reader));
return list;
}
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
ValidateItemContract(serializer.ContractResolver);
var list = value as ICollection<TItem>;
if (list == null)
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
if (list.Count == 1)
{
foreach (var item in list)
{
serializer.Serialize(writer, item);
break;
}
}
else
{
writer.WriteStartArray();
foreach (var item in list)
serializer.Serialize(writer, item);
writer.WriteEndArray();
}
}
}
class Item
{
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
[JsonConverter(typeof(SingleOrArrayCollectionConverter<ObservableCollection<string>, string>))]
public ObservableCollection<string> Category { get; set; }
}
public class SendGridEvent
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("timestamp")]
public long Timestamp { get; set; }
[JsonProperty("category"), JsonConverter(typeof(SafeCollectionConverter))]
public string[] Category { get; set; }
[JsonProperty("event")]
public string Event { get; set; }
}
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
namespace stackoverflow.question18994685
{
public class SafeCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//This not works for Populate (on existingValue)
return serializer.Deserialize<JToken>(reader).ToObjectCollectionSafe(objectType, serializer);
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
using System;
namespace Newtonsoft.Json.Linq
{
public static class SafeJsonConvertExtensions
{
public static object ToObjectCollectionSafe(this JToken jToken, Type objectType)
{
return ToObjectCollectionSafe(jToken, objectType, JsonSerializer.CreateDefault());
}
public static object ToObjectCollectionSafe(this JToken jToken, Type objectType, JsonSerializer jsonSerializer)
{
var expectArray = typeof(System.Collections.IEnumerable).IsAssignableFrom(objectType);
if (jToken is JArray jArray)
{
if (!expectArray)
{
//to object via singel
if (jArray.Count == 0)
return JValue.CreateNull().ToObject(objectType, jsonSerializer);
if (jArray.Count == 1)
return jArray.First.ToObject(objectType, jsonSerializer);
}
}
else if (expectArray)
{
//to object via JArray
return new JArray(jToken).ToObject(objectType, jsonSerializer);
}
return jToken.ToObject(objectType, jsonSerializer);
}
public static T ToObjectCollectionSafe<T>(this JToken jToken)
{
return (T)ToObjectCollectionSafe(jToken, typeof(T));
}
public static T ToObjectCollectionSafe<T>(this JToken jToken, JsonSerializer jsonSerializer)
{
return (T)ToObjectCollectionSafe(jToken, typeof(T), jsonSerializer);
}
}
}