JEP 540: Simple JSON API (Incubator)

title
JEP 540: Simple JSON API (Incubator)
type
summary
summary
OpenJDK's incubating jdk.incubator.json β€” a deliberately minimal, strict, DOM-only JSON API
tags
java, json, api-design, openjdk
created
2026-07-29
updated
2026-07-29

JEP 540 adds a JSON parser and generator to the JDK as an incubator module, jdk.incubator.json. It supersedes JEP 198 (Light-Weight JSON API, 2014), which was written in different circumstances and took a different approach. The interesting part of the document is not the API surface, which is small by design, but the list of things the authors decided not to build and why.

The stated goal, and the one non-goal

The goal is that RFC 8259 documents can be processed with low ceremony, that navigation code reads as a de facto schema for a document that has no schema of its own, that unfamiliar documents can be explored quickly because errors fail fast with clear messages, that missing and unexpected values can be handled without the code falling over, and that the JDK itself gains the ability to parse and generate JSON.

The single non-goal: it is not a goal to create an API that supplants established external JSON libraries. Jackson, Gson, Jakarta JSON Processing and Binding, and Fastjson 2 are named up front and left in place. The JEP explicitly accepts that an application may start on this API and later migrate to a richer one, and says that outcome is not a failure.

The motivating example is averaging forecast temperatures from a U.S. National Weather Service REST response. The argument is that the equivalent Python or Golang code is short and the Java code should be too, without an external dependency and without the suspicion that another language would have been faster to write. This sits alongside the JDK's other low-ceremony work: collection factory methods, var, running programs straight from source files, and compact source files with instance main methods.

The second motivation is internal. The JDK cannot take external dependencies, so it has no JSON at all. Its configuration files use the property format, which cannot express structure, and so ends up with the numbered-key workaround:

security.provider.1=SUN
security.provider.2=SunRsaSign
security.provider.3=SunEC

which a built-in JSON parser would let become "providers": [ "SUN", "SunRsaSign", "SunEC" ].

Shape of the API

Everything hangs off the JsonValue interface, which is sealed with exactly six sub-interfaces mirroring JSON's four primitives and two structures: JsonString, JsonNumber, JsonBoolean, JsonNull, JsonObject, JsonArray. Sealing is what makes an exhaustive switch over a JSON value legal without a default clause, which is how the JEP expects variable-shaped documents to be handled.

The access methods get(String) and get(int) are declared on JsonValue itself rather than on JsonObject and JsonArray, so a chain of navigation never needs a downcast:

long tid = threadDump.get("threadContainers").get(0)
                     .get("threads").get(0).get("tid").asLong();

Conversion happens only at the end of the chain, through asString(), asInt(), asLong(), asDouble(), asBoolean(), asMap() and asList(). Wrong type or missing member throws JsonValueException, which is unchecked so that small programs and scripts stay readable. The exception message carries the path from the document root plus a line and position, which is what makes the design workable given that any navigation step can fail:

jdk.incubator.json.JsonValueException: JsonNumber is not a JsonBoolean. Path:
 "{threadDump{threadContainers[0{threads[0{tid". Location: line 13, position 19.

Two try* methods cover the two ways a document can disappoint you. tryGet(String) returns an Optional for a member that may not exist. tryValue() returns an empty Optional when the value is a JSON null, which is why there is no asNull() β€” JsonNull is handled by instanceof or by tryValue. Both come from real cases in the JDK's own JSON thread dumps: a thread object only has waitingOn when the thread is waiting, and the root thread container's parent is null rather than a string.

Document evolution gets the same treatment. Thread dumps in JDK 26 and earlier emit tid as a JSON string; JDK 27 emits it as a number. Code written against either breaks on the other, and the recommended fix is a type-pattern switch that accepts both.

Where the strictness is

Parsing is strict against RFC 8259 with no configuration knobs. Trailing commas and comments are out. So are objects with duplicate member names, unconditionally.

The duplicate-name decision gets the longest defence in the document, because the RFC only says names SHOULD be unique β€” wording retained in 2013 after an ECMAScript Discussion List thread worried that requiring uniqueness would invalidate existing documents. The JEP's reading is that an object with duplicate names is ambiguous, that libraries have historically disagreed about which value wins, and that the resulting unpredictability across a system built from several independently-developed JSON libraries produces hard-to-diagnose bugs, security holes and interop failures. It cites RFC 9413 ("Maintaining Robust Protocols") and bets that the documents people were protecting in 2013 have since been fixed.

Number handling is where "strict" gets specific. JSON numbers have arbitrary precision and range; asDouble() follows the RFC's interoperability advice and rounds to the nearest IEEE 754 double, throwing on out-of-range input. asInt() and asLong() require exact representability, but accept syntactic fractions whose value is integral:

int i1 = Json.parse("123.0").asInt();       // succeeds
int i2 = Json.parse("234.56E2").asInt();    // succeeds
int i3 = Json.parse("345.6").asInt();       // fails, not integral
int i4 = Json.parse("2147483648").asInt();  // fails, out of range

Lossless handling is available but not as a method. There is no asBigDecimal(); you write new BigDecimal(jn.toString()). The JEP's reasoning is that being trivial to implement is not on its own a reason to add a method, and that each convenience method is also one more opinionated decision baked into a set that is deliberately small and uniform across JsonValue.

What was cut

Data binding is the largest omission. The JEP calls it undeniably useful and rejects it on API footprint and maintenance cost, noting that Jackson and Jakarta both factor binding into separate modules, which is itself an admission that plenty of use cases do not need it. Streaming goes for the opposite reason: essential for narrow specialized cases, but it makes even simple extraction complicated.

Dropping both leaves a DOM-like tree, and the JEP is content with that. A related limit falls out of the same model: input has to fit in memory as a String or char[]. Files and network connections are not accepted as sources, because a tree-based parser reading an unbounded source runs out of memory.

Two alternatives were considered and rejected wholesale. Forking an external library into the JDK would create licensing and governance problems plus permanent tension over specification quality, compatibility and release schedules β€” the JEP cites past experience with the XML APIs. Doing nothing fails the low-ceremony goal, and leaves applications that would benefit from JSON avoiding it because a dependency carries cost and risk.

Conformance is to be tested against the JDK's own unit tests plus JSONTestSuite, the established edge-case corpus. The acknowledged risks are mostly social: the new API showing up inside applications that already use an external library, and the possibility that incoming pattern-matching language features change what the design should look like before it exits incubation.

Using it

The module is disabled by default, so --add-modules jdk.incubator.json is required at both compile time and run time, jshell included:

$ java --add-modules jdk.incubator.json Weather.java
WARNING: Using incubator modules: jdk.incubator.json
53.357142857142854

Notes

The split between JsonParseException and JsonValueException draws the same line as json-pass-value-accuracy-gap does for LLM output: a document can be syntactically perfect and still hand you a string where your code wanted a number. The JEP's answer is to make that failure loud, positioned and cheap to recover from β€” the type-pattern switch on tid is exactly the defensive shape that the structured-output-benchmark results argue is needed whenever the producer of a document is not under your control.