Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

System.Text.Json: Deserialize JSON with automatic casting

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?

like image 812
Johnny Oshika Avatar asked Nov 29 '19 01:11

Johnny Oshika


People also ask

How do I deserialize text JSON?

A common way to deserialize JSON is to first create a class with properties and fields that represent one or more of the JSON properties. Then, to deserialize from a string or a file, call the JsonSerializer. Deserialize method.

What is System JSON deserialize?

Deserializes the specified JSON string into an Apex object of the specified type. deserializeUntyped(jsonString) Deserializes the specified JSON string into collections of primitive data types. serialize(objectToSerialize) Serializes Apex objects into JSON content.

What is JsonSerializer C#?

The JsonSerializer is a static class in the System. Text. Json namespace. It provides functionality for serializing objects to a JSON string and deserializing from a JSON string to objects. The JsonSerializer has the serialize() method with multiple overloads, which is a highly performant method for serialization in .

What is JSON text JSON?

JavaScript Object Notation (JSON) is a standard text-based format for representing structured data based on JavaScript object syntax. It is commonly used for transmitting data in web applications (e.g., sending some data from the server to the client, so it can be displayed on a web page, or vice versa).


Video Answer


3 Answers

Edit: You can use JsonNumberHandlingAttribute and it handles everything correctly in 1 line, no need to write any code:

[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://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");
            }
        }
    }
    
like image 136
itminus Avatar answered Oct 25 '22 07:10

itminus


You can use JsonNumberHandlingAttribute in your model class in order to specify how to treat number deserialization. The allowed options are specified in JsonNumberHandling enum.

Example of usage:

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

If serialization from string to int is required, you can use JsonNumberHandling.AllowReadingFromString

like image 22
SvjMan Avatar answered Oct 25 '22 08:10

SvjMan


In the options, set the NumberHandling property to AllowReadingFromString:

var o = JsonSerializer.Deserialize<Product>(json, new JsonSerializerOptions
{
    // [...]
    NumberHandling = JsonNumberHandling.AllowReadingFromString
});
like image 6
marsze Avatar answered Oct 25 '22 08:10

marsze