skip to Main Content

I’ve got a frontend application which is producing a JSON object (let’s call it a RuleGroup). Each RuleGroup contains a list of either subgroups or nodes.

Here is a rough example of what this may look like:

{
    "combinator": "OR",   <---RuleGroup 
    "not": true,
    "criteria": [
        {
            "field": "path", <---RuleCriteria
            "key": "",
            "value": "",
            "operator": "="
        },
        {
            "combinator": "AND", <---RuleGroup 
            "not": false,
            "criteria": [
                {
                    "field": "header", <---Node
                    "key": "",
                    "value": "",
                    "operator": "="
                },
            ]
        }
    ]
}

How can I deserialise an object like this on my API controller?

I’ve tried setting up some types like this however my criteria list ends up full of BaseRule objects that cannot be cast to the other two types.

public class RuleGroup : BaseRule
{
    public string Combinator { get; set; }
    public bool Not { get; set; }
    public List<BaseRule> Criteria { get; set; }
}

public class RuleCriteria : BaseRule
{
    public string Field { get; set; }
    public string Operator { get; set; }
    public string Key { get; set; }
    public string Value { get; set; }
}

public class BaseRule
{
}

I suspect what I need is something like https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0#polymorphic-type-discriminators however I don’t really want to add a dedicated type field to my data.

My last option is to manually deserialise the data back into the appropriate objects.

If there’s a way to do this without me rolling my own deserialiser, please let me know.

Thanks

2

Answers


  1. I used https://json2csharp.com/ to see what it will generate based on your example and I received:

    // Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
    public class Criterion
    {
        public string Field { get; set; }
        public string Key { get; set; }
        public string Value { get; set; }
        public string Operator { get; set; }
        public string Combinator { get; set; }
        public bool? Not { get; set; }
        public List<Criterion> Criteria { get; set; }
    }
    
    public class Root
    {
        public string Combinator { get; set; }
        public bool Not { get; set; }
        public List<Criterion> Criteria { get; set; }
    }
    

    In my opinion, it makes sense. Using this classes you will get objects. The problem is: "How to distinguish RuleGroup and RuleCriteria". To tackle this I would extend Criterion class:

    public class Criterion
    {
        // all properties
    
        // conditions that distinguish the "type"
        public bool IsRuleGroup() => !string.IsNullOrEmpty(Combinator);
        public bool IsRuleCriteria() => string.IsNullOrEmpty(Combinator);
    }
    
    Login or Signup to reply.
  2. Personally I would go with "standard" polymorphic approach with extra property for type discriminator, IMO it is the cleanest one code-wise, but if it is not feasible (for example JSON is generated but external system and you can’t control it) or any other reason – rolling out a custom converter is the option I would recommend to consider. You can use presence of some marker property to determine the destination type, simple one can look like the following:

    public class RuleConverter : JsonConverter<BaseRule>
    {
        public override BaseRule? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            using var doc = JsonDocument.ParseValue(ref reader);
            if (doc.RootElement.TryGetProperty("combinator", out _))
            {
                return doc.Deserialize<RuleGroup>(options);
            }
    
            // other types, validate presence of marker prop, etc.
            return doc.Deserialize<RuleCriteria>(options);
        }
    
        public override void Write(Utf8JsonWriter writer, BaseRule value, JsonSerializerOptions options) 
            => throw new NotImplementedException();
    }
    

    And mark the base class with it:

    [JsonConverter(typeof(RuleConverter))]
    public class BaseRule;
    

    Full demo @dotnetfiddle

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search