Creating operators

Every operator should be defined as a subclass of the Operator abstract base class. In this example, we will implement the > operator.

As the base class is defined as a dataclass(), we will follow that path for our operator.

Implementing the from_expression() constructor

The > (greater than) operator should take two arguments (no more, no less). No specific constraints have to be applied on these arguments [1].

from dataclasses import dataclass
from tying import Self

from jsonlogic import JSONLogicSyntaxError, Operator
from jsonlogic.typing import OperatorArgument

@dataclass
class GreaterThan(Operator):
    # Attributes will be defined below

    @classmethod
    def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
        # Any syntax error should raise a `JSONLogicSyntaxError`:
        if len(arguments) != 2:
            raise JSONLogicSyntaxError(f"{operator!r} expects two arguments, got {len(arguments)}")

Once you have validated the provided arguments, an instance of the GreaterThan operator can be created:

@dataclass
class GreaterThan(Operator):
    # Each operator defines its own attributes:
    left: OperatorArgument
    right: OperatorArgument

    @classmethod
    def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self:
        ...
        # We map the provided arguments to the operator's attributes:
        return cls(operator=operator, left=arguments[0], right=arguments[1])

Implementing the typecheck() method

By default, the typecheck() method of the base Operator class returns the type AnyType.

For more details on how JSON Schema types are represented, see Representing types.

This method is responsible for

  • typechecking the children:

    from jsonlogic.typechecking import TypecheckContext, get_type
    from jsonlogic.json_schema import from_value
    from jsonlogic.json_schema.types import AnyType, BooleanType, UnsupportedOperation
    
    class GreaterThan(Operator):
        ...
    
        def typecheck(self, context: TypecheckContext) -> BooleanType:
            left_type = get_type(self.left, context)
            right_type = get_type(self.right, context)
    

    get_type() is a utility function to typecheck the argument if it is an Operator, or infer the type from the primitive value. For more details on how this inference works, see Converting types from a "format" specifier.

  • typechecking the current operator:

    class GreaterThan(Operator):
        ...
    
        def typecheck(self, context: TypecheckContext) -> BooleanType:
            left_type = get_type(self.left, context)
            right_type = get_type(self.right, context)
    
            try:
                return left_type.binary_op(right_type, ">")
            except UnsupportedOperation:
                context.add_diagnostic(
                    f"Cannot compare {left_type.name} with {right_type.name}",
                    "not_comparable",
                    self
                )
                # Usually, you will want to return "AnyType" if typechecking failed:
                return AnyType()
    

    The TypecheckContext object is used to emit diagnostics and access the JSON Schema of the data provided when using typecheck().

    Every JSON Schema type class defines two methods: unary_op() and binary_op(). The op argument is a string literal representing the Python operator, e.g. ">" or %.

Implementing the evaluate() method

The evaluate() method is used to evaluate the operator.

Similar to the typecheck() method, it is responsible for:

  • evaluating the children:

    from jsonlogic.evaluation import EvaluationContext, get_value
    
    class GreaterThan(Operator):
        ...
    
        def evaluate(self, context: EvaluationContext) -> bool:
            left_value = get_value(self.left, context)
            right_value = get_value(self.right, context)
    

    get_value() is a utility function to evaluate the argument if it is an Operator, or return the primitive value.

  • evaluating the current operator:

    class GreaterThan(Operator):
        ...
    
        def evaluate(self, context: EvaluationContext) -> bool:
            left_value = get_value(self.left, context)
            right_value = get_value(self.right, context)
    
            return left_value > right_value
    

Footnotes