在 JSON 中将逻辑表示为数据

出于业务原因,我们需要将一些条件逻辑外部化到外部文件中: 最好是 JSON。

可以通过添加如下节点来处理一个简单的过滤方案:

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

数组中的其他值可以处理多个条件。

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

然而,当我们处理涉及 AND 或 OR 的复杂条件时,它会变得有点混乱。

问: 是否有一种标准化的(甚至是广泛接受的)格式来表示 JSON 中的这种逻辑?如果由你决定,你会怎么做?

注意: 第一个答案是一个可编辑的维基,所以任何人都可以改进它。

70055 次浏览

My colleague suggested this possible solution:

"all OR conditions would be an array while AND conditions would be objects,

For example,OR can match any of the objects in the array:

[
{
"var1":"value1"
},
{
"var2":"value2"
},
{
"var3":"value3"
}
]

AND would be

{
"var1":"val1",
"var2":"val2",
"var3":"val3"
}

If you must implement this using standard JSON, i'd recommend something akin to Lisp's "S-expressions". A condition could be either a plain object, or an array whose first entry is the logical operation that joins them.

For example:

["AND",
{"var1" : "value1"},
["OR",
{ "var2" : "value2" },
{ "var3" : "value3" }
]
]

would represent var1 == value1 AND (var2 == value2 OR var3 == value3).

If you prefer brevity over consistency, you could also allow an object to have multiple properties, which would implicitly be joined by an AND. For example, { "a": "b", "c": "d" } would be equivalent to ["AND", { "a": "b" }, { "c": "d" }]. But there are cases (like the example) where the former syntax can not faithfully represent the condition as written; you'd need additional trickery like translating the condition or using dummy property names. The latter syntax should always work.

I had a similar need (to build up a sql where clause in javascript). I create dthe following javascript function:

  function parseQuery(queryOperation){
var query="";
if (queryOperation.operator == 'and')
query = "(" + parseQuery(queryOperation.leftOp) + ") AND (" + parseQuery(queryOperation.rightOp) + ")";
if (queryOperation.operator == 'or')
query = "(" + parseQuery(queryOperation.leftOp) + ") OR (" + parseQuery(queryOperation.rightOp) + ")";
if (queryOperation.operator == '=')
query = "(" + queryOperation.leftOp +" = "+ queryOperation.rightOp + ")";
return query;
}

I create my queryOperation Like this:

 var queryObject =             {
operator: 'and',
leftOp: {
leftOp: 'tradedate',
operator: '=',
rightOp: new Date()
},
rightOp: {
operator: 'or',
leftOp: {
leftOp: 'systemid',
operator: '=',
rightOp: 9
},
rightOp: {
leftOp: 'systemid',
operator: '=',
rightOp:10
}
}
};

When I pass my queryOperation to ParseQuery it returns ((tradedate= Thu Jul 24 17:30:37 EDT 2014)) AND (((systemid= 9)) OR ((systemid= 10)))

I need to add some type conversions and other operators, but the basic structure works.

I needed a format that would:

  1. Support comparisons other than equality.
  2. Let variables appear in any position, not just be compared to literals.
  3. Be consistent, terse, secure, and extensible.

So I built up a format I'm calling JsonLogic. A rule is a JSON object, with the operator in the key position, and one or an array of arguments in the value position. (Inspired by Amazon CloudFormation functions.) Any argument can be another rule, so you can build arbitrarily deep logic.

I've also written two parsers for it: JsonLogic for JavaScript and JsonLogic for PHP.

cHao's example would be written as

{ "and", [
{"==", [ {"var" : "var1"}, "value1" ]},
{ "or", [
{"==", [ {"var" : "var2"}, "value2" ]},
{"==", [ {"var" : "var3"}, "value3" ]}
]}
]}

var here is the operator to get a property of the "data" object, passed along with the "rule" object to the parser, e.g.:

jsonLogic(
{"==", [{"var":"filling"}, "apple"]}    // rule, is this pie apple?
{"filling":"apple", "temperature":100}  // data, a pie I'm inspecting
);
// true

There are lots more possible operators (greater than, not-equals, in-array, ternary, etc) and both parsers are available on GitHub (with unit tests and documentation).

By the way, IBM DB2 supports logic statements encoded in JSON.

Boolean operations look like a cross between cHao's solution and Amazon CloudFormation:

{"$and":[{"age":5},{"name":"Joe"}]}

Comparison operations look, to me, like transliterated SQL. (Instead of Amazon or Russellg or cHao's movement toward an abstract syntax tree.)

{"age":{"$lt":3}}

Please check out (JSL)[https://www.npmjs.com/package/lib-jsl ]. It seems to fit the description given.

Here is a sample :

var JSL = require('lib-jsl');


var bugs = [
[{ bug : { desc: 'this is bug1 open', status : 'open' } }],
[{ bug : { desc: 'this is bug2 resolved', status : 'resolved' } }],
[{ bug : { desc: 'this is bug3 closed' , status : 'closed' } }],
[{ bug : { desc: 'this is bug4 open', status : 'open' } }],
[{ bug : { desc: 'this is bug5 resolved', status : 'resolved' } }],
[{ bug : { desc: 'this is bug6 open', status : 'open' } }],


[   { workInProgress : '$bug'},
{ bug : '$bug'},
{ $or : [
{ $bind : [ '$bug', { status : 'open'} ] },
{ $bind : [ '$bug', { status : 'resolved'} ] }
] }
]
];
var query = [{workInProgress : '$wip'}]
var transform = '$wip'
var jsl = new JSL ({
rules : bugs,
query : query,
transform : transform
});
var retval = jsl.run();
console.log(JSON.stringify(retval, null,2));

The response is :

[
{
"desc": "this is bug1 open",
"status": "open"
},
{
"desc": "this is bug2 resolved",
"status": "resolved"
},
{
"desc": "this is bug4 open",
"status": "open"
},
{
"desc": "this is bug5 resolved",
"status": "resolved"
},
{
"desc": "this is bug6 open",
"status": "open"
}
]

The main work is done by the query defined in the rule workInProgress :

[   { workInProgress : '$bug'},
{ bug : '$bug'},
{ $or : [
{ $bind : [ '$bug', { status : 'open'} ] },
{ $bind : [ '$bug', { status : 'resolved'} ] }
] }
]

This rule can be read as :

To satisfy the query with workInProgress, we define a variable {workInProgress : '$bug'}, which we then proceed to match against all bugs in the database using the next part of the rule {bug : '$bug'}. This part matches all bugs since the shape of the object (it's keys: 'bug') matches the bug records in the database. The rule further asks the $bug variable to be $bind(ed) against patterns containing relevant status values (open and closed) within a $or. Only those bug records whose status value in $bug satisfies all parts of the rule's body qualify for the result.

The result is finally transformed using the transform specification : transform : '$wip' which literally asks for an array of all values returned in the $wip variable of the query.

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 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

Formula parser + a bit of JS codes to put data into formulas, is another solution described with example in this answer.

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) )  )

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 ) )'

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

Following Jeremy Wadhams comment, I mapped the json by MongoDB logical query operator and MongoDB comparison query operator but MongoDB don't allow $ character in content:

{"and":[
{"age":{"eq": 5}},
{"name":{"eq": "Joe"}
]}

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