如何在 .net core 3.0 中使用 System.Text.Json 反序列化 json 的一部分?

How to deserialize part of json using System.Text.Json in .net core 3.0?

我有一个 json 从这里 https://api.nasa.gov/insight_weather/?api_key=DEMO_KEY&feedtype=json&ver=1.0 看起来像:

{
  "782": {
    "First_UTC": "2021-02-06T17:08:11Z",
    "Last_UTC": "2021-02-07T17:47:46Z",
    "Month_ordinal": 12,
    "Northern_season": "late winter",
    "PRE": {
      "av": 721.77,
      "ct": 113450,
      "mn": 698.8193,
      "mx": 742.2686
    },
    "Season": "winter",
    "Southern_season": "late summer",
    "WD": {
      "most_common": null
    }
  },
  "783": {
    "First_UTC": "2021-02-07T17:47:46Z",
    "Last_UTC": "2021-02-08T18:27:22Z",
    "Month_ordinal": 12,
    "Northern_season": "late winter",
    "PRE": {
      "av": 722.186,
      "ct": 107270,
      "mn": 698.7664,
      "mx": 743.1983
    },
    "Season": "winter",
    "Southern_season": "late summer",
    "WD": {
      "most_common": null
    }
  },
  "sol_keys": [ "782", "783" ],
  "validity_checks": { /* Some complex object */ }
}

我只需要部分信息,所以我创建了以下内容 类:

public class MarsWheather {
    [JsonPropertyName("First_UTC")]
    public DateTime FirstUTC { get; set; }
    [JsonPropertyName("Last_UTC")]
    public DateTime LastUTC { get; set; }
    [JsonPropertyName("Season")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public Season MarsSeason { get; set; }
    [JsonPropertyName("PRE")]
    public DataDescription AtmosphericPressure { get; set; }
}

public enum Season {
    winter,
    spring,
    summer,
    autumn
}

public class DataDescription{
    [JsonPropertyName("av")]
    public double Average { get; set; }
    [JsonPropertyName("ct")]
    public double TotalCount { get; set; }
    [JsonPropertyName("mn")]
    public double Minimum { get; set; }
    [JsonPropertyName("mx")]
    public double Maximum { get; set; }
} 

问题是来自 NASA 的 JSON 根对象包含我不需要并想跳过的属性 "validity_checks""sol_keys"。在 Newton.Json 中,我使用 JObject.Parse 来执行此操作,但在 System.Text.Json 中,我想使用

JsonSerializer.DeserializeAsync<Dictionary<string, MarsWheather>>(stream, new JsonSerializerOptions { IgnoreNullValues = true });

不幸的是,当我这样做时出现异常:

System.Text.Json.JsonException: The JSON value could not be converted to MarsWheather. Path: $.sol_keys | LineNumber: 120 | BytePositionInLine: 15.

演示 fiddle here.

可能吗?

您的 JSON 根对象由某些固定键("sol_keys""validity_checks")组成,每个键的值都有一些固定模式,以及任意数量的可变键("782" 数字键),其值都共享一个与固定键值的模式不同的公共模式:

{
  "782": {
    // Properties corresponding to your MarsWheather object
  },
  "783": {
    // Properties corresponding to your MarsWheather object
  },
  // Other variable numeric key/value pairs corresponding to KeyValuePair<string, MarsWheather>
  "sol_keys": [
    // Some array values you don't care about
  ],
  "validity_checks": {
    // Some object you don't care about
  }
}

您只想反序列化可变键,但是当您尝试反序列化为 Dictionary<string, MarsWheather> 时会出现异常,因为序列化程序会尝试将固定键值反序列化,就好像它是可变键值一样 - - 但由于固定键具有数组值而可变键具有对象值,因此会抛出异常。如何让 System.Text.Json 跳过已知的固定键而不是尝试反序列化它们?

如果您只想反序列化可变键并跳过固定的已知键,则需要创建一个 custom JsonConverter。最简单的方法是首先为您的字典创建一些根对象:

[JsonConverter(typeof(MarsWheatherRootObjectConverter))]
public class MarsWheatherRootObject
{
    public Dictionary<string, MarsWheather> MarsWheathers { get; } = new Dictionary<string, MarsWheather>();
}

然后为其定义如下转换器:

public class MarsWheatherRootObjectConverter : FixedAndvariablePropertyNameObjectConverter<MarsWheatherRootObject, Dictionary<string, MarsWheather>, MarsWheather>
{
    static readonly Dictionary<string, ReadFixedKeyMethod> FixedKeyReadMethods = new Dictionary<string, ReadFixedKeyMethod>(StringComparer.OrdinalIgnoreCase)
    {
        { "sol_keys", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
        { "validity_checks", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
    };

    protected override Dictionary<string, MarsWheather> GetDictionary(MarsWheatherRootObject obj) => obj.MarsWheathers;
    protected override void SetDictionary(MarsWheatherRootObject obj, Dictionary<string, MarsWheather> dictionary) => throw new RowNotInTableException();
    protected override bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method) => FixedKeyReadMethods.TryGetValue(name, out method);
    protected override IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options) => Enumerable.Empty<KeyValuePair<string, WriteFixedKeyMethod>>();
}

public abstract class FixedAndvariablePropertyNameObjectConverter<TObject, TDictionary, TValue> : JsonConverter<TObject> 
    where TDictionary : class, IDictionary<string, TValue>, new()
    where TObject : new()
{
    protected delegate void ReadFixedKeyMethod(ref Utf8JsonReader reader, TObject obj, string name, JsonSerializerOptions options);
    protected delegate void WriteFixedKeyMethod(Utf8JsonWriter writer, TObject value, JsonSerializerOptions options);
        
    protected abstract TDictionary GetDictionary(TObject obj);
    protected abstract void SetDictionary(TObject obj, TDictionary dictionary);
    protected abstract bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method);
    protected abstract IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options);
        
    public override TObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
            return (typeToConvert.IsValueType && Nullable.GetUnderlyingType(typeToConvert) == null)
                ? throw new JsonException(string.Format("Unepected token {0}", reader.TokenType))
                : default(TObject);
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException(string.Format("Unepected token {0}", reader.TokenType));
        var obj = new TObject();
        var dictionary = GetDictionary(obj);
        var valueConverter = (typeof(TValue) == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.PropertyName)
            {
                var name = reader.GetString();
                reader.ReadAndAssert();
                if (TryGetFixedKeyReadMethod(name, options, out var method))
                {
                    method(ref reader, obj, name, options);
                }
                else
                {
                    if (dictionary == null)
                        SetDictionary(obj, dictionary = new TDictionary());
                    dictionary.Add(name, valueConverter.ReadOrDeserialize(ref reader, typeof(TValue), options));
                }
            }
            else if (reader.TokenType == JsonTokenType.EndObject)
            {
                return obj;
            }
            else
            {
                throw new JsonException(string.Format("Unepected token {0}", reader.TokenType));
            }
        }
        throw new JsonException(); // Truncated file
    }

    public override void Write(Utf8JsonWriter writer, TObject value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        var dictionary = GetDictionary(value);
        if (dictionary != null)
        {
            var valueConverter = (typeof(TValue) == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter
            foreach (var pair in dictionary)
            {
                // TODO: handle DictionaryKeyPolicy 
                writer.WritePropertyName(pair.Key);
                valueConverter.WriteOrSerialize(writer, pair.Value, typeof(TValue), options);
            }
        }
        foreach (var pair in GetFixedKeyWriteMethods(options))
        {
            writer.WritePropertyName(pair.Key);
            pair.Value(writer, value, options);
        }
        writer.WriteEndObject();
    }
}

