【问题标题】:System.Text.Json: Deserialize JSON with automatic castingSystem.Text.Json:使用自动转换反序列化 JSON
【发布时间】:2020-03-24 15:18:26
【问题描述】:

使用 .Net Core 3 的新 System.Text.Json JsonSerializer,如何自动转换类型(例如 int 为 string 和 string 为 int)?例如,这会引发异常,因为 JSON 中的 id 是数字,而 C# 的 Product.Id 需要一个字符串:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        var json = @"{""id"":1,""name"":""Foo""}";
        var o = JsonSerializer.Deserialize<Product>(json, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
        });

        return View();
    }
}

public class Product
{
    public string Id { get; set; }
    public string Name { get; set; }
}

Newtonsoft 的 Json.Net 处理得很好。如果您在 C# 期待一个字符串(或反之亦然)时传入一个数值并不重要,一切都会按预期反序列化。如果您无法控制作为 JSON 传入的类型格式,您如何使用 System.Text.Json 处理此问题?

【问题讨论】:

    标签: c# asp.net-core .net-core json.net system.text.json


    【解决方案1】:

    别担心。只需向类中添加一个属性,该属性将以您想要的类型返回您想要的项目。

    public class Product
    {
        public int Id { get; set; }
    
        public string IdString 
        {
            get
            {
                return Id.ToString();
            }
        }
    
        public string Name { get; set; }
    }
    

    【讨论】:

    • 如果客户端将 JSON 中的 id 作为字符串传递,那么 JsonSerializer 将使用您的示例抛出异常。
    • 所以你的问题不清楚。根据您的问题,客户端正在发送数字 ID。现在您说客户端保留在 Id 位置发送数字或字符串值的权利。
    【解决方案2】:

    编辑:您可以使用JsonNumberHandlingAttribute,它可以在 1 行中正确处理所有内容,无需编写任何代码:

    [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
    public class HomeController : Controller
    ....
    

    原答案:

    1. 新的System.Text.Json api 公开了一个JsonConverter api,它允许我们随意转换类型。

      例如,我们可以创建一个通用的numberstring 转换器:

      public class AutoNumberToStringConverter : JsonConverter<object>
      {
          public override bool CanConvert(Type typeToConvert)
          {
              return typeof(string) == typeToConvert;
          }
          public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
          {
              if(reader.TokenType == JsonTokenType.Number) {
                  return reader.TryGetInt64(out long l) ?
                      l.ToString():
                      reader.GetDouble().ToString();
              }
              if(reader.TokenType == JsonTokenType.String) {
                  return reader.GetString();
              }
              using(JsonDocument document = JsonDocument.ParseValue(ref reader)){
                  return document.RootElement.Clone().ToString();
              }
          }
      
          public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
          {
              writer.WriteStringValue( value.ToString());
          }
      }
      
    2. 在使用 MVC/Razor Page 时,我们可以在启动时注册这个转换器:

      services.AddControllersWithViews().AddJsonOptions(opts => {
          opts.JsonSerializerOptions.PropertyNameCaseInsensitive= true;
          opts.JsonSerializerOptions.Converters.Insert(0, new AutoNumberToStringConverter());
      });
      

      然后 MVC/Razor 会自动处理类型转换。

    3. 或者如果您想手动控制序列化/反序列化:

      var opts = new JsonSerializerOptions {
          PropertyNameCaseInsensitive = true,
      };
      opts.Converters.Add(new AutoNumberToStringConverter());
      var o = JsonSerializer.Deserialize<Product>(json,opts) ;
      
    4. 以类似的方式,您可以启用字符串到数字类型的转换,如下所示:

      public class AutoStringToNumberConverter : JsonConverter<object>
      {
          public override bool CanConvert(Type typeToConvert)
          {
              // see https://stackoverflow.com/questions/1749966/c-sharp-how-to-determine-whether-a-type-is-a-number
              switch (Type.GetTypeCode(typeToConvert))
              {
                  case TypeCode.Byte:
                  case TypeCode.SByte:
                  case TypeCode.UInt16:
                  case TypeCode.UInt32:
                  case TypeCode.UInt64:
                  case TypeCode.Int16:
                  case TypeCode.Int32:
                  case TypeCode.Int64:
                  case TypeCode.Decimal:
                  case TypeCode.Double:
                  case TypeCode.Single:
                  return true;
                  default:
                  return false;
              }
          }
          public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
          {
              if(reader.TokenType == JsonTokenType.String) {
                  var s = reader.GetString() ;
                  return int.TryParse(s,out var i) ? 
                      i :
                      (double.TryParse(s, out var d) ?
                          d :
                          throw new Exception($"unable to parse {s} to number")
                      );
              }
              if(reader.TokenType == JsonTokenType.Number) {
                  return reader.TryGetInt64(out long l) ?
                      l:
                      reader.GetDouble();
              }
              using(JsonDocument document = JsonDocument.ParseValue(ref reader)){
                  throw new Exception($"unable to parse {document.RootElement.ToString()} to number");
              }
          }
      
      
          public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
          {
              var str = value.ToString();             // I don't want to write int/decimal/double/...  for each case, so I just convert it to string . You might want to replace it with strong type version.
              if(int.TryParse(str, out var i)){
                  writer.WriteNumberValue(i);
              }
              else if(double.TryParse(str, out var d)){
                  writer.WriteNumberValue(d);
              }
              else{
                  throw new Exception($"unable to parse {str} to number");
              }
          }
      }
      

    【讨论】:

    • (1)中的转换器不应该是JsonConverter而不是JsonConverter吗?当前实现在将 [12345] 之类的数组反序列化为 string[] 字段时,会抛出 System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List1[System.String]' 以键入 'System.Collections.Generic.IList1[System.Object]'。你也不需要重写 CanConvert() 方法。
    • 您应该在不变的语言环境中序列化和解析数字,而不是当前的文化语言环境,例如int.TryParse(s,NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i) double.TryParse(s, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var d)
    • StringToNumber 和 NumberToString 对我有用,但同时添加两者都会给我错误:无法将“System.Double”类型的对象转换为“System.Int64”类型。
    【解决方案3】:

    不幸的是,itminus 的示例不起作用,这是我的变体。

    public class AutoNumberToStringConverter : JsonConverter<string>
    {
        public override bool CanConvert(Type typeToConvert)
        {
            return typeof(string) == typeToConvert;
        }
    
        public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.Number)
            {
                if (reader.TryGetInt64(out long number))
                {
                    return number.ToString(CultureInfo.InvariantCulture);
                }
    
                if (reader.TryGetDouble(out var doubleNumber))
                {
                    return doubleNumber.ToString(CultureInfo.InvariantCulture);
                }
            }
    
            if (reader.TokenType == JsonTokenType.String)
            {
                return reader.GetString();
            }
    
            using var document = JsonDocument.ParseValue(ref reader);
            return document.RootElement.Clone().ToString();
        }
    
        public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value);
        }
    }
    

    【讨论】:

      【解决方案4】:

      您可以在模型类中使用JsonNumberHandlingAttribute 来指定如何处理数字反序列化。允许的选项在JsonNumberHandling 枚举中指定。

      使用示例:

      public class Product
      {
          [JsonNumberHandling(JsonNumberHandling.WriteAsString)]
          public string Id { get; set; }
          
          public string Name { get; set; }
      }
      
      

      如果需要从string 序列化到int,您可以使用JsonNumberHandling.AllowReadingFromString

      【讨论】:

      • 应该注意,这仅适用于在 .NET 5.0 或 .NET 6.0 Preview 7(截至 2021 年 8 月)框架版本中使用 System.Text.Json。请参阅答案中引用JsonNumberHandlingAttribute 的链接,特别是“适用于”部分。
      • 嗯,当我这样放置时,我收到了When 'JsonNumberHandlingAttribute' is placed on a property or field, the property or field must be a number or a collection 错误
      • 是的,我认为这仅在您要将字符串反序列化为数字而不是将数字反序列化为字符串时才有效。如果您查看迁移指南,不幸的是,System.Text.Json 本身并不支持此功能,即使在 .NET 6 上也是如此:docs.microsoft.com/en-us/dotnet/standard/serialization/…
      【解决方案5】:

      在选项中,将NumberHandling属性设置为AllowReadingFromString

      var o = JsonSerializer.Deserialize<Product>(json, new JsonSerializerOptions
      {
          // [...]
          NumberHandling = JsonNumberHandling.AllowReadingFromString
      });
      

      【讨论】:

      • Json 数据就像 { "No": 2 } 并且类型是记录数据(字符串否)它无法反序列化它。现在我明白 AllowReadingFromString 期待像 { "No": "2" } 这样的数据我的意思是数字作为字符串......所以这是一个不同的情况。
      • @Freshblood 选项是读取 from 字符串(将字符串反序列化为数字),而不是反序列化 as 字符串。不知道该怎么做。
      • @Freshblood 应该注意的是,这仅适用于在 .NET 5.0 或 .NET 6.0 Preview 7(截至 2021 年 8 月)框架版本中使用 System.Text.Json。请参阅答案中引用NumberHandling 的链接,特别是“适用于”部分。
      【解决方案6】:

      截至撰写本文时,NumberHandling property 仅在 .NET 5.0 和 .NET 6.0 RC 中可用,我无法使用。 不幸的是,itminus 的字符串到数字转换器对我也不起作用。

      所以我制作了另一个解决方案来处理不同的数字类型及其可为空的变体。我试图使代码尽可能干燥。

      数字和可为空的数字类型

      首先,字符串到数字和字符串到可空数字转换的主要泛型类:

      public delegate T FromStringFunc<T>(string str);
      public delegate T ReadingFunc<T>(ref Utf8JsonReader reader);
      public delegate void WritingAction<T>(Utf8JsonWriter writer, T value);
      
      public class StringToNumberConverter<T> : JsonConverter<T> where T : struct
      {
          protected ISet<TypeCode> AllowedTypeCodes { get; }
          protected FromStringFunc<T> FromString { get; }
          protected ReadingFunc<T> ReadValue { get; }
          protected WritingAction<T> WriteValue { get; }
      
          public StringToNumberConverter(ISet<TypeCode> allowedTypeCodes, FromStringFunc<T> fromString, ReadingFunc<T> read, WritingAction<T> write)
          : base()
          {
              AllowedTypeCodes = allowedTypeCodes;
              FromString = fromString;
              ReadValue = read;
              WriteValue = write;
          }
      
          public override bool CanConvert(Type typeToConvert)
          {
              return AllowedTypeCodes.Contains(Type.GetTypeCode(typeToConvert));
          }
      
          public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
          {
              if (reader.TokenType == JsonTokenType.String)
              {
                  var s = reader.GetString();
                  return FromString(s);
              }
      
              if (reader.TokenType == JsonTokenType.Number)
                  return ReadValue(ref reader);
      
              using JsonDocument document = JsonDocument.ParseValue(ref reader);
              throw new Exception($"unable to parse {document.RootElement} to number");
          }
      
          public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
          {
              WriteValue(writer, value);
          }
      }
      
      public class StringToNullableNumberConverter<T> : JsonConverter<T?> where T : struct
      {
          private readonly StringToNumberConverter<T> stringToNumber;
          protected WritingAction<T> WriteValue { get; }
      
          public StringToNullableNumberConverter(ISet<TypeCode> allowedTypeCodes, FromStringFunc<T> fromString, ReadingFunc<T> read, WritingAction<T> write)
          : base()
          {
              stringToNumber = new StringToNumberConverter<T>(allowedTypeCodes, fromString, read, write);
              WriteValue = write;
          }
      
          public override bool CanConvert(Type typeToConvert)
          {
              return stringToNumber.CanConvert(Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert);
          }
      
          public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
          {
              if (reader.TokenType == JsonTokenType.Null)
                  return null;
      
              return stringToNumber.Read(ref reader, typeToConvert, options);
          }
      
          public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
          {
              if (!value.HasValue)
                  writer.WriteNullValue();
              else
                  stringToNumber.Write(writer, value.Value, options);
          }
      }
      

      然后是一个 util 类来简化它们的使用。它拥有不可泛化的、类型精确的转换方法和设置:

      static class StringToNumberUtil
      {
          public static readonly ISet<TypeCode> intCodes = new HashSet<TypeCode> { TypeCode.Byte, TypeCode.Int16, TypeCode.Int32 };
          public static readonly ISet<TypeCode> longCodes = new HashSet<TypeCode> { TypeCode.Int64 };
          public static readonly ISet<TypeCode> decimalCodes = new HashSet<TypeCode> { TypeCode.Decimal };
          public static readonly ISet<TypeCode> doubleCodes = new HashSet<TypeCode> { TypeCode.Double };
      
          public static int ParseInt(string s) => int.Parse(s, CultureInfo.InvariantCulture);
          public static long ParseLong(string s) => long.Parse(s, CultureInfo.InvariantCulture);
          public static decimal ParseDecimal(string s) => decimal.Parse(s, CultureInfo.InvariantCulture);
          public static double ParseDouble(string s) => double.Parse(s, CultureInfo.InvariantCulture);
      
          public static int ReadInt(ref Utf8JsonReader reader) => reader.GetInt32();
          public static long ReadLong(ref Utf8JsonReader reader) => reader.GetInt64();
          public static decimal ReadDecimal(ref Utf8JsonReader reader) => reader.GetDecimal();
          public static double ReadDouble(ref Utf8JsonReader reader) => reader.GetDouble();
      
          public static void WriteInt(Utf8JsonWriter writer, int value) => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
          public static void WriteLong(Utf8JsonWriter writer, long value) => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
          public static void WriteDecimal(Utf8JsonWriter writer, decimal value) => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
          public static void WriteDouble(Utf8JsonWriter writer, double value) => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture));
      }
      

      最后,您可以为各个数字类型定义便利类...

      public class StringToIntConverter : StringToNumberConverter<int>
      {
          public StringToIntConverter()
              : base(StringToNumberUtil.intCodes, StringToNumberUtil.ParseInt, StringToNumberUtil.ReadInt, StringToNumberUtil.WriteInt)
          {
          }
      }
      
      public class StringToNullableIntConverter : StringToNullableNumberConverter<int>
      {
          public StringToNullableIntConverter()
              : base(StringToNumberUtil.intCodes, StringToNumberUtil.ParseInt, StringToNumberUtil.ReadInt, StringToNumberUtil.WriteInt)
          {
          }
      }
      

      ...并像这样在 JsonSerializerOptions 中注册它们:

      var options = new JsonSerializerOptions {
          ...
      };
      options.Converters.Add(new StringToIntConverter());
      options.Converters.Add(new StringToNullableIntConverter());
      ...
      

      (如果您愿意,也可以直接注册转换器。)

      options.Converters.Add(new StringToNumberConverter<int>(StringToNumberUtil.intCodes, StringToNumberUtil.ParseInt, StringToNumberUtil.ReadInt, StringToNumberUtil.WriteInt));
      options.Converters.Add(new StringToNullableNumberConverter<int>(StringToNumberUtil.intCodes, StringToNumberUtil.ParseInt, StringToNumberUtil.ReadInt, StringToNumberUtil.WriteInt));
      

      应该反序列化为枚举的数字

      如果您的 JSON 包含字符串编码的数字属性,您可以添加此属性,其值具有可表示为枚举的预定义含义。

      public class StringToIntEnumConverter<T> : JsonConverter<T> where T : struct, System.Enum
      {
          private StringToIntConverter stringToInt = new StringToIntConverter();
      
          public override bool CanConvert(Type typeToConvert)
          {
              return typeToConvert == typeof(T);
          }
      
          public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
          {
              int val = stringToInt.Read(ref reader, typeToConvert, options);
              string underlyingValue = val.ToString(CultureInfo.InvariantCulture);
      
              return (T)Enum.Parse(typeof(T), underlyingValue);
          }
      
          public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
          {
              var number = Convert.ChangeType(value, Enum.GetUnderlyingType(typeof(T)), CultureInfo.InvariantCulture);
      
              writer.WriteStringValue(number.ToString());
          }
      }
      
      public class StringToNullableIntEnumConverter<T> : JsonConverter<T?> where T : struct, System.Enum
      {
          private StringToIntEnumConverter<T> stringToIntEnum = new StringToIntEnumConverter<T>();
      
          public override bool CanConvert(Type typeToConvert)
          {
              return stringToIntEnum.CanConvert(Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert);
          }
      
          public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
          {
              if (reader.TokenType == JsonTokenType.Null)
                  return null;
      
              return stringToIntEnum.Read(ref reader, typeToConvert, options);
          }
      
          public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
          {
              if (!value.HasValue)
              {
                  writer.WriteNullValue();
                  return;
              }
      
              stringToIntEnum.Write(writer, value.Value, options);
          }
      }
      

      在 JsonSerializerOptions 中的使用:

      var options = new JsonSerializerOptions {
          ...
      };
      options.Converters.Add(new StringToIntEnumConverter<OrderFlags>());
      options.Converters.Add(new StringToNullableIntEnumConverter<OrderFlags>());
      ...
      

      【讨论】:

      • 很好的解决方案。并且及时。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-22
      • 2021-12-08
      • 2021-12-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多