Representing logic as data in JSON

Viewed 67381

For business reasons we need to externalize some conditional logic into external files: preferably JSON.

A simple filter-by scenario could be handled by adding a node as follows:

"filter": [
  {
    "criteria": "status",
    "value": "open",
    "condition": "=="
  }
]

Multiple conditions could be handled by additional values in the array.

"filter": [
  {
    "criteria": "status",
    "value": "open",
    "condition": "=="
  },
  {
    "criteria": "condition2",
    "value": "value2",
    "condition": "=="
  }
]

However, it gets a little confusing when we have handle complex conditions involving ANDs or ORs.

Question: is there a standardized (or even widely accepted) format for representing such logic within JSONs? How would you do it if it were up to you?

NOTE: The first answer has been made an editable wiki so it can be improved by anyone who feels it can be.

16 Answers

I came up with this format with the primary goal of reading as close as possible to actually SQL.

Here's the Type def in typescript:

type LogicalOperator = 'AND' | 'OR';
type Operator = '=' | '<=' | '>=' | '>' | '<' | 'LIKE' | 'IN' | 'NOT IN';
type ConditionParams = {field: string, opp: Operator, val: string | number | boolean};
type Conditions = ConditionParams | LogicalOperator | ConditionsList;
interface ConditionsList extends Array<Conditions> { }

