Search code examples
c#jsontypescripttelerikdeserialization

Deserialize a Json with a field being of 2 types and one type is recursive


Having a typescript part from which I get the json result, need to parse the result in c# to get the object for working with it, but because we have here:

  1. multiple type field
  2. recursion

it is getting hard to understand how the deserializer should look, I know that i need to use the JsonConverter for multiple type field, but how to deal with recursion along multiple type field?

here is the typescript code that is making the json:

export interface FilterDescriptor {

  field ? : string | Function;

  operator: string | Function;

  value ? : any;

  ignoreCase ? : boolean;
}

export interface CompositeFilterDescriptor {
  logic: 'or' | 'and';

  filters: Array < FilterDescriptor | CompositeFilterDescriptor > ;
}

export declare
const isCompositeFilterDescriptor: (source: FilterDescriptor | CompositeFilterDescriptor) => source is CompositeFilterDescriptor;

an example of json's: with recursion

{
"logic": "and",
"filters": [
  {
    "field": "type.group",
    "logic": "and",
    "filters": [
      {
        "field": "type.group",
        "operator": "neq",
        "value": 2
      },
      {
        "field": "type.group",
        "operator": "neq",
        "value": 5
      }
    ]
  }
]}

without recursion:

{
"logic": "and",
"filters": [
  {
    "field": "type.group",
    "operator": "eq",
    "value": 2
  }
]}

this json is produced by using Kendo Ui for Angular from Telerik "CompositeFilterDescriptor"

thank you.


Solution

  • The problem is that C# does not have an equivalent for the 'discriminated union' construct (at least, I think that's what it's called ;-))

    The only constructs you have available for polymorphism are interfaces and inheritance.

    In this case, you need two types (FilterDescriptor and CompositeFilterDescriptor), and you need polymorphism because CompositeFilterDescriptor has an array that can contain either of these. In C#, you would need an interface or base class to express that:

    interface IFilterDescriptor { }
    
    class FilterDescriptor : IFilterDescriptor
    {
        // ... (fields of FilterDescriptor)
    }
    
    class CompositeFilterDescriptor : IFilterDescriptor
    {
        public string @logic { get; set; }
        public IFilterDescriptor[] filters { get; set; }
    }
    

    Additionally, you can't use Json.NET default deserialization, because that requires type information for polymorphic deserialization. You can however create a custom converter where you can check on the presence of certain fields to decide how to deserialize an object:

    public class FilterDescriptorConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType) => typeof(IFilterDescriptor).IsAssignableFrom(objectType);
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject o = JObject.Load(reader);
    
            // if the 'logic' field is present, it's a CompositeFilter
            var item = o["logic"] != null 
                ? (IFilterDescriptor)new CompositeFilterDescriptor() 
                : (IFilterDescriptor)new FilterDescriptor();
            serializer.Populate(o.CreateReader(), item);
            return item;
        }
    
        public override bool CanWrite => false;
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
    }
    

    You can use it like this:

    var result = JsonConvert.DeserializeObject<IFilterDescriptor>(json, 
              new FilterDescriptorConverter());