Patterns récurrents avec Champollion : streaming évènementiel, manipulation par JSON Pointer / Patch, customization JSON-B (adapters, sérialiseurs, formats date, locales). Toutes les recettes sont compatibles virtual threads — pas de synchronized, pas de ThreadLocal exporté.
JSON-P streaming — parser et generator
Parser pull (JsonParser)
try (var p = Json.createParser(input)) {
while (p.hasNext()) {
switch (p.next()) {
case START_OBJECT -> { /* descente */ }
case KEY_NAME -> handleKey(p.getString());
case VALUE_STRING -> handleString(p.getString());
case VALUE_NUMBER -> handleNumber(p.getBigDecimal());
case END_OBJECT -> { /* remontée */ }
// VALUE_TRUE, VALUE_FALSE, VALUE_NULL, START_ARRAY, END_ARRAY
}
}
}
p.skipObject() / p.skipArray() permettent de zapper sans matérialiser. p.getValue() matérialise un sous-arbre JsonValue à la position courante (utile pour les feuilles).
Generator push (JsonGenerator)
try (var g = Json.createGenerator(System.out)) {
g.writeStartObject()
.write("name", "Champollion")
.writeStartArray("works")
.write("Lettre à M. Dacier (1822)")
.write("Précis du système hiéroglyphique (1824)")
.writeEnd()
.writeEnd();
}
Configuration via Json.createGeneratorFactory(Map.of(JsonGenerator.PRETTY_PRINTING, true)).
JSON-P object model
JsonValue est une sealed interface — pattern matching exhaustif possible :
JsonValue v = ...;
String label = switch (v) {
case JsonString s -> "str: " + s.getString();
case JsonNumber n -> "num: " + n.bigDecimalValue();
case JsonObject o -> "obj(" + o.size() + " keys)";
case JsonArray a -> "arr(" + a.size() + " items)";
case JsonValue x -> x.getValueType().name(); // TRUE / FALSE / NULL
};
JSON Pointer (RFC 6901)
JsonObject doc = ...;
JsonPointer p = Json.createPointer("/users/0/name");
JsonValue name = p.getValue(doc);
JsonObject patched = (JsonObject) p.replace(doc, Json.createValue("Jean-François"));
L’erreur sur ~ mal formé (ex. ~n) est différée à la résolution, conformément au TCK (cf. TCK Jakarta JSON-P 2.1 + JSON-B 3.0).
JSON Patch (RFC 6902)
JsonPatch patch = Json.createPatchBuilder()
.add("/users/0/role", "admin")
.replace("/version", 2)
.remove("/draft")
.build();
JsonObject after = patch.apply(before);
JSON Merge Patch (RFC 7396)
JsonMergePatch mp = Json.createMergePatch(Json.createObjectBuilder()
.add("name", "new")
.addNull("toRemove")
.build());
JsonValue merged = mp.apply(before);
JSON-B — JsonbConfig
Jsonb jsonb = JsonbBuilder.newBuilder()
.withConfig(new JsonbConfig()
.withFormatting(true)
.withNullValues(false)
.withLocale(Locale.FRANCE)
.withDateFormat("yyyy-MM-dd", Locale.FRANCE)
.withPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE_WITH_DASHES))
.build();
Toutes les propriétés standard de jakarta.json.bind.JsonbConfig sont supportées (cf. https://jakarta.ee/specifications/jsonb/3.0/).
JSON-B — annotations courantes
public record Person(
@JsonbProperty("first_name") String firstName,
@JsonbDateFormat("dd/MM/yyyy") LocalDate birthDate,
@JsonbNumberFormat("#,##0.00") BigDecimal balance,
@JsonbTransient String internalToken,
@JsonbTypeAdapter(MoneyAdapter.class) Money salary
) {}
JSON-B — adapter custom
public final class MoneyAdapter implements JsonbAdapter<Money, String> {
@Override public String adaptToJson(Money m) { return m.amount() + " " + m.currency(); }
@Override public Money adaptFromJson(String s) {
var parts = s.split(" ");
return new Money(new BigDecimal(parts[0]), parts[1]);
}
}
Enregistrement global via JsonbConfig.withAdapters(new MoneyAdapter()) ou local via @JsonbTypeAdapter.
JSON-B — sérialiseur / désérialiseur bas niveau
Quand un adapter ne suffit pas (cas multi-champs polymorphique), implémenter directement JsonbSerializer<T> / JsonbDeserializer<T> qui poussent / lisent via le JsonGenerator / JsonParser de la couche JSON-P :
public final class GeoPointSerializer implements JsonbSerializer<GeoPoint> {
@Override public void serialize(GeoPoint p, JsonGenerator g, SerializationContext ctx) {
g.writeStartArray().write(p.lon()).write(p.lat()).writeEnd();
}
}
Polymorphisme (JSON-B 3.0)
@JsonbTypeInfo(key = "@type", value = {
@JsonbSubtype(alias = "rect", type = Rect.class),
@JsonbSubtype(alias = "circle", type = Circle.class)
})
public sealed interface Shape permits Rect, Circle {}
Virtual threads et ScopedValue
Champollion est sans synchronized ni ThreadLocal exporté. Le contexte courant pendant un toJson / fromJson est porté par ScopedValue (cf. Fonctionnement interne) — utiliser librement depuis un Executors.newVirtualThreadPerTaskExecutor() sans précaution particulière.