How to parse JSON with a recursive structure representing a query

帅比萌擦擦* 提交于 2021-02-08 09:36:26

问题


I have a specified JSON document, like this:

{
    "user":"human1",
    "subsystems":[1,2,3],
    "query":{"AND":[
                        {"eq":["key1","val1"]},
                        {"eq":["key2","val2"]},
                        {"OR":[
                            {"eq":["subkey1","subval1"]},
                            {"eq":["subkey2","subval2"]}]}
        ]
    }
}

Expected transformation of the query field:

(key1 eq val1 and key2 eq val2 and (subkey1 eq subval1 OR subkey2 eq subval2))

I am using Newtonsoft.Json (JsonConvert.DeserializeObject), and I don't understand how to transform this field.


回答1:


Well, like most things, there are a couple of ways to handle this. I'll first show you the "quick and dirty" way, and then show you what I think is a better alternative.

Get 'er Done

If you really don't care too much about what the code looks like, and you just want to arrive at the final result as quickly as possible, you can use the following classes to deserialize into:

class RootObject
{
    public string User { get; set; }
    public List<int> Subsystems { get; set; }
    public MessyQueryExpression Query { get; set; }
}

class MessyQueryExpression
{
    public List<string> EQ { get; set; }
    public List<MessyQueryExpression> AND { get; set; }
    public List<MessyQueryExpression> OR { get; set; }
}

Then deserialize like this:

var root = JsonConvert.DeserializeObject<RootObject>(json);

This works because Json.Net is able match the query operator name with the corresponding property in the MessyQueryExpression class (while leaving the other two non-matching properties null). It handles the recursion automatically by virtue of the AND and OR properties being lists of the same class.

Of course, the obvious problem with this approach is, once the JSON is deserialized, it's not very clear what each MessyQueryExpression really represents. You have to "look under rocks" until you find the collection that has the data, and only then do you know what the operator is. (And if you wanted to add support for more operators in the future, then you would need to add another list property to the class for each one, which muddies things up even more.)

To show what I mean, here is how you would need to implement the ToString() method to turn the query expression tree into a readable string:

    public override string ToString()
    {
        if (EQ != null && EQ.Count > 0) return string.Join(" eq ", EQ);
        if (AND != null && AND.Count > 0) return "(" + string.Join(" AND ", AND) + ")";
        if (OR != null && OR.Count > 0) return "(" + string.Join(" OR ", OR) + ")";
        return "";
    }

It works, but... yuck.

Fiddle: https://dotnetfiddle.net/re019O


A Better Approach

A more sensible way to handle the recursive query expression in the JSON is to use a composite class structure like this:

abstract class QueryExpression
{
    public string Operator { get; set; }
}

class CompositeExpression: QueryExpression  // AND, OR
{
    public List<QueryExpression> SubExpressions { get; set; }

    public override string ToString()
    {
        return "(" + string.Join(" " + Operator + " ", SubExpressions) + ")";
    }
}

class BinaryExpression: QueryExpression  // EQ
{
    public string Value1 { get; set; }
    public string Value2 { get; set; }

    public override string ToString()
    {
        return Value1 + " " + Operator + " " + Value2;
    }
}

Now we have a clear-cut Operator property to hold the operator name. Each kind of expression has its own subclass with appropriate properties to hold the data. It's much easier to understand what is going on. You can see that the ToString() methods on each class are simple and straightforward. And, if you want to support other binary comparison operators (e.g. GT, LT, NE, etc.), you don't need to change anything -- it will work as is.

So, just change our root class to use this new QueryExpression class instead of the MessyQueryExpression and we're ready to go, right?

class RootObject
{
    public string User { get; set; }
    public List<int> Subsystems { get; set; }
    public QueryExpression Query { get; set; }
}

Not so fast. Since the class structure no longer matches the JSON, Json.Net isn't going to know how to populate the classes the way we want. To bridge the gap, we need to make a custom JsonConverter. The converter works by loading the JSON into an intermediate JObject and then looking at the operator name to determine which subclass to instantiate and populate. Here is the code:

class QueryExpressionConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(QueryExpression).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JProperty prop = JObject.Load(reader).Properties().First();
        var op = prop.Name;
        if (op == "AND" || op == "OR")
        {
            var subExpressions = prop.Value.ToObject<List<QueryExpression>>();
            return new CompositeExpression { Operator = op, SubExpressions = subExpressions };
        }
        else
        {
            var values = prop.Value.ToObject<string[]>();
            if (values.Length != 2)
                throw new JsonException("Binary expression requires two values. Got " + values.Length + " instead: " + string.Join(",", values));
            return new BinaryExpression { Operator = op, Value1 = values[0], Value2 = values[1] };
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        QueryExpression expr = (QueryExpression)value;
        JToken array;
        if (expr is CompositeExpression)
        {
            var composite = (CompositeExpression)expr;
            array = JArray.FromObject(composite.SubExpressions);
        }
        else
        {
            var bin = (BinaryExpression)expr;
            array = JArray.FromObject(new string[] { bin.Value1, bin.Value2 });
        }
        JObject jo = new JObject(new JProperty(expr.Operator, array));
        jo.WriteTo(writer);
    }
}

To tie the converter class to the QueryExpression class, we need to mark it with a [JsonConverter] attribute like this:

[JsonConverter(typeof(QueryExpressionConverter))]
abstract class QueryExpression
{
    public string Operator { get; set; }
}

Now everything should work:

var root = JsonConvert.DeserializeObject<RootObject>(json);
Console.WriteLine(root.Query.ToString());

Fiddle: https://dotnetfiddle.net/RdBnAG



来源:https://stackoverflow.com/questions/51495326/how-to-parse-json-with-a-recursive-structure-representing-a-query

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!