System.Text.Json:使用自动转换反序列化JSON

50

如何使用 .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处理此问题?

7个回答

61

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

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

Original answer:

  1. The new System.Text.Json api exposes a JsonConverter api which allows us to convert the type as we like.

    For example, we can create a generic number to string converter:

    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. When working with MVC/Razor Page, we can register this converter in startup:

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

    and then the MVC/Razor will handle the type conversion automatically.

  3. Or if you like to control the serialization/deserialization manually:

    var opts = new JsonSerializerOptions {
        PropertyNameCaseInsensitive = true,
    };
    opts.Converters.Add(new AutoNumberToStringConverter());
    var o = JsonSerializer.Deserialize<Product>(json,opts) ;
    
  4. In a similar way you can enable string to number type conversion as below :

    public class AutoStringToNumberConverter : JsonConverter<object>
    {
        public override bool CanConvert(Type typeToConvert)
        {
            // see https://dev59.com/questions/NnI-5IYBdhLWcg3wpaF1
            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");
            }
        }
    }
    

4
(1) 中的转换器应该是 JsonConverter<string> 而不是 JsonConverter<object>,当前实现在将像 [12345] 这样的数组反序列化为 string[] 字段时会抛出 System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List1[System.String]' to type 'System.Collections.Generic.IList1[System.Object]' 的异常。此外,您不需要覆盖 CanConvert() 方法。 - SlimShaggy
5
在序列化和解析数字时,应该使用不可变语言环境而不是当前的本地化环境,例如 int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var i)double.TryParse(s, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var d) - dbc
StringToNumber和NumberToString对我很有用,但是将它们两个加起来会出现错误:无法将类型为'System.Double'的对象强制转换为类型'System.Int64'。 - Bharat Vasant

14

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

使用示例:

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

如果需要将string序列化为int,可以使用JsonNumberHandling.AllowReadingFromString


5
需要注意的是,这仅适用于使用.NET 5.0或.NET 6.0预览版7(截至2021年8月)框架版本的System.Text.Json。请参见答案中引用的指向JsonNumberHandlingAttribute的链接,特别是“适用于”部分。 - Blair Allen
13
当我将其放置在这里时,我得到了当在属性或字段上放置“JsonNumberHandlingAttribute”时,该属性或字段必须是数字或集合的错误提示。 - Andrey Stukalin
6
是的,我认为这只适用于将字符串反序列化为数字,而不是将数字作为字符串。如果你查看迁移指南,似乎System.Text.Json不支持本机处理此问题,即使在.NET 6上也是如此: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-6-0#non-string-values-for-string-properties - olucafont6

11
在选项中,将NumberHandling属性设置为AllowReadingFromString
var o = JsonSerializer.Deserialize<Product>(json, new JsonSerializerOptions
{
    // [...]
    NumberHandling = JsonNumberHandling.AllowReadingFromString
});

2
Json数据是这样的{ "No": 2 },类型为记录Data(string No),但无法反序列化。现在我明白了AllowReadingFromString期望的数据格式应该是{ "No": "2" },也就是将数字作为字符串处理...所以这是一个不同的情况。 - Freshblood
2
@Freshblood 这个选项是从字符串中读取(将字符串反序列化为数字),而不是将其反序列化为字符串。不确定如何做到这一点。 - marsze
2
需要注意的是,这仅适用于使用 .NET 5.0 或 .NET 6.0 预览版7(截至2021年8月)框架版本的 System.Text.Json 的情况。请参见答案中引用到 NumberHandling 的链接,特别是其中的“适用范围”部分。 - Blair Allen

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

这是一个非常好的实现。为了改进,我认为不需要克隆document.RootElement,因为我们只需要在返回之前执行.ToString(),而且document会超出作用域。 - undefined

2
截至撰写本文时,NumberHandling property仅在.NET 5.0和.NET 6.0 RC中可用,而我无法使用它。不幸的是,itminus的字符串转数字转换器对我也没有起作用。
因此,我制作了另一个解决方案,处理不同的数字类型及其可空变体。我尽量使代码尽可能DRY。

数字和可空数字类型

首先,是用于字符串转数字和字符串转可空数字转换的主要通用类:
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);
    }
}

接下来是一个简化使用的工具类。它包含非通用的、类型精确的转换方法和设置:

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>());
...

1
伟大的解决方案。而且及时。 - Sparafusile

2

以下方法可以解决该问题。

  • 安装System.Text.Json Nuget

  • 更新startup.cs文件,添加以下内容

    services.AddControllers().AddJsonOptions(options => { options.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString; });


1
不用担心,只需向类添加一个属性,该属性将以您想要的类型返回所需的项。
public class Product
{
    public int Id { get; set; }

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

    public string Name { get; set; }
}

4
如果客户端将JSON中的id作为字符串传递,那么根据您提供的示例,JsonSerializer会抛出异常。 - Johnny Oshika
2
所以你的问题不够清晰。根据你的问题,客户端正在发送一个数字ID。现在你说客户端保留在ID位置发送数字或字符串值的权利。 - robbpriestley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接