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 <[email protected]> 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.