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

System.Text.Json: Deserialize JSON with automatic casting

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

别担心。只需将 属性 添加到 class 即可 return 您想要的项目的类型。

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

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

    public string Name { get; set; }
}

编辑:您可以使用 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页面时,我们可以在启动时注册这个转换器:

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

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

  3. 或者如果你喜欢手动控制serialization/deserialization:

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

不幸的是,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);
    }
}

您可以使用 JsonNumberHandlingAttribute in your model class in order to specify how to treat number deserialization. The allowed options are specified in JsonNumberHandling 枚举。

用法示例:

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

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

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

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

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

所以我提出了另一个解决方案来处理不同的数字类型及其可为 null 的变体。我试图让代码尽可能干。

数字和可为空的数字类型

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

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

然后是一个实用程序 class 来简化它们的使用。它包含不可概括的、类型精确的转换方法和设置:

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

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

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