Fusion JSON proposes a JsonMapper API which intends to support record models.

Here the supported features list:

  • Models must be records or List<X> of a supported type, or a Map<String, X> of a supported type

  • Supported primitives are
    • String
    • String, BigDecimal (represented as string in JSON but incoming data can be a number), {b,B}oolean, {d,D}ouble, int/Integer, {l,L}ong, OffsetDateTime, ZonedDateTime, LocalDate, LocalDateTime

  • Generic mapper is supported, it will bind Object as a Map<String, Object> values being String for JSON strings, BigDecimal for JSON numbers, another Map<String, Object> for JSON objects and List<Object> for JSON lists,

  • A simple post processor prettifier (takes a JSON as input and formats it). It is used decorating the default JsonMapper: io.yupiik.fusion.json.pretty.PrettyJsonMapper,

  • The Fusion annotation processor will generate the JSON "codecs" from the code when a record is marked with @JsonModel, the codec will be reflection free,

  • You can customize the attribute names using @JsonProperty on the record members,

  • You can map all unknown attributes in a Map<String, Object> member marked with @JsonOthers annotation.

IMPORTANT

static model (annotations) are in fusion-build-api which is a provided bundle - build time only.

Runtime Dependency

<dependency>
  <groupId>${project.groupId}</groupId>
  <artifactId>fusion-json</artifactId>
  <version>${project.version}</version>
</dependency>

Example

To modelise the flow you just have to define a record marked with @JsonModel:

@JsonModel
public record MyModel(
    @JsonProperty("boolean") boolean aBool,
    BigDecimal bigDecimal,
    int integer,
    Integer nullableInt,
    long lg,
    double more,
    String simplest,
    LocalDate date,
    LocalDateTime dateTime,
    OffsetDateTime offset,
    ZonedDateTime zoned,
    Object generic,
    AnotherModel nested,
    List<Boolean> booleanList,
    List<BigDecimal> bigDecimalList,
    List<Integer> intList,
    Collection<Long> longList,
    List<Double> doubleList,
    Set<String> stringList,
    List<LocalDate> dateList,
    List<LocalDateTime> dateTimeList,
    List<OffsetDateTime> offsetList,
    List<ZonedDateTime> zonedList,
    List<Object> genericList,
    List<AnotherModel> nestedList,
    Map<String, String> mapStringString,
    Map<String, Integer> mapStringInt,
    Map<String, AnotherModel> mapNested) {
}
WARNING

BigDecimal is supported but avoid to use toBigInteger() or scale related methods without size validation due to java implementation.

Then read/write data using JsonMapper:

@Bean
public class MyService extends HttpServlet {
    @Injection
    JsonMapper mapper;

    @Override
    public void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
        try (final var out = resp.getWriter()) {
            mapper.write(createMyModelInstance(req), out);
        }
    }
}

Enums

Enumerations (de)serialization behavior can be customized by using some specific methods:

public enum MyEnum {
    A, B;

    public String toJsonString() { (1)
        return this == A ? "first" : "second";
    }

    public static MyEnum fromJsonString(final String v) { (2)
        return switch (v) {
            case "first" -> MyEnum.A;
            case "second" -> MyEnum.B;
            default -> throw new IllegalArgumentException("Unsupported '" + v + "'");
        };
    }
}
  1. toJsonString is an instance method with no parameter used to replace .name() call during serialization,
  2. fromJsonString is a static method with a String parameter used to replace .valueOf(String) call during deserialization.

JSON-Pointer and JSON-Patch

JSON-Pointer ( https://datatracker.ietf.org/doc/html/rfc6901) and JSON-Patch ( https://www.rfc-editor.org/rfc/rfc6902.html) are available for generic types today (ie you type as `Object` the serialized/deserialized instances to use `Map<String, Object>` and `List<Object>` as JSON-Object/JSON-Array).

To use them, rely on GenericJsonPointer and GenericJsonPatch classes:

// is reusable
final var patch = new GenericJsonPatch(List.of(new JsonPatchOperation(add, "/baz", null, "qux")));

// "runtime"
final var object = Map.of("foo", "bar");
final var patched = patch.apply(object);
// patched={"foo":"bar","baz":"qux"}

Json Schema Validator

The Json Schema Validator can be used to validate a json content according to a json-schema ( https://json-schema.org/understanding-json-schema/reference).

@Bean
public class MyService {

    @Injection
    JsonMapper mapper;

    public boolean validate(final MyModel myModel) throws IOException {
        final var schema = jsonMapper.fromString(Object.class,
                """
                        {
                          "$id": "https://spec.openapis.org/oas/3.1/schema/2022-10-07",
                          "$schema": "https://json-schema.org/draft/2020-12/schema",
                          "description": "The description of OpenAPI v3.1.x documents without schema validation, as defined by https://spec.openapis.org/oas/v3.1.0",
                          "type": "object"
                          ...
                        }
                      """); (1)
        final JsonSchemaValidatorFactory factory = new JsonSchemaValidatorFactory(); (2)
        final JsonSchemaValidator validator = factory.newInstance((Map<String, Object>) schema); (3)
        final var result = validator.apply(myModel); (4)
        validator.close();
        return result.isSuccess(); (5)
    }
}
  1. use the jsonMapper to load the json schema you want to use,
  2. the factory instance can be reuse and is thread safe,
  3. create a validator from the json schema loaded,
  4. apply the validator to the input model object,
  5. result of the validation.

If the validation failed, the result object of the apply method will contain a list of the messages for each validation check:

final var result = validator.apply(myModel);
final var error = result.errors().iterator().next();
logger.info("Error on field '" + error.field() + "' " + error.message());