# JEP 540: Simple JSON API (Incubator)

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:

```java
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:

```java
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](https://github.com/nst/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.
