Recurring patterns with Champollion: event-based streaming, manipulation by JSON Pointer / Patch, JSON-B customization (adapters, serializers, date formats, locales). All recipes are virtual-thread-safe — no synchronized, no exported ThreadLocal.

JSON-P streaming — parser and generator

Pull parser (JsonParser)

try (var p = Json.createParser(input)) {
    while (p.hasNext()) {
        switch (p.next()) {
            case START_OBJECT -> { /* descent */ }
            case KEY_NAME     -> handleKey(p.getString());
            case VALUE_STRING -> handleString(p.getString());
            case VALUE_NUMBER -> handleNumber(p.getBigDecimal());
            case END_OBJECT   -> { /* ascent */ }
            // VALUE_TRUE, VALUE_FALSE, VALUE_NULL, START_ARRAY, END_ARRAY
        }
    }
}

p.skipObject() / p.skipArray() skip without materializing. p.getValue() materializes the sub-tree JsonValue at the current position (handy for leaves).

Push generator (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 is a sealed interface — exhaustive pattern matching is 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"));

The error on a malformed ~ (e.g. ~n) is deferred to resolution, as required by the 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.US)
            .withDateFormat("yyyy-MM-dd", Locale.US)
            .withPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE_WITH_DASHES))
        .build();

All standard jakarta.json.bind.JsonbConfig properties are supported (cf. https://jakarta.ee/specifications/jsonb/3.0/).

JSON-B — common annotations

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 — custom adapter

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]);
    }
}

Global registration through JsonbConfig.withAdapters(new MoneyAdapter()) or local through @JsonbTypeAdapter.

JSON-B — low-level serializer / deserializer

When an adapter is not enough (multi-field polymorphic case), implement JsonbSerializer<T> / JsonbDeserializer<T> directly — they push to / read from the JSON-P JsonGenerator / JsonParser:

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();
    }
}

Polymorphism (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 and ScopedValue

Champollion has no synchronized and no exported ThreadLocal. The current context during toJson / fromJson is carried by ScopedValue (cf. Internals) — use it freely from Executors.newVirtualThreadPerTaskExecutor() without precaution.