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.