JSON.netを使用して同じプロパティの単一のアイテムと配列の両方を処理する方法


101

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を使用してこれを処理できる他の方法はありますか?

回答:


203

この状況を処理する最良の方法は、カスタムを使用することですJsonConverter

コンバーターに到達する前に、データを非シリアル化するクラスを定義する必要があります。Categories単一のアイテムと配列の間で異なる可能性があるプロパティについては、それをとして定義し、JSON.Netがそのプロパティにカスタムコンバーターを使用することを認識できるようにList<string>[JsonConverter]属性でマーク します。また、[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、それをreturn に変更してください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


5
パーフェクト!あなたは男です。幸い、私はJsonPropertyを使用してプロパティをより意味のあるものにするために他のすべてのことをすでに行っていました。驚くほど完全な答えをありがとう。:)
Robert McLaws 2013

問題ない; お役に立てて嬉しいです。
ブライアンロジャース

1
優れた!これは私が探していたものです。@BrianRogers、あなたがアムステルダムにいるなら、飲み物は私にあります!
Mad Dog Tannen

2
@israelaltar 上記の回答に示すように、クラスのリストプロパティの属性DeserializeObjectを使用する場合は、コンバータを呼び出しに追加する必要はありません[JsonConverter]。この属性を使用しない場合は、はい、コンバーターをに渡す必要がありますDeserializeObject
ブライアンロジャース

1
@ShaunLangleyコンバーターがリストではなく配列を使用するようにするにList<T>は、コンバーター内のすべての参照をにT[]変更.Countし、に変更し.Lengthます。 dotnetfiddle.net/vnCNgZ
ブライアンロジャース

6

私は長い間これに取り組んでおり、ブライアンの答えに感謝します。私が追加しているのは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)

これがあなたの時間を節約することを願っています


入力ミス:<JsonConverter(GetType(SingleValueArrayConverter(Of YourObject)))> _ Public Property YourLocalName As List(Of YourObject)
GlennG

3

マイナーバリエーションとして素晴らしい答えによってブライアン・ロジャース、ここでの2つの微調整のバージョンがありますSingleOrArrayConverter<T>

最初に、これはそれ自体がコレクションではないList<T>すべてのタイプのすべてに対して機能するバージョンですT

public class SingleOrArrayListConverter : JsonConverter
{
    // Adapted from this answer https://stackoverflow.com/a/18997172
    // to /programming/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事前に割り当てられたifを使用して、取得専用リストメンバーの入力をサポートします。

次に、次のような他の一般的なコレクションと連携するバージョンがありますObservableCollection<T>

public class SingleOrArrayCollectionConverter<TCollection, TItem> : JsonConverter
    where TCollection : ICollection<TItem>
{
    // Adapted from this answer https://stackoverflow.com/a/18997172
    // to /programming/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>for for some 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; }
}

ノート:

  • 以下のための注意事項および制限事項に加えてSingleOrArrayListConverterTCollectionタイプは、読み取り/書き込み可能とパラメータなしのコンストラクタを持っている必要があります。

ここでは、基本的な単体テストのフィドルをデモします


0

私は非常に似た問題を抱えていました。私のJsonリクエストは完全に不明でした。私は知っていました。

その中にobjectIdがあり、いくつかの匿名のキーと値のペアと配列があります。

私はEAVモデルにそれを使用しました:

私のJSONリクエスト:

{objectId ":2、" firstName ":" Hans "、" email ":[" a@b.de "、" a@c.de "]、" name ":" Andre "、" 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; }
}

そして今私は未知の属性をその値とその中の配列でデシリアライズしたいので、私のコンバーターは次のようになります:

   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を取得するたびに、Dictionaryを反復処理でき、フラグ "ValueDummyForEAV"があるたびに、リストに切り替え、最初の行を読み取り、値を分割します。その後、リストから最初のエントリを削除し、辞書から繰り返し処理を続けます。

多分誰かが同じ問題を抱えており、これを使用することができます:)

アンドレよろしく


0

次の場所にあるを使用できますJSONConverterAttributehttp : //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();
  }
}

これをありがとう、しかしそれはまだ問題を解決しません。実際の配列が入ってきても、実際の配列を持つオブジェクトに対してコードが実行される前に、エラーがスローされます。'追加情報:オブジェクトを逆シリアル化するときに予期しないトークン:String。パス '[2] .category [0]'、17行、27位。 '
Robert McLaws 2013

+ "\"イベント\ ":\"処理済み\ "、\ n" + "} \ n" + "]";
ロバートマクローズ2013

最初のオブジェクトを細かく処理し、配列がないものを美しく処理しました。しかし、2番目のオブジェクトの配列を作成すると失敗しました。
Robert McLaws 2013

@AdvancedREIコードを確認しないと、JSONを読み取った後、リーダーが正しく配置されていないと思います。リーダーを直接使用する代わりに、リーダーからJTokenオブジェクトをロードしてそこから移動することをお勧めします。コンバーターの実際の実装については、私の回答を参照してください。
ブライアンロジャース

ブライアンの答えのはるかに詳細。それを使用してください:)
Tim Gabrhel 2013

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

それは正確に何をしますか?コンバーター属性を配置すると、コンバーターがこのプロパティに使用されます。結果が1またはまったくないjson配列が予想される場合は、通常のオブジェクトで使用できます。またはIEnumerable、jsonオブジェクトまたはjson配列が必要な場所で使用します。(知っていることarray- object[]-であるIEnumerable)欠点は、彼がすべてを変えることができると考えているので、このコンバータは唯一の財産の上に配置することができるということです。そして警告されます。A stringIEnumerableです。

そして、それは質問への答え以上のものを提供します:idで何かを検索すると、1つまたは結果なしで配列が返されることがわかります。のToObjectCollectionSafe<TResult>()この方法は、あなたのためにそれを扱うことができます。

これは、JSON.netを使用した単一の結果と配列で使用でき、同じプロパティの単一の項目と配列の両方を処理し、配列を単一のオブジェクトに変換できます。

配列で1つの結果を返すフィルターを持つサーバーでRESTリクエストに対してこれを行いましたが、コード内の単一のオブジェクトとして結果を取得したいと考えました。また、配列内の1つのアイテムを含む拡張結果を含むOData結果応答の場合も同様です。

楽しんでください。


-2

オブジェクトを使用して、カテゴリを文字列または配列として処理できる別のソリューションを見つけました。このようにして、jsonシリアライザーを混乱させる必要はありません。

お時間がありましたらご覧になって、感想をお聞かせください。https://github.com/MarcelloCarreira/sendgrid-csharp-eventwebhook

https://sendgrid.com/blog/tracking-email-using-azure-sendgrid-event-webhook-part-1/にあるソリューションに基づいています。が、タイムスタンプからの日付変換も追加し、変数を反映するようにアップグレードしました現在のSendGridモデル(および作成されたカテゴリが機能します)。

また、オプションとして基本認証を使用してハンドラーを作成しました。ashxファイルと例を参照してください。

ありがとうございました!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.