Or BNF (ish? my cs teachers wouldn't be proud)

WHEREGROUP: = [ CONDITION | ('AND'|'OR') | WHEREGROUP ]
CONDITION: = {field, opp, val}

With the following Parsing Rules:

  1. AND is optional (I typically add it for readability). If logical LogicalOperator is left out between conditions, it will automatically joins them with AND
  2. Inner arrays are parsed as nested groups (EG get wrapped in ())
  3. this type does not restrict multiple logical operators consecutively (unfortunately). I handled this by just using the last one, although I could have thrown a runtime error instead.

Here are some examples (typescript playground link):

1 AND 2 (AND inferred)

[
    { field: 'name', opp: '=', val: '123' },
    { field: 'otherfield', opp: '>=', val: 123 }
]

1 OR 2

[
    { field: 'name', opp: '=', val: '123' },
    'OR',
    { field: 'annualRevenue', opp: '>=', val: 123 }
]

(1 OR 2) AND (3 OR 4)

[
    [
        { field: 'name', opp: '=', val: '123' },
        'OR',
        { field: 'name', opp: '=', val: '456' }
    ],
    'AND',
    [
        { field: 'annualRevenue', opp: '>=', val: 123 },
        'OR',
        { field: 'active', opp: '=', val: true }
    ]
]

1 AND (2 OR 3)

[
    { field: 'name', opp: '=', val: '123' },
    'AND',
    [
        { field: 'annualRevenue', opp: '>=', val: 123 },
        'OR',
        { field: 'active', opp: '=', val: true }
    ]
]

1 AND 2 OR 3

[
    { field: 'name', opp: '=', val: '123' },
    'AND',
    { field: 'annualRevenue', opp: '>=', val: 123 },
    'OR',
    { field: 'active', opp: '=', val: true }
]

1 OR (2 AND (3 OR 4))

[
    { field: 'name', opp: '=', val: '123' },
    'OR',
    [
        { field: 'annualRevenue', opp: '>=', val: 123 },
        'AND',
        [
            { field: 'active', opp: '=', val: true },
            'OR',
            { field: 'accountSource', opp: '=', val: 'web' }
        ]
    ]
]

As you can see, if you were to remove , and property names, then just replace the [] with (), you'd basically have the condition in SQL format

Following Jeremy Wadhams comment, I implemented a parser I hope it can help you:

https://play.golang.org/p/QV0FQLrTlyo

The idea is to set all logic operators in special keys with $ character like $and or $lte.

As an example:

{ 
   "$or":[ 
      { 
         "age":{ 
            "$lte":3
         }
      },
      { 
         "name":"Joe"
      },
      { 
         "$and":[ 
            { 
               "age":5
            },
            { 
               "age ":{ 
                  " $nin ":[ 
                     1,
                     2,
                     3
                  ]
               }
            }
         ]
      }
   ]
}

Regards.

Is translated as:

 ( age  <= 3 OR  name  = Joe  OR  ( age  = 5  AND  age  NOT IN (1,2,3) )  )  

We created an npm package json-conditions to handle this. It's not as full featured as some of the others here, but it's easy to translate into a simple UI for non-technically savvy clients as complex rules are possible without nesting and it covers virtually all use cases they can come up with.

Example on runkit

const objectToTest = {
  toy: {
    engines: 1,
  },
  batteries: 'AA',
  fun: true,
};

const simpleRules = [
    // required: true means This first condition must always be satisfied
    { property: 'fun', op: 'eq', value: true, required: true },
    { property: 'toy.engines', op: 'gt', value: 2 },
    { property: 'batteries', op: 'present' },
];

// Returns true
checkConditions({
    rules: simpleRules,
    satisfy: 'ANY', // or ALL to require all conditions to pass
    log: console.log,
}, objectToTest);

Logic can be implemented with "logicOp": "Operator" on a "set": ["a","b" ...] For cHau's example:

"var": {
         "logicOp": "And",
         "set": ["value1",
                 {
                    "LogicOp": "Or",
                    "set": ["value2", "value3"]
                 }
              ]
       }

There can also be other attributes/operations for the set for example

"val": { "operators": ["min": 0, "max": 2], "set": ["a", "b", "c"] } 

For a sundae with two scoops of one or more icecream types, 1 toppings and whipcream

"sundae": {
            "icecream": { 
                          "operators": [ "num": 2,
                                        "multipleOfSingleItem": "true"],
                          "set": ["chocolate", "strawberry", "vanilla"]
                        },
            "topping": {
                          "operators": ["num": 1],
                          "set": ["fudge", "caramel"]
                       },
            "whipcream": "true"
          }

I just wanted to help by defining a parsing logic in JavaScript for the JSON structure mentioned in the answer: https://stackoverflow.com/a/53215240/6908656

This would be helpful for people having a tough time in writing a parsing logic for this.

evaluateBooleanArray = (arr, evaluated = true) => {
    if (arr.length === 0) return evaluated;
    else if (typeof arr[0] === "object" && !Array.isArray(arr[0])) {
      let newEvaluated = checkForCondition(arr[0]);
      return evaluateBooleanArray(arr.splice(1), newEvaluated);
    } else if (typeof arr[0] === "string" && arr[0].toLowerCase() === "or") {
      return evaluated || evaluateBooleanArray(arr.splice(1), evaluated);
    } else if (typeof arr[0] === "string" && arr[0].toLowerCase() === "and") {
      return evaluated && evaluateBooleanArray(arr.splice(1), evaluated);
    } else if (Array.isArray(arr[0])) {
      let arrToValuate = [].concat(arr[0]);
      return evaluateBooleanArray(
        arr.splice(1),
        evaluateBooleanArray(arrToValuate, evaluated) 
      );
    } else {
      throw new Error("Invalid Expression in Conditions");
    }
  };

So the param arr here would be an array of conditions defined in the format as described by the attached link.

The first came to mind would be the recurisve

dict1={'$lte':'<','$nin':'not in '}

def updateOp(subdictItem):

    for ites in subdictItem:
        ops = ites
        print dict1.get(ops), subdictItem.get(ops), type(subdictItem.get(ops))
        if type(subdictItem.get(ops)) is list:
            valuelist=subdictItem.get(ops)
            strlist=' ,'.join([str(x) for x in valuelist])
            sub = dict1.get(ops) + "(" +strlist +")"
        else:
            sub = dict1.get(ops) +' ' + str(subdictItem.get(ops))
    return sub

def jsonString(input_dict):
    items=''
    itemslist=[]
    list = []
    for item in input_dict:
        op=item
        itemssublist=[]

        # print "item",op
        for subitem in input_dict.get(op):
            # print("subitem",subitem)
            for ite in subitem:
                if ite not in ('and','or'):
                    # print('ite_raw',ite,subitem.get(ite))
                    sub=''
                    if type(subitem.get(ite)) is dict:
                        sub=updateOp(subitem.get(ite))
                    else:
                        sub='=' + str(subitem.get(ite))
                    itemssublist.append(ite+sub)
                else:
                    item1=jsonString(subitem)
                    itemssublist.append(item1)

        delimiter=" "+op+ " "
        items= "("+delimiter.join(itemssublist)+")"
    return items 





if __name__ == "__main__":

    input_dict={}
    with open('ops.json','r') as f:
        input_dict=json.load(f)

    print input_dict

    test= jsonString(input_dict)

#result : (age< 3 or name=Joe or (age=5 and age not in (1 ,2 ,3)))
ops.json file:
{
   "or":[
      {
         "age":{
            "$lte":3
         }
      },
      {
         "name":"Joe"
      },
      {
         "and":[
            {
               "age":5
            },
            {
               "age ":{
                  "$nin":[
                     1,
                     2,
                     3
                  ]
               }
            }
         ]
      }
   ]
}

Use arrays, alternate between OR and AND conditions:

const rule0 = [
    [ "ruleA1", "ruleA2", "ruleA3" ],
    [ "ruleB5", "ruleB6" ],
    [ "ruleB7" ]
];

const rule1 =  [
    [ "ruleA1", "ruleA2", [ [ "ruleA3A" ], [ "ruleA3B1", "ruleA31B2" ] ] ],
    [ "ruleB5", "ruleB6" ],
    [ "ruleC7" ]
];
function ruler (rules) {
    return "( " +
        rules.map(or_rule =>
            "( " +
            or_rule.map(and_rule =>
                Array.isArray(and_rule) ? ruler(and_rule) : and_rule
            ).join(" AND ") +
            " )"
        ).join(" OR ") + " )";
}

Output:

ruler(rule0)
'( ( ruleA1 AND ruleA2 AND ruleA3 ) OR ( ruleB5 AND ruleB6 ) OR ( ruleB7 ) )'

ruler(rule1)
'( ( ruleA1 AND ruleA2 AND ( ( ruleA3A ) OR ( ruleA3B1 AND ruleA31B2 ) ) ) OR ( ruleB5 AND ruleB6 ) OR ( ruleC7 ) )'

I created a structure thinking about a sequential iteration:

[ 
    {
       "operator": null,
       "field": "age",
       "condition": "eq",
       "value": 5
    },
    {
       "operator": "and",
       "field": "name",
       "condition": "eq",
       "value": "Joe"
    }
]
Related