6

配列である場合もあれば、単一の項目である場合もある値を含む JSON を逆シリアル化しようとしています。System.Text.Jsonと でこれを行うにはどうすればよいJsonSerializerですか? (この質問は、 Robert McLawsによる Json.NETに関するこの質問に触発されています。)

次の JSON を受け取りました。

[
  {
    "email": "john.doe@sendgrid.com",
    "timestamp": 1337966815,
    "category": [
      "newuser",
      "transactional"
    ],
    "event": "open"
  },
  {
    "email": "jane.doe@sendgrid.com",
    "timestamp": 1337966815,
    "category": "olduser",
    "event": "open"
  }
]

そして、それを次のタイプのリストに逆シリアル化したい:

class Item
{
    public string Email { get; set; }
    public int Timestamp { get; set; }
    public string Event { get; set; }

    public List<string> Category { get; set; }
}

次のコードを使用します。

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var list = JsonSerializer.Deserialize<List<Item>>(json, options);

ただし、そうすると、次の例外が発生します。

System.Text.Json.JsonException: The JSON value could not be converted to System.String. Path: > $[1].category | LineNumber: 13 | BytePositionInLine: 25.
   at System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType)
   at System.Text.Json.JsonPropertyInfo.Read(JsonTokenType tokenType, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.JsonSerializer.ReadCore(JsonSerializerOptions options, Utf8JsonReader& reader, ReadStack& readStack)
   at System.Text.Json.JsonSerializer.ReadCore(Type returnType, JsonSerializerOptions options, Utf8JsonReader& reader)
   at System.Text.Json.JsonSerializer.Deserialize(String json, Type returnType, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)

"category"の値が単一の文字列の場合もあれば、文字列の配列の場合もあるために、例外が発生します。このようなプロパティを で逆シリアル化するにはどうすればよいSystem.Text.Jsonですか?

4

2 に答える 2

9

Brian Rogersによるこの回答、 JSON.net を使用して同じプロパティの単一項目と配列の両方を処理する方法に対する他の回答に触発されて、着信 JSON 値が配列であるかどうかをチェックするジェネリックを作成できます。、タイプのアイテムを逆シリアル化し、適切なリストにラップされたアイテムを返します。さらに良いことに、シリアライゼーション グラフで検出されたすべてのリスト タイプに対して、そのようなコンバーターを作成する を作成できます。JsonConverter<List<T>>TJsonConverterFactoryList<T>

最初に、次のコンバーターとコンバーター ファクトリを定義します。

public class SingleOrArrayConverter<TItem> : SingleOrArrayConverter<List<TItem>, TItem>
{
    public SingleOrArrayConverter() : this(true) { }
    public SingleOrArrayConverter(bool canWrite) : base(canWrite) { }
}

public class SingleOrArrayConverterFactory : JsonConverterFactory
{
    public bool CanWrite { get; }

    public SingleOrArrayConverterFactory() : this(true) { }

    public SingleOrArrayConverterFactory(bool canWrite) => CanWrite = canWrite;

    public override bool CanConvert(Type typeToConvert)
    {
        var itemType = GetItemType(typeToConvert);
        if (itemType == null)
            return false;
        if (itemType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(itemType))
            return false;
        if (typeToConvert.GetConstructor(Type.EmptyTypes) == null || typeToConvert.IsValueType)
            return false;
        return true;
    }

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var itemType = GetItemType(typeToConvert);
        var converterType = typeof(SingleOrArrayConverter<,>).MakeGenericType(typeToConvert, itemType);
        return (JsonConverter)Activator.CreateInstance(converterType, new object [] { CanWrite });
    }

    static Type GetItemType(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];
                // Add here other generic collection types as required, e.g. HashSet<> or ObservableCollection<> or etc.
            }
            type = type.BaseType;
        }
        return null;
    }
}

public class SingleOrArrayConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
{
    public SingleOrArrayConverter() : this(true) { }
    public SingleOrArrayConverter(bool canWrite) => CanWrite = canWrite;

