Towards a JSON API for the JDK
Paul Sandoz
paul.sandoz at oracle.com
Mon May 19 20:36:11 UTC 2025
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.horstmann at 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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/core-libs-dev/attachments/20250519/2ef8bc07/attachment-0001.htm>
More information about the core-libs-dev
mailing list