This library is a JSON Parser built specifically for JNATS to avoid a 3rd party library dependency.
IMPORTANT
Until the minor version reaches 1, the api and behavior is subject to change.
JDK Version
This project uses Java 8 Language Level api, but builds jars compiled with and targeted for Java 8, 17, 21 and 25.
It creates different artifacts for each. All have the same group id io.nats and the same version but have different artifact names.
Java Target Level
Artifact Id
Maven Central
1.8
jnats-json
17
jnats-json-jdk17
21
jnats-json-jdk21
25
jnats-json-jdk25
Dependency Management
The NATS client is available in the Maven central repository,
and can be imported as a standard dependency in your build.gradle or pom.xml file,
The examples shown use the Jdk 8 version. To use other versions, change the artifact id.
The library provides three JSON parser implementations with increasing levels of laziness.
All three share the same JsonParser.Option enum for configuration:
KEEP_NULLS – retain null values in maps (normally filtered out)
DECIMALS – enable decimal/floating-point number support (BigDecimal, Double, etc.)
By default, all parsers assume integers only, which is appropriate for NATS protocol messages
where numbers are always integers (sequence numbers, byte counts, timestamps in nanoseconds, etc.).
JsonParser / JsonValue / JsonValueUtils (Eager)
The original parser. Parses the entire JSON document eagerly – all strings are copied,
all numbers are parsed, and the full tree of HashMaps and ArrayLists is built during the
parse() call.
JsonValue v = JsonParser.parse(json);
String name = JsonValueUtils.readString(v, "name");
Best when you need every field in the document and want the simplest API.
JsonValue exposes all typed fields directly as public final fields (string, i, l, map, array, etc.).
Defers leaf materialization. During parsing, strings and numbers are stored as offset ranges
into the source char[] rather than being copied or parsed. The full tree structure (HashMaps,
ArrayLists) is still built eagerly. Data is only copied/parsed when accessor methods are called
(getString(), getInteger(), getLong(), etc.), and results are cached after first access.
IndexedJsonValue v = IndexedJsonParser.parse(json);
String name = IndexedJsonValueUtils.readString(v, "name");
In integers-only mode (the default), numbers are parsed directly from the char[] to long
with no intermediate String allocation.
Best as a general-purpose upgrade over the eager parser – faster across the board with no
behavioral surprises.
Defers everything. In addition to lazy leaf materialization, nested objects ({...}) and
arrays ([...]) are not parsed during the initial parse() call. Instead, a brace/bracket-counting
skip scan records their byte-range offsets. Children are parsed one level deep on demand when
getMap() or getArray() is first called.
LazyJsonValue v = LazyJsonParser.parse(json);
String name = LazyJsonValueUtils.readString(v, "name");
// The "cluster", "state", "sources" subtrees were never parsed
The initial parse() call shallow-parses only the outermost container. Nested containers
within that are unresolved offset ranges until accessed. This means parsing cost is proportional
to what you actually read, not the total document size.
Trade-offs vs the indexed parser:
Skipped regions are not validated until accessed. Malformed JSON inside a nested object you never
read will not produce an error.
On-demand resolution has a small per-access overhead. For small flat documents where you read
every field, the lazy parser can be marginally slower than the indexed parser.
Best when parsing large responses where you only access a subset of the top-level fields
(e.g., reading config from a StreamInfo response while ignoring cluster, state, sources).
Comparison
Benchmarks were run parsing real NATS JSON payloads (StreamInfo and ConsumerInfo) in both
prettified and compacted (no whitespace) form. Compacted JSON matches what the NATS server
actually sends on the wire. All numbers are operations per second. Ratios are relative to
the eager parser.
Minimal StreamInfo (flat config, no nested objects to skip):
Scenario
Eager
Indexed
Lazy
Parse only
pretty (338 chars)
763K
1,055K (1.38x)
1,295K (1.70x)
compact (250 chars)
810K
1,100K (1.36x)
1,516K (1.87x)
Name only
pretty
818K
983K (1.20x)
1,290K (1.58x)
compact
816K
1,043K (1.28x)
1,650K (2.02x)
All fields
pretty
710K
853K (1.20x)
750K (1.06x)
compact
717K
937K (1.31x)
876K (1.22x)
Minimal ConsumerInfo (flat config):
Scenario
Eager
Indexed
Lazy
Parse only
pretty (360 chars)
665K
951K (1.43x)
1,106K (1.66x)
compact (283 chars)
805K
1,104K (1.37x)
1,385K (1.72x)
Name only
pretty
763K
1,071K (1.40x)
1,151K (1.51x)
compact
769K
996K (1.30x)
1,371K (1.78x)
All fields
pretty
738K
875K (1.18x)
812K (1.10x)
compact
800K
937K (1.17x)
860K (1.07x)
Summary:
Compact JSON dramatically amplifies the lazy parser’s advantage. The skip scan
flies through dense compacted JSON. For StreamInfo parse-only: pretty=2.56x, compact=4.27x.
Even reading 100% of config fields: pretty=1.98x, compact=2.96x. This matters because
the NATS server always sends compact JSON.
Lazy dominates when nested structures go untouched. The sibling subtrees (cluster,
state, sources, etc.) are skipped entirely regardless of how many config fields you read.
Indexed is the reliable all-rounder at 1.2-1.5x faster than eager across the board.
It never underperforms and wins on small flat documents where every field is read.
For flat minimal JSON reading all fields, indexed edges out lazy (1.31x vs 1.22x compact)
because lazy’s on-demand resolution overhead exceeds skip savings when there is nothing to skip.
Both are well above baseline.
Integers-only mode (the default) provides a small but consistent additional speedup
over DECIMALS mode by skipping decimal-indicator scanning and using direct char[]-to-long
parsing with no String allocation.
Examples
The examples subproject demonstrates how to convert a plain POJO into a
JsonSerializable class for each parser implementation, including handling of nested
objects and lists of nested objects. See the examples README for
full details, code samples, and a field type mapping reference.
License
Unless otherwise noted, the NATS source files are distributed
under the Apache Version 2.0 license found in the LICENSE file.
JNATS JSON
This library is a JSON Parser built specifically for JNATS to avoid a 3rd party library dependency.
IMPORTANT
Until the minor version reaches 1, the api and behavior is subject to change.
JDK Version
This project uses Java 8 Language Level api, but builds jars compiled with and targeted for Java 8, 17, 21 and 25. It creates different artifacts for each. All have the same group id
io.natsand the same version but have different artifact names.jnats-jsonjnats-json-jdk17jnats-json-jdk21jnats-json-jdk25Dependency Management
The NATS client is available in the Maven central repository, and can be imported as a standard dependency in your
build.gradleorpom.xmlfile, The examples shown use the Jdk 8 version. To use other versions, change the artifact id.Gradle
If you need the latest and greatest before Maven central updates, you can use:
If you need a snapshot version, you must add the url for the snapshots and change your dependency.
Maven
If you need the absolute latest, before it propagates to maven central, you can use the repository:
If you need a snapshot version, you must enable snapshots and change your dependency.
Implementations
The library provides three JSON parser implementations with increasing levels of laziness. All three share the same
JsonParser.Optionenum for configuration:KEEP_NULLS– retain null values in maps (normally filtered out)DECIMALS– enable decimal/floating-point number support (BigDecimal, Double, etc.)By default, all parsers assume integers only, which is appropriate for NATS protocol messages where numbers are always integers (sequence numbers, byte counts, timestamps in nanoseconds, etc.).
JsonParser / JsonValue / JsonValueUtils (Eager)
The original parser. Parses the entire JSON document eagerly – all strings are copied, all numbers are parsed, and the full tree of HashMaps and ArrayLists is built during the
parse()call.Best when you need every field in the document and want the simplest API.
JsonValueexposes all typed fields directly as public final fields (string,i,l,map,array, etc.).IndexedJsonParser / IndexedJsonValue / IndexedJsonValueUtils (Indexed)
Defers leaf materialization. During parsing, strings and numbers are stored as offset ranges into the source
char[]rather than being copied or parsed. The full tree structure (HashMaps, ArrayLists) is still built eagerly. Data is only copied/parsed when accessor methods are called (getString(),getInteger(),getLong(), etc.), and results are cached after first access.In integers-only mode (the default), numbers are parsed directly from the
char[]tolongwith no intermediateStringallocation.Best as a general-purpose upgrade over the eager parser – faster across the board with no behavioral surprises.
LazyJsonParser / LazyJsonValue / LazyJsonValueUtils (Lazy)
Defers everything. In addition to lazy leaf materialization, nested objects (
{...}) and arrays ([...]) are not parsed during the initialparse()call. Instead, a brace/bracket-counting skip scan records their byte-range offsets. Children are parsed one level deep on demand whengetMap()orgetArray()is first called.The initial
parse()call shallow-parses only the outermost container. Nested containers within that are unresolved offset ranges until accessed. This means parsing cost is proportional to what you actually read, not the total document size.Trade-offs vs the indexed parser:
Best when parsing large responses where you only access a subset of the top-level fields (e.g., reading
configfrom a StreamInfo response while ignoringcluster,state,sources).Comparison
Benchmarks were run parsing real NATS JSON payloads (StreamInfo and ConsumerInfo) in both prettified and compacted (no whitespace) form. Compacted JSON matches what the NATS server actually sends on the wire. All numbers are operations per second. Ratios are relative to the eager parser.
StreamInfo (has nested
cluster,state,mirror,sources,alternates):ConsumerInfo (has nested
cluster,delivered,ack_floor):Minimal StreamInfo (flat config, no nested objects to skip):
Minimal ConsumerInfo (flat config):
Summary:
cluster,state,sources, etc.) are skipped entirely regardless of how many config fields you read.char[]-to-longparsing with noStringallocation.Examples
The examples subproject demonstrates how to convert a plain POJO into a
JsonSerializableclass for each parser implementation, including handling of nested objects and lists of nested objects. See the examples README for full details, code samples, and a field type mapping reference.License
Unless otherwise noted, the NATS source files are distributed under the Apache Version 2.0 license found in the LICENSE file.