System.Text.Json: Deserialize JSON with automatic casting

帅比萌擦擦* 提交于 2020-06-23 01:56:51

问题


Using .Net Core 3's new System.Text.Json JsonSerializer, how do you automatically cast types (e.g. int to string and string to int)? For example, this throws an exception because id in JSON is numeric while C#'s Product.Id is expecting a string:

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's Json.Net handled this beautifully. It didn't matter if you were passing in a numeric value while C# was expecting a string (or vice versa), everything got deserialized as expected. How do you handle this using System.Text.Json if you have no control over the type format being passed in as JSON?


回答1:


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



回答2:


Don't worry about it. Just add a property to the class that will return the item you want in the type that you want it.

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

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

    public string Name { get; set; }
}


来源:https://stackoverflow.com/questions/59097784/system-text-json-deserialize-json-with-automatic-casting

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!