我正在尝试修复我的SendGridPlus库来处理SendGrid事件,但是我在API中对类别的处理方式不一致时遇到了一些麻烦。
在以下示例从SendGrid API reference获取的有效内容中,您会注意到每个项目的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处理此问题?
答案 0 :(得分:146)
处理这种情况的最佳方法是使用自定义JsonConverter
。
在我们到达转换器之前,我们需要定义一个类来反序列化数据。对于可在单个项和数组之间变化的Categories
属性,请将其定义为List<string>
并使用[JsonConverter]
属性进行标记,以便JSON.Net知道使用自定义该属性的转换器。我还建议使用[JsonProperty]
属性,以便可以为成员属性提供有意义的名称,而不依赖于JSON中定义的名称。
class Item
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("timestamp")]
public int Timestamp { get; set; }
[JsonProperty("event")]
public string Event { get; set; }
[JsonProperty("category")]
[JsonConverter(typeof(SingleOrArrayConverter<string>))]
public List<string> Categories { get; set; }
}
以下是我将如何实现转换器。注意我已经使转换器通用,以便它可以根据需要与字符串或其他类型的对象一起使用。
class SingleOrArrayConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(List<T>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Array)
{
return token.ToObject<List<T>>();
}
return new List<T> { token.ToObject<T>() };
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
这是一个简短的程序,演示了使用样本数据的转换器:
class Program
{
static void Main(string[] args)
{
string json = @"
[
{
""email"": ""john.doe@sendgrid.com"",
""timestamp"": 1337966815,
""category"": [
""newuser"",
""transactional""
],
""event"": ""open""
},
{
""email"": ""jane.doe@sendgrid.com"",
""timestamp"": 1337966815,
""category"": ""olduser"",
""event"": ""open""
}
]";
List<Item> list = JsonConvert.DeserializeObject<List<Item>>(json);
foreach (Item obj in list)
{
Console.WriteLine("email: " + obj.Email);
Console.WriteLine("timestamp: " + obj.Timestamp);
Console.WriteLine("event: " + 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
小提琴:https://dotnetfiddle.net/lERrmu
修改强>
如果你需要采用另一种方式,即序列化,同时保持相同的格式,你可以实现转换器的WriteJson()
方法,如下所示。 (请务必删除CanWrite
覆盖或将其更改为返回true
,否则永远不会调用WriteJson()
。)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
List<T> list = (List<T>)value;
if (list.Count == 1)
{
value = list[0];
}
serializer.Serialize(writer, value);
}
答案 1 :(得分:5)
我正在研究这个问题多年了,感谢Brian的回答。 我要添加的是vb.net的答案!:
Public Class SingleValueArrayConverter(Of T)
sometimes-array-and-sometimes-object
Inherits JsonConverter
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Throw New NotImplementedException()
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim retVal As Object = New [Object]()
If reader.TokenType = JsonToken.StartObject Then
Dim instance As T = DirectCast(serializer.Deserialize(reader, GetType(T)), T)
retVal = New List(Of T)() From { _
instance _
}
ElseIf reader.TokenType = JsonToken.StartArray Then
retVal = serializer.Deserialize(reader, objectType)
End If
Return retVal
End Function
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return False
End Function
End Class
然后在你班上:
<JsonProperty(PropertyName:="JsonName)> _
<JsonConverter(GetType(SingleValueArrayConverter(Of YourObject)))> _
Public Property YourLocalName As List(Of YourObject)
希望这能节省你一些时间
答案 2 :(得分:0)
您可以使用此处的JSONConverterAttribute
:http://james.newtonking.com/projects/json/help/
假设您有一个类似于
的课程public class RootObject
{
public string email { get; set; }
public int timestamp { get; set; }
public string smtpid { get; set; }
public string @event { get; set; }
public string category[] { get; set; }
}
你可以在这里看到类别属性的装饰:
[JsonConverter(typeof(SendGridCategoryConverter))]
public string category { get; set; }
public class SendGridCategoryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true; // add your own logic
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// do work here to handle returning the array regardless of the number of objects in
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Left as an exercise to the reader :)
throw new NotImplementedException();
}
}
答案 3 :(得分:0)
我有一个非常相似的问题。 我的Json请求完全不为我所知。 我只知道。
其中将有一个objectId和一些匿名键值对AND数组。
我将它用于我做过的EAV模型:
我的JSON请求:
{objectId“:2, “firstName”:“汉斯”, “email”:[“a@b.de”,“a@c.de”], “名字”:“安德烈”, “某事”:[“232”,“123”] }
我的班级我定义了:
[JsonConverter(typeof(AnonyObjectConverter))]
public class AnonymObject
{
public AnonymObject()
{
fields = new Dictionary<string, string>();
list = new List<string>();
}
public string objectid { get; set; }
public Dictionary<string, string> fields { get; set; }
public List<string> list { get; set; }
}
现在我想用它的值和数组反序列化未知属性我的转换器看起来像这样:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
AnonymObject anonym = existingValue as AnonymObject ?? new AnonymObject();
bool isList = false;
StringBuilder listValues = new StringBuilder();
while (reader.Read())
{
if (reader.TokenType == JsonToken.EndObject) continue;
if (isList)
{
while (reader.TokenType != JsonToken.EndArray)
{
listValues.Append(reader.Value.ToString() + ", ");
reader.Read();
}
anonym.list.Add(listValues.ToString());
isList = false;
continue;
}
var value = reader.Value.ToString();
switch (value.ToLower())
{
case "objectid":
anonym.objectid = reader.ReadAsString();
break;
default:
string val;
reader.Read();
if(reader.TokenType == JsonToken.StartArray)
{
isList = true;
val = "ValueDummyForEAV";
}
else
{
val = reader.Value.ToString();
}
try
{
anonym.fields.Add(value, val);
}
catch(ArgumentException e)
{
throw new ArgumentException("Multiple Attribute found");
}
break;
}
}
return anonym;
}
所以现在每次我得到一个AnonymObject我可以遍历字典,每次有我的标志“ValueDummyForEAV”我切换到列表,读取第一行并拆分值。之后,我从列表中删除第一个条目,然后从字典继续迭代。
也许有人有同样的问题,可以使用它:)
此致 安德烈
答案 4 :(得分:0)
great answer是Brian Rogers的一个小变体,这里是SingleOrArrayConverter<T>
的两个调整版本。
首先,这是一个适用于所有List<T>
且本身不是集合的每种类型T
的版本:
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);
注意:
转换器无需将整个JSON值作为JToken
层次结构预先加载到内存中。
该转换器不适用于其项目也被序列化为集合的列表,例如List<string []>
传递给构造函数的布尔值canWrite
控制是否将序列化单元素列表作为JSON值或JSON数组。
转换器的ReadJson()
使用existingValue
(如果已预先分配),以支持填充仅获取列表成员。
第二,这是可与其他通用集合(例如ObservableCollection<T>
)一起使用的版本:
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();
}
}
}
然后,如果您的模型为某个ObservableCollection<T>
使用了T
,则可以按以下方式应用它:
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; }
}
注意:
SingleOrArrayListConverter
的注意事项和限制之外,TCollection
类型还必须是可读写的,并且必须具有无参数的构造函数。使用基本单元测试here进行演示。
答案 5 :(得分:0)
要处理此问题,您必须使用自定义JsonConverter。但是您可能已经想到了这一点。 您只是在寻找可以立即使用的转换器。这不仅为上述情况提供了解决方案。 我举一个例子来问这个问题。
如何使用我的转换器:
在属性上方放置一个JsonConverter属性。 JsonConverter(typeof(SafeCollectionConverter))
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);
}
}
}
它到底有什么作用?
如果放置转换器属性,则转换器将用于此属性。如果期望json数组为1或没有结果,则可以在普通对象上使用它。或者,您可以在需要IEnumerable
的位置使用json对象或json数组。 (知道array
-object[]
-是IEnumerable
)
缺点是该转换器只能放在属性上方,因为他认为自己可以转换所有内容。并且受到警告。 string
也是IEnumerable
。
它提供的不仅仅是这个问题的答案:
如果您通过ID搜索某物,您将知道返回一个数组,结果为一个或没有结果。
ToObjectCollectionSafe<TResult>()
方法可以为您解决这个问题。
这可用于使用JSON.net的单结果与数组 并处理同一个属性的单个项目和数组 并可以将数组转换为单个对象。
我对服务器上的REST请求使用了过滤器,该过滤器在数组中返回一个结果,但希望将结果作为单个对象返回到我的代码中。同样,对于OData结果响应,其结果具有扩展的数组中的一项。
玩得开心。
答案 6 :(得分:-2)
我找到了另一种解决方案,它可以通过使用对象将类别作为字符串或数组来处理。这样我就不需要搞砸了json序列化器。
如果你有时间请告诉我你的看法。 https://github.com/MarcelloCarreira/sendgrid-csharp-eventwebhook
它基于https://sendgrid.com/blog/tracking-email-using-azure-sendgrid-event-webhook-part-1/的解决方案,但我还从时间戳添加了日期转换,升级了变量以反映当前的SendGrid模型(并使类别工作)。
我还创建了一个带有基本身份验证选项的处理程序。请参阅ashx文件和示例。
谢谢!