diff --git a/README.md b/README.md index 45645413c..1e4dbaeb8 100644 --- a/README.md +++ b/README.md @@ -1,524 +1,104 @@ -Jayway JsonPath -===================== +### FunctionProvider SPI (Custom Function Plugins) -**A Java DSL for reading JSON documents.** +JsonPath allows you to define and register custom functions (plugins) for use in path expressions and filter predicates. -[![Build Status](https://travis-ci.org/json-path/JsonPath.svg?branch=master)](https://travis-ci.org/json-path/JsonPath) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.jayway.jsonpath/json-path/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.jayway.jsonpath/json-path) -[![Javadoc](https://www.javadoc.io/badge/com.jayway.jsonpath/json-path.svg)](http://www.javadoc.io/doc/com.jayway.jsonpath/json-path) +#### Example: `JoinFunction` -Jayway JsonPath is a Java port of [Stefan Goessner JsonPath implementation](http://goessner.net/articles/JsonPath/). +JsonPath includes [`com.jayway.jsonpath.spi.function.JoinFunction`](file:///Users/rpond/IdeaProjects/JsonPath/json-path/src/main/java/com/jayway/jsonpath/spi/function/JoinFunction.java) as an example of a custom plugin that joins values or JSON-path expressions with a delimiter. -Getting Started ---------------- - -JsonPath is available at the Central Maven Repository. Maven users add this to your POM. - -> [!NOTE] -> Version 3.0.0 Uses Java 17 baseline to support Jackson 3 - -```xml - - - com.jayway.jsonpath - json-path - 3.0.0 - -``` - -If you need help ask questions at [Stack Overflow](http://stackoverflow.com/questions/tagged/jsonpath). Tag the -question 'jsonpath' and 'java'. - -JsonPath expressions always refer to a JSON structure in the same way as XPath expression are used in combination -with an XML document. The "root member object" in JsonPath is always referred to as `$` regardless if it is an -object or array. - -JsonPath expressions can use the dot–notation - -`$.store.book[0].title` - -or the bracket–notation - -`$['store']['book'][0]['title']` - -Operators ---------- - -| Operator | Description | -|:--------------------------|:----------------------------------------------------------------| -| `$` | The root element to query. This starts all path expressions. | -| `@` | The current node being processed by a filter predicate. | -| `*` | Wildcard. Available anywhere a name or numeric are required. | -| `..` | Deep scan. Available anywhere a name is required. | -| `.` | Dot-notated child | -| `['' (, '')]` | Bracket-notated child or children | -| `[ (, )]` | Array index or indexes | -| `[start:end]` | Array slice operator | -| `[?()]` | Filter expression. Expression must evaluate to a boolean value. | - -Functions ---------- - -Functions can be invoked at the tail end of a path - the input to a function is the output of the path expression. -The function output is dictated by the function itself. - -| Function | Description | Output type | -|:------------|:-------------------------------------------------------------------------------------|:---------------------| -| `min()` | Provides the min value of an array of numbers | Double | -| `max()` | Provides the max value of an array of numbers | Double | -| `avg()` | Provides the average value of an array of numbers | Double | -| `stddev()` | Provides the standard deviation value of an array of numbers | Double | -| `length()` | Provides the length of an array | Integer | -| `sum()` | Provides the sum value of an array of numbers | Double | -| `keys()` | Provides the property keys (An alternative for terminal tilde `~`) | `Set` | -| `concat(X)` | Provides a concatinated version of the path output with a new item | like input | -| `append(X)` | add an item to the json path output array | like input | -| `first()` | Provides the first item of an array | Depends on the array | -| `last()` | Provides the last item of an array | Depends on the array | -| `index(X)` | Provides the item of an array of index: X, if the X is negative, take from backwards | Depends on the array | - -Filter Operators ------------------ - -Filters are logical expressions used to filter arrays. A typical filter would be `[?(@.age > 18)]` where `@` represents -the current item being processed. More complex filters can be created with logical operators `&&` and `||`. String -literals must be enclosed by single or double quotes (`[?(@.color == 'blue')]` or `[?(@.color == "blue")]`). - -| Operator | Description | -|:-----------|:-------------------------------------------------------------------| -| `==` | left is equal to right (note that 1 is not equal to '1') | -| `!=` | left is not equal to right | -| `<` | left is less than right | -| `<=` | left is less or equal to right | -| `>` | left is greater than right | -| `>=` | left is greater than or equal to right | -| `=~` | left matches regular expression [?(@.name =~ /foo.*?/i)] | -| `in` | left exists in right [?(@.size in ['S', 'M'])] | -| `nin` | left does not exists in right | -| `subsetof` | left is a subset of right [?(@.sizes subsetof ['S', 'M', 'L'])] | -| `anyof` | left has an intersection with right [?(@.sizes anyof ['M', 'L'])] | -| `noneof` | left has no intersection with right [?(@.sizes noneof ['M', 'L'])] | -| `size` | size of left (array or string) should match right | -| `empty` | left (array or string) should be empty | - -Path Examples -------------- +```java +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.function.ParamType; +import com.jayway.jsonpath.internal.function.Parameter; +import com.jayway.jsonpath.spi.function.PathFunction; -Given the json +import java.util.ArrayList; +import java.util.List; -```javascript -{ - "store": { - "book": [ - { - "category": "reference", - "author": "Nigel Rees", - "title": "Sayings of the Century", - "price": 8.95 - }, - { - "category": "fiction", - "author": "Evelyn Waugh", - "title": "Sword of Honour", - "price": 12.99 - }, - { - "category": "fiction", - "author": "Herman Melville", - "title": "Moby Dick", - "isbn": "0-553-21311-3", - "price": 8.99 - }, - { - "category": "fiction", - "author": "J. R. R. Tolkien", - "title": "The Lord of the Rings", - "isbn": "0-395-19395-8", - "price": 22.99 +public class JoinFunction implements PathFunction { + @Override + public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List parameters) { + String delimiter = ""; + List targetParams = new ArrayList<>(); + + if (parameters == null || parameters.isEmpty()) { + delimiter = ""; + } else if (parameters.size() == 1) { + Object d = parameters.get(0).getValue(); + delimiter = d != null ? d.toString() : ""; + } else { + Parameter first = parameters.get(0); + Parameter last = parameters.get(parameters.size() - 1); + if (first.getType() != ParamType.PATH && last.getType() == ParamType.PATH) { + // Delimiter is first argument: join(delimiter, path1, path2, ...) + delimiter = first.getValue() != null ? first.getValue().toString() : ""; + targetParams = parameters.subList(1, parameters.size()); + } else if (first.getType() == ParamType.PATH && last.getType() != ParamType.PATH) { + // Delimiter is last argument: join(path1, path2, ..., delimiter) + delimiter = last.getValue() != null ? last.getValue().toString() : ""; + targetParams = parameters.subList(0, parameters.size() - 1); + } else { + delimiter = first.getValue() != null ? first.getValue().toString() : ""; + targetParams = parameters.subList(1, parameters.size()); } - ], - "bicycle": { - "color": "red", - "price": 19.95 } - }, - "expensive": 10 -} -``` - -| JsonPath | Result | -|:----------------------------------------|:-------------------------------------------------------------| -| `$.store.book[*].author` | The authors of all books | -| `$..author` | All authors | -| `$.store.*` | All things, both books and bicycles | -| `$.store..price` | The price of everything | -| `$..book[2]` | The third book | -| `$..book[-2]` | The second to last book | -| `$..book[0,1]` | The first two books | -| `$..book[:2]` | All books from index 0 (inclusive) until index 2 (exclusive) | -| `$..book[1:2]` | All books from index 1 (inclusive) until index 2 (exclusive) | -| `$..book[-2:]` | Last two books | -| `$..book[2:]` | All books from index 2 (inclusive) to last | -| `$..book[?(@.isbn)]` | All books with an ISBN number | -| `$.store.book[?(@.price < 10)]` | All books in store cheaper than 10 | -| `$..book[?(@.price <= $['expensive'])]` | All books in store that are not "expensive" | -| `$..book[?(@.author =~ /.*REES/i)]` | All books matching regex (ignore case) | -| `$..*` | Give me every thing -| `$..book.length()` | The number of books | - -Reading a Document ------------------- -The simplest most straight forward way to use JsonPath is via the static read API. - -```java -String json = "..."; - -List authors = JsonPath.read(json, "$.store.book[*].author"); -``` - -If you only want to read once this is OK. In case you need to read an other path as well this is not the way -to go since the document will be parsed every time you call JsonPath.read(...). To avoid the problem you can -parse the json first. - -```java -String json = "..."; -Object document = Configuration.defaultConfiguration().jsonProvider().parse(json); - -String author0 = JsonPath.read(document, "$.store.book[0].author"); -String author1 = JsonPath.read(document, "$.store.book[1].author"); -``` - -JsonPath also provides a fluent API. This is also the most flexible one. - -```java -String json = "..."; - -ReadContext ctx = JsonPath.parse(json); - -List authorsOfBooksWithISBN = ctx.read("$.store.book[?(@.isbn)].author"); - - -List> expensiveBooks = JsonPath - .using(configuration) - .parse(json) - .read("$.store.book[?(@.price > 10)]", List.class); -``` - -What is Returned When? ----------------------- -When using JsonPath in java its important to know what type you expect in your result. JsonPath will automatically -try to cast the result to the type expected by the invoker. - -```java -//Will throw an java.lang.ClassCastException -List list = JsonPath.parse(json).read("$.store.book[0].author"); - -//Works fine -String author = JsonPath.parse(json).read("$.store.book[0].author"); -``` - -When evaluating a path you need to understand the concept of when a path is `definite`. A path is `indefinite` if it -contains: - -* `..` - a deep scan operator -* `?()` - an expression -* `[, (, )]` - multiple array indexes - -`Indefinite` paths always returns a list (as represented by current JsonProvider). - -By default a simple object mapper is provided by the MappingProvider SPI. This allows you to specify the return type you -want and the MappingProvider will -try to perform the mapping. In the example below mapping between `Long` and `Date` is demonstrated. - -```java -String json = "{\"date_as_long\" : 1411455611975}"; - -Date date = JsonPath.parse(json).read("$['date_as_long']", Date.class); -``` - -If you configure JsonPath to use `JacksonMappingProvider`, `Jackson3MappingProvider`, `GsonMappingProvider`, -or `JakartaJsonProvider` you can even -map your JsonPath output directly into POJO's. - -```java -Book book = JsonPath.parse(json).read("$.store.book[0]", Book.class); -``` - -To obtain full generics type information, use TypeRef. - -```java -TypeRef> typeRef = new TypeRef>() { -}; - -List titles = JsonPath.parse(JSON_DOCUMENT).read("$.store.book[*].title", typeRef); -``` - -Predicates ----------- -There are three different ways to create filter predicates in JsonPath. - -### Inline Predicates - -Inline predicates are the ones defined in the path. - -```java -List> books = JsonPath.parse(json) - .read("$.store.book[?(@.price < 10)]"); -``` - -You can use `&&` and `||` to combine multiple predicates `[?(@.price < 10 && @.category == 'fiction')]` , -`[?(@.category == 'reference' || @.price > 10)]`. - -You can use `!` to negate a predicate `[?(!(@.price < 10 && @.category == 'fiction'))]`. - -### Filter Predicates - -Predicates can be built using the Filter API as shown below: - -```java -import static com.jayway.jsonpath.JsonPath.parse; -import static com.jayway.jsonpath.Criteria.where; -import static com.jayway.jsonpath.Filter.filter; -... - ... - -Filter cheapFictionFilter = filter( - where("category").is("fiction").and("price").lte(10D) -); - -List> books = - parse(json).read("$.store.book[?]", cheapFictionFilter); - -``` -Notice the placeholder `?` for the filter in the path. When multiple filters are provided they are applied in order -where the number of placeholders must match -the number of provided filters. You can specify multiple predicate placeholders in one filter operation `[?, ?]`, both -predicates must match. - -Filters can also be combined with 'OR' and 'AND' - -```java -Filter fooOrBar = filter( - where("foo").exists(true)).or(where("bar").exists(true) -); - -Filter fooAndBar = filter( - where("foo").exists(true)).and(where("bar").exists(true) -); -``` - -### Roll Your Own - -Third option is to implement your own predicates + List items = new ArrayList<>(); + if (!targetParams.isEmpty()) { + for (Parameter param : targetParams) { + appendValue(param.getValue(), ctx, items); + } + } else if (model != null) { + appendValue(model, ctx, items); + } -```java -Predicate booksWithISBN = new Predicate() { - @Override - public boolean apply(PredicateContext ctx) { - return ctx.item(Map.class).containsKey("isbn"); + return String.join(delimiter, items); } -}; - -List> books = - reader.read("$.store.book[?].isbn", List.class, booksWithISBN); -``` - -Path vs Value -------------- -In the Goessner implementation a JsonPath can return either `Path` or `Value`. `Value` is the default and what all the -examples above are returning. If you rather have the path of the elements our query is hitting this can be achieved with -an option. - -```java -Configuration conf = Configuration.builder() - .options(Option.AS_PATH_LIST).build(); - -List pathList = using(conf).parse(json).read("$..author"); - -assertThat(pathList). - -containsExactly( - "$['store']['book'][0]['author']", - "$['store']['book'][1]['author']", - "$['store']['book'][2]['author']", - "$['store']['book'][3]['author']"); -``` - -Set a value ------------ -The library offers the possibility to set a value. - -```java -String newJson = JsonPath.parse(json).set("$['store']['book'][0]['author']", "Paul").jsonString(); -``` - -Tweaking Configuration ----------------------- - -### Options - -When creating your Configuration there are a few option flags that can alter the default behaviour. - -**DEFAULT_PATH_LEAF_TO_NULL** -
-This option makes JsonPath return null for missing leafs. Consider the following json - -```javascript -[ - { - "name" : "john", - "gender" : "male" - }, - { - "name" : "ben" - } -] -``` - -```java -Configuration conf = Configuration.defaultConfiguration(); - -//Works fine -String gender0 = JsonPath.using(conf).parse(json).read("$[0]['gender']"); -//PathNotFoundException thrown -String gender1 = JsonPath.using(conf).parse(json).read("$[1]['gender']"); - -Configuration conf2 = conf.addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL); - -//Works fine -String gender0 = JsonPath.using(conf2).parse(json).read("$[0]['gender']"); -//Works fine (null is returned) -String gender1 = JsonPath.using(conf2).parse(json).read("$[1]['gender']"); -``` - -**ALWAYS_RETURN_LIST** -
-This option configures JsonPath to return a list even when the path is `definite`. - -```java -Configuration conf = Configuration.defaultConfiguration(); - -//ClassCastException thrown -List genders0 = JsonPath.using(conf).parse(json).read("$[0]['gender']"); - -Configuration conf2 = conf.addOptions(Option.ALWAYS_RETURN_LIST); - -//Works fine -List genders0 = JsonPath.using(conf2).parse(json).read("$[0]['gender']"); -``` - -**SUPPRESS_EXCEPTIONS** -
-This option makes sure no exceptions are propagated from path evaluation. It follows these simple rules: - -* If option `ALWAYS_RETURN_LIST` is present an empty list will be returned -* If option `ALWAYS_RETURN_LIST` is **NOT** present null returned - -**REQUIRE_PROPERTIES** -
-This option configures JsonPath to require properties defined in path when an `indefinite` path is evaluated. - -```java -Configuration conf = Configuration.defaultConfiguration(); - -//Works fine -List genders = JsonPath.using(conf).parse(json).read("$[*]['gender']"); - -Configuration conf2 = conf.addOptions(Option.REQUIRE_PROPERTIES); - -//PathNotFoundException thrown -List genders = JsonPath.using(conf2).parse(json).read("$[*]['gender']"); + // ... helper to traverse arrays and scalars +} ``` -### JsonProvider SPI - -JsonPath is shipped with five different JsonProviders: +#### Registering Custom Functions -* [JsonSmartJsonProvider](https://github.com/netplex/json-smart-v2) (default) -* [JacksonJsonProvider](https://github.com/FasterXML/jackson) -* [JacksonJsonNodeJsonProvider](https://github.com/FasterXML/jackson) -* [JacksonJson3Provider](https://github.com/FasterXML/jackson) -* [JacksonJson3NodeJsonProvider](https://github.com/FasterXML/jackson) -* [GsonJsonProvider](https://code.google.com/p/google-gson/) -* [JsonOrgJsonProvider](https://github.com/stleary/JSON-java) -* [JakartaJsonProvider](https://javaee.github.io/jsonp/) +Functions can be registered either per-`Configuration` (recommended) or globally. -Changing the configuration defaults as demonstrated should only be done when your application is being initialized. -Changes during runtime is strongly discouraged, especially in multi threaded applications. +**Configuration-scoped registration (isolated):** ```java -Configuration.setDefaults(new Configuration.Defaults() { +// Register by Class +Configuration conf = Configuration.builder() + .registerFunction("join", JoinFunction.class) + .build(); - private final JsonProvider jsonProvider = new JacksonJsonProvider(); - private final MappingProvider mappingProvider = new JacksonMappingProvider(); +// Join multiple paths with delimiter: +String fullName = JsonPath.using(conf).parse(json) + .read("$.join(' ', $.user.firstName, $.user.lastName)"); // e.g. "John Doe" - @Override - public JsonProvider jsonProvider () { - return jsonProvider; - } +// Join wildcard/array path with delimiter: +String fruits = JsonPath.using(conf).parse(json) + .read("$.join(', ', $.fruits[*])"); // e.g. "apple, banana, cherry" - @Override - public MappingProvider mappingProvider () { - return mappingProvider; - } +// Or invoke directly on an array: +String joined = JsonPath.using(conf).parse(json) + .read("$.fruits.join('-')"); // e.g. "apple-banana-cherry" - @Override - public Set