0

I need my web-api to return a list of Rule instances serialized in json format.

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

For now, there are 2 kinds of rules, each with specific properties on top of the ones shared in the Rule class; one rule is called RuleWithExpiration and the other 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;
    }
}

The problem I have is that this class hierarchy has issues when I try to deserialized it. After the deserialization, I end up with a list of Rule instances since I do not ask the serializer to include the type information, as it is considered a security issue.

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

Here is the serialized string:

[{"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"}]

And here is the list of Rule instances I get after it is deserialized (as shown in LINQPad):

Deserialized instances

Question

Is it possible to keep this inheritance tree in this context or do I have to rearrange these classes somehow? If so, what would be the way to do this?

Solution

I have not found solutions that felt good.

For instance, I could have some RuleAggregate class like this one, but everytime I introduce a new kind of rule, I have to edit this class and deal with the 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;
    }
}

The solution I found with the less trade-offs -if I want to keep the inheritance tree- is to fall back on the good ol' XML serialization.

Kzryzstof
  • 7,688
  • 10
  • 61
  • 108

1 Answers1

1

Ok, right, plain TypeNameHandling.All makes it vulnerable. What about this approach?

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; }
}
ZorgoZ
  • 2,974
  • 1
  • 12
  • 34
  • Interesting. Is SerializationBinder the new thing added? – Kzryzstof Jul 26 '18 at 18:53
  • I don't think so. I have to admit, I never used it before, but I surely will. – ZorgoZ Jul 26 '18 at 19:06
  • I realized I skipped the part about SerializationBinder in the article I mentionned in my question and this would help avoid the security issue. I am still wondering if there is a better way to do this and avoid the need of the $type field in the JSON object; it is not great to have to type it in postman... – Kzryzstof Jul 26 '18 at 20:10
  • I dont think you can get rid of the discriminator in json without loosing much of the flexibility. But you can make it more confortable. Look a the answer of Dejan here: https://stackoverflow.com/questions/9490345/json-net-change-type-field-to-another-name – ZorgoZ Jul 27 '18 at 05:33