JSON
This page details JSON.
Runtime Dependency
<dependency>
<groupId>io.yupiik.fusion</groupId>
<artifactId>fusion-json</artifactId>
<version>${fusion.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
|
|
Then read/write data using JsonMapper:
@ApplicationScoped
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);
}
}
}
|
TIP
|
for byte oriented sources/sinks prefer the |
Ignoring attributes
To tell the generated codec to totally ignore a record attribute, mark it with @JsonIgnore:
@JsonModel
public record MyModel(
String name,
@JsonIgnore String internalSecret) { (1)
}
-
internalSecretis a pure build metadata attribute: it is never written to the JSON payload and, if an incoming payload happens to carry theinternalSecretkey, it is skipped so the attribute ends up with its default value (primitive default for primitives such as0/false/0.0,nullfor anything else).
@JsonIgnore is entirely handled at build time: it only drives the generated keysets, no reflection is involved at runtime.
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 + "'");
};
}
}
-
toJsonStringis an instance method with no parameter used to replace.name()call during serialization, -
fromJsonStringis a static method with aStringparameter 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 ).
@ApplicationScoped
public class MyService {
@Injection
JsonMapper mapper;
public boolean validate(final MyModel myModel) throws IOException {
final var schema = mapper.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)
}
}
-
use the
jsonMapperto load the json schema you want to use, - the factory instance can be reuse and is thread safe,
- create a validator from the json schema loaded,
- apply the validator to the input model object,
- 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());
Json Schema Draft 2020-12 conversion
The JSON schemas emitted by the annotation processor use a lightweight Draft-07/OpenAPI hybrid format: they rely on a nullable keyword and reference shared models with #/schemas/<fqn> pointers. When you need a Draft 2020-12
document, use io.yupiik.fusion.json.schema.JsonSchemaService to convert them in a purely structural and reflectionless way.
The conversion is applied on the generic JSON tree ({`Map`}/{`List`}/scalar values as produced by JsonMapper) and rewrites the schema keywords only, never the payload values:
-
nullable: truebecomestype: ["<base>", "null"](2020-12 droppednullablein favor oftypeunions), -
definitionsand the generatedschemascontainer are renamed$defs, -
#/definitions/...and#/schemas/...references are relocated to#/$defs/..., -
a
$schema: https://json-schema.org/draft/2020-12/schemaheader is added at the root when missing.
Use it on a single schema, or on a whole META-INF/fusion/json/schemas.json bundle to get a self-contained document:
final JsonSchemaService service = new JsonSchemaService(); // or @Injection
// convert a single generic schema
final Map<String, Object> converted = service.toJsonSchema202012(rawSchemaMap); // (1)
// convert the whole "schemas.json" bundle {"schemas": {...}} into a document with a root $defs
final var bundle = mapper.fromString(Object.class, json);
final Map<String, Object> document = service.to2020Bundle((Map<String, Object>) bundle); // (2)
-
nullable:truebecomestype:["<base>","null"], refs become#/$defs/..., -
the document gets a
$schemaheader and a root$defsmap holding every model keyed by its$id.
The processor also ships a typed view (RawBuildJsonSchema) and a hand-written reflectionless JsonCodec (RawBuildJsonSchemaJsonCodec) so the generated schema payloads can be deserialized with a JsonMapper and then upgraded:
// read one schema-json payload into the typed model with the standard mapper
final RawBuildJsonSchema schema = mapper.read(RawBuildJsonSchema.class, reader);
final Map<String, Object> converted = service.toJsonSchema202012(schema); // -> Draft 2020-12
Pretty mapper
Fusion provide a json pretty mapper to print a json string.
import io.yupiik.fusion.framework.build.api.scanning.Injection;
import io.yupiik.fusion.json.JsonMapper;
import io.yupiik.fusion.json.pretty.PrettyJsonMapper;
@ApplicationScoped
public class MyClass {
@Injection
JsonMapper jsonMapper;
public void printJson() {
final JsonMapper prettyJsonMapper = new PrettyJsonMapper(jsonMapper); (1)
logger.info(prettyJsonMapper.toString(instance)); (2)
}
}
-
use the injected
jsonMapperto create the instance of the PrettyJsonMapper, - pretty-print your model instance using the decorated mapper.
JsonConfigurationSource
JsonConfigurationSource is a ConfigurationSource that reads a JSON document and flattens it into a Map<String, String> accessible via dot-notation keys. It uses the generic Object mapping (JsonMapper.fromString(Object.class, ...)) to parse the JSON and then recursively flattens the result. The goal is to be able to register a custom JSON resource which serve as Yupiik Fusion ConfigurationSource - backing Configuration API.
Creating a JsonConfigurationSource
Use ReaderSupplier to load the JSON from a classpath resource, file, or inline reader:
import io.yupiik.fusion.framework.api.io.ReaderSupplier;
import io.yupiik.fusion.json.configuration.JsonConfigurationSource;
// from classpath
final var source = new JsonConfigurationSource(
ReaderSupplier.fromClasspath("config.json"));
// from a file
final var source = new JsonConfigurationSource(
ReaderSupplier.fromFile(Path.of("/etc/app/config.json")));
// from a file
final var source = new JsonConfigurationSource(
ReaderSupplier.fromInline("{\"my\":\"content\"}");
// from a resource or file with a default
final var source = new JsonConfigurationSource(
ReaderSupplier.from("config.json", "{\"myDefaultPort\": 8080}"));
Flattening rules
The JSON structure is flattened as follows:
-
Primitive values (
Boolean,String,BigDecimal) are stored directly as strings at the current path. -
Maps (JSON objects) are flattened as bean properties: each key becomes a path segment using dot-notation. Nested objects are recursively flattened.
-
Collections (JSON arrays) of primitives are serialized as comma-separated values. Collections of objects use a
.lengthkey and indexed children (path.0.xxx,path.1.xxx, ...). -
$asListattribute on a map changes the flattening to use the list convention (path.length,path.N.key,path.N.value), which matches theMap<String, V>reading pattern inConfigurationFactoryGenerator. When all values are primitives, the map is serialized as aProperties-format string. This enables to comply toMap<String, Object>handling of the configuration mapper.
Examples
Objects
final var source = new JsonConfigurationSource(mapper,
() -> new StringReader("""
{"server": {"host": "localhost", "port": 8080}}
"""));
source.get("server.host"); // "localhost"
source.get("server.port"); // "8080"
Collections
final var source = new JsonConfigurationSource(mapper,
() -> new StringReader("""
{"tags": ["tag1", "tag2"], "items": [{"name": "a"}, {"name": "b"}]}
"""));
// primitive collection → CSV
source.get("tags"); // "tag1, tag2"
// object collection → length + indexed children
source.get("items.length"); // "2"
source.get("items.0.name"); // "a"
source.get("items.1.name"); // "b"
Map support with $asList
When a JSON object represents a Map<String, V>, mark it with "$asList": true:
final var source = new JsonConfigurationSource(mapper,
() -> new StringReader("""
{
"config": {
"$asList": true,
"key1": {"enabled": true},
"key2": {"enabled": false}
}
}
"""));
source.get("config.length"); // "2"
source.get("config.0.key"); // "key1"
source.get("config.0.value.enabled"); // "true"
source.get("config.1.key"); // "key2"
source.get("config.1.value.enabled"); // "false"
For Map<String, String> with $asList, all-primitive values are serialized as a Properties-format string:
final var source = new JsonConfigurationSource(mapper,
() -> new StringReader("""
{"metadata": {"$asList": true, "dc": "us-east", "env": "prod"}}
"""));
// uses Properties.store() format, readable by Properties.load()
final var value = source.get("metadata"); // contains "dc=us-east" and "env=prod" - 2 lines
Registration
Since ConfigurationSource are just plain bean you can create a method marked with @Bean and return your instance of JsonConfigurationSource.
|
IMPORTANT
|
if you do use |
An alternative to avoid that is to register the configuration source in a listener, listening for ConfigurationRegistration event:
public class MyListener {
public void onStart(@OnEvent final ConfigurationRegistration event) {
event.addSource().accept(new JsonConfigurationSource(
ReaderSupplier.from(
event.configuration().get("my.config.path").orElse("config.json"),
"{\"myDefaultPort\": 8080}")));
}
}