- Accessing Data
var
missing
missing_some
- Logic and Boolean Operations
if
==
===
!=
!==
!
!!
or
and
- Numeric Operations
>
,>=
,<
, and<=
- Between
max
andmin
- Arithmetic,
+
-
*
/
%
- Array Operations
map
,reduce
, andfilter
all
,none
, andsome
merge
in
- String Operations
in
cat
substr
- Miscellaneous
log
Accessing Data
var
Retrieve data from the provided data object.
Most JsonLogic rules operate on data supplied at run-time. Typically this data is an object, in which case the argument to var
is a property name.
Note, every operation will be demonstrated with a live example box. Feel free to edit the logic and the data and see what happens when you apply your change! Here’s what the example above would look like in JavaScript:
jsonLogic.apply(
{ "var" : ["a"] }, // Logic
{ "a":1, "b":2 } // Data
);
// 1
If you like, we support syntactic sugar to skip the array around single arguments :
You can supply a default, as the second argument, for values that might be missing in the data object. (Note, the skip-the-array sugar won’t work here because you’re passing two arguments to var
):
The key passed to var can use dot-notation to get the property of a property (to any depth you need):
You can also use the var
operator to access an array by numeric index:
Here’s a complex rule that mixes literals and data. The pie isn’t ready to eat unless it’s cooler than 110 degrees, and filled with apples.
You can also use var
with an empty string to get the entire data object – which is really useful in map
, filter
, and reduce
rules.
missing
Takes an array of data keys to search for (same format as var
). Returns an array of any keys that are missing from the data object, or an empty array.
Note, in JsonLogic, empty arrays are falsy. So you can use missing
with if
like:
missing_some
Takes a minimum number of data keys that are required, and an array of keys to search for (same format as var
or missing
). Returns an empty array if the minimum is met, or an array of the missing keys otherwise.
This is useful if you’re using missing
to track required fields, but occasionally need to require N of M fields.
Logic and Boolean Operations
if
The if
statement typically takes 3 arguments: a condition (if), what to do if it’s true (then), and what to do if it’s false (else), like:
If can also take more than 3 arguments, and will pair up arguments like if/then elseif/then elseif/then else. Like:
See the Fizz Buzz implementation for a larger example.
==
Tests equality, with type coercion. Requires two arguments.
===
Tests strict equality. Requires two arguments.
!=
Tests not-equal, with type coercion.
!==
Tests strict not-equal.
!
Logical negation (“not”). Takes just one argument.
Note: unary operators can also take a single, non array argument:
!!
Double negation, or “cast to a boolean.” Takes a single argument.
Note that JsonLogic has its own spec for truthy to ensure that rules will run consistently across interpreters. (e.g., empty arrays are falsy, string “0” is truthy.)
or
or
can be used for simple boolean tests, with 1 or more arguments.
At a more sophisticated level, or
returns the first truthy argument, or the last argument.
and
and
can be used for simple boolean tests, with 1 or more arguments.
At a more sophisticated level, and
returns the first falsy argument, or the last argument.
Numeric Operations
>
, >=
, <
, and <=
Greater than:
Greater than or equal to:
Less than:
Less than or equal to:
Between
You can use a special case of <
and <=
to test that one value is between two others:
Between exclusive:
Between inclusive:
This is most useful with data:
max
and min
Return the maximum or minimum from a list of values.
Arithmetic, +
-
*
/
Addition, subtraction, multiplication, and division.
Because addition and multiplication are associative, they happily take as many args as you want:
Passing just one argument to -
returns its arithmetic negative (additive inverse).
Passing just one argument to +
casts it to a number.
%
Modulo. Finds the remainder after the first argument is divided by the second argument.
This can be paired with a loop in the language that parses JsonLogic to create stripes or other effects.
In Javascript:
var rule = {"if": [{"%": [{"var":"i"}, 2]}, "odd", "even"]};
for(var i = 1; i <= 4 ; i++){
console.log(i, jsonLogic.apply(rule, {"i":i}));
}
/* Outputs:
1 "odd"
2 "even"
3 "odd"
4 "even"
*/
Array Operations
map
, reduce
, and filter
You can use map
to perform an action on every member of an array. Note, that inside the logic being used to map, var
operations are relative to the array element being worked on.
You can use filter
to keep only elements of the array that pass a test. Note, that inside the logic being used to map, var
operations are relative to the array element being worked on.
Also note, the returned array will have contiguous indexes starting at zero (typical for JavaScript, Python and Ruby) it will not preserve the source indexes (making it unlike PHP’s array_filter
).
You can use reduce
to combine all the elements in an array into a single value, like adding up a list of numbers. Note, that inside the logic being used to reduce, var
operations only have access to an object like:
{
"current" : // this element of the array,
"accumulator" : // progress so far, or the initial value
}
all
, none
, and some
These operations take an array, and perform a test on each member of that array.
The most interesting part of these operations is that inside the test code, var
operations are relative to the array element being tested.
It can be useful to use {"var":""}
to get the entire array element within the test.
Or it can be useful to test an object based on its properties:
Note that none
will return true
for an empty array, while all
and some
will return false
.
merge
Takes one or more arrays, and merges them into one array. If arguments aren’t arrays, they get cast to arrays.
Merge can be especially useful when defining complex missing
rules, like which fields are required in a document. For example, this vehicle paperwork always requires the car’s VIN, but only needs the APR and term if you’re financing.
in
If the second argument is an array, tests that the first argument is a member of the array:
String Operations
in
If the second argument is a string, tests that the first argument is a substring:
cat
Concatenate all the supplied arguments. Note that this is not a join or implode operation, there is no “glue” string.
substr
Get a portion of a string.
Give a positive start position to return everything beginning at that index. (Indexes of course start at zero.)
Give a negative start position to work backwards from the end of the string, then return everything.
Give a positive length to express how many characters to return.
Give a negative length to stop that many characters before the end.
Miscellaneous
log
Logs the first value to console, then passes it through unmodified.
This can be especially helpful when debugging a large rule.
(Check your developer console!)
Go JSON Logic
Implementation of JSON Logic in Go Lang.
What’s JSON Logic?
JSON Logic is a DSL to write logic decisions in JSON. It’s has a great specification and is very simple to learn.
The official website has a great documentation with examples.
How to use it
The use of this library is very straightforward. Here’s a simple example:
package main import ( "bytes" "fmt" "strings" "github.com/diegoholiveira/jsonlogic/v3" ) func main() { logic := strings.NewReader(`{"==": [1, 1]}`) data := strings.NewReader(`{}`) var result bytes.Buffer jsonlogic.Apply(logic, data, &result) fmt.Println(result.String()) }
This will output true
in your console.
Here’s another example, but this time using variables passed in the data
parameter:
package main import ( "bytes" "encoding/json" "fmt" "strings" "github.com/diegoholiveira/jsonlogic/v3" ) type ( User struct { Name string `json:"name"` Age int `json:"age"` Location string `json:"location"` } Users []User ) func main() { logic := strings.NewReader(`{ "filter": [ {"var": "users"}, {">=": [ {"var": ".age"}, 18 ]} ] }`) data := strings.NewReader(`{ "users": [ {"name": "Diego", "age": 33, "location": "Florianópolis"}, {"name": "Jack", "age": 12, "location": "London"}, {"name": "Pedro", "age": 19, "location": "Lisbon"}, {"name": "Leopoldina", "age": 30, "location": "Rio de Janeiro"} ] }`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { fmt.Println(err.Error()) return } var users Users decoder := json.NewDecoder(&result) decoder.Decode(&users) for _, user := range users { fmt.Printf(" - %sn", user.Name) } }
If you have a function you want to expose as a JSON Logic operation, you can use:
package main import ( "bytes" "fmt" "strings" "github.com/diegoholiveira/jsonlogic/v3" ) func main() { // add a new operator "strlen" for get string length jsonlogic.AddOperator("strlen", func(values, data interface{}) interface{} { v, ok := values.(string) if ok { return len(v) } return 0 }) logic := strings.NewReader(`{ "strlen": { "var": "foo" } }`) data := strings.NewReader(`{"foo": "bar"}`) var result bytes.Buffer jsonlogic.Apply(logic, data, &result) fmt.Println(result.String()) // the string length of "bar" is 3 }
License
This project is licensed under the MIT License — see the LICENSE file for details
Go JSON Logic
Implementation of JSON Logic in Go Lang.
What’s JSON Logic?
JSON Logic is a DSL to write logic decisions in JSON. It’s has a great specification and is very simple to learn.
The official website has a great documentation with examples: http://jsonlogic.com
How to use it
The use of this library is very straightforward. Here’s a simple example:
package main
import (
"encoding/json"
"fmt"
"github.com/diegoholiveira/jsonlogic"
)
func main() {
var logic interface{}
err := json.Unmarshal([]byte(`{"==": [1, 1]}`), &logic)
if err != nil {
fmt.Println(err.Error())
return
}
var result interface{}
jsonlogic.Apply(
logic,
nil,
&result,
)
fmt.Println(result)
}
This will output true
in your console.
Here’s another example, but this time using variables passed in the data
parameter:
package main
import (
"encoding/json"
"fmt"
"github.com/diegoholiveira/jsonlogic"
)
func main() {
var logic interface{}
var data interface{}
var result interface{}
json.Unmarshal([]byte(`{
"filter": [
{"var": "users"},
{">=": [
{"var": ".age"},
18
]}
]
}`), &logic)
json.Unmarshal([]byte(`{
"users": [
{"name": "Diego", "age": 33, "location": "Florianópolis"},
{"name": "Jack", "age": 12, "location": "London"},
{"name": "Pedro", "age": 19, "location": "Lisbon"},
{"name": "Leopoldina", "age": 30, "location": "Rio de Janeiro"}
]
}`), &data)
err := jsonlogic.Apply(logic, data, &result)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("Users older than 18:")
for _, _user := range result.([]interface{}) {
user := _user.(map[string]interface{})
fmt.Printf(" - %sn", user["name"].(string))
}
}
Limitations
The Apply
function have three params as input:
- the first one is the logic to be executed;
- next you have the data that can be used by the logic;
- and the last one is the variable to store the result.
The type of those params must be interface{}
to be easy to reflect of it and use it.
Also, all values passed in any of the two input params must be one of those:
bool for JSON booleans,
float64 for JSON numbers,
string for JSON strings, and
nil for JSON null.
This is the same values that encoding/json
work with.
Here’s an example of an invalid data:
func main() {
var rules interface{}
var result interface{}
json.Unmarshal([]byte(`{
"filter": [
{"var": "users"},
{">=": [
{"var": ".age"},
18
]}
]
}`), &rules)
data := interface{}(map[string]interface{}{
"users": []interface{}{
map[string]interface{}{
"name": string("Diego"),
"age": int(33),
"location": string("Florianópolis"),
},
},
})
err := jsonlogic.Apply(rules, data, &result)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println("Users older than 18:")
for _, _user := range result.([]interface{}) {
user := _user.(map[string]interface{})
fmt.Printf(" - %sn", user["name"].(string))
}
}
This will produce this error: panic: interface conversion: interface {} is int, not float64
.
So, to avoid this error, make sure to always work with types compatible with encoding/json
.
License
This project is licensed under the MIT License — see the LICENSE file for details
- Introduction
- Error Objects
- Error Lists
- Responses
- HTTP Status
- JSON:API Exceptions
- Helper Methods
- Validation Errors
- Source Pointers
- Error Rendering
- JSON Responses
- Middleware Responses
- Always Rendering JSON:API Errors
- Custom Rendering Logic
- Converting Exceptions
- Error Reporting
# Introduction
The JSON:API specification defines error objects (opens new window)
that are used to provide information to a client about problems encountered
while performing an operation.
Errors are returned to the client in the top-level errors
member of the
JSON document.
Laravel JSON:API makes it easy to return errors to the client — either
as responses, or by throwing exceptions. In addition, the exception
renderer you added to your exception handler during
installation takes care of
converting standard Laravel exceptions to JSON:API error responses
if the client has sent an Accept: application/vnd.api+json
header.
# Error Objects
Use our LaravelJsonApiCoreDocumentError
object to create a JSON:API
error object. The easiest way to construct an error object is using the
static fromArray
method. For example:
The fromArray
method accepts all the error object members
defined in the specification. (opens new window)
Alternatively, if you want to use setters, use the static make
method
to fluently construct your error object:
The available setters are:
setAboutLink
setCode
setDetail
setId
setLinks
setMeta
setStatus
setSource
setSourceParameter
setSourcePointer
setTitle
# Error Lists
If you need to return multiple errors at once, use our
LaravelJsonApiCoreDocumentErrorList
class. This accepts any number
of error objects to its constructor. For example:
Use the push
method to add errors after constructing the error list:
# Responses
Both the Error
and ErrorList
classes implement Laravel’s Responsable
interface. This means you can return them directly from controller actions
and they will be converted to a JSON:API error response.
If you need to customise the error response, then you need to use our
LaravelJsonApiCoreResponsesErrorResponse
class. Either create
a new one, passing in the Error
or ErrorList
object:
Or alternatively, use the prepareResponse
method on either the Error
or ErrorList
object:
The ErrorResponse
class has all the helper methods
required to customise both the headers and the JSON:API document that
is returned in the response.
For example, if we were adding a header and meta to our response:
# HTTP Status
The JSON:API specification says:
When a server encounters multiple problems for a single request,
the most generally applicable HTTP error code SHOULD be used in the response.
For instance,400 Bad Request
might be appropriate for multiple 4xx errors
or500 Internal Server Error
might be appropriate for multiple 5xx errors.
Our ErrorResponse
class takes care of calculating the HTTP status for you.
If there is only one error, or all the errors have the same status, then
the response status will match this status.
If you have multiple errors with different statuses, the response status
will be 400 Bad Request
if the error objects only have 4xx
status codes.
If there are any 5xx
status codes, the response status will be
500 Internal Server Error
.
If the response has no error objects, or none of the error objects have a
status, then the response will have a 500 Internal Server Error
status.
If you want the response to have a specific HTTP status, use the
withStatus
method. For example:
# JSON:API Exceptions
Our LaravelJsonApiCoreExceptionsJsonApiException
allows you to terminate
processing of a request by throwing an exception with JSON:API errors
attached.
The exception expects its first argument to be either an Error
or an
ErrorList
object. For example:
The JsonApiException
class has all the helper methods
required to customise both the headers and the JSON:API document that
is returned in the response. Use the static make
method if you need to
call any of these methods. For example:
There is also a handy static error
method. This allows you to fluently
construct an exception for a single error, providing either an Error
object or an array. For example:
# Helper Methods
The JsonApiException
class has a number of helper methods:
- is4xx
- is5xx
- getErrors
# is4xx
Returns true
if the HTTP status code is a client error, i.e. in the 400-499
range.
# is5xx
Returns true
if the HTTP status code is a server error, i.e. in the 500-599
range.
# getErrors
Use the getErrors()
method to retrieve the JSON:API error objects from the
exception. For example, if we wanted to log the errors:
# Validation Errors
Our implementation of resource requests and
query parameter requests already takes
care of converting Laravel validation error messages to JSON:API errors.
If however you have a scenario where you want to convert a failed validator
to JSON:API errors manually, we provide the ability to do this.
You will need to resolve an instance of LaravelJsonApiValidationFactory
out of the service container. For example, you could use the app
helper,
or use dependency injection by type-hinting it in a constructor of a service.
Once you have the factory instance, use the createErrors
method,
providing it with the validator instance. For example, in a controller
action:
The object this returns is Responsable
— so you can return it directly
from a controller action. If you want to convert it to an error response,
use our prepareResponse
pattern as follows:
# Source Pointers
By default this process will convert validation error keys to JSON source
pointers. For example, if you have a failed message for the foo.bar
value, the resulting error object will have a source pointer of
/foo/bar
.
If you need to prefix the pointer value, use the withSourcePrefix
method.
The following example would convert foo.bar
to /data/attributes/foo/bar
:
If you need to fully customise how the validation key should be converted,
provide a Closure
to the withPointers
method:
# Error Rendering
As described in the installation instructions,
the following should have been added to the register
method on your
application’s exception handler:
The Laravel exception handler already takes care of converting exceptions to
either application/json
or text/html
responses. Our exception handler
effectively adds JSON:API error responses as a third media type. If the
client has sent a request with an Accept
header of application/vnd.api+json
,
then they will receive the exception response as a JSON:API error response —
even if the endpoint they are hitting is not one of your JSON:API server
endpoints.
WARNING
There are some scenarios where the Laravel exception handler does not call
any registered renderables. For example, if an exception implements Laravel’s
Responsable
interface, our exception parser will not be invoked as the handler
uses the Responsable::toResponse()
method on the exception to generate a
response.
# JSON Responses
If a client encounters an exception when using an Accept
header of
application/json
, they will still receive Laravel’s default JSON exception
response, rather than a JSON:API response.
If you want our exception parser to render JSON exception responses instead
of the default Laravel response, use the acceptsJson()
method when registering
our exception parser:
# Middleware Responses
Sometimes you may want exceptions to be converted to JSON:API errors if the
current route has a particular middleware applied to it. The most common example
of this would be if you want JSON:API errors to always be rendered if the
current route has the api
middleware.
In this scenario, use the acceptsMiddleware()
method when registering our
exception parser. For example:
TIP
You can provide multiple middleware names to the acceptsMiddleware()
method.
When you do this, it will match a route that contains any of the provided
middleware.
# Always Rendering JSON:API Errors
If you want our exception parser to always convert exceptions to JSON:API
errors, use the acceptsAll()
helper method:
# Custom Rendering Logic
If you want your own logic for when a JSON:API exception response should be
rendered, pass a closure to the accept()
method.
For example, let’s say we wanted our API to always return JSON:API exception
responses, regardless of what Accept
header the client sent. We would use
the request is()
method to check if the path is our API:
TIP
If you return false
from the callback, the normal exception rendering logic
will run — meaning a client that has sent an Accept
header with the JSON:API
media type will still receive a JSON:API response. This is semantically correct,
as the Accept
header value should be respected.
# Converting Exceptions
Our exception parser is built so that you can easily add support for
custom exceptions to the JSON:API rendering process. The implementation works
using a pipeline, meaning you can add your own handlers for converting
exceptions to JSON:API errors.
For example, imagine our application had a PaymentFailed
exception, that
we wanted to convert to JSON:API errors if thrown to the exception handler.
We would write the following class:
We can then add it to the JSON:API exception parser using either the
prepend
or append
method:
# Error Reporting
As described in the installation instructions,
the following should have been added to the $dontReport
property on your
application’s exception handler:
This prevents our JsonApiException
from being reported in your application’s
error log. This is a sensible starting position, as the JsonApiException
class is effectively a HTTP exception that needs to be rendered to the client.
However, this does mean that any JsonApiException
that has a 5xx
status
code (server-side error) will not be reported in your error log. Therefore,
an alternative is to use the helper methods on the JsonApiException
class
to determine whether or not the exception should be reported.
To do this, we will use the reportable()
method to register a callback
for the JSON:API exception class. (At the same time, we remove the exception
class from the $dontReport
property.) For example, the following will stop
the propagation of JSON:API exceptions to the default logging stack if the
exception does not have a 5xx status:
In the following example, we log 4xx statuses as debug information, while
letting all other JSON:API exceptions propagate to the default logging stack: