Hi Cay,

It would be really helpful to share some more detailed use-cases on 
editing/modification that you may reasonably expect users to perform.

I hope we might be able to devise a transformation API, hopefully layered on 
top of the public API and possibly with structural sharing for unmodified 
parts. One such experiment is presented below that copies the flat map 
transformation patterns used by the class file API and code reflection (a 
combination of traversal + building). It can be used like this:

        JsonObject o = ...

        System.out.println(transformObject(o, (jc, c) -> {
            if (jc instanceof JsonObjectEntry(var name, JsonNumber n) && 
name.equals("UnitPrice")) {
                // Replace number
                c.accept(new JsonObjectEntry(name, 
JsonNumber.of(n.value().doubleValue() + 10.0)));
            } else {
                // Copy
                c.accept(jc);
            }
        }));

This needs a lot more thought but there might be something to this.

Paul.


public class JsonTransform {
    public sealed interface JsonComponent {
    }

    public record JsonObjectMember(String name, JsonValue v) implements 
JsonComponent {
    }

    public record JsonArrayElement(JsonValue v) implements JsonComponent {
    }

    public static JsonObject transformObject(JsonObject o, 
BiConsumer<JsonComponent, Consumer<JsonComponent>> f) {
        /* value */
        class ObjectConsumer implements Consumer<JsonComponent> {
            final Map<String, JsonValue> outputEntries = new HashMap<>();

            JsonObjectMember input;

            @Override
            public void accept(JsonComponent _output) {
                JsonObjectMember output = (JsonObjectMember) _output;

                JsonValue outputValue;
                if (input == output) {
                    // traverse
                    outputValue = switch (input.v()) {
                        case JsonArray ja -> transformArray(ja, f);
                        case JsonObject jo -> transformObject(jo, f);
                        case JsonValue jv -> jv;
                    };
                } else {
                    // replace
                    outputValue = output.v();
                }

                outputEntries.put(output.name(), outputValue);
            }
        }
        ObjectConsumer c = new ObjectConsumer();

        for (Map.Entry<String, JsonValue> inputEntry : o.members().entrySet()) {
            JsonObjectMember input = c.input = new 
JsonObjectMember(inputEntry.getKey(), inputEntry.getValue());
            f.accept(input, c);
        }

        return JsonObject.of(c.outputEntries);
    }

    public static JsonArray transformArray(JsonArray a, 
BiConsumer<JsonComponent, Consumer<JsonComponent>> f) {
        /* value */
        class ArrayConsumer implements Consumer<JsonComponent> {
            final ArrayList<JsonValue> outputElements = new ArrayList<>();

            JsonArrayElement input;

            @Override
            public void accept(JsonComponent _output) {
                JsonArrayElement output = (JsonArrayElement) _output;

                JsonValue outputValue;
                if (input == output) {
                    // traverse
                    outputValue = switch (input.v()) {
                        case JsonArray ja -> transformArray(ja, f);
                        case JsonObject jo -> transformObject(jo, f);
                        case JsonValue jv -> jv;
                    };
                } else {
                    // replace
                    outputValue = output.v();
                }

                outputElements.add(outputValue);
            }
        }
        ArrayConsumer c = new ArrayConsumer();

        List<JsonValue> values = a.values();
        for (int i = 0; i < values.size(); i++) {
            // @@@ pass index?
            JsonArrayElement input = c.input = new 
JsonArrayElement(values.get(i));
            f.accept(input, c);
        }
        return JsonArray.of(c.outputElements);
    }
}


On May 17, 2025, at 10:55 PM, Cay Horstmann <cay.horstm...@gmail.com> wrote:

+1 for having a JSON battery included with the JDK. And for "Our primary goal 
is that the library be simple to use for parsing, traversing, and generating 
conformant JSON documents."

Generating JSON could be easier. Why not convenience methods Json.newObject and 
Json.newArray like in https://github.com/arkanovicz/essential-json?

Parsing with instanceof will work, but is obviously painful today, as your 
example shows. The simplification with deconstruction patterns is not 
impressive either.

JsonValue doc = Json.parse(inputString);
if (doc instanceof JsonObject(var members)
    && members.get("name") instanceof JsonString(String name)
    && members.get("age") instanceof JsonNumber(int age)) {
            // use "name" and "age"
} else throw new NoSuchArgumentException();

vs. Jackson

String name = doc.get("name").asText();
int age = doc.get("age").asInt();
...

If only there was some deconstruction magic that approximates the JavaScript 
code

const doc = { name: "John", age: 30 }
const { name, age } = doc

What about editing documents? With Jackson, you can mutate objects and arrays. 
I see the appeal of immutability, but then there needs to be a convenient 
transform API. Right now, making John one year older is not pretty:

var nextYearDoc = switch (doc) {
   case JsonObject(var members) if
       members.get("name") instanceof JsonString(String name)
       && members.get("age") instanceof JsonNumber(int age)) ->
           Json.fromUntyped(Map.of("name", name, "age", age + 1));
   default -> throw new NoSuchArgumentException();
}

And it gets worse if John is nested more deeply in a document.

I have worked a lot with immutable XML in Scala. One minimally needs a 
mechanism for recursive rewriting with a node replacement function. I am not 
aware of an existing library that attempts this in Java for JSON. I am sure it 
can be done, but it may not be trivial to do such an API well.

Cheers,

Cay

PS. Trying to create and show the youthful John gives me grief right now:

Json.fromUntyped(Map.of("name", "John", "age", 30)).toString()
|  Exception java.lang.NullPointerException: Cannot read the array length 
because "value" is null
|        at String.rangeCheck (String.java:307)
|        at String.<init> (String.java:303)
|        at JsonNumberImpl.toString (JsonNumberImpl.java:105)
|        at JsonObjectImpl.toString (JsonObjectImpl.java:56)
|        at (#23:1)

The JsonNumberImpl.toString method needs to handle the case that it was 
constructed from a Number.

Reply via email to