如何使用 JSON.net 处理同一属性的单个项和数组

我正在尝试修复我的 SendGridPlus 库来处理 SendGrid 事件,但是我遇到了一些 API 中类别处理不一致的问题。

在以下取自 SendGrid API 引用的有效负载示例中,您将注意到每个项的 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 来处理这个问题?

55029 次浏览

您可以使用在这里找到的 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();
}
}

处理这种情况的最佳方法是使用自定义 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);
}

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

我找到了另一个解决方案,它可以通过使用 object 将类别处理为字符串或数组。这样我就不需要搞砸 json 序列化程序。

如果你有时间,请给我看一看,并告诉我你的想法

它基于 https://sendgrid.com/blog/tracking-email-using-azure-sendgrid-event-webhook-part-1/的解决方案,但是我也添加了来自时间戳的日期转换,升级了变量以反映当前的 SendGrid 模型(并使类别工作)。

我还创建了一个处理程序,它使用了基本的 auth 作为选项。

谢谢!

我研究这个很久了,谢谢 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)

希望这能为你节省点时间

我也有过类似的问题。 我的 Json 请求对我来说完全是未知的。 我只知道。

它将包含一个 objectId 和一些匿名键值对 AND 数组。

我用它做了一个 EAV 型号:

我的 JSON 要求:

{ objectId“ : 2, “姓名”“汉斯” “ email”[“ a@b.de”,“ a@c.de”], “名字”“安德烈” “ something”: [“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; }
}

现在我想用它的值和数组反序列化未知的属性,我的 Converter 看起来像这样:

   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;
}

因此,现在每次我得到一个匿名对象,我可以通过字典和每次有我的标志“ ValueDummyForEAV”我切换到列表,读取第一行和分割的值。之后,我从列表中删除第一个条目,并继续从 Dictionary 中进行迭代。

也许有人也有同样的问题,可以用这个:)

问候 安德烈

作为 布莱恩 · 罗杰斯答得好的一个小变化,下面是 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,以支持填充 get-only 列表成员。

其次,这里有一个可以与其他通用集合(如 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类型必须是读/写的,并且有一个无参数的构造函数。

演示基本单元测试 给你

要处理这个问题,您必须使用自定义的 JsonConverter。 您只是在寻找一个可以立即使用的转换器。这不仅仅提供了解决所描述的情况的办法。 我举一个例子来回答这个问题。

如何使用我的转换器:

将 JsonConverter 属性置于属性之上

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或没有结果,可以在普通对象上使用它。或者在需要 json 对象或 json 数组的 IEnumerable上使用它。(知道 array-object[]-是 IEnumerable) 一个缺点是,这个转换器只能放置在属性之上,因为他认为他可以转换一切。还有 警告你string也是 IEnumerable

它不仅提供了这个问题的答案: 如果通过 id 搜索某个内容,您知道将返回一个数组,其中只有一个或没有结果。 ToObjectCollectionSafe<TResult>()方法可以为您处理这个问题。

这对于使用 JSON.net 的 Single Result vs Array 是可用的 并处理同一属性的单个项和数组 并且可以将数组转换为单个对象。

我为服务器上的 REST 请求制作了这个过滤器,该过滤器在数组中返回一个结果,但是希望在我的代码中以单个对象的形式返回结果。也适用于带有数组中一个项的扩展结果的 OData 结果响应。

好好享受吧。

只是想添加到 @ dbc上面的 SingleOrArrayCollectionConverter 上的出色响应。我能够将其修改为与来自 HTTP 客户端的流一起使用。下面是一个代码片段(您必须设置 requestUrl (字符串)和 httpClient (使用 System)。网。).

public async Task<IList<T>> HttpRequest<T>(HttpClient httpClient, string requestedUrl, CancellationToken cancellationToken)
{
using (var request = new HttpRequestMessage(HttpMethod.Get, requestedUrl))
using (var httpResponseMessage = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
if (httpResponseMessage.IsSuccessStatusCode)
{
using var stream = await httpResponseMessage.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(stream);
using var jsonTextReader = new JsonTextReader(streamReader );
var settings = new JsonSerializerSettings
{
// Pass true if you want single-item lists to be reserialized as single items
Converters = { new SingleOrArrayCollectionConverter(true) },
};
var jsonSerializer = JsonSerializer.Create(settings);
return jsonSerializer.Deserialize<List<T>>(jsonTextReader);
}

我道歉,如果有缺少括号或拼写错误,这是不容易粘贴代码在这里。

您不需要任何自定义转换器,在本例中,我通常创建一个非常简单的 JsonConstruction

public partial class Item
{
[JsonProperty("email")]
public string Email { get; set; }


[JsonProperty("timestamp")]
public long Timestamp { get; set; }


[JsonProperty("category")]
public List<string> Category { get; set; }


[JsonProperty("event")]
public string Event { get; set; }


[JsonConstructor]
public Item(JToken category)
{
if (category.GetType().Name == "JArray")
Category = category.ToObject<List<string>>();
else
Category = new List<string> { category.ToString() };
}
public Item() { }
}

在此之后,您可以使用公共代码反序列化您的 json

List<Item> items = JsonConvert.DeserializeObject<List<Item>>(json);