Comment gérer les classes héritées lors de l'utilisation du format JSON ?


Kzrystof

J'ai besoin que mon API Web renvoie une liste d' Ruleinstances sérialisées au format json.

[HttpGet]
[SwaggerOperation(nameof(GetRules))]
[SwaggerResponse(StatusCodes.Status200OK, typeof(List<Rule>), "Rules")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetRules()
{
    List<Rule> rules = /* retrieve rule from some storage */;

    return Ok(rules);
}

Pour l'instant, il existe 2 types de règles, chacune avec des propriétés spécifiques en plus de celles partagées dans la classe Rule ; une règle est appelée RuleWithExpirationet l'autre RuleWithGracePeriod.

[JsonObject(MemberSerialization.OptIn)]
public class Rule
{
    [JsonProperty("id")]
    public Guid Id { get; }

    [JsonProperty("name")]
    public string Name { get; }

    [JsonConstructor]
    public Rule(Guid id, string name)
    {
        Id = id;
        Name = name;
    }
}

[JsonObject(MemberSerialization.OptIn)]
public class RuleWithExpiration : Rule
{
    [JsonProperty("someInfo")]
    public string SomeInfo { get; }

    [JsonProperty("expiration")]
    DateTime Expiration { get; }

    [JsonConstructor]
    public RuleWithExpiration(Guid id, string name, string someInfo, DateTime expiration) : base(id, name)
    {
        SomeInfo = someInfo;
        Expiration = expiration;
    }
}

[JsonObject(MemberSerialization.OptIn)]
public class RuleWithGracePeriod : Rule
{
    [JsonProperty("gracePeriod")]
    public TimeSpan GracePeriod { get; }

    [JsonConstructor]
    public RuleWithGracePeriod(Guid id, string name, TimeSpan gracePeriod) : base(id, name)
    {
        GracePeriod = gracePeriod;
    }
}

Le problème que j'ai est que cette hiérarchie de classes a des problèmes lorsque j'essaie de la désérialiser. Après la désérialisation, je me retrouve avec une liste d' Ruleinstances puisque je ne demande pas au sérialiseur d'inclure les informations de type, car cela est considéré comme un problème de sécurité .

void Main()
{
    List<Rule> rules = new List<Rule>
    {
        new RuleWithExpiration(Guid.NewGuid(), "Rule with expiration", "Wat?", DateTime.UtcNow.AddHours(1d)),
        new RuleWithGracePeriod(Guid.NewGuid(), "Rule with grace period", TimeSpan.FromHours(1d)),
    };

    var serializedRule = JsonConvert.SerializeObject(rules);

    serializedRule.Dump();

    List<Rule> deserializedRule = JsonConvert.DeserializeObject<List<Rule>>(serializedRule);

    deserializedRule.Dump();
}

Voici la chaîne sérialisée :

[{"someInfo":"Wat?","expiration":"2018-07-26T13:32:06.2287669Z","id":"29fa0603-c103-4a95-b627-0097619a7645","name":"Rule with expiration"},{"gracePeriod":"01:00:00","id":"bd8777bb-c6b3-4172-916a-546775062eb1","name":"Rule with grace period"}]

Et voici la liste des Ruleinstances que je reçois après sa désérialisation (comme indiqué dans LINQPad):

Instances désérialisées

Question

Est-il possible de conserver cet arbre d'héritage dans ce contexte ou dois-je réorganiser ces classes d'une manière ou d'une autre ? Si oui, quelle serait la manière de procéder ?

Solution

Je n'ai pas trouvé de solutions qui me fassent du bien.

Par exemple, je pourrais avoir une RuleAggregateclasse comme celle-ci, mais chaque fois que j'introduis un nouveau type de règle, je dois éditer cette classe et gérer l'impact :

[JsonObject(MemberSerialization.OptIn)]
public class RuleAggregate
{
    [JsonProperty("expirations")]
    public List<RuleWithExpiration> Expirations {get;}

    [JsonProperty("gracePeriods")]
    public List<RuleWithGracePeriod> GracePeriods {get;}

    [JsonConstructor]
    public RuleAggregate(List<RuleWithExpiration> expirations, List<RuleWithGracePeriod> gracePeriods)
    {
        Expirations = expirations;
        GracePeriods = gracePeriods;
    }
}

La solution que j'ai trouvée avec le moins de compromis - si je veux conserver l'arbre d'héritage - est de me rabattre sur la bonne vieille sérialisation XML.

ZorgoZ

Ok, c'est vrai, la plaine le TypeNameHandling.Allrend vulnérable. Qu'en est-il de cette approche?

void Main()
{
    Stockholder stockholder = new Stockholder
    {
        FullName = "Steve Stockholder",
        Businesses = new List<Business>
        {
            new Hotel
            {
                Name = "Hudson Hotel",
                Stars = 4
            }
        }

    };

    var settings = new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.Objects,
        SerializationBinder = new KnownTypesBinder { KnownTypes = new List<Type> { typeof(Stockholder), typeof(Hotel) }}
    };

    string ok;

    /*
    ok = JsonConvert.SerializeObject(stockholder, Newtonsoft.Json.Formatting.Indented, settings);

    Console.WriteLine(ok);*/

    ok = @"{
  ""$type"": ""Stockholder"",
  ""FullName"": ""Steve Stockholder"",
  ""Businesses"": [
    {
      ""$type"": ""Hotel"",
      ""Stars"": 4,
      ""Name"": ""Hudson Hotel""
    }
  ]
}";

    JsonConvert.DeserializeObject<Stockholder>(ok, settings).Dump();

    var vector = @"{
  ""$type"": ""Stockholder"",
  ""FullName"": ""Steve Stockholder"",
  ""Businesses"": [
    {
      ""$type"": ""System.IO.FileInfo, System.IO.FileSystem"",
      ""fileName"": ""d:\rce-test.txt"",
      ""IsReadOnly"": true
    }
  ]
}";

    JsonConvert.DeserializeObject<Stockholder>(vector, settings).Dump(); // will fail
}

public class KnownTypesBinder : ISerializationBinder
{
    public IList<Type> KnownTypes { get; set; }

    public Type BindToType(string assemblyName, string typeName)
    {
        return KnownTypes.SingleOrDefault(t => t.Name == typeName);
    }

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        typeName = serializedType.Name;
    }
}

public abstract class Business
{
    public string Name { get; set; }
}

public class Hotel: Business
{
    public int Stars { get; set; }
}

public class Stockholder
{
    public string FullName { get; set; }
    public IList<Business> Businesses { get; set; }
}

Articles connexes