<div dir="ltr"><font face="monospace">I'm writing this to drive some forward motion and to nerd-snipe those who know better than I do into putting their thoughts into words.<br><br>There are three ways to process JSON[1]<br>- Streaming (Push or Pull)<br>- Traversing a Tree (Realized or Lazy)<br>- Declarative Databind (N ways)<br><br>Of these, JEP-198 explicitly ruled out providing "JAXB style type safe data binding."<br><br>No justification is given, but if I had to insert my own: mapping the Json model to/from the Java/JVM object model is a cursed combo of<br>- Huge possible design space<br>- Unpalatably large surface for backwards compatibility<br>- Serialization! Boo![2]<br><br>So for an artifact like the JDK, it probably doesn't make sense to include. That tracks.<br>It won't make everyone happy, people like databind APIs, but it tracks.<br><br>So for the "read flow" these are the things to figure out.<br><br>                | Should Provide? | Intended User(s) |<br>----------------+-----------------+------------------+<br> Streaming Push |                 |                  |<br>----------------+-----------------+------------------+<br> Streaming Pull |                 |                  |<br>----------------+-----------------+------------------+<br> Realized Tree  |                 |                  |<br>----------------+-----------------+------------------+<br> Lazy Tree      |                 |                  |<br>----------------+-----------------+------------------+<br><br>At which point, we should talk about what "meets needs of Java developers using JSON" implies.<br><br>JSON is ubiquitous. Most kinds of software us schmucks write could have a reason to interact with it.<br>The full set of "user personas" therefore aren't practical for me to talk about.[3]<br><br>JSON documents, however, are not so varied.<br><br>- There are small ones (1-10kb)<br>- There are medium ones (10-1000kb)<br>- There are big ones (1000kb-???)<br><br>- There are shallow ones<br>- There are deep ones<br><br>So that feels like an easier direction to talk about it from.<br><br><br>This repo[4] has some convenient toy examples of how some of those APIs look in libraries<br>in the ecosystem. Specifically the Streaming Pull and Realized Tree models.<br><br>        User r = new User();<br>        while (true) {<br>            JsonToken token = reader.peek();<br>            switch (token) {<br>                case BEGIN_OBJECT:<br>                    reader.beginObject();<br>                    break;<br>                case END_OBJECT:<br>                    reader.endObject();<br>                    return r;<br>                case NAME:<br>                    String fieldname = reader.nextName();<br>                    switch (fieldname) {<br>                        case "id":<br>                            r.setId(reader.nextString());<br>                            break;<br>                        case "index":<br>                            r.setIndex(reader.nextInt());<br>                            break;<br>                        ...<br>                        case "friends":<br>                            r.setFriends(new ArrayList<>());<br>                            Friend f = null;<br>                            carryOn = true;<br>                            while (carryOn) {<br>                                token = reader.peek();<br>                                switch (token) {<br>                                    case BEGIN_ARRAY:<br>                                        reader.beginArray();<br>                                        break;<br>                                    case END_ARRAY:<br>                                        reader.endArray();<br>                                        carryOn = false;<br>                                        break;<br>                                    case BEGIN_OBJECT:<br>                                        reader.beginObject();<br>                                        f = new Friend();<br>                                        break;<br>                                    case END_OBJECT:<br>                                        reader.endObject();<br>                                        r.getFriends().add(f);<br>                                        break;<br>                                    case NAME:<br>                                        String fn = reader.nextName();<br>                                        switch (fn) {<br>                                            case "id":<br>                                                f.setId(reader.nextString());<br>                                                break;<br>                                            case "name":<br>                                                f.setName(reader.nextString());<br>                                                break;<br>                                        }<br>                                        break;<br>                                }<br>                            }<br>                            break;<br>                    }<br>            }<br><br>I think its not hard to argue that the streaming apis are brutalist. The above is Gson, but Jackson, moshi, etc<br>seem at least morally equivalent.<br><br>Its hard to write, hard to write *correctly*, and theres is a curious protensity towards pairing it<br>with anemic, mutable models.<br><br>That being said, it handles big documents and deep documents really well. It also performs<br>pretty darn well and is good enough as a "fallback" when the intended user experience <br>is through something like databind.<br><br>So what could we do meaningfully better with the language we have today/will have tommorow?<br><br>- Sealed interfaces + Pattern matching could give a nicer model for tokens<br><br>        sealed interface JsonToken {<br>            record Field(String name) implements JsonToken {}<br>            record BeginArray() implements JsonToken {}<br>            record EndArray() implements JsonToken {}<br>            record BeginObject() implements JsonToken {}<br>            record EndObject() implements JsonToken {}<br>            // ...<br>        }<br><br>        // ...<br><br>        User r = new User();<br>        while (true) {<br>            JsonToken token = reader.peek();<br>            switch (token) {<br>                case BeginObject __:<br>                    reader.beginObject();<br>                    break;<br>                case EndObject __:<br>                    reader.endObject();<br>                    return r;<br>                case Field("id"):<br>                    r.setId(reader.nextString());<br>                    break;<br>                case Field("index"):<br>                    r.setIndex(reader.nextInt());<br>                    break;<br><br>                // ...<br><br>                case Field("friends"):<br>                    r.setFriends(new ArrayList<>());<br>                    Friend f = null;<br>                    carryOn = true;<br>                    while (carryOn) {<br>                        token = reader.peek();<br>                        switch (token) {<br>                // ...<br><br>- Value classes can make it all more efficient<br><br>        sealed interface JsonToken {<br>            value record Field(String name) implements JsonToken {}<br>            value record BeginArray() implements JsonToken {}<br>            value record EndArray() implements JsonToken {}<br>            value record BeginObject() implements JsonToken {}<br>            value record EndObject() implements JsonToken {}<br>            // ...<br>        }<br><br>- (Fun One) We can transform a simpler-to-write push parser into a pull parser with Coroutines<br><br>    This is just a toy we could play with while making something in the JDK. I'm pretty sure<br>    we could make a parser which feeds into something like<br><br>        interface Listener {<br>            void onObjectStart();<br>            void onObjectEnd();<br>            void onArrayStart();<br>            void onArrayEnd();<br>            void onField(String name);<br>            // ...<br>        }<br><br>    and invert a loop like<br><br>        while (true) {<br>            char c = next();<br>            switch (c) {<br>                case '{':<br>                    listener.onObjectStart();<br>                    // ...<br>                // ...<br>            }<br>        }<br><br>    by putting a Coroutine.yield in the callback.<br><br>    That might be a meaningful simplification in code structure, I don't know enough to say.<br><br>But, I think there are some hard questions like<br><br>- Is the intent[5] to be make backing parser for ecosystem databind apis?<br>- Is the intent that users who want to handle big/deep documents fall back to this?<br>- Are those new language features / conveniences enough to offset the cost of committing to a new api?<br>- To whom exactly does a low level api provide value?<br>- What benefit is standardization in the JDK?<br><br>and just generally - who would be the consumer(s) of this?<br><br>The other kind of API still on the table is a Tree. There are two ways to handle this<br><br>1. Load it into `Object`. Use a bunch of instanceof checks/casts to confirm what it actually is.<br><br>        Object v;<br>        User u = new User();<br><br>        if ((v = jso.get("id")) != null) {<br>            u.setId((String) v);<br>        }<br>        if ((v = jso.get("index")) != null) {<br>            u.setIndex(((Long) v).intValue());<br>        }<br>        if ((v = jso.get("guid")) != null) {<br>            u.setGuid((String) v);<br>        }<br>        if ((v = jso.get("isActive")) != null) {<br>            u.setIsActive(((Boolean) v));<br>        }<br>        if ((v = jso.get("balance")) != null) {<br>            u.setBalance((String) v);<br>        }<br>        // ...<br>        if ((v = jso.get("latitude")) != null) {<br>            u.setLatitude(v instanceof BigDecimal ? ((BigDecimal) v).doubleValue() : (Double) v);<br>        }<br>        if ((v = jso.get("longitude")) != null) {<br>            u.setLongitude(v instanceof BigDecimal ? ((BigDecimal) v).doubleValue() : (Double) v);<br>        }<br>        if ((v = jso.get("greeting")) != null) {<br>            u.setGreeting((String) v);<br>        }<br>        if ((v = jso.get("favoriteFruit")) != null) {<br>            u.setFavoriteFruit((String) v);<br>        }<br>        if ((v = jso.get("tags")) != null) {<br>            List<Object> jsonarr = (List<Object>) v;<br>            u.setTags(new ArrayList<>());<br>            for (Object vi : jsonarr) {<br>                u.getTags().add((String) vi);<br>            }<br>        }<br>        if ((v = jso.get("friends")) != null) {<br>            List<Object> jsonarr = (List<Object>) v;<br>            u.setFriends(new ArrayList<>());<br>            for (Object vi : jsonarr) {<br>                Map<String, Object> jso0 = (Map<String, Object>) vi;<br>                Friend f = new Friend();<br>                f.setId((String) jso0.get("id"));<br>                f.setName((String) jso0.get("name"));<br>                u.getFriends().add(f);<br>            }<br>        }<br><br>2. Have an explicit model for Json, and helper methods that do said casts[6]<br><br><br>                   this.setSiteSetting(readFromJson(jsonObject.getJsonObject("site")));<br>                        JsonArray groups = jsonObject.getJsonArray("group");<br>                        if(groups != null)<br>                    {<br>                             int len = groups.size();<br>                              for(int i=0; i<len; i++)<br>                           {<br>                                     JsonObject grp = groups.getJsonObject(i);<br>                                     SNMPSetting grpSetting = readFromJson(grp);<br>                                   String grpName = grp.getString("dbgroup", null);<br>                                    if(grpName != null && grpSetting != null)<br>                                             this.groupSettings.put(grpName, grpSetting);<br>                          }<br>                     }<br>                     JsonArray hosts = jsonObject.getJsonArray("host");<br>                  if(hosts != null)<br>                     {<br>                             int len = hosts.size();<br>                               for(int i=0; i<len; i++)<br>                           {<br>                                     JsonObject host = hosts.getJsonObject(i);<br>                                     SNMPSetting hostSetting = readFromJson(host);<br>                                 String hostName = host.getString("dbhost", null);<br>                                   if(hostName != null && hostSetting != null)<br>                                           this.hostSettings.put(hostName, hostSetting);<br>                         }<br>                     }<br><br>I think what has become easier to represent in the language nowadays is that explicit model for Json.<br>Its the 101 lesson of sealed interfaces.[7] It feels nice and clean.<br><br>        sealed interface Json {<br>            final class Null implements Json {}<br>            final class True implements Json {}<br>            final class False implements Json {}<br>            final class Array implements Json {}<br>            final class Object implements Json {}<br>            final class String implements Json {}<br>            final class Number implements Json {}<br>        }<br><br>And the cast-and-check approach is now more viable on account of pattern matching.<br><br>        if (jso.get("id") instanceof String v) {<br>            u.setId(v);<br>        }<br>        if (jso.get("index") instanceof Long v) {<br>            u.setIndex(v.intValue());<br>        }<br>        if (jso.get("guid") instanceof String v) {<br>            u.setGuid(v);<br>        }<br><br>        // or <br><br>        if (jso.get("id") instanceof String id &&<br>                jso.get("index") instanceof Long index &&<br>                jso.get("guid") instanceof String guid) {<br>            return new User(id, index, guid, ...); // look ma, no setters!<br>        }<br>        <br><br>And on the horizon, again, is value types.<br><br>But there are problems with this approach beyond the performance implications of loading into<br>a tree.<br><br>For one, all the code samples above have different behaviors around null keys and missing keys<br>that are not obvious from first glance.<br><br>This won't accept any null or missing fields<br><br>        if (jso.get("id") instanceof String id &&<br>                jso.get("index") instanceof Long index &&<br>                jso.get("guid") instanceof String guid) {<br>            return new User(id, index, guid, ...);<br>        }<br>        <br>This will accept individual null or missing fields, but also will silently ignore <br>fields with incorrect types<br><br>        if (jso.get("id") instanceof String v) {<br>            u.setId(v);<br>        }<br>        if (jso.get("index") instanceof Long v) {<br>            u.setIndex(v.intValue());<br>        }<br>        if (jso.get("guid") instanceof String v) {<br>            u.setGuid(v);<br>        }<br><br>And, compared to databind where there is information about the expected structure of the document<br>and its the job of the framework to assert that, I posit that the errors that would be encountered<br>when writing code against this would be more like <br><br>    "something wrong with user" <br><br>than <br>    <br>    "problem at users[5].name, expected string or null. got 5"<br><br>Which feels unideal.<br><br><br>One approach I find promising is something close to what Elm does with its decoders[8]. Not just combining assertion<br>and binding like what pattern matching with records allows, but including a scheme for bubbling/nesting errors.<br><br>    static String string(Json json) throws JsonDecodingException {<br>        if (!(json instanceof Json.String jsonString)) {<br>            throw JsonDecodingException.of(<br>                    "expected a string",<br>                    json<br>            );<br>        } else {<br>            return jsonString.value();<br>        }<br>    }<br><br>    static <T> T field(Json json, String fieldName, Decoder<? extends T> valueDecoder) throws JsonDecodingException {<br>        var jsonObject = object(json);<br>        var value = jsonObject.get(fieldName);<br>        if (value == null) {<br>            throw JsonDecodingException.atField(<br>                    fieldName,<br>                    JsonDecodingException.of(<br>                            "no value for field",<br>                            json<br>                    )<br>            );<br>        }<br>        else {<br>            try {<br>                return valueDecoder.decode(value);<br>            } catch (JsonDecodingException e) {<br>                throw JsonDecodingException.atField(<br>                        fieldName,<br>                        e<br>                );<br>            }  catch (Exception e) {<br>                throw JsonDecodingException.atField(fieldName, JsonDecodingException.of(e, value));<br>            }<br>        }<br>    }<br><br>Which I think has some benefits over the ways I've seen of working with trees.<br><br><br><br>- It is declarative enough that folks who prefer databind might be happy enough.<br><br>        static User fromJson(Json json) {<br>            return new User(<br>                Decoder.field(json, "id", Decoder::string),<br>                Decoder.field(json, "index", Decoder::long_),<br>                Decoder.field(json, "guid", Decoder::string),<br>            );<br>        }<br><br>        / ...<br><br>        List<User> users = Decoders.array(json, User::fromJson);<br><br>- Handling null and optional fields could be less easily conflated<br><br>    Decoder.field(json, "id", Decoder::string);<br><br>    Decoder.nullableField(json, "id", Decoder::string);<br><br>    Decoder.optionalField(json, "id", Decoder::string);<br><br>    Decoder.optionalNullableField(json, "id", Decoder::string);<br><br><br>- It composes well with user defined classes<br><br>    record Guid(String value) {<br>        Guid {<br>            // some assertions on the structure of value<br>        }<br>    }<br><br>    Decoder.string(json, "guid", guid -> new Guid(Decoder.string(guid)));<br><br>    // or even<br><br>    record Guid(String value) {<br>        Guid {<br>            // some assertions on the structure of value<br>        }<br><br>        static Guid fromJson(Json json) {<br>            return new Guid(Decoder.string(guid));<br>        }<br>    }<br><br>    Decoder.string(json, "guid", Guid::fromJson);<br><br><br>- When something goes wrong, the API can handle the fiddlyness of capturing information for feedback.<br><br>    In the code I've sketched out its just what field/index things went wrong at. Potentially<br>    capturing metadata like row/col numbers of the source would be sensible too.<br><br>    Its just not reasonable to expect devs to do extra work to get that and its really nice to give it.<br><br>There are also some downsides like<br><br>-  I do not know how compatible it would be with lazy trees.<br><br>     Lazy trees being the only way that a tree api could handle big or deep documents.<br>     The general concept as applied in libraries like json-tree[9] is to navigate without<br>     doing any work, and that clashes with wanting to instanceof check the info at the<br>     current path.<br><br>- It *almost* gives enough information to be a general schema approach<br><br>    If one field fails, that in the model throws an exception immediately. If an API should<br>    return "errors": [...], that is inconvenient to construct.<br><br>- None of the existing popular libraries are doing this<br><br>     The only mechanics that are strictly required to give this sort of API is lambdas. Those have<br>     been out for a decade. Yes sealed interfaces make the data model prettier but in concept you<br>     can build the same thing on top of anything.<br><br>     I could argue that this is because of "cultural momentum" of databind or some other reason,<br>     but the fact remains that it isn't a proven out approach.<br><br>     Writing Json libraries is a todo list[10]. There are a lot of bad ideas and this might be one of the,<br><br>- Performance impact of so many instanceof checks<br><br>    I've gotten a 4.2% slowdown compared to the "regular" tree code without the repeated casts.<br>    <br>    But that was with a parser that is 5x slower than Jacksons. (using the same benchmark project as for the snippets).<br>    I think there could be reason to believe that the JIT does well enough with repeated instanceof<br>    checks to consider it.<br><br><br>My current thinking is that - despite not solving for large or deep documents - starting with a really "dumb" realized tree api<br>might be the right place to start for the read side of a potential incubator module.<br><br>But regardless - this feels like a good time to start more concrete conversations. I fell I should cap this email since I've reached the point of decoherence and haven't even mentioned the write side of things<br><br><br><br><br>[1]: <a href="http://www.cowtowncoder.com/blog/archives/2009/01/entry_131.html">http://www.cowtowncoder.com/blog/archives/2009/01/entry_131.html</a><br>[2]: <a href="https://security.snyk.io/vuln/maven?search=jackson-databind">https://security.snyk.io/vuln/maven?search=jackson-databind</a><br>[3]: I only know like 8 people<br>[4]: <a href="https://github.com/fabienrenaud/java-json-benchmark/blob/master/src/main/java/com/github/fabienrenaud/jjb/stream/UsersStreamDeserializer.java">https://github.com/fabienrenaud/java-json-benchmark/blob/master/src/main/java/com/github/fabienrenaud/jjb/stream/UsersStreamDeserializer.java</a><br>[5]: When I say "intent", I do so knowing full well no one has been actively thinking of this for an entire Game of Thrones <br>[6]: <a href="https://github.com/yahoo/mysql_perf_analyzer/blob/master/myperf/src/main/java/com/yahoo/dba/perf/myperf/common/SNMPSettings.java">https://github.com/yahoo/mysql_perf_analyzer/blob/master/myperf/src/main/java/com/yahoo/dba/perf/myperf/common/SNMPSettings.java</a><br>[7]: <a href="https://www.infoq.com/articles/data-oriented-programming-java/">https://www.infoq.com/articles/data-oriented-programming-java/</a><br>[8]: <a href="https://package.elm-lang.org/packages/elm/json/latest/Json-Decode">https://package.elm-lang.org/packages/elm/json/latest/Json-Decode</a><br>[9]: <a href="https://github.com/jbee/json-tree">https://github.com/jbee/json-tree</a><br>[10]: <a href="https://stackoverflow.com/a/14442630/2948173">https://stackoverflow.com/a/14442630/2948173</a><br>[11]: In 30 days JEP-198 it will be recognizably PI days old for the 2nd time in its history.<br>[12]: To me, the fact that is still an open JEP is more a social convenience than anything. I could just as easily writing this exact same email about TOML. </font><br></div>