public static partial class JsonExtensions
{
    public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, Type type, JsonSerializerOptions options)
    {
        if (converter != null)
            converter.Write(writer, value, options);
        else
            JsonSerializer.Serialize(writer, value, type, options);
    }

    public static T ReadOrDeserialize<T>(this JsonConverter<T> converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => converter != null ? converter.Read(ref reader, typeToConvert, options) : (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options);

    public static void ReadAndAssert(this ref Utf8JsonReader reader)
    {
        if (!reader.Read())
            throw new JsonException();
    }
}

现在您可以反序列化为 MarsWheatherRootObject,如下所示:

var root = await System.Text.Json.JsonSerializer.DeserializeAsync<MarsWheatherRootObject>(
    stream, 
    new System.Text.Json.JsonSerializerOptions 
    { 
        PropertyNameCaseInsensitive = true 
    });

演示 fiddle #1 here.

备注:

  • FixedAndvariablePropertyNameObjectConverter<TObject, TDictionary, TValue> 提供了一个通用框架,用于序列化和反序列化具有固定和可变属性的对象。如果稍后您决定反序列化,例如"sol_keys",你可以修改MarsWheatherRootObject如下:

    [JsonConverter(typeof(MarsWheatherRootObjectConverter))]
    public class MarsWheatherRootObject
    {
        public Dictionary<string, MarsWheather> MarsWheathers { get; } = new Dictionary<string, MarsWheather>();
        public List<string> SolKeys { get; set; } = new List<string>();
    }
    

    而转换器如下:

    public class MarsWheatherRootObjectConverter : FixedAndvariablePropertyNameObjectConverter<MarsWheatherRootObject, Dictionary<string, MarsWheather>, MarsWheather>
    {
        static readonly Dictionary<string, ReadFixedKeyMethod> FixedKeyReadMethods = new(StringComparer.OrdinalIgnoreCase)
        {
            { "sol_keys", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => 
                {
                    obj.SolKeys = JsonSerializer.Deserialize<List<string>>(ref reader, options);
                } 
            },
            { "validity_checks", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
        };
        static readonly Dictionary<string, WriteFixedKeyMethod> FixedKeyWriteMethods = new Dictionary<string, WriteFixedKeyMethod>()
        {
            { "sol_keys", (w, v, o) => 
                {
                    JsonSerializer.Serialize(w, v.SolKeys, o);
                } 
            },
        };
    
        protected override Dictionary<string, MarsWheather> GetDictionary(MarsWheatherRootObject obj) => obj.MarsWheathers;
        protected override void SetDictionary(MarsWheatherRootObject obj, Dictionary<string, MarsWheather> dictionary) => throw new RowNotInTableException();
        protected override bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method) => FixedKeyReadMethods.TryGetValue(name, out method);
        protected override IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options) => FixedKeyWriteMethods;
    }
    

    演示 fiddle #2 here.