web-dev-qa-db-fra.com

System.Text.Json: désérialiser JSON avec la conversion automatique

En utilisant le nouveau System.Text.Json JsonSerializer de .Net Core 3, comment pouvez-vous convertir automatiquement les types (par exemple, int en chaîne et chaîne en int)? Par exemple, cela lève une exception car id dans JSON est numérique alors que C # Product.Id attend une chaîne:

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

Json.Net de Newtonsoft a géré cela magnifiquement. Peu importe que vous passiez une valeur numérique alors que C # attendait une chaîne (ou vice versa), tout s'est désérialisé comme prévu. Comment gérez-vous cela en utilisant System.Text.Json si vous n'avez aucun contrôle sur le format de type transmis en tant que JSON?

2
Johnny Oshika
  1. Le nouveau System.Text.Json api expose un JsonConverter api qui nous permet de convertir le type comme nous le souhaitons.

    Par exemple, nous pouvons créer un convertisseur générique number vers string:

    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. Lorsque vous travaillez avec MVC/Razor Page, nous pouvons enregistrer ce convertisseur au démarrage:

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

    puis le MVC/Razor gérera automatiquement la conversion de type.

  3. Ou si vous souhaitez contrôler manuellement la sérialisation/désérialisation:

    var opts = new JsonSerializerOptions {
        PropertyNameCaseInsensitive = true,
    };
    opts.Converters.Add(new AutoNumberToStringConverter());
    var o = JsonSerializer.Deserialize<Product>(json,opts) ;
    
  4. De la même manière, vous pouvez activer la conversion de type chaîne en nombre comme ci-dessous:

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

Ne t'en fais pas. Ajoutez simplement une propriété à la classe qui renverra l'élément que vous voulez dans le type que vous voulez.

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

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

    public string Name { get; set; }
}
0
robbpriestley