Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/maif/functional-json
Parse and write json the functional way
https://github.com/maif/functional-json
functional-programming jackson json
Last synced: about 3 hours ago
JSON representation
Parse and write json the functional way
- Host: GitHub
- URL: https://github.com/maif/functional-json
- Owner: MAIF
- License: apache-2.0
- Created: 2020-09-21T12:05:50.000Z (about 4 years ago)
- Default Branch: master
- Last Pushed: 2021-12-03T14:59:03.000Z (almost 3 years ago)
- Last Synced: 2024-04-16T19:39:02.315Z (7 months ago)
- Topics: functional-programming, jackson, json
- Language: Java
- Homepage:
- Size: 121 KB
- Stars: 10
- Watchers: 7
- Forks: 1
- Open Issues: 3
-
Metadata Files:
- Readme: readme.md
- License: LICENSE
- Code of conduct: .github/CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# Functional json [![ga-badge][]][ga] [![jar-badge][]][jar]
[ga]: https://github.com/MAIF/functional-json/actions?query=workflow%3ABuild
[ga-badge]: https://github.com/MAIF/functional-json/workflows/Build/badge.svg
[jar]: https://maven-badges.herokuapp.com/maven-central/fr.maif/functional-json
[jar-badge]: https://maven-badges.herokuapp.com/maven-central/fr.maif/functional-json/badge.svgThis library inspired by [playframework scala json](https://github.com/playframework/play-json) lib and [json-lib](https://github.com/mathieuancelin/json-lib) provide helpers to manipulate [Jackson](https://github.com/FasterXML/jackson) json nodes.
With this lib you can have a total control on json serialization and deserialization.
You can also separate POJO's definition from its serialization, which can be helpful in an hexagonal architecture.
Another benefit is to get various ser/des for the same class.At the end you will get a better error handling which can be very pleasant in RESTful API to help the client of the API to understand why the validation has failed.
To read and write from json, there is two important interface :
* `JsonRead` to read json
* `JsonWrite` to write jsonThis lib also comes with helpers to build json from scratch easily than with raw jackson.
## Import
Jcenter hosts this library.
### Maven
```xml
fr.maif
functional-json
${VERSION}```
### Gradle
```
implementation 'fr.maif:functional-json:${VERSION}'
```## Creating json
You can create json using the `$` static function or his alias `$$` in case `$` is already in scope (eg `vavr` pattern matching) to create a json object.
```java
import fr.maif.json.Json.*;
import fr.maif.json.JsonWrite.*;
``````java
ObjectNode myJson = Json.obj(
$("name", "Ragnar Lodbrock"),
$("city", Some("Kattegat")),
$("weight", 80),
$("birthDate", LocalDate.of(766, 1, 1), $localdate()),
$("sons", Json.arr(
Json.obj($("name", "Bjorn")),
Json.obj($("name", "Ubbe")),
Json.obj($("name", "Hvitserk")),
Json.obj($("name", "Sigurd")),
Json.obj($("name", "Ivar"))
))
);
```## JsonRead
Reading JSON is basic :
```java
@FunctionalInterface
public interface JsonRead {JsResult read(JsonNode jsonNode);
}
```
A `JsResult` can rather be a `JsSuccess` or a `JsError`. The `JsError` will stack all errors, in order to let you a detailed report of what happend.With a lambda you can define a read this way :
```java
JsonRead strRead = json -> {
if (json.isTextual()) {
return JsResult.success(json.asText());
} else {
return JsResult.error(List.of(JsResult.Error.error("string.expected")));
}
};
```This lib already provides readers for all common types so you probably won't have to write this kind of code.
If you need to implement a specific reader for a POJO, this will look like this.
The POJO with a builder and immutable fields.
The `@FieldNameConstants` is a lombok annotation that will create a sub class with static fields with the name of each fields.
```java
@FieldNameConstants
@Builder
@AllArgsConstructor
public static class Viking {
public final String firstName;
public final String lastName;
public final Option city;
}
```And the reader is the following :
```java
public static JsonRead reader() {
return _string("firstName", Viking.builder()::firstName)
.and(_string("lastName"), Viking.VikingBuilder::lastName)
.and(_opt("city", _string()), Viking.VikingBuilder::city)
.map(Viking.VikingBuilder::build);
}
```For a better understanding, here is the decomposition of the previous code :
This part reads a string at path "firstName"
```java
JsonRead stringJsonRead = _string("firstName");
```Here is an alternative, that reads a `String` at a path in order to use this `String` for something else.
Then you'll need to create a builder and apply the string to the `firstName` field :
```java
JsonRead vikingBuilderJsonRead = _string("firstName", str -> Viking.builder().firstName(str));
```The same with method reference :
```java
JsonRead vikingBuilderJsonRead2 = _string("firstName", Viking.builder()::firstName);
```Now we can use the method `and`.
With `and` you can read another field and combine with the current builder like this :
```java
JsonRead vikingBuilderJsonReadStep2 = vikingBuilderJsonRead2
.and(_string("lastName"), (previousBuilder, str) -> previousBuilder.lastName(str));
```The same with method reference :
```java
JsonRead vikingBuilderJsonReadStep2 = vikingBuilderJsonRead2
.and(_string("lastName"), VikingBuilder::lastName);
```
At the end, when all the fields have been read, we can use `map` to transform the builder in the `Viking` instance :```java
JsonRead vikingJsonRead = vikingBuilderJsonReadStep2.map(b -> b.build());
```The same with method reference :
```java
JsonRead vikingJsonRead = vikingBuilderJsonReadStep2.map(VikingBuilder::build);
```Wiring all together we'll have :
```java
public static JsonRead reader() {
return _string("firstName", Viking.builder()::firstName)
.and(_string("lastName"), Viking.VikingBuilder::lastName)
.and(_opt("city", _string()), Viking.VikingBuilder::city)
.map(Viking.VikingBuilder::build);
}
```We provide readers for :
* `String`: `_string(...)`
* `Integer`: `_int(...)`
* `Long`: `_long(...)`
* `Boolean`: `_boolean(...)`
* `BigDecimal`: `_bigDecimal(...)`
* `Enum`: `_enum(...)`
* `LocalDate`: `_localDate(...)`, `_isoLocalDate(...)`
* `LocalDateTime`: `_localDateTime(...)`, `_isoLocalDateTime(...)`
* `Option`: `_opt(...)`
* `List`: `_list(...)`
* `Set`: `_set(...)`
* Generic read at path: `__()`## Json Write
A json write is :
```java
@FunctionalInterface
public interface JsonWrite {
JsonNode write(T value);
}
```With a lambda you can define a write as :
```java
JsonWrite strWrite = str -> new TextNode(str);
```To define a JSON object or arrays there is helpers so for the `Viking` POJO a write look like :
```java
public static JsonWrite writer() {
return viking -> Json.obj(
$("firstName", viking.firstName),
$("lastName", viking.lastName),
$("city", viking.city)
);
}
```Just as we do for the readers, we provide writers common types :
* `String`: `$string()`
* `Integer`: `$int()`
* `Long`: `$long()`
* `Boolean`: `$boolean()`
* `BigDecimal`: `_bigdecimal()`
* `Enum`: `$enum()`
* `LocalDate`: `$localdate()`
* `LocalDateTime`: `$localdatetime()`
* `Traversable`: `$list(JsonWrite)`
* Jackson array: `Json.newArray()` or `Json.arr(...nodes)`## JsonFormat
A json format is the combinaison of a `JsonRead` and a `JsonWrite`. You can create a format using of:
```java
JsonFormat format() {
return JsonFormat.of(reader(), writer());
}
```## Parsing json and converting it to POJOs
At the end with the following definition :
```java
@FieldNameConstants
@Builder
@AllArgsConstructor
public class Viking {// A JsonFormat is both JsonRead and JsonWrite
public static JsonFormat format() {
return JsonFormat.of(reader(), writer());
}public static JsonRead reader() {
return _string("firstName", Viking.builder()::firstName)
.and(_string("lastName"), Viking.VikingBuilder::lastName)
.and(_opt("city", _string()), Viking.VikingBuilder::city)
.map(Viking.VikingBuilder::build);
}public static JsonWrite writer() {
return viking -> Json.obj(
$("firstName", viking.firstName),
$("lastName", viking.lastName),
$("city", viking.city)
);
}public final String firstName;
public final String lastName;
public final Option city;
}
```We can do :
```java
Viking viking = Viking.builder()
.firstName("Ragnar")
.lastName("Lodbrock")
.city(Some("Kattegat"))
.build();JsonNode jsonNode = Json.toJson(viking, Viking.format());
String stringify = Json.stringify(jsonNode);JsonNode parsed = Json.parse(stringify);
JsResult vikingJsResult = Json.fromJson(parsed, Viking.format());
```
## Handling `sum types` or polymorphism
Sometime you need to serialize or deserialize sum types. For example :
```java
public interface Animal { }
@Builder
@Value
public class Dog implements Animal {
String name;
}@Builder
@Value
public class Cat implements Animal {
String name;
}```
In the following example, there's also different version of the json object serialization.
The parsing is done using two fields `type` and `version`:
* `type` is the type of the animal: dog or cat
* `version` is the version of it's JSON representation
Here, we have two versions of the dog JSON, one with the `name` in the field `legacyName` (the `v1` version) and one with the `name` in the field `name` (the `v2`version).The readers are the followings :
```java
JsonRead dogJsonReadV1 =
_string("legacyName", Dog.builder()::name)
.map(Dog.DogBuilder::build);JsonRead dogJsonReadV2 =
_string("name", Dog.builder()::name)
.map(Dog.DogBuilder::build);JsonRead catJsonRead =
_string("name", Cat.builder()::name)
.map(Cat.CatBuilder::build);
```Now to read an animal you can do this :
```java
JsonRead oneOfRead = JsonRead.oneOf(_string("type"), _string("version"), "data", List(
caseOf((t, v) -> t.equals("dog") && v.equals("v1"), dogJsonReadV1),
caseOf((t, v) -> t.equals("dog") && v.equals("v2"), dogJsonReadV2),
caseOf((t, v) -> t.equals("cat") && v.equals("v1"), catJsonRead)
));
```Or with the `vavr` helpers :
```java
JsonRead oneOfRead = JsonRead.oneOf(_string("type"), _string("version"), "data", List(
caseOf($Tuple2($("dog"), $("v1")), dogJsonReadV1),
caseOf($Tuple2($("dog"), $("v2")), dogJsonReadV2),
caseOf($Tuple2($("cat"), $("v1")), catJsonRead)
));
```## Json schema
`JsonRead` exposes a JSON schema https://json-schema.org/. This could be usefull when you have to share the schema to other teams.
In the case you need to override or enrich the schema, there is some helpers to do that :
```java
JsonRead.ofRead(oneOfRead, JsonSchema.emptySchema()
.title("A title")
.description("Blah blah blah")
// ...
);
```