    public bool CanWrite { get; }

    public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.Null:
                return null;
            case JsonTokenType.StartArray:
                var list = new TCollection();
                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndArray)
                        break;
                    list.Add(JsonSerializer.Deserialize<TItem>(ref reader, options));
                }
                return list;
            default:
                return new TCollection { JsonSerializer.Deserialize<TItem>(ref reader, options) };
        }
    }

    public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
    {
        if (CanWrite && value.Count == 1)
        {
            JsonSerializer.Serialize(writer, value.First(), options);
        }
        else
        {
            writer.WriteStartArray();
            foreach (var item in value)
                JsonSerializer.Serialize(writer, item, options);
            writer.WriteEndArray();
        }
    }
}

JsonSerializerOptions.Converters次に、逆シリアル化の前にコンバーター ファクトリを追加します。

var options = new JsonSerializerOptions
{
    Converters = { new SingleOrArrayConverterFactory() },
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var list = JsonSerializer.Deserialize<List<Item>>(json, options);

または、次を使用して、オプションまたはデータ モデルに特定のコンバーターを直接追加しますJsonConverterAttribute

class Item
{
    public string Email { get; set; }
    public int Timestamp { get; set; }
    public string Event { get; set; }

    [JsonConverter(typeof(SingleOrArrayConverter<string>))]
    public List<string> Category { get; set; }
}

データ モデルが他のタイプのコレクションを使用している場合、たとえば、次のようObservableCollection<string>に下位レベルのコンバーターを適用できます。SingleOrArrayConverter<TCollection, TItem>

    [JsonConverter(typeof(SingleOrArrayConverter<ObservableCollection<string>, string>))]
    public ObservableCollection<string> Category { get; set; }

ノート:

  • 逆シリアル化中にのみコンバーターを適用する場合はcanWrite: false、パラメーター化されたコンストラクターに渡します。

    Converters = { new SingleOrArrayConverterFactory(canWrite: false) }
    

    コンバーターは引き続き使用されますが、無条件にデフォルトのシリアライゼーションを生成します。

  • 2dコンバーターは、ギザギザやnDなどのコレクションには実装されていませんList<List<string>>。また、配列および読み取り専用コレクションに対しても実装されていません。

  • Serializer support for easy object and collection converters #1562によるとJsonConverter<T>、非同期Read()メソッドがないため、

    既存の [JsonConverter] モデルの制限は、デシリアライゼーション中に「先読み」して、現在の JSON レベルまでバッファを完全に設定する必要があることです。この先読みは、async+stream JsonSerializer deserialize メソッドが呼び出され、そのコンバーターの現在の JSON が StartArray または StartObject トークンで始まる場合にのみ発生します。

    したがって、このコンバーターを使用して、潜在的に非常に大きな配列を逆シリアル化すると、パフォーマンスが低下する可能性があります。

    同じスレッドで説明されているように、コンバーター API は System.Text.Json - 5.0 で再設計され、配列とオブジェクトのコンバーターによる逆シリアル化を完全にサポートする可能async性があります。 "Core") が最終的にリリースされます。

ここでデモフィドル。

于 2019-12-20T19:59:59.877 に答える
0

これを行う最も簡単な方法は、「オブジェクト」タイプを使用することです。以下の例を参照してください

    public class Example
    {
        public string Email { get; set; }
        public int Timestamp { get; set; }
        public string Event { get; set; }
        [JsonPropertyName("category")]
        public object CategoryObjectOrArray { get; set; }
        [JsonIgnore]
        public List<string> Category
        {
            get
            {
                if (CategoryObjectOrArray is JsonElement element)
                {
                    switch (element.ValueKind)
                    {
                        case JsonValueKind.Array:
                            return JsonSerializer.Deserialize<List<string>>(element.GetRawText());
                        case JsonValueKind.String:
                            return new List<string> { element.GetString() };
                    }
                }
                return null;
            }
        }
    }
于 2020-12-04T15:55:04.217 に答える