27

使用 .Net Core 3 的新 System.Text.Json JsonSerializer,您如何自动转换类型(例如 int 到 string 和 string 到 int)?例如,这会引发异常,因为idJSON 是数字,而 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 处理此问题?

4

6 回答 6

22

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

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

原答案:

  1. 新的System.Text.Jsonapi 公开了一个JsonConverterapi,它允许我们根据需要转换类型。

    例如,我们可以创建一个泛型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");
            }
        }
    }
    
于 2019-11-29T06:13:55.220 回答
12

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

使用示例:

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

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

于 2021-06-04T12:54:59.157 回答
4

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

var o = JsonSerializer.Deserialize<Product>(json, new JsonSerializerOptions
{
    // [...]
    NumberHandling = JsonNumberHandling.AllowReadingFromString
});
于 2021-07-06T17:03:25.283 回答
1

截至撰写本文时,NumberHandling 属性仅在 .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>());
...
于 2021-09-26T11:03:13.267 回答
1

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

public class Product
{
    public int Id { get; set; }

    public string IdString 
    {
        get
        {
            return Id.ToString();
        }
    }

    public string Name { get; set; }
}
于 2019-11-29T02:10:32.483 回答
0

不幸的是,对我来说 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);
    }
}
于 2020-08-25T11:10:48.987 回答