Skip to main content

Release 10.0.0: Marshall, commons and config

This page is part of the Release 10.0.0 release notes. The overview page lists the major changes, the breaking changes and the API changes for the whole release.

juneau-marshall / juneau-commons​

View-based property projection (@MarshalledProp(view=...))​

Juneau now supports view-based projection: declare named views on bean properties and select an active view per serialize/parse call so only the in-view properties are emitted or consumed. This is the Juneau equivalent of Jackson's @JsonView / MapperFeature.DEFAULT_VIEW_INCLUSION.

Tag a property with one or more string view names via the new view member on @MarshalledProp:

public class Person {
public String id; // untagged — visible in every view

@MarshalledProp(view = "summary")
public String name;

@MarshalledProp(view = "detail")
public String description;
}

Activate a view on any serializer or parser builder, or override it per call on a session:

// Context-level default:
var s = Json5Serializer.DEFAULT.copy().activeView("summary").build();
s.write(person); // → {id:'1',name:'Alice'}

// Per-call session override:
s.createSession().activeView("detail").build().write(person); // → {description:'A person',id:'1'}

Key behaviours:

  • Default-view policy: untagged properties are included in all views by default (mirrors Jackson's DEFAULT_VIEW_INCLUSION=true). Call disableDefaultViewInclusion() to reverse.
  • Union semantics: @MarshalledProp(view = {"A","B"}) makes a property visible when A or B is active.
  • Flat string names: no marker-class hierarchy required.
  • Parse-side: out-of-view input properties are routed through the existing unknown/ignored-property mechanism (silently ignored by default; ignoreUnknownBeanProperties(false) makes them throw).
  • Mix-in support: view membership can be applied to unmodifiable classes via @MarshalledPropApply / applyAnnotations(), just like any other @MarshalledProp attribute.
  • Cross-format: projection is model-level and behaves identically across JSON, XML, HTML, MsgPack, and all other Juneau formats.

See the View-Based Projection topic page for full documentation and a Jackson migration table.

Hibernate lazy-proxy unwrap + JPA cookbook​

Marshalling now deproxifies objects whose runtime class ClassUtils.getProxyFor recognizes, including Hibernate 6/7 lazy association proxies ($HibernateProxy$ in the class name). Serializers use the unwrapped class (immediate superclass for subclass proxies; first interface for JDK proxies) so handler fields and proxy _type names are not emitted. This is a core getClassMetaForObject change across all formats — not a juneau-marshall-hibernate module, and not a parse-into-@Entity feature.

See JPA and Hibernate.

juneau-config / juneau-commons​

Config profiles + relaxed binding (cloud-native environment config)​

Juneau 10.0 adds two Spring-Boot-style ergonomics to the config + settings system, both additive and off-by-default so existing configs are unaffected:

  • Config profiles — a <name>-<profile> overlay (e.g. my-stage.cfg, or my-stage.yml for YAML bases) is merged over the base config when a profile is active. Profile entries win over base, base-only keys survive, and for multiple active profiles the last one wins. Activated via Config.Builder.profiles(...) or the juneau.profiles.active key (system property / env var / config). Resolution happens at the store layer (ProfileConfigStore + ProfileMerge), so variable resolution, format handling, and live reload (a change to the base or any active profile file re-fires change listeners) all behave exactly as for a single-file config. When the Spring Boot bridge is present, juneau.profiles.active piggybacks on Environment.getActiveProfiles() for a single source of truth.
  • Relaxed binding — a single logical config key resolves from any common environment spelling, so k8s-style env vars map cleanly: my.prop / myProp / MySection/myKey are satisfied by MY_PROP / MY_SECTION_MY_KEY. The default Settings env source is wrapped with RelaxedPropertySource (verbatim-first, so exact-match is preserved); it can wrap any other PropertySource too.

See the new Config Profiles & Relaxed Binding topic page for the activation conventions, overlay/merge precedence, and the candidate-generation rules.

juneau-marshall​

Token-Streaming and Record-Streaming API​

Juneau 10.0 introduces a public low-level streaming surface that complements the POJO databind path with cursor-based push and pull primitives, layered:

  • RecordReader / RecordWriter — whole-value record cursor (canRead() / read(Class) / write(Object)). Format-neutral, works on every Juneau format.
  • TokenReader / TokenWriter — fine-grained structural cursor (next(), startObject(), fieldName(...), etc.). For formats whose wire encoding is already token-shaped: JSON, JSON5, JSONL, JSON5L, CBOR, MsgPack.

TokenReader extends RecordReader and TokenWriter extends RecordWriter, so token-capable formats inherit record-stream semantics for free; record-only formats expose only the record surface so the type system enforces "no nested structure visible at the cursor."

Capability is expressed through role interfaces in org.apache.juneau.marshall.stream and detected with instanceof (there is no capability enum):

  • RecordReadable / RecordWritable — readRecords(input) / writeRecords(output); implemented by every streaming format.
  • TokenReadable / TokenWritable — readTokens(input) / writeTokens(output); implemented by json, json5, jsonl, json5l, cbor, msgpack. (These extend the record roles.)
  • ArrayRecordReadable / ArrayRecordWritable — readArrayRecords(input) / writeArrayRecords(output); implemented by json, json5, cbor, msgpack, bson, parquet, yaml, xml, html.

A cursor-quality signal (isRecordStreaming() / isArrayRecordStreaming()) distinguishes a true O(1) streaming cursor from a buffered one. Writer-only formats (jsonschema, jcs, soap-xml) implement only the *Writable roles.

Naturally-multi-record formats (jsonl, json5l, csv, sse) expose readRecords/writeRecords as a true multi-record cursor: N read calls yield N records; N write calls produce wire output equivalent to serialize(List.of(records)).

Opt-in readArrayRecords / writeArrayRecords for streaming the elements of a top-level wire array on JSON / JSON5 / CBOR (actually streaming, O(1) memory in array length), plus MsgPack / BSON / Parquet / YAML / XML / HTML (buffered). MsgPack offers a count-overload writeArrayRecords(output, expectedCount) that writes the length-prefix header up front and serializes each element directly to the output stream — no per-element buffering. Mismatch between declared count and actual write count throws on close.

XML and HTML expose readArrayRecords(input, rootElementName) for caller-specified array root elements.

REST integration​

TokenReader, RecordReader, TokenWriter, RecordWriter and concrete subtypes (e.g. JsonlTokenReader) are first-class @RestOp-method parameter types. Two-layer resolution: declared parameter type filters eligible parsers/serializers; HTTP Content-Type / Accept negotiates within that filter. Mismatch returns HTTP 415. See the new REST Streaming Integration topic page.

Documentation​

A full topic-page family lives under "2.50. Token / Record Streaming" in the Marshalling section: Basics, Record Streaming, Token Streaming, Array-Record Streaming, and REST Streaming Integration.

The streaming surface is purely structural — object swaps and @Schema annotations apply only on the POJO databind path, never at the token layer. Both surfaces are additive; the existing POJO ↔ document API remains the primary, recommended path.

JSON5L line-delimited format​

Juneau 10.0 adds JSON5L — a new line-delimited format combining the relaxed JSON5 dialect with JSONL's one-document-per-line framing. It ships as a new org.apache.juneau.marshall.json5l package (Json5lSerializer / Json5lParser / sessions / Json5lTokenReader), a Json5l marshaller, and a @Json5lConfig annotation.

  • Strict by default, sugar opt-in — the serializer emits strict RFC-8259 JSON per line by default (byte-identical to JsonlSerializer), keeping output maximally portable. Json5lSerializer.create().json5Sugar() switches the per-line output to JSON5 sugar (single-quoted strings, unquoted field names where safe).
  • Full JSON5 parse leniency — the parser accepts the full JSON5 dialect on each line (comments, unquoted/single-quoted keys, trailing commas, relaxed numbers). Because JSON5 is a strict superset of JSON, it also reads plain JSONL input unchanged. Blank and comment-only lines are skipped; inline comments are tolerated; /* ... */ block comments must open and close on the same line.
  • Token-streaming and array-record streaming parity with JSONL (O(1) memory line cursor).
  • First-class REST media type application/json5l (text/json5l, plus reduced-q-value cross-acceptance of the JSONL family), registered in BasicUniversalConfig and the RestClient.universal() set.
  • Media-type aliases — application/jsonlines (the type suggested by the JSON Lines spec) is accepted as an alias of application/jsonl, and application/json5lines as an alias of application/json5l, for both Accept and Content-Type negotiation. Produced responses keep the canonical application/jsonl / application/json5l Content-Type. ContentType.APPLICATION_JSONLINES and ContentType.APPLICATION_JSON5LINES constants are added.

See the new JSON5L Basics topic page.

Inclusion knob: nonDefault() on Serializer.Builder​

Juneau 10.0 adds a new serializer-side inclusion knob to suppress properties whose value equals the type's default — the Juneau equivalent of Jackson's @JsonInclude(NON_DEFAULT). It lives alongside the existing keepNullProperties / trimEmptyCollections / trimEmptyMaps / trimStrings family and applies uniformly across every format (no per-format opt-in).

public class Person {
public String name = "Anonymous";
public int age = 0;
}

var s = JsonSerializer.create().nonDefault().build();
s.write(new Person()); // → {}

Default values are resolved as:

  • For primitives / boxed wrappers — the Java type default (0, false, null, …).
  • For bean properties — the value held on a freshly-constructed instance via the bean's no-arg constructor. The reference instance is built once and cached on ClassMeta. If the bean cannot be instantiated, the knob silently skips that bean rather than aborting the serialization.

Equality uses value-based numeric comparison (so 1 ≡ 1.0 ≡ 1L); everything else uses Objects.equals.

The matching annotation setting is @SerializerConfig(nonDefault = "true").

Precedence (first "omit" wins): keepNullProperties (handles null and Optional.empty()) → trimStrings → trimEmptyCollections / trimEmptyMaps → nonDefault.

Parser-side null coercion: Nulls policy​

A new org.apache.juneau.marshall.Nulls enum + matching nulls setting on Parser.Builder / @ParserConfig / @MarshalledProp controls what happens when a null reaches a bean property during parsing — the Juneau equivalent of Jackson's @JsonSetter(nulls = …). Four modes:

  • LEAVE — pass null through to the setter (current behavior, default).
  • EMPTY — substitute "", an empty mutable Collection / Map, or Optional.empty().
  • DEFAULT — substitute the bean's reference-instance value for that property.
  • SKIP — do not invoke the setter at all; any pre-existing field initializer is preserved.

Per-property setting wins over session, which wins over context default:

public class Bean {
@MarshalledProp(nulls = Nulls.EMPTY)
public String s;

@MarshalledProp(nulls = Nulls.SKIP)
public String t = "initial";

public String u; // falls through to context default
}

var p = JsonParser.create().nulls(Nulls.LEAVE).build();
var b = p.read("{\"s\":null,\"t\":null,\"u\":null}", Bean.class);
// b.s == "" b.t == "initial" b.u == null

See the new Null & Inclusion Policies topic page for the full contract, including a Jackson migration table.

First-class JDK datatype coverage (OptionalInt / OptionalLong / OptionalDouble / BitSet)​

Juneau 10.0 extends its direct, in-box JDK datatype coverage with first-class handling for the primitive Optional variants — OptionalInt, OptionalLong, OptionalDouble — and for BitSet.

The primitive Optional variants are handled directly on the serializer/parser path, mirroring the existing Optional<T> plumbing rather than going through a swap. They share the same wire contract as Optional<T>: empty maps to null (omitted by default, emitted as null when keepNullProperties() is set), present emits the unwrapped primitive, and parse-side null/absent both resolve back to the matching empty() sentinel — never a bare null inside the Optional.

BitSet is likewise a first-class type with its own BitSetFormat enum — no swap required. The new bitSetFormat knob selects the textual wire form (INDICES, the default comma-delimited set-bit-index list; BITS, a little-endian bit string; or HEX, a hex-encoded byte array) and is configurable at all four precedence levels (@MarshalledProp, @Marshalled, @MarshalledConfig, and the bitSetFormat(...) builder setter), exactly like the other *Format enums.

A new Supported JDK Datatypes topic page enumerates the full coverage matrix and contrasts Juneau's first-class-in-box approach with Jackson's per-module jackson-datatype-* family.

MarshalledNode typed tree façade + RFC 6901 JSON-Pointer addressing​

Juneau 10.0 adds a typed tree façade and an RFC 6901 JSON-Pointer surface over the existing MarshalledMap/MarshalledList collections model (in org.apache.juneau.marshall.collections):

  • MarshalledNode — a public, live typed-tree façade (not a parallel Jackson-style node hierarchy) wrapping a backing Map/List/scalar/null. Provides node-type introspection (isObject/isArray/isValue/isNull), typed accessors (asString/asInt/asLong/asDouble/asBoolean/as(Class)/value), navigation (get(String)/get(int)/size), fluent in-place builders (put/add), a deep-copy copy() snapshot, and the RFC 6901 pointer methods at/find/set/remove. Created via MarshalledNode.of(Object), MarshalledNode.objectNode(), and MarshalledNode.arrayNode().
  • JsonPointer — a public RFC 6901 JSON-Pointer helper: JsonPointer.of(String), eval(root) (read, null-miss), set(root, value) (auto-vivifying write; - appends), remove(root), and static encodeToken/decodeToken (~0→~, ~1→/). The empty string "" addresses the whole document; a non-empty pointer must begin with /.
  • Convenience — MarshalledMap.at(String) and MarshalledList.at(String) resolve a pointer and return a MarshalledNode.
JsonMap doc = Json.to("{foo:{bar:['x','y']}}", JsonMap.class);

String y = doc.at("/foo/bar/1").asString(); // "y"
MarshalledNode.of(doc).set("/foo/bar/-", "z"); // appends -> ['x','y','z']

Both MarshalledNode and JsonPointer are marked Beta — API subject to change. See the new Tree Model & RFC 6901 JSON-Pointer topic page.

Protobuf binary wire-format codec​

Juneau 10.0 adds a native, bean-driven Protocol Buffers binary serializer and parser in the new org.apache.juneau.marshall.protobuf package — distinct from the existing text-format marshall.prototext codec (text/protobuf). It emits the compact, non-self-describing protobuf binary wire format used by protoc-generated code, with no .proto schema file or code generation required: the field-number/type schema is derived directly from Juneau bean metadata.

public class Person {
public String name;
public int age;
public Person() {}
public Person(String name, int age) { this.name = name; this.age = age; }
}

byte[] protobuf = Protobuf.of(new Person("Alice", 30));
Person parsed = Protobuf.to(protobuf, Person.class); // target type required on read

It is interop-capable but bean-driven: lossless out of the box for Juneau-to-Juneau messaging, and upgradable field-by-field to true external protoc interop via the new @Protobuf(fieldNumber=…, type=…) annotation.

  • Media types: produces application/protobuf; accepts application/protobuf and application/x-protobuf (no collision with text text/protobuf).
  • Field numbers: explicit @Protobuf(fieldNumber=N) wins; otherwise auto-assigned sequentially from 1 ordered alphabetically by property name, skipping the reserved 19000–19999 band.
  • Scalar mapping: sensible Java→proto defaults (int→int32, long→int64, String→string, enum→int32 ordinal, byte[]→bytes, dates/BigInteger→lossless string), each overridable via @Protobuf(type=…) (zigzag sint*, fixed*/sfixed*, uint*, enum-by-name).
  • Containers: packed repeated scalars, repeated tagged strings/bytes/messages, length-delimited nested messages, and Map<K,V> as proto map<> entry sub-messages. Parsing accepts both packed and unpacked repeated forms.
  • Deferred (assess-only): oneof, full proto2 presence, unknown-field retention, and well-known types (Timestamp/Duration/Any/wrappers).

See the new Protobuf Binary Format Basics topic page for the full scalar/container mapping tables, field-number rules, and round-trip guarantees.

Parquet external-read interop + binary-native logical types​

Juneau 10.0 extends the Parquet codec to read spec-compliant files produced by external writers (parquet-mr / Spark / Hive), not just files Juneau itself emits, and to understand the binary-native logical types those tools use.

  • External-read fidelity — the reader now handles multi-page column chunks, multi-row-group files, and dictionary-encoded pages (DICTIONARY_PAGE + PLAIN_DICTIONARY/RLE_DICTIONARY, the parquet-mr default). Previously only the single-page / single-row-group / PLAIN shape Juneau wrote was readable.
  • Snappy decompression — a built-in, dependency-free Snappy block decoder reads Snappy-compressed pages (the default Spark/parquet-mr compression). Decode-only; write stays UNCOMPRESSED/GZIP. LZ4 / Zstd / Brotli / LZO remain hard errors (no silent fallback).
  • Binary-native logical types — INT96 timestamps, DECIMAL (INT32/INT64-backed, scale-aware), DATE, TIME(micros), and TIMESTAMP(micros) are always decoded on read. On write they are opt-in via ParquetSerializer.create().nativeLogicalTypes(true); the default keeps the existing portable string / TIMESTAMP_MILLIS normalization, so default output is byte-for-byte unchanged.
  • Write knobs — pageSize and rowGroupSize are now honored on write, splitting output into multiple pages / row groups.

See the Parquet Basics topic page for details.

Stream-based static marshaller shortcuts​

Every marshaller facade now provides stream-based static shortcuts alongside the existing String / byte[] shortcuts, so you can parse from and serialize to streams without going through a DEFAULT instance:

  • Character facades (Json, Xml, Html, Yaml, Ini, Hjson, …) add to(Reader, Class<T>), to(Reader, Type, Type...), and of(Object, Writer).
  • Binary facades (MsgPack, Cbor, Bson, Protobuf, Parquet) add to(InputStream, Class<T>), to(InputStream, Type, Type...), and of(Object, OutputStream).
MyPojo a = Json.to(reader, MyPojo.class);
Json.of(myPojo, writer);

MyPojo b = MsgPack.to(inputStream, MyPojo.class);
MsgPack.of(myPojo, outputStream);

Unlike the instance read(...) / write(...) methods (which throw a checked IOException), these static shortcuts wrap any underlying IOException into an unchecked ParseException (read) or SerializeException (serialize), so they compose cleanly in lambdas and streams. Consistent with the 10.0.0 stream-only I/O narrowing, there are intentionally no File overloads — open a Reader/Writer/InputStream/OutputStream yourself (e.g. via Files.newBufferedReader(...)).

See the Marshallers topic page for details.

Variant marshaller classes (Json5R / IniR / HjsonC)​

Three commonly-used non-default marshaller configurations are now first-class variant facade classes, each a subclass of its base format facade with its own DEFAULT instance and the full static shortcut surface (including the new stream-based shortcuts above) bound to that instance:

  • Json5R — readable (indented) JSON5, backed by Json5Serializer.DEFAULT_READABLE.
  • IniR — readable (spaced) INI, backed by IniSerializer.DEFAULT_READABLE.
  • HjsonC — compact (single-line) Hjson, backed by HjsonSerializer.DEFAULT_COMPACT.
String pretty  = Json5R.of(myPojo);   // readable JSON5
String compact = HjsonC.of(myPojo); // compact Hjson

These replace the former Json5.DEFAULT_READABLE, Ini.DEFAULT_READABLE, and Hjson.DEFAULT_COMPACT instance constants (see Breaking Changes below). The Marshallers, JSON5, Hjson, and Ini topic pages were updated accordingly.

Additional variant marshaller classes for full format symmetry​

Every remaining non-default *Serializer.DEFAULT_* / *Parser.DEFAULT_* configuration across all marshaller formats now has its own first-class variant facade class, extending the Json5R/IniR/HjsonC pattern above to full symmetry. This is a purely additive change — no existing class or constant was removed or modified. 27 new variant classes were added:

  • Readable variants: JsonR, TomlR, YamlR, PrototextR, UonR, UrlEncodingR, XmlSqR, XmlNsSqR, HtmlSqR.
  • UON: UonE (percent-encoding).
  • URL-encoding: UrlEncodingPlain (unencoded value parts), UrlEncodingExpanded (repeated key=value for arrays/collections).
  • HOCON: HoconBraces (root { } braces), HoconC (compact).
  • XML: XmlSq (single-quoted attrs), XmlNs (namespace-aware), XmlNsSq (namespace + single-quoted).
  • HTML: HtmlSq (single-quoted attrs), HtmlSimpleSq (single-quoted + JSON type-hint tags disabled).
  • Binary formats (Bson/Cbor/MsgPack): *SpacedHex and *Base64 (alternate byte[] wire representations), plus CborNative/MsgPackNative (native-mode parsing — exposes low-level CBOR tag / MessagePack extension-type info; pairs the plain serializer with the native-mode parser).
String readable = JsonR.of(myPojo);      // readable (indented) JSON
byte[] cbor = CborSpacedHex.of(myPojo); // spaced-hex byte[] binary format

See the Marshallers topic page for the full variant-class table.

MCP (Model Context Protocol) support (new modules)​

Juneau's first Model Context Protocol support lands in 10.0.0 across four new modules. (An earlier MCP write-up appeared in the draft, unreleased 9.5 notes describing a single-revision juneau-bean-mcp / juneau-rest-server-mcp pair; MCP itself had not shipped in any released Juneau version at that point, so that content has been withdrawn from 9.5.0 and replaced by this re-layered design, which lands for the first time here.) The implementation is split into a revision-neutral foundation plus a versioned adapter pair, so that a future MCP protocol revision can be added as a second adapter pair without touching the JSON-RPC envelope or the neutral core:

  • juneau-bean-jsonrpc — revision-neutral JSON-RPC 2.0 envelope beans.
  • juneau-rest-server-mcp — revision-neutral REST-server core (tool/prompt/resource registry, dispatch contract, pagination, the two HTTP entry points), with zero compile-time knowledge of any MCP protocol revision.
  • juneau-bean-mcp-v20250618 — MCP revision 2025-06-18 wire beans (renamed from the withdrawn draft's juneau-bean-mcp).
  • juneau-rest-server-mcp-v20250618 — the 2025-06-18 REST-server adapter that binds the neutral core to those wire beans; this is the module application code depends on to actually expose an MCP endpoint today.

MCP client modules (juneau-rest-client-mcp*)​

10.0.0 adds Juneau's first first-party MCP client stack:

  • juneau-rest-client-mcp — revision-neutral client core on juneau-rest-client (AbstractMcpClient) with raw JSON-RPC send/receive, auth-token seam (McpAuthInterceptor), response-cache seam (McpResponseCache + InMemoryMcpResponseCache), and an opaque server->client duplex dispatch seam (McpServerRequestHandler).
  • juneau-rest-client-mcp-v20250618 — typed McpClient facade for MCP 2025-06-18.
  • juneau-rest-client-mcp-v20260728 — typed McpClient facade for MCP 2026-07-28, including v2 _meta/header plumbing and cache-hint-aware client behavior.

The duplex path builds directly on the new general-purpose SSE APIs in juneau-rest-client (RestRequest.openEventStream(), ResponseBody.asEventStream()) and keeps server->client payloads raw/opaque by design; typed sampling/elicitation modeling remains owned by later MCP server slices.

Read-only-method stale-connection retry. Every MCP call is transported as an HTTP POST, which a transport can never safely replay on a stale pooled-connection failure (the POST may already have executed server-side). AbstractMcpClient.send() therefore adds its own once-only retry keyed on JSON-RPC method idempotency rather than HTTP method: a pre-response stale-connection failure on a side-effect-free method (initialize, server/discover, ping, tools/list, prompts/list, prompts/get, resources/list, resources/read, resources/templates/list, completion/complete) is replayed once on a fresh connection, while mutating methods (e.g. tools/call), subscription/sampling/notification methods, and any method not on that allowlist are never replayed. Detection reuses the shared TransportException.isStaleConnectionFailure(...) predicate.

juneau-bean-jsonrpc (new module)​

A new bean module, juneau-bean-jsonrpc, models the revision-neutral JSON-RPC 2.0 envelope as Juneau @Marshalled POJOs. It carries no MCP-specific knowledge — it depends on juneau-marshall only — and is the shared framing layer beneath protocol-specific bean modules such as juneau-bean-mcp-v20250618.

Coverage​

  • JsonRpcRequest — jsonrpc, id, method, params.
  • JsonRpcResponse — jsonrpc, id, result, error. Also carries three public static helpers used by dispatch implementations: ok(id, result) and errorResponse(id, code, message[, data]) (both response factories), plus notification(id) (a boolean predicate testing whether an id represents a JSON-RPC notification, i.e. id == null).
  • JsonRpcError — code, message, data.
  • McpException — a RuntimeException carrying JSON-RPC error fields (code, data) for handler-side propagation, with toJsonRpcError() converting it to a JsonRpcError.

Example​

import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.json.*;

JsonRpcRequest req = new JsonRpcRequest()
.setJsonrpc("2.0")
.setId(1)
.setMethod("tools/list");

String wire = JsonSerializer.DEFAULT.serialize(req);
JsonRpcRequest back = JsonParser.DEFAULT.parse(wire, JsonRpcRequest.class);

JsonRpcResponse ok = JsonRpcResponse.ok(1, "pong");
JsonRpcResponse err = JsonRpcResponse.errorResponse(1, -32601, "Method not found");

juneau-rest-server-mcp (re-layered into a revision-neutral core)​

juneau-rest-server-mcp has been re-layered from a single-revision implementation into a revision-neutral core with zero compile-time knowledge of any MCP protocol revision — enforced by a maven-enforcer banned-dependency rule that fails the build if this module ever depends on juneau-bean-mcp-* or juneau-rest-server-mcp-*. A protocol revision is supplied by an McpRevision implementation living in its own adapter module (see juneau-rest-server-mcp-v20250618 below); a consumer binds exactly one revision at compile time by extending that revision's abstract servlet or composing its endpoint mixin. There is deliberately no ServiceLoader auto-discovery — unlike the transport-provider pattern in juneau-rest-client-apache-httpclient-45, there is never a runtime set of candidate revisions to choose between.

New Classes​

  • McpRevision — the 3-method SPI a protocol revision implements: protocolVersion(), dispatch(McpExchange, McpServerConfig, BeanStore), and errorCode(McpErrorKind).
  • McpExchange — the inbound JSON-RPC envelope plus request-header access, with no servlet or HTTP types attached.
  • McpErrorKind — revision-neutral classification of a dispatch failure (INVALID_REQUEST, UNKNOWN_METHOD, TOOL_NOT_FOUND, PROMPT_NOT_FOUND, RESOURCE_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR, PARSE_ERROR); each revision maps kinds to its own JSON-RPC error codes via McpRevision.errorCode(McpErrorKind).
  • McpServerConfig — re-shaped aggregate registry of tools, prompts, resources, name/version, instructions, and pagination strategy (cursor). No longer holds protocolVersion (now owned by McpRevision.protocolVersion()) or capabilities (now owned by a revision-specific hook — see the 2025-06-18 adapter below); setProtocolVersion(String) has no replacement.
  • McpRestServlet — abstract BasicRestServlet subclass exposing POST /. Subclasses implement createMcpConfig(); a new abstract revision() method supplies the bound protocol revision.
  • McpEndpoint — mixin interface exposing POST /mcp on any @Rest resource, re-shaped the same way (getMcpConfig() plus an abstract revision()).
  • Neutral model — McpToolSpec/McpToolOutcome, McpPromptSpec/McpPromptArgument/McpPromptMessage/McpPromptOutcome, McpResourceSpec/McpResourceOutcome, McpContentBlock, McpResourceContents, McpRole, and the unconstrained JSON-object carrier McpSchema. This is what a handler is written against, so a handler compiles against exactly one protocol revision's worth of assumptions: none.
  • McpToolHandler, McpPromptHandler, McpResourceHandler — raw @FunctionalInterface handlers, now typed against the neutral spec/outcome types above (previously typed against wire beans). Each keeps its default-throwing descriptor(), so a lambda implementing only the call/get/read method still compiles.
  • McpCursor, McpPage<T> — pagination strategy seam, unchanged: McpCursor.SINGLE_PAGE (default, returns everything in one page) and McpCursor.fixedSize(n) (opaque integer-offset paging).
  • McpParamUtils — shared params-coercion plumbing (asMap/strParam/mapParam) used by revision dispatch() implementations.

Removed: McpDispatcher and the static Mcp façade are gone with no drop-in replacement class — their method-table logic now lives in each revision's own McpRevision.dispatch() implementation (see org.apache.juneau.rest.server.mcp.v20250618.McpRevision below), and their internal plumbing helpers were redistributed onto JsonRpcResponse, McpCursor, and the new McpParamUtils.

See juneau-rest-server-mcp for the full topic.

juneau-bean-mcp-v20250618 (renamed from juneau-bean-mcp)​

The bean module has been renamed from juneau-bean-mcp to juneau-bean-mcp-v20250618 (package org.apache.juneau.bean.mcp.v20250618) as part of the re-layering above — the name change makes room for a future sibling module under the same org.apache.juneau.bean.mcp namespace should a later MCP protocol revision need one. The module still models the MCP 2025-06-18 wire format as Juneau @Marshalled POJOs that round-trip cleanly through any Juneau serializer/parser (JSON, JSON5, MessagePack, CBOR, YAML, etc.). Its 4 JSON-RPC envelope classes (JsonRpcRequest, JsonRpcResponse, JsonRpcError, McpException) have moved out to the new revision-neutral juneau-bean-jsonrpc module (above); this module still depends on juneau-marshall only.

Coverage​

  • Initialization — InitializeRequest, InitializeResult, Implementation, ClientCapabilities, ServerCapabilities, and the per-feature capability beans (ToolCapability, PromptCapability, ResourceCapability, LoggingCapability, RootsCapability).
  • Tools — Tool, CallToolRequest, CallToolResult, ListToolsResult, plus the JsonSchema argument-schema bean. Tool.outputSchema and CallToolResult.structuredContent — part of MCP structured tool output since 2025-06-18 but previously missing from this module — are now present, closing a prior gap in this revision's own bean coverage; see below.
  • Prompts — Prompt, PromptArgument, GetPromptRequest, GetPromptResult, ListPromptsResult, PromptMessage, Role.
  • Resources — Resource, ReadResourceRequest, ReadResourceResult, ListResourcesResult, plus polymorphic ResourceContents (TextResourceContents, BlobResourceContents).
  • Polymorphic content blocks — Content interface with TextContent, ImageContent, EmbeddedResourceContent discriminated by a type property.
  • Constants — McpMethods (JSON-RPC method names) and McpProtocol (version constants, including the JSON_RPC_2_0 literal).

Polymorphic Serialization​

Content and ResourceContents use @Marshalled(typePropertyName="type", dictionary={...}) to discriminate at the wire level. Configure your serializer with addBeanTypes() to emit the discriminator on output:

JsonSerializer ser = JsonSerializer.create()
.addBeanTypes()
.typePropertyName(Content.class, "type")
.typePropertyName(ResourceContents.class, "type")
.build();

Example​

import org.apache.juneau.bean.jsonrpc.*;
import org.apache.juneau.bean.mcp.v20250618.*;
import org.apache.juneau.json.*;

JsonRpcRequest req = new JsonRpcRequest()
.setJsonrpc(McpProtocol.JSON_RPC_2_0)
.setId(1)
.setMethod(McpMethods.TOOLS_LIST);

String wire = JsonSerializer.DEFAULT.serialize(req);
JsonRpcRequest back = JsonParser.DEFAULT.parse(wire, JsonRpcRequest.class);

See juneau-bean-mcp-v20250618 for the full topic.

juneau-rest-server-mcp-v20250618 (new module)​

A new adapter module, juneau-rest-server-mcp-v20250618 (package org.apache.juneau.rest.server.mcp.v20250618), binds the revision-neutral juneau-rest-server-mcp core to the 2025-06-18 wire beans in juneau-bean-mcp-v20250618.

New Classes​

  • org.apache.juneau.rest.server.mcp.v20250618.McpRevision — the McpRevision implementation for protocol revision 2025-06-18. Replaces the old McpDispatcher/Mcp façade: owns the JSON-RPC method table (initialize, ping, tools/list|call, prompts/list|get, resources/list|read) and the error-code table. The error-code table preserves a known-wrong mapping carried over unmodified from the pre-re-layering dispatcher for three of the four "not found" kinds — unknown-method, tool-not-found, and prompt-not-found all report the generic JSON-RPC -32601 ("method not found"), which is only actually correct for the first; the fix for the remaining two is tracked as a separate follow-on. (Resource-not-found originally shared that mapping but is corrected to the spec's dedicated -32002 in 10.0.0 — see the resource-not-found error-code correction below.)
  • org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet — concrete abstract servlet for this revision; subclass it (instead of the core McpRestServlet) and implement createMcpConfig(). Exposes a protected ServerCapabilities capabilities() hook (returns null by default) for explicitly overriding the initialize capabilities advertisement instead of relying on auto-derivation from the registered tool/prompt/resource lists.
  • org.apache.juneau.rest.server.mcp.v20250618.McpEndpoint — mixin interface for this revision, at parity with org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet: implement getMcpConfig(), and optionally override its default ServerCapabilities capabilities() hook.
  • McpTypedPromptHandler<A>, McpTypedHandlers (prompt-only) — the typed prompt sugar layer stays here, since it's genuinely revision-specific by construction: it binds arguments into this revision's wire-bean argument types and adapts the result to the neutral McpPromptHandler raw interface the core registry holds.

Typed tool sugar is revision-neutral, not revision-specific. McpTypedToolHandler<A,R> and its adaptation into a raw McpToolHandler now live in the core (org.apache.juneau.rest.server.mcp), not this adapter — one typed tool handler serves either dated adapter unchanged, deriving inputSchema/outputSchema from its declared argument/result Types and producing structuredContent automatically. This corrects an earlier plan-time assumption that all typed sugar was dated by construction; only typed prompt sugar is.

Gap closed: structured tool output, present since 2025-06-18, is now fully supported end to end. Tool.outputSchema and CallToolResult.structuredContent were part of the MCP 2025-06-18 specification from the start (constrained to object-shaped schemas and a JSON-object structuredContent value) but were missing from this module's Tool/CallToolResult beans and unreachable from the neutral core. 10.0.0 adds both fields to the 2025-06-18 wire beans, adds matching outputSchema/structuredContent fields to the neutral McpToolSpec/McpToolOutcome, and connects them through the neutral typed-tool bridge above — so both this adapter and the 2026-07-28 adapter populate them from the same typed handler. 2026-07-28 broadens the feature (arbitrary JSON Schema Draft 2020-12 shapes, any JSON value); it does not introduce it. See juneau-rest-server-mcp for the typed-tool walkthrough.

New behavior: on a given McpServerConfig's first dispatch, this revision validates that every registered tool's McpSchema uses only the 6 JSON Schema keywords its wire JsonSchema bean can represent (type, properties, required, additionalProperties, items, $defs) — throwing IllegalArgumentException naming the offending tool and keyword if not, rather than silently dropping an unsupported keyword on the wire. This same first-dispatch check now also covers outputSchema, and requires structuredContent to be a JSON object.

Drop-in servlet​

@Rest(path="/mcp")
public class MyMcpServlet extends McpRestServlet {
@Override
protected McpServerConfig createMcpConfig() {
return new McpServerConfig().setName("my-server").setVersion("1.0.0").addTool(new MyEchoTool());
}
}

Mixin on an existing resource​

@Rest(path="/api")
public class MyResource extends BasicRestServlet implements McpEndpoint {
@Override
public McpServerConfig getMcpConfig() {
return new McpServerConfig().addTool(new MyEchoTool());
}
}

Typed handlers​

Typed tools use the revision-neutral McpTypedToolHandler<A,R> (core), so the same handler works unchanged against this adapter or the 2026-07-28 adapter:

public class WeatherArgs { String city; String unit; /* getters/setters */ }
public class WeatherResult { String summary; /* getters/setters */ }

McpTypedToolHandler<WeatherArgs, WeatherResult> typed = new McpTypedToolHandler<>() {
@Override public McpToolSpec descriptor() { return new McpToolSpec().setName("weather"); }
@Override public Type argumentType() { return WeatherArgs.class; }
@Override public Type resultType() { return WeatherResult.class; }
@Override public WeatherResult call(WeatherArgs a, BeanStore ctx) { return new WeatherResult(); }
};

config.addTool(typed);

inputSchema/outputSchema are derived from argumentType()/resultType(); the returned R is canonicalized once into structuredContent and mirrored as compact JSON text by default (override mirrorStructuredContentAsText() to opt out). Typed prompts still use the dated McpTypedPromptHandler<A> (see "New Classes" above) and adapt via McpTypedHandlers.adaptPrompt(...).

Pagination​

config.setCursor(McpCursor.fixedSize(50));

McpCursor lives in the neutral core (above) and is unchanged: it receives the full descriptor list and the inbound cursor token, and returns an McpPage<T> containing the slice plus an opaque nextCursor. Custom strategies plug in via the McpCursor functional interface.

Capability override​

@Rest(path="/mcp")
public class MyMcpServlet extends McpRestServlet {
@Override
protected McpServerConfig createMcpConfig() {
return new McpServerConfig().addTool(new MyEchoTool());
}

@Override
protected ServerCapabilities capabilities() {
return new ServerCapabilities().setLogging(new LoggingCapability());
}
}

Returning null (the default) leaves capabilities auto-derived from the registered tool/prompt/resource lists. Returning a non-null value bypasses auto-derivation entirely — the way to advertise resources.subscribe, logging, listChanged, or experimental, none of which are derivable from a handler registry. McpEndpoint exposes the equivalent hook as a default interface method for the mixin path.

Notifications​

Requests without an id are treated as JSON-RPC notifications: handlers run, exceptions are swallowed, and dispatch() returns null. The REST servlet/mixin writes an empty body so transports can map this to 204 No Content.

See juneau-rest-server-mcp-v20250618 for the full topic.

MCP 2026-07-28 — cache hints (SEP-2549) + resources/templates/list​

The 2026-07-28 bean and REST-server adapter modules (juneau-bean-mcp-v20260728, juneau-rest-server-mcp-v20260728) add SEP-2549 cache hints and a resource-template listing endpoint. Both are 2026-07-28-only — the neutral juneau-rest-server-mcp core and the 2025-06-18 adapter are untouched.

  • Cache hints — tools/list, prompts/list, resources/list, resources/templates/list, and resources/read results can each carry a nullable, top-level ttlMs (Integer) and cacheScope (McpCacheScope: public/private) pair, via the new CacheableResult<T> CRTP base that ListToolsResult, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, and ReadResourceResult all extend. Fields are omitted from the wire when unset — there is no forced default scope or TTL.
  • resources/templates/list — a new v2-only endpoint (McpMethods.RESOURCES_TEMPLATES_LIST) listing ResourceTemplate descriptors (uriTemplate, name, title, description, mimeType) registered via the neutral McpServerConfig.addResourceTemplate(McpResourceTemplateSpec). Registering at least one resource template auto-derives the resources capability, same as resources. The pre-built neutral McpResourceTemplateSpec descriptor and this endpoint are designed for reuse by the planned resources/templates/read completion work.
  • Static, adapter-owned configuration only — a server configures cache hints once, at construction time, via the new McpCacheConfig/McpCacheHint types, mutated in place through the cache(Consumer<McpCacheConfig>) configure-block on the McpOptions aggregate (see the config-consolidation entry further down this page). Precedence is atomic per list method (method-specific hint, else a shared default, else no cache fields) and, for resources/read, adds an exact-URI override map ahead of a resourcesRead default. Juneau does not implement client-side caching, does not emit HTTP cache headers (Cache-Control, Expires, Vary, etc.), does not add a dynamic/per-request cache callback, and does not add any cache-related type to the neutral core — McpCacheConfig/McpCacheHint/McpCacheScope all live in the 2026-07-28 adapter/bean modules only.

See juneau-rest-server-mcp and juneau-bean-mcp-v20260728 for the full topics.

MCP — cross-revision resource-template reads and completions (completion/complete)​

Building on the resources/templates/list endpoint above, 10.0.0 makes registered resource templates executable through ordinary resources/read, and makes prompt arguments and resource-template variables completable through the new completion/complete method — on both 2025-06-18 and 2026-07-28. This closes 2025-06-18's prior resource-template gap (it previously had no ResourceTemplate/ListResourceTemplatesResult beans, no resources/templates/list, and no way to serve a template at all) and adds an entirely new feature to both revisions in one pass.

  • McpResourceTemplateHandler (new, revision-neutral core) — pairs the existing McpResourceTemplateSpec descriptor with a variable-aware read(String uri, Map<String,String> variables, BeanStore ctx) and an optional per-variable completer(String variableName). McpServerConfig's primary resource-template registration is now handler-based (getResourceTemplates()/setResourceTemplates(List<McpResourceTemplateHandler>)/addResourceTemplate(McpResourceTemplateHandler...)); the existing descriptor-only addResourceTemplate(McpResourceTemplateSpec...) registration style remains as a listing-only convenience, now implemented as a built-in handler whose read/completer both return null. Bulk descriptor replacement is the new setResourceTemplateSpecs(List<McpResourceTemplateSpec>) (a second setResourceTemplates(List<McpResourceTemplateSpec>) overload isn't possible under Java generic erasure). The complete registry is validated at registration time — malformed/duplicate templates fail fast with IllegalArgumentException naming the offending index and template, before any request reaches dispatch.
  • A dependency-free RFC 6570 reverse matcher (new, McpUriTemplateMatcher, revision-neutral core) — reliably reverse-matches one narrow, invertible subset of RFC 6570 (simple/reserved/fragment scalars, path/label segments, ordered query expressions) and extracts decoded, immutable, insertion-ordered variables. This does not cover every legal RFC 6570 form and Juneau does not perform any URI-template expansion (client-side or otherwise): exploded/prefix/matrix/multi-variable/ambiguous-adjacency forms remain syntactically legal, listable, and completable, but are never selected for a template-backed read. resources/read resolves exact resources first (unconditionally), then the most-specific matching template by literal-octet count, then fewer declared variables, then registration order.
  • Neutral completion domain and SPI (new) — McpCompleter (@FunctionalInterface), McpCompletionRef, McpCompletionRequest, and McpCompletionResult model a completion/complete request/result independent of either dated wire format. McpPromptArgument gains a nullable, non-wire completer property (its existing name/description/required fields are unchanged and remain the sole wire-mapped fields). Unknown/uncompletable completion targets return a successful empty completion ({"completion":{"values":[]}}), not an error; malformed requests are -32602; a completer that throws, returns null, or returns an invalid result is -32603. At most 100 values are ever emitted, forcing hasMore=true on truncation — Juneau performs no ranking, filtering, deduplication, or additional pagination of completion values.
  • 2025-06-18 backfill — adds ResourceTemplate and ListResourceTemplatesResult beans (the same five/two-field shape 2026-07-28 already had, minus cache fields), McpMethods.RESOURCES_TEMPLATES_LIST, and resources/templates/list dispatch, closing this revision's prior gap. Missing-resource reads (exact or template) continue to report this revision's existing known-wrong -32601.
  • 2026-07-28 — reuses its existing C2 ResourceTemplate/ListResourceTemplatesResult/mapping/pagination/cache-hint machinery unchanged; template-backed reads apply the same cache-hint precedence as exact-resource reads, keyed by the original concrete request URI. Missing-resource reads report RESOURCE_NOT_FOUND (-32602).
  • New completion wire beans (both revisions, independent copies) — CompleteRequest, CompletionReference (closed polymorphic base with PromptReference/ResourceTemplateReference, discriminated ref/prompt/ref/resource), CompletionArgument, CompletionContext, CompleteResult, Completion, and an empty CompletionCapability marker under a new nullable ServerCapabilities.completions, auto-derived only when at least one completer is registered. McpMethods.COMPLETION_COMPLETE ("completion/complete") is the shared method-name value on both revisions. For 2026-07-28, completion/complete is deliberately nameless for SEP-2243 (Mcp-Method=completion/complete, Mcp-Name="", since the target is nested under params.ref); normal per-request _meta validation still applies.
  • Included refactor: McpJsonValueSafety relocated to juneau-commons. The MCP core's bounded-depth/node/deadline JSON-value walker (used for structured tool-output safety) moves to juneau-commons as org.apache.juneau.commons.utils.JsonValueSafety, with an identical API and behavior — a rename-only move (drops the Mcp prefix, since the class was already protocol-neutral) bundled into this same change because it touches the same neutral core and both dated adapters. juneau-commons gains no MCP dependency from this move.
  • Scope fence, explicitly out of this change — resource subscriptions (resources/subscribe/resources/unsubscribe on 2025-06-18, the newer subscriptions/listen streaming request on 2026-07-28) remain unimplemented and are tracked separately; client-side RFC 6570 expansion and a general-purpose URI-template API are out of scope; and Juneau does not rank, filter, deduplicate, or paginate completion values beyond the protocol's single-response 100-value cap.

See juneau-rest-server-mcp and juneau-bean-mcp-v20250618 for the full topics.

MCP 2026-07-28 — _meta wire compliance + trace-context propagation + Spring Boot convenience class​

2026-07-28-only. Brings juneau-bean-mcp-v20260728's per-request/per-result metadata into exact alignment with the MCP 2026-07-28 schema, adds distributed trace-context propagation through that same _meta carrier, and adds a Spring Boot convenience base class. 2025-06-18 is untouched by all three changes.

  • _meta wire-compliance reshape. The neutral JSON-RPC envelope beans (JsonRpcRequest/JsonRpcResponse) carry no _meta property; it now lives under params._meta (new RequestMeta, via a new RequestParams<T> CRTP base on CallToolRequest/GetPromptRequest/ReadResourceRequest/CompleteRequest, plus RequestParamsOnly for the metadata-only methods) with the exact io.modelcontextprotocol/-prefixed negotiation keys the schema mandates, and under result._meta (new ResultMeta, via a new Result<T> CRTP base every successful result extends) alongside a new required resultType ("complete") discriminator. ServerDiscoverResult now extends CacheableResult<ServerDiscoverResult> and carries its server identity on the inherited _meta rather than a top-level serverInfo field. Implementation gains title/description/icons (new Icon bean); ClientCapabilities/ServerCapabilities gain an extensions map; a new typed PingResult replaces a bare JsonMap for ping. This is a correction to the wire shape, not a compatibility mode — there is no old-shape fallback.
  • Trace-context propagation. params._meta may additionally carry bare (unprefixed) W3C traceparent/tracestate/baggage keys. When an active TracerHook is registered, an explicit params._meta value wins over the equivalent HTTP header (independently per field); the active trace context is echoed back into result._meta on success; and the OpenTelemetry bridge (when present) enriches the span with pinned, experimental gen_ai.*/mcp.* attributes and maps JSON-RPC errors (which always ride an HTTP 200) onto span error status via a new Scope.recordRpcError(code, message). Built on three new neutral juneau-rest-server types (TraceContextCarrier, TraceContextExtractor, TraceOperation) plus a TracerHook.startSpan(RestRequest, TraceContextCarrier, TraceOperation) default overload — all source-compatible, so existing TracerHook/Scope implementations keep compiling unchanged. The gen_ai.*/mcp.* attribute names are pinned to a specific OpenTelemetry GenAI semantic-conventions snapshot and marked experimental.
  • SpringMcpRestServlet convenience class (both revisions). A new SpringMcpRestServlet (org.apache.juneau.rest.server.mcp.v20250618.SpringMcpRestServlet and .v20260728.SpringMcpRestServlet) combines BasicSpringRestServlet with the revision's McpEndpoint mixin — the simplest way to expose MCP from a Spring Boot resource with full Spring dependency-injection support in tool/prompt/resource handlers; implement only getMcpConfig(). Don't subclass the plain McpRestServlet under Spring Boot instead — it's a sibling of SpringRestServlet, not a parent of BasicSpringRestServlet, so it still dispatches but silently loses Spring bean resolution inside handlers, with no compile-time signal.

See juneau-rest-server-mcp (Spring Boot + trace-context sections), juneau-bean-mcp-v20260728 (bean catalog), and Observability (framework-level seam + OTel bridge) for the full topics.

MCP 2026-07-28 — McpOptions config consolidation​

2026-07-28-only. The six previously-separate per-binding behavior-config hooks the v2 adapter accumulated across the MRTR, cache-hints, and subscriptions work above (capabilities, instructions, cache, mrtr, subscriptions, subscriptionBroker — spread across mismatched xxxConfig() single-method and createXxx()/getXxx() pair idioms, with divergent defaults between the mixin and servlet paths) are consolidated into a single aggregate, before this never-shipped API's first release:

  • New McpOptions (org.apache.juneau.rest.server.mcp.v20260728) — a mutable-setup fluent bean, same idiom as the neutral McpServerConfig and the nested McpCacheConfig/McpMrtrConfig/McpSubscriptionsConfig types it wraps. Plain getXxx()/setXxx(...) for capabilities/instructions/subscriptionBroker, plus Consumer-style configure-blocks (cache(Consumer<McpCacheConfig>), mrtr(Consumer<McpMrtrConfig>), subscriptions(Consumer<McpSubscriptionsConfig>)) that mutate a framework-owned nested instance in place.
  • One override replaces six. McpRestServlet gains createMcpOptions() (mirroring createMcpConfig()); McpEndpoint gains getMcpOptions() (mirroring getMcpConfig()). The prior six hooks (capabilities(), instructions(), cacheConfig()/createCacheConfig(), mrtrConfig()/createMrtrConfig(), subscriptionsConfig()/createSubscriptionsConfig(), the v2 subscriptionBroker() override) and the wiring seam subscriptionsConfigBean() are gone from the user-visible surface.
  • Uniform per-binding memoization. McpOptions is invoked once per binding and published as a single internal @Bean, exactly like tools/prompts — and this internal publication is authoritative, taking precedence over any Spring @Bean McpOptions in the application context (Spring users inject their configuration into the createMcpOptions()/getMcpOptions() override instead of publishing a bare @Bean McpOptions). This also unifies a prior default-sharing inconsistency: the mixin path previously shared its MRTR AES key and subscription broker JVM-wide across every endpoint instance (via now-deleted SharedMrtrConfig/SharedSubscriptionBroker singletons) while the servlet path was already per-instance. Both paths are now uniformly per-binding — two distinct bindings always get distinct mrtr keys and broker registries.
  • Subscription broker folded in. McpOptions.getSubscriptionBroker() defaults to null ("framework-derived"): the binding lazily derives and memoizes a BasicMcpSubscriptionBroker sized from subscriptions' queueSize. Call setSubscriptionBroker(...) to supply a custom broker instead.
  • Client-side elicitation naming split, for symmetry. org.apache.juneau.rest.client.mcp.v20260728.ElicitationAccess is replaced by an ElicitationRequests (reading server→client requests: isInputRequired/requests/requestState) + ElicitationResponses (encoding client→server answers: toInputResponse/toInputResponses) pair, matching the naming already used by the server-side helpers of the same names.
  • Neutral core untouched. juneau-rest-server-mcp's revision-neutral subscriptionBroker()/getSubscriptionBroker() seam is unchanged; only the v2 adapter delegates through McpOptions.

Because McpOptions and the v2 adapter are never-shipped 10.0.0 APIs, this is a clean replace with no deprecation shims.

See juneau-rest-server-mcp and juneau-rest-client-mcp for the full topics.

MCP 2026-07-28 — MRTR KeyProvider SPI for shared/rotating sealing keys​

2026-07-28-only. Introduces a key-provider abstraction beneath the MRTR AeadRequestStateCodec (shipped unreleased, above) so operators can supply a stable shared sealing key — fixing horizontally-scaled RESUME — and roll keys via a keyId, while keeping today's per-process ephemeral key as the zero-config default.

  • New KeyProvider SPI (org.apache.juneau.rest.server.mcp.v20260728) — currentKey(): KeyedSecret (the key to seal new tokens with, plus its keyId) and resolveKey(keyId): Optional<SecretKey> (never throws; empty on an unknown/retired keyId). KeyedSecret is a small keyId/SecretKey record with non-blank/length-bounded keyId validation.
  • Two built-ins. EphemeralKeyProvider preserves today's exact zero-config behavior (a random AES-256 key and a random keyId per instance, still not restart-durable or cross-instance-shareable). StaticKeyProvider is an immutable, statically-configured keyring for the shared/rotating case — a StaticKeyProvider.of(keyId, key) one-liner, or a builder (create().addKey(...).current(...).build()) supporting overlapping keys during a rollover — plus an aesKey(byte[] | String) helper for building the SecretKey from raw config/secret material.
  • AeadRequestStateCodec now takes a KeyProvider. The no-arg constructor is unchanged behaviorally (this(new EphemeralKeyProvider())); a new AeadRequestStateCodec(KeyProvider) constructor is the extension point. The sealed-token wire format grows from two to four dot-joined segments — version . b64url(keyId) . b64url(nonce) . b64url(ciphertext+tag) — with keyId authenticated (folded into the AEAD's AAD) but not encrypted (it must be readable before decryption selects the key). Because MCP 2026-07-28 support is itself unshipped, there is no legacy two-segment fallback: unseal accepts only the new format.
  • McpMrtrConfig.setKeyProvider(KeyProvider) (new convenience). Sugar for setCodec(new AeadRequestStateCodec(value)), reachable through the McpOptions aggregate via new McpOptions().mrtr(m -> m.setKeyProvider(kp)). Last-wins against setCodec(...); no getter for the provider (getCodec() remains the sole accessor).
  • Error mapping unchanged. The never-throw unseal contract and the existing -32602/-32022/-32023 error codes are untouched — an unknown/retired keyId flows through the same Optional.empty() path as any other unseal failure, surfacing as the standard -32602 "Invalid or tampered requestState".
  • Scope fence, explicitly out of this change — the OAuth/OIDC authorization SEPs and the MRTR hardenings (principal-in-AAD, replay cache, argument-hash binding) are tracked separately and untouched here; the pre-existing -32602/-32002 missing-resource v1 error-code mismatch is now resolved — see the resource-not-found error-code correction below.

See juneau-rest-server-mcp and the MRTR key management recipes for the full topics.

MCP 2026-07-28 — MRTR replay protection + argument-hash sealing​

2026-07-28-only. Adds two optional hardenings on top of the MRTR pause/resume foundation above: a replay cache to reject reuse of a consumed requestState token, and an always-on hash check binding a resume's arguments back to what the original, paused call authorized.

  • Replay protection, opt-in. By default a requestState token is unchanged — a multi-use bearer credential, replayable any number of times within its TTL. A new ReplayCache SPI (org.apache.juneau.rest.server.mcp.v20260728) — a single atomic checkAndRecord(String jti, long expiresAtMs): boolean — narrows that default to single-use once wired via McpMrtrConfig.setReplayCache(ReplayCache); unset (the default) means no replay check at all. The built-in InMemoryReplayCache is a per-process, self-evicting seen-jti set — not shareable across instances, mirroring KeyProvider/EphemeralKeyProvider's own non-shareable-default precedent, so cross-node single-use in a horizontally-scaled deployment requires an operator-supplied shared implementation. Fail-mode is a deliberate split: a thrown exception is fail-open (logged, treated as first-seen); an operator wanting fail-closed returns false from their own checkAndRecord instead of throwing. A detected replay is rejected with the new CODE_REQUEST_STATE_REPLAYED (-32025).
  • Argument-hash sealing, always-on. The original tools/call/prompts/get arguments are now canonicalized (RFC 8785 JSON Canonicalization Scheme) and SHA-256-hashed at PAUSE time, then re-verified against the RESUME request's own arguments on every round — no configuration, no opt-out. A faithful client that re-sends identical arguments (the documented, expected case) sees no change in behavior; a resume with different arguments is now hard-rejected with the new CODE_REQUEST_STATE_ARGUMENTS_MISMATCH (-32026), closing the previously-documented gap where a client was merely expected to re-send its original arguments faithfully. resources/read's exact-path branch (no arguments) is unaffected.
  • Bounded-JSON safety now covers arguments too. Because the always-on hash above canonicalizes the full arguments value, Juneau's existing bounded-JSON safety checks (max depth, max node count) now also apply to tool/prompt arguments on every PAUSE and RESUME — including schemaless tools/call and prompts/get, which previously had no input-shape guard at all — so pathologically large or deep arguments are rejected with -32602 (invalid params) before the handler ever runs.
  • McpRequestState gains two additive fields (jti, argumentsHash) alongside its existing continuation/method/round/expiresAtMs. Both ride inside the AEAD-sealed plaintext, so neither is forgeable without breaking the existing integrity check; no change to RequestStateCodec/AeadRequestStateCodec or the sealed-token wire format.

See juneau-rest-server-mcp for the full topic.

MCP 2026-07-28 — schema-safety compute budget measured as thread CPU time​

2026-07-28-only. The bounded, no-fetch tools/call input-schema validation guard enforces a compute budget so a pathological schema (for example a catastrophically-backtracking pattern) can't hang the request thread — tripping -32602 ("Tool input schema validation exceeded 100 ms") instead. That budget is now charged against the validating thread's actual CPU time rather than wall-clock time: OS scheduler preemption and thread-pool queueing no longer count against it, only real CPU work does. This removes a false-positive that could fire on trivial input under heavy concurrent load (many parallel test/JVM forks, or a saturated production deployment), while a genuinely expensive validation still trips the budget with the same error. On a JVM that can't report per-thread CPU time, the guard transparently falls back to the original wall-clock measurement.

MCP 2026-07-28 — OAuth 2.1 authorization: client acquisition + server resource server​

2026-07-28-only, and off by default — an endpoint that doesn't opt in behaves exactly as before. 10.0.0 lands the OAuth 2.1 / MCP authorization baseline in three slices: client-side token acquisition (F1), a server-side resource-server (F2), and dynamic client registration + authorization hardening (F3). Together they let a Juneau MCP client authenticate to, and a Juneau MCP endpoint protect, a 2026-07-28 MCP resource end to end, covering the MCP authorization SEPs at a baseline level (RFC 9728 protected-resource metadata, RFC 8707 resource-indicated audience binding, RFC 8414 / OIDC authorization-server discovery, RFC 7636 PKCE, RFC 9207 / SEP-2468 issuer validation, SEP-837 dynamic client registration with application_type + loopback redirects, SEP-2352 issuer-keyed credential binding, SEP-2350 scoped step-up authorization, SEP-2207 refresh-token rotation).

  • New client module juneau-rest-client-mcp-auth (package org.apache.juneau.rest.client.mcp.auth, on the Nimbus OAuth 2.0 SDK) — feeds the existing McpAuthInterceptor token seam (a Supplier<String>):
    • McpTokenProvider — a thread-safe headless token manager in three modes: pre-provisioned static (ofStaticToken), client-credentials (clientCredentials(), RFC 6749 §4.4), and refresh-token (refreshToken(...), RFC 6749 §6 / SEP-2207 with rotation capture via currentRefreshToken() and terminal-invalid_grant latching). The dynamic modes require resource(URI) (RFC 8707), carried on every token request; interceptor() wires it straight into a client builder.
    • McpAuthorizationCodeAcquirer — interactive authorization-code + PKCE (S256) for CLI/native use: opens a LoopbackRedirectReceiver, launches the authorization URL (pluggable browserLauncher), validates the callback (single-use state CSRF, SEP-2468 iss — expectedIssuer(URI) required unless skipIssuerValidation(true)), and exchanges the code. offlineAccess(true) requests a refresh token.
    • McpProtectedResourceMetadataClient — RFC 9728 PRM discovery with an expectedResource(URI) §3.3 identity check and an https-by-default scheme requirement (loopback exempt), plus discoverAuthorizationServer(...); WwwAuthenticateChallenge parses the 401/403 Bearer challenge to recover the resource_metadata pointer.
    • OidcDiscoveryClient — RFC 8414 Authorization Server Metadata with OIDC Discovery fallback (both required by the spec), each performing the issuer-identity check.
  • New server RS baseline on juneau-rest-server-mcp-v20260728 — McpResourceServerConfig, folded into McpOptions as the resourceServer(Consumer<McpResourceServerConfig>) nested config. When enabled it requires a valid RFC 6750 bearer token on the MCP POST endpoint (validation mode chosen by the supplied TokenValidator: OAuthIntrospectionValidator for opaque tokens, JwtTokenValidator for JWTs), serves an RFC 9728 PRM document from .well-known/oauth-protected-resource, answers missing/invalid tokens with 401/403 + a WWW-Authenticate: Bearer … challenge carrying resource_metadata, and enforces RFC 8707 audience matching (requireAudienceClaim defaults to true, rejecting a claims-less principal). Bearer extraction reuses the OAuthFilter from juneau-rest-server-auth-oauth.
  • Origin-root mount constraint. Because RFC 9728 requires the PRM document at an origin-root well-known location — the exact URL advertised in the resource_metadata challenge — RS auth requires the MCP endpoint to be mounted at the origin root (no context path, no non-root servlet path, no @Mixin(path=...) re-mount prefix). A non-root or re-mounted mount fails fast with a 500 carrying an origin-root diagnostic (on both the POST gate and the well-known route) rather than silently advertising an unreachable document. A configurable external PRM URL to relax this is a deferred future enhancement.

F3 — dynamic client registration + authorization hardening. The third slice completes the client story with RFC 7591 / OIDC Dynamic Client Registration, issuer-keyed credential persistence (SEP-2352), and scoped step-up authorization (SEP-2350, both halves), all in juneau-rest-client-mcp-auth except the server step-up gate:

  • Dynamic Client Registration (SEP-837 / RFC 7591 / OIDC). McpDynamicClientRegistrar POSTs client metadata to an AS registration_endpoint and returns an immutable, secret-redacting McpClientRegistration. McpApplicationType is a Juneau-owned NATIVE/WEB enum (native is the SEP-837 SHOULD for CLI/desktop/localhost clients) so the public API does not leak the Nimbus provided type. Loopback redirects are built with LoopbackRedirectUris: portAgnostic(path) is the RFC 8252 §7.3 spec-blessed easy default (http://127.0.0.1/callback + http://localhost/callback, any port matches), and forPort(port, path) is the bind-first strategy for strict exact-match ASes — now genuinely usable end to end because LoopbackRedirectReceiver can bind a caller-chosen fixed port, McpAuthorizationCodeAcquirer.Builder.redirectPort(int) opens the receiver on it, and McpClientRegistrations.configure(...) carries the registered port (not just the path) onto the acquirer. McpClientRegistrations bridges a registration into the F1 flows unchanged (credential provenance only). The registration endpoint must be https (loopback exempt) — the Builder.requireSecure(boolean) escape hatch that would have allowed a plaintext endpoint did not survive to release; the check is unconditional.
  • Issuer-keyed credential binding (SEP-2352). Persisted credentials MUST be keyed by the AS issuer and never reused across authorization servers. McpClientRegistrationStore is the persistence SPI; InMemoryMcpClientRegistrationStore is the thread-safe, secret-redacting default. McpClientRegistrationManager orchestrates mechanism selection, issuer-keying, and migration: on-demand (no store), store-hit reuse, or re-registration when discovery indicates a different issuer — surfacing an McpAuthException rather than reusing a cross-AS credential (a defensive issuer-mismatch check also guards a buggy durable store). The token half of SEP-2352 is caller discipline: after an AS migration the caller must build a new McpTokenProvider for the new issuer.
  • Scoped step-up authorization (SEP-2350, both halves). Server side, McpResourceServerConfig.addOperationScope(operation, scopes…) declares per-operation required scopes; the RS answers an under-scoped call with 403 insufficient_scope + a WWW-Authenticate scope= hint, and the baseline 401 now also carries the scope hint when a baseline required-scope set is configured. Both the baseline and per-operation gates are hierarchy-aware (a broader granted scope satisfies a narrower required one). Client side, McpScopeAccumulator computes the order-preserving {previously-requested} ∪ {challenge} union and McpStepUpAuthorizer.execute(operation, call) drives a caller-supplied re-authorizer and retries under a per-(resource, operation) attempt cap; a successful call clears that operation's counter, and reset(operation)/resetAll() let a long-lived authorizer recover.

See the OAuth 2.1 recipe (end-to-end client + server), the DCR + step-up recipes, the juneau-rest-client-mcp auth reference, and the juneau-rest-server-mcp resource-server reference for the full topics.

MCP 2026-07-28 — first-party runnable example module (juneau-examples-mcp)​

A new example module, juneau-examples-mcp (under juneau-examples/, alongside juneau-examples-core), ships the single highest-value adoption artifact for the now-complete MCP framework above: a runnable, copy-me reference implementation of both the MCP server and client sides, built against the 2026-07-28 revision. One cohesive in-memory "notes" service (NoteStore, a title -> body map) is exercised end to end by every headline surface:

  • tool — publishNote(title, body), which stores a note and pushes a resources/updated change to subscribers.
  • tool + elicitation/MRTR — deleteNote(title), which pauses with input_required to confirm before resuming to delete.
  • prompt + completion — summarize(title), whose title argument offers completion/complete suggestions from existing note titles.
  • resource — the fixed note:///index resource, listing all note titles.
  • resource template + completion — note:///{title}, reading one note by title, with the same title-completion behavior on its {title} variable.
  • subscription (SEP-2575) — subscriptions/listen held-open SSE, delivering the resources/updated change publishNote fires.

ExampleMcpServer (a v2 McpRestServlet) registers every surface above; ExampleServer is an embedded-Jetty launcher (Microservice + JettyConfiguration, main(...) plus a start(port) handle); ExampleClient is a guided, numbered McpClient walkthrough printing each step's request/response top-to-bottom like a tutorial; and a Spring Boot variant (SpringExampleMcpServer/SpringExampleApplication) uses SpringMcpRestServlet to resolve a Spring-managed GreetingService through the per-request BeanStore — the distinguishing feature of SpringMcpRestServlet over the plain McpRestServlet. ExampleMcpEndToEnd_Test boots the server in-process and drives the real client through every surface, asserting each outcome. The module packages jar (deliberately simpler than juneau-examples-core's bundle/assembly ceremony — this is teaching code) but still ships in the Apache release as a standalone downloadable project zip via juneau-distrib, alongside juneau-examples-core.

See the MCP Recipes, juneau-rest-server-mcp, and juneau-rest-client-mcp topics for pointers to the module, and the module's own README.md for run instructions.

MCP 2026-07-28 — OAuth 2.1-secured variant of juneau-examples-mcp (secured/ package)​

The example module above gets a second, fully offline-runnable variant demonstrating the OAuth 2.1 authorization baseline end to end: org.apache.juneau.examples.mcp.secured wraps the identical notes service in a live resource-server gate, with a demo authorization server standing in for a real IdP so the whole walkthrough needs no external network access.

  • SecuredExampleMcpServer extends ExampleMcpServer — the entire security story is one createMcpOptions() override layering McpResourceServerConfig on top of the inherited notes surface: RS auth enabled, setResource(...) (the RFC 9728 resource identity / RFC 8707 default audience), a JwtTokenValidator checking iss/aud/exp/nbf against the offline AS, addAuthorizationServer(...), a baseline addRequiredScope("mcp.read"), and SEP-2350 per-operation step-up (addOperationScope("tools/call", "publishNote", "mcp.write"), likewise for deleteNote).
  • OfflineAuthorizationServer — a clearly-marked DEMO-ONLY stand-in AS: a fresh self-generated RSA signing key per run, an offline JWKSource handed straight to the validator (no JWKS HTTP fetch), a real RFC 6749 §4.4 client-credentials /token endpoint gated by a fixed scope allowlist (mcp.read/mcp.write, else invalid_scope), and an RFC 8414 /.well-known/oauth-authorization-server metadata document (issuer = its own loopback base URL, plus token_endpoint) enabling genuine discovery.
  • SecuredExampleServer/SecuredExampleClient — a launcher plus a real, discovery-driven end-to-end client walkthrough: an unauthenticated call rejected with 401 + WWW-Authenticate: Bearer, RFC 9728 PRM discovery (authorization_servers, scopes_supported), RFC 8414 AS-metadata discovery of the token_endpoint, a client-credentials token acquisition via juneau-rest-client-mcp-auth's McpTokenProvider, a successful mcp.read read, the same token rejected with 403 insufficient_scope on a write, and a step-up to mcp.read mcp.write that publishes successfully. Nothing is hardcoded — the token endpoint is discovered, not passed in.

See the OAuth 2.1 recipe for the full walkthrough, and the module's own README.md for run instructions.

MCP 2026-07-28 — client/server adoption convenience methods​

Four small, additive convenience methods land alongside the example module above, each replacing a small piece of repetitive code the example (and real-world adopters) would otherwise repeat at every call site. None deprecates or replaces the lower-level API it sits on top of.

  • CallToolResult.firstText() (juneau-bean-mcp-v20260728) — returns the text of the first TextContent block found while scanning the result's content list in order, or null if the list is empty/unset or contains no TextContent block. Kills the ((TextContent) result.getContent().get(i)).getText() cast-and-scan a caller who only wants the tool's text result would otherwise repeat everywhere.
  • McpClient.callToolText(String name, Map<String,Object> arguments) (juneau-rest-client-mcp-v20260728) — convenience for callTool(...) followed by CallToolResult.firstText(); null-safe if callTool(...) itself returns a null result.
  • ServerDiscoverResult.getServerInfo() (juneau-bean-mcp-v20260728) — null-safe shortcut for getMeta() == null ? null : getMeta().getServerInfo(), so a caller doesn't have to null-check the intermediate _meta envelope just to reach the server's Implementation identity. @BeanIgnored so it does not add a second, top-level serverInfo member to the server/discover wire format.
  • McpMrtrResumeContext.continuationAsString() (juneau-rest-server-mcp-v20260728) — shorthand for continuationAs(String.class), for the common case of a plain String continuation.
  • ElicitationResponses.getBoolean(ctx, id, field) / getString(ctx, id, field) (juneau-rest-server-mcp-v20260728) — decline/omission-safe typed reads of a single field from an elicitation answer: a missing id, a missing field, or a non-ACCEPT action (declined/cancelled) all resolve to a safe default (false for getBoolean, null for getString) instead of requiring the caller to null-check get(...) and its content map by hand.

See the confirm-delete recipe and the client recipe in MCP Recipes, the juneau-rest-server-mcp elicitation reference, and the juneau-rest-client-mcp client reference for these methods in context.

MCP 2025-06-18 — resource-not-found error code corrected to -32002​

2025-06-18-only bug fix. A resources/read for a URI the server does not serve now returns the JSON-RPC error code -32002 ("Resource not found") — the 2025-06-18 spec's dedicated missing-resource code — instead of the previous generic -32601 ("method not found"). This was one of the four "not found" kinds that the re-layered 2025-06-18 McpRevision initially collapsed onto -32601 (see the juneau-rest-server-mcp-v20250618 module notes above); resource-not-found is now split out and mapped correctly (McpRevision.CODE_RESOURCE_NOT_FOUND), applying to both exact-resource and template-backed reads. The remaining known-wrong -32601 mappings for tool-not-found and prompt-not-found are unchanged and still tracked as a separate follow-on. Migration: a 2025-06-18 client that special-cased -32601 to detect a missing resource should switch to -32002; a client already using the spec code needs no change. The 2026-07-28 adapter is unaffected (it reports -32602 for resource-not-found).

See juneau-rest-server-mcp for the resolution-order and error-code details.

OIDC Relying Party — injectable LoginStateStore SPI for clustered login​

juneau-rest-server-auth-oidc-rp gains a pluggable LoginStateStore SPI so an OIDC login flow can survive a redirect/callback landing on different nodes — i.e. clustered login without sticky sessions. Previously the transient state → (nonce, code_verifier, redirectTarget) map was per-JVM only.

  • New LoginStateStore interface — store(state, PendingLogin) + atomic single-use consume(state), with a framework-owned MAX_TTL ceiling (30 min). PendingLogin (moved onto LoginStateStore) now carries a framework-minted expiresAt and a toString() that redacts the secret-bearing nonce / code_verifier.
  • Builder.loginStateStore(...) — inject a shared (Redis / JDBC) store for clustered deployments; the stateNonceTtl cap (0, MAX_TTL] is enforced on the custom-store path too, not just the default implementation.
  • Consume-time fail-closed re-checks — after consume(state) and before the token exchange, the framework re-validates redirectTarget via safeRelativePath (a writable shared store cannot smuggle in an off-site redirect) and re-checks expiresAt against its own clock (a null-timestamp or over-TTL record is rejected as expired rather than trusted). The store is treated as an untrusted blob store, not an oracle.
  • Default implementation renamed EphemeralStore → InMemoryLoginStateStore. As this module is new in 10.0.0, the rename, the PendingLogin move onto LoginStateStore, and the new store(String, PendingLogin) signature are source-level deltas within the unreleased line — no migration for released users.

See OIDC Relying Party Login for the clustering guidance and store contract.

juneau-marshall​

Duration/Period wire format controls​

  • Added new enum strategies:
    • org.apache.juneau.DurationFormat (ISO_8601, ISO_8601_WITH_DAYS, NANOS, MILLIS, SECONDS, HOCON)
    • org.apache.juneau.PeriodFormat (ISO_8601, DAYS)
  • Added global marshalling-context settings:
    • MarshallingContext.Builder.durationFormat(...) / periodFormat(...)
  • Added annotation-level overrides:
    • @Marshalled(durationFormat=..., periodFormat=...)
    • @MarshalledProp(durationFormat=..., periodFormat=...)
    • @MarshalledConfig(durationFormat=..., periodFormat=...)
  • Updated serializer sessions across JSON/HJSON/HOCON/UON/XML/HTML/Markdown/TOML/INI/CSV/PROTO plus BSON/CBOR/MessagePack so configured duration/period formats are honored consistently. Binary serializers emit native numeric wire types for numeric duration formats.
  • Parsers now sniff duration/period wire shapes more broadly and support mixed input styles, with parser-side format settings acting as hints for ambiguous numeric values.

Migration note (default behavior change):

  • Default Duration serialization now uses ISO_8601_WITH_DAYS (for example Duration.ofHours(48) now writes as 'P2D' instead of 'PT48H').

  • To restore prior wire output style, configure serializers explicitly:

    JsonSerializer s = JsonSerializer.create()
    .marshallingContext(MarshallingContext.create().durationFormat(org.apache.juneau.DurationFormat.ISO_8601))
    .build();

Date/time + Locale wire format controls​

This is a major-release breaking change. The legacy Temporal*Swap inner-class family and named root-level temporal/locale/timezone swaps are removed without deprecation shims; configure formats via MarshallingContext or the new annotations instead.

  • Added new enum strategies under org.apache.juneau:
    • CalendarFormat (default ISO_OFFSET_DATE_TIME; ISO variants, RFC_1123_DATE_TIME, MILLIS, plus an opt-in XML_FORMAT for Calendar/GregorianCalendar).
    • DateFormat (default ISO_LOCAL_DATE_TIME; ISO variants, RFC_1123_DATE_TIME, MILLIS).
    • TemporalFormat (default DEFAULT = per-subtype default; ISO variants, RFC_1123_DATE_TIME, ISO_YEAR, ISO_YEAR_MONTH, MILLIS).
    • TimeZoneFormat (default ID; shared by TimeZone and ZoneId; OFFSET, NAME_LONG, NAME_SHORT).
    • LocaleFormat (default BCP_47; also UNDERSCORE).
  • Added global marshalling-context settings:
    • MarshallingContext.Builder.calendarFormat(...) / dateFormat(...) / temporalFormat(...) / timeZoneFormat(...) / localeFormat(...).
    • Mirrored on MarshallingContextable.Builder so every serializer/parser builder inherits the fluent setters with concrete return types.
  • Added annotation-level overrides on the three Marshalled annotations:
    • @Marshalled(calendarFormat=..., dateFormat=..., temporalFormat=..., timeZoneFormat=..., localeFormat=...)
    • @MarshalledProp(calendarFormat=..., dateFormat=..., temporalFormat=..., timeZoneFormat=..., localeFormat=...)
    • @MarshalledConfig(calendarFormat=..., dateFormat=..., temporalFormat=..., timeZoneFormat=..., localeFormat=...)
  • Implemented uniform precedence resolution in MarshalledPropertyPostProcessor: @MarshalledProp > @Marshalled > MarshallingContext setting > enum default. Resolution installs StringSwap instances built directly from the enum's format/parse methods on the matching property type (Calendar/GregorianCalendar, Date, TemporalAccessor subtypes, TimeZone/ZoneId, Locale). XMLGregorianCalendar is hard-wired to XML lexical format regardless of any configured CalendarFormat; the CalendarFormat.XML_FORMAT constant is the opt-in path for Calendar/GregorianCalendar.
  • Updated serializer and parser sessions across JSON/HJSON/HOCON/UON/XML/HTML/Markdown/TOML/INI/CSV/PROTO plus BSON/CBOR/MessagePack and Parquet so configured calendar/date/temporal formats are honored at the root level. Iso8601Utils gained format-aware overloads that route through the session's resolved format.
  • Downgraded TimeZoneSwap, ZoneIdSwap, and LocaleSwap to thin delegators that read the resolved TimeZoneFormat / LocaleFormat from the active session, so root-level (de)serialization of TimeZone/ZoneId/Locale follows the same context configuration as bean properties.

Removed (hard break, no @Deprecated):

  • org.apache.juneau.marshall.swaps.TemporalCalendarSwap and all 17 inner-class variants (IsoOffsetDateTime, IsoInstant, IsoLocalDateTime, etc.).
  • org.apache.juneau.marshall.swaps.TemporalDateSwap and all 17 inner-class variants.
  • org.apache.juneau.marshall.swaps.TemporalSwap and all 18 inner-class variants.
  • org.apache.juneau.marshall.swaps.XMLGregorianCalendarSwap (its always-XML behavior is now built into the post-processor for XMLGregorianCalendar properties; the XML_FORMAT CalendarFormat constant covers the opt-in case for Calendar).

Notable behavior — MILLIS for TemporalFormat:

  • Instant, OffsetDateTime, ZonedDateTime use toInstant().toEpochMilli().
  • LocalDateTime uses ldt.toInstant(ZoneOffset.UTC).toEpochMilli().
  • LocalDate uses ld.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli().
  • YearMonth and Year use the first day at midnight UTC.
  • LocalTime and MonthDay fall back to the per-subtype DEFAULT ISO string form.
  • This asymmetry is documented in the MILLIS constant's Javadoc.

Migration:

  • Replace any @Swap(TemporalCalendarSwap.IsoXxx.class) with @MarshalledProp(calendarFormat=CalendarFormat.ISO_XXX).
  • Replace @Swap(TemporalDateSwap.IsoXxx.class) with @MarshalledProp(dateFormat=DateFormat.ISO_XXX).
  • Replace @Swap(TemporalSwap.IsoXxx.class) with @MarshalledProp(temporalFormat=TemporalFormat.ISO_XXX).
  • For context-level configuration, replace swaps(TemporalXxxSwap.IsoYyy.class) on serializer/parser builders with xxxFormat(XxxFormat.ISO_YYY).

Binary + Enum wire format controls​

This is a major-release breaking change. The legacy global ByteArraySwap family, the OutputStreamSerializer.Builder / InputStreamParser.Builder binaryFormat(...) setters, and the useEnumNames(boolean) setter are removed without deprecation shims; configure these via MarshallingContext or the new annotations instead.

  • Extended org.apache.juneau.BinaryFormat with two new constants:
    • NOT_SET — sentinel meaning "no value configured" (falls through to the next-higher precedence level; mirrors the convention used by every other <Type>Format enum).
    • BASE64_URL — RFC 4648 §5 URL-safe Base64 encoding (- / _ instead of + / /, no padding on the wire but accepted on input).
  • BinaryFormat now exposes format(byte[]) and parse(String) methods so a single enum drives both serialize and parse paths. The parse(String) method is intentionally format-agnostic — it sniffs the wire shape (spaced hex, URL-safe Base64, hex, standard Base64) and decodes accordingly so parsers accept any of the formats produced by format(byte[]) regardless of the configured constant.
  • Added new org.apache.juneau.EnumFormat with the constants NOT_SET, TO_STRING (default; calls Enum.toString()), NAME (calls Enum.name()), LOWER_HYPHEN (e.g. MY_VALUE → my-value), UPPER_HYPHEN, LOWER_UNDERSCORE, LOWER, UPPER, and ORDINAL (numeric on the wire). format(Enum) and parse(String, Class) cover both directions; the parser is lenient (case-insensitive, hyphen/underscore-insensitive, accepts ordinal as integer) so any wire shape produced by any constant round-trips.
  • Added global marshalling-context settings on MarshallingContext.Builder:
    • binaryFormat(BinaryFormat) (default NOT_SET).
    • enumFormat(EnumFormat) (default TO_STRING).
    • Mirrored on MarshallingContextable.Builder so every serializer / parser builder inherits the fluent setters with concrete return types.
  • Added annotation-level overrides on the three Marshalled annotations:
    • @Marshalled(binaryFormat=..., enumFormat=...)
    • @MarshalledProp(binaryFormat=..., enumFormat=...)
    • @MarshalledConfig(binaryFormat=..., enumFormat=...)
  • Implemented uniform precedence resolution in MarshalledPropertyPostProcessor: @MarshalledProp > @Marshalled > MarshallingContext setting > enum default. Resolution installs the appropriate session-aware ObjectSwap on each byte[] / enum bean property. For EnumFormat.ORDINAL the swap produces a Number so binary serializers emit a native int.
  • Downgraded the global byte[] default swap (org.apache.juneau.marshall.swaps.BinarySwap) to a thin session-aware delegate that reads the resolved BinaryFormat from the active MarshallingContext at swap time. Skips binary serializers (BSON / CBOR / MsgPack / Proto / Parquet emit native bytes), CSV (defers to csv.CsvByteArrayCellFormat), and OpenAPI (uses its schema-directed BYTE / BINARY / BINARY_SPACED encoding). When the resolved format is NOT_SET, the swap is a no-op so byte[] falls back to the surrounding serializer's native array representation (e.g. JSON emits [1,2,3]).
  • OutputStreamSerializerSession#serializeToString(Object) now always emits BinaryFormat.HEX regardless of the configured binaryFormat, providing a stable copy-pasteable hex dump for debug / display irrespective of the surrounding context's wire-format setting.

Removed (hard break, no @Deprecated):

  • org.apache.juneau.marshall.swaps.ByteArraySwap and the Base64, Hex, SpacedHex inner-class variants. Replace @Swap(ByteArraySwap.Base64.class) etc. with @MarshalledProp(binaryFormat=BASE64) etc.
  • OutputStreamSerializer.Builder.binaryFormat(BinaryFormat) and InputStreamParser.Builder.binaryFormat(BinaryFormat). The setting is now on MarshallingContext.Builder (and inherited by every serializer / parser builder via MarshallingContextable.Builder) so the same wire-format setting drives both binary and textual sessions.
  • BeanContext.Builder.useEnumNames(boolean) and the useEnumNames field on MarshallingContext. Replace useEnumNames() with enumFormat(EnumFormat.NAME); replace @Bean(useEnumNames=true) with @Marshalled(enumFormat=EnumFormat.NAME).

Renamed:

  • org.apache.juneau.marshall.csv.ByteArrayFormat → org.apache.juneau.marshall.csv.CsvByteArrayCellFormat. The new name disambiguates the CSV-only cell-encoding role from the new top-level BinaryFormat.

Migration:

  • Replace @Swap(ByteArraySwap.Base64.class) with @MarshalledProp(binaryFormat=BinaryFormat.BASE64).
  • Replace @Swap(ByteArraySwap.Hex.class) with @MarshalledProp(binaryFormat=BinaryFormat.HEX).
  • Replace @Swap(ByteArraySwap.SpacedHex.class) with @MarshalledProp(binaryFormat=BinaryFormat.SPACED_HEX).
  • Replace OutputStreamSerializer.Builder#binaryFormat(...) / InputStreamParser.Builder#binaryFormat(...) calls with the inherited binaryFormat(...) from MarshallingContextable.Builder (no source change needed for builder chains; the inherited setter has the same signature).
  • Replace useEnumNames() / useEnumNames(true) with enumFormat(EnumFormat.NAME).

UUID + BigInteger/BigDecimal wire format controls​

  • Added new enum strategies under org.apache.juneau:
    • UuidFormat (default STANDARD; also NO_DASHES for the 32-hex-character compact form, and URN for the RFC 4122 urn:uuid: namespace).
    • BigNumberFormat (default NUMBER; also STRING for always-quoted output, and AUTO which emits a bare numeric token when the value is JS-safe (|v| ≤ 2^53−1) and a quoted string otherwise to avoid silent precision loss in JavaScript clients).
  • Both enums expose static format(...) and parse(...) helpers; parsers are intentionally lenient and accept every textual shape produced by any constant regardless of the configured value (the setting is informational only on parse).
  • Binary serializers (BSON / CBOR / MsgPack) continue to emit native UUID bytes and native big-number types regardless of the textual format setting; the new enums apply to textual wires only.
  • Added global marshalling-context settings on MarshallingContext.Builder:
    • uuidFormat(UuidFormat) (default STANDARD).
    • bigNumberFormat(BigNumberFormat) (default NUMBER).
    • Mirrored on MarshallingContextable.Builder so every serializer / parser builder inherits the fluent setters with concrete return types.
  • Added annotation-level overrides on the three Marshalled annotations:
    • @Marshalled(uuidFormat=..., bigNumberFormat=...)
    • @MarshalledProp(uuidFormat=..., bigNumberFormat=...)
    • @MarshalledConfig(uuidFormat=..., bigNumberFormat=...)
  • Extended uniform precedence resolution in MarshalledPropertyPostProcessor: @MarshalledProp > @Marshalled > MarshallingContext setting > enum default. Resolution installs an ObjectSwap<T, Object> on UUID / BigInteger / BigDecimal properties whose swap(...) returns either a Number (bare numeric token) or a String (quoted), so binary serializers continue to see native types and text serializers branch on the formatted value. For BigNumberFormat.AUTO the swap dynamically picks Number vs String per value.

This completes the umbrella wire-format-control plan (Phases 1, 2, and 3 are all delivered in 10.0.0).

Boolean / Float / Currency / Class wire format controls​

Round-2 follow-up extending the MarshallingContext format-control surface to four additional types. Same template (precedence chain, lenient parsers, binary-native fall-through) as the UUID / BigNumber phases.

  • Added four new enums under org.apache.juneau:
    • BooleanFormat (default TRUE_FALSE; also ZERO_ONE for numeric 0/1 tokens, YES_NO / Y_N / ON_OFF for textual forms). ZERO_ONE emits a bare numeric token (not a quoted string) on JSON.
    • FloatFormat (NaN_AS_NULL (default per spec, but install-skipped on primitive float/double — see note below), NaN_AS_STRING for quoted "NaN" / "Infinity" / "-Infinity", NaN_AS_NUMBER for the bare token (non-strict JSON), NaN_AS_ERROR for fail-fast).
    • CurrencyFormat (default ISO_CODE — locale-independent, round-trip-safe; also SYMBOL and DISPLAY_NAME which are locale-sensitive and best-effort on parse).
    • ClassFormat (default FQCN — Class.getCanonicalName() with getName() fallback for local/anonymous classes; also BINARY_NAME for Class.getName() with $ separators, and SIMPLE_NAME for Class.getSimpleName() which is serialize-only — parsing SIMPLE_NAME throws UnsupportedOperationException).
  • Each enum exposes static format(...) / parse(...) helpers; parsers are intentionally lenient and accept every textual shape produced by any constant regardless of the configured value (the setting is informational only on parse).
  • Added global marshalling-context settings on MarshallingContext.Builder:
    • booleanFormat(BooleanFormat) (default TRUE_FALSE).
    • floatFormat(FloatFormat) (default NaN_AS_NULL).
    • currencyFormat(CurrencyFormat) (default ISO_CODE).
    • classFormat(ClassFormat) (default FQCN).
    • Mirrored on MarshallingContextable.Builder and overridden on Serializer.Builder / Parser.Builder so every serializer / parser builder inherits the fluent setters with concrete return types.
  • Added annotation-level overrides on the three Marshalled annotations:
    • @Marshalled(booleanFormat=..., floatFormat=..., currencyFormat=..., classFormat=...)
    • @MarshalledProp(booleanFormat=..., floatFormat=..., currencyFormat=..., classFormat=...)
    • @MarshalledConfig(booleanFormat=..., floatFormat=..., currencyFormat=..., classFormat=...)
  • Extended uniform precedence resolution in MarshalledPropertyPostProcessor: @MarshalledProp > @Marshalled > MarshallingContext setting > enum default. Resolution installs an ObjectSwap on Boolean / Float / Double / Currency properties as appropriate. Binary serializers continue to see native primitives via the swap's swap(...) short-circuit on OutputStreamSerializerSession.

Breaking changes:

  • ClassSwap deleted outright. The legacy org.apache.juneau.marshall.swaps.ClassSwap is removed in 9.6. Its responsibilities split into two replacements:
    • For bean properties, an ObjectSwap<Class, String> is installed by MarshalledPropertyPostProcessor when @MarshalledProp(classFormat=...) or @Marshalled(classFormat=...) is configured.
    • For standalone Class<?> values (root-level, map keys, list elements), a new org.apache.juneau.marshall.swaps.ClassFormatSwap is registered in DefaultSwaps and reads MarshallingContext.classFormat at swap-time.
    • Default behavior (FQCN ≈ Class.getCanonicalName()) is functionally compatible with the old ClassSwap.getName() for top-level classes. Differences: FQCN returns int[] for int[].class (vs. [I from Class.getName()); use BINARY_NAME if you need the JVM binary form. Callers that explicitly wanted getSimpleName() must move to @MarshalledProp(classFormat=ClassFormat.SIMPLE_NAME).
    • Replace @Swap(impl=ClassSwap.class) with @MarshalledProp(classFormat=ClassFormat.FQCN) (or simply remove it — the new default matches old behavior for top-level classes).

Notable caveats:

  • FloatFormat.NaN_AS_NUMBER emits the bare NaN / Infinity / -Infinity token, which is not strict JSON per RFC 8259. Use only with lenient consumers (Juneau's own JSON parser, V8, several Python libraries). Strict consumers reject these tokens.
  • FloatFormat on primitive float / double fields: Juneau's bean machinery applies a null-to-primitive-default convention (e.g. m.put("d1", null) → d1 = 0.0) that conflicts with swap-driven NaN_AS_NULL semantics. To preserve this contract, the post-processor installs the float swap only on boxed Float / Double properties for context-level format settings. Explicit @MarshalledProp(floatFormat=...) or @Marshalled(floatFormat=...) overrides install for the explicit type the user declared. Net effect: declaring public Double n rather than public double n is required when you want the wire format to differ from the bare-token default.
  • CurrencyFormat.SYMBOL / DISPLAY_NAME round-trip is best-effort and locale-dependent on parse. ISO_CODE is the only constant with strict round-trip guarantees — $ can resolve to USD / CAD / AUD / etc.; the parser prefers the locale's default currency on ambiguity and throws IllegalArgumentException when no match is found.
  • ClassFormat.SIMPLE_NAME is serialize-only — parse(...) throws UnsupportedOperationException. Use FQCN (default) or BINARY_NAME for round-trippable wires.

Format-control round-trip hardening​

Closes out the format-control extension work with a cross-serializer/parser round-trip test matrix over all 16 *Format enums and 8 additional TemporalFormat subtypes. The matrix surfaced and closed 18 production bugs across the format-control dispatch layer; new public helpers and capabilities were added to support the fixes.

Highlights:

  • TemporalFormat carryover — all 10 supported TemporalAccessor subtypes (Instant, LocalDateTime, LocalDate, OffsetDateTime, ZonedDateTime, Year, YearMonth, LocalTime, OffsetTime, MonthDay) now round-trip across every TemporalFormat value. Date-bearing-only / time-bearing-only formats are handled lossily via the existing DefaultingTemporalAccessor mechanism (year defaults to 1970, time defaults to 00:00:00, etc.).
  • MonthDay support — java.time.MonthDay is a TemporalAccessor but not a Temporal, so it was previously falling through to standard bean serialization (and failing on parse for lack of a public no-arg constructor). Now fully round-trippable via a new sibling TemporalAccessor-keyed swap factory; configured TemporalFormat values are silently ignored for MonthDay properties (the only stable wire shape is the native --MM-DD toString() / parse() form, since no DateTimeFormatter from the standard ISO family matches that pattern).
  • hasNativeBytes() capability check — new public methods on OutputStreamSerializerSession and InputStreamParserSession (default true) consulted by BinarySwap.match and MarshalledPropertyPostProcessor.binarySwap. Overridden to false on ParquetSerializerSession / ParquetParserSession / RdfStreamSerializerSession / RdfStreamParserSession. Wire-format change: at any non-NOT_SET BinaryFormat, Parquet and the two binary RDF serializers (RDF/Thrift, RDF/Proto) now route byte[] through the configured text wire form (UTF-8 BASE64 / HEX / SPACED_HEX string column or literal) instead of raw bytes / xsd:base64Binary typed literals. MsgPack / CBOR / BSON are unchanged at every format. At BinaryFormat.NOT_SET, behavior is unchanged (Parquet uses its native byte[] column type, RDF/Thrift and RDF/Proto use plain string literals containing the raw bytes). Flag in migration guide.
  • 18 production bugs closed across the format-control dispatch layer. Aggregated by theme:
    • Format-hint correctness across binary serializers — MsgPack INT8 / INT16 / NEGFIXINT sign-extension on parse (negative Period / BigInteger values no longer come back as unsigned magnitudes); Proto text-format Float / Double decimal-literal sign loss and 0-prefix tokenization for 0.x / 0eN / 0f shapes; Proto nan / inf identifier-vs-token disambiguation.
    • JSON-family precision tier (Bug #5) — StringUtils.parseNumber no longer auto-classifies bare decimals as Float when targeting Number / Object / Double. JSON / JSON5 / JSONL / XML / HTML / UON / UrlEncoding / JCS parsers now return Double for parse("3.14", Object.class) and preserve full double precision in Double bean properties. Caller-visible if you were instance-checking the parsed Number for Float — see migration guide.
    • Bean-property Map<Enum, V> key coercion (Bug #7b) — Hjson / Hocon / Proto / Bson parsers now thread the bean property's ClassMeta<K> for the map key into the key-coercion step, so typed Map<MyEnum, V> properties round-trip cleanly at every EnumFormat value. Proto-specific ordinal-keyed maps (EnumFormat.ORDINAL) now emit bare integer field tags (0: "first") instead of quoted-string keys, which previously triggered Proto's adjacent-string-literal concatenation on parse.
    • Parquet schema fidelity (Bugs #7a, #11) — Parquet now correctly round-trips UUID (via LOGICAL_TYPE_UUID), Class<?> (via the default-swap dispatch), Enum at EnumFormat.ORDINAL (via INT32 column), and byte[] (via the TYPE_BYTE_ARRAY schema element without convertedType).
    • Parser dispatch for collection-element / top-level byte[] (Bug #12) — Toml / Proto / Hjson / Hocon parsers now consult BinarySwap.unswap for byte[] collection elements and top-level values at non-NOT_SET formats, so wire forms like "0001ff807f102030" (HEX) decode to the correct 8-byte array instead of a 16-byte ASCII char array.
    • BinaryFormat.BASE64_URL hint (Bug #9) — BinaryFormat.parse(String) now honors the BASE64_URL constant directly (routing to Base64.getUrlDecoder() which accepts missing padding) before falling through to the format-agnostic wire-shape sniff. Non-3-aligned URL-safe payloads without - / _ chars no longer fail with "Invalid BASE64 string length".
    • Hocon parser-fragility (Bugs #10, #15) — HoconWriter.QUOTE_VALUE_CHARS extended to cover = (BASE64 padding), + (+NNNN timezone offsets), and $ (nested-class BINARY_NAME); HoconTokenizer recursion guard now throws a clean IOException instead of StackOverflowError; HoconParserSession.parseArray / parseObject array-flatten bug fixed (nested arrays no longer collapse via spurious array-concatenation).
    • HoconTokenizer =+ lookahead bug — HoconTokenizer.readTokenImmediate incorrectly guarded the EQUALS emission with peekChar() != '+', causing any =+ character sequence in the input to throw IOException("Unexpected character '='") instead of emitting EQUALS. Since the += (PLUS_EQUALS) operator is already handled by an independent branch that reads + first, the lookahead on the = branch was both unnecessary and wrong. The guard has been removed; = is now unconditionally emitted as EQUALS.
    • ClassFormat.FQCN nested-types / arrays parser-fragility (Bug #16) — ClassFormat.parse now resolves nested types (java.util.Map.Entry → java.util.Map$Entry), array suffixes (int[], java.lang.String[][]), and leaf primitives (Class.forName("int") previously rejected; now resolves through a primitive-name table).
    • Currency default-swap registration (Bug #6) — Currency added to DefaultSwaps (sibling of the existing Locale / TimeZone / ZoneId entries) so Currency bean properties round-trip at the default CurrencyFormat.ISO_CODE / NOT_SET levels.
    • UrlEncoding-expanded empty byte[] (Bug #13) — empty byte[] properties now round-trip as byte[0] instead of null through the expanded-params UrlEncoding flavor.
    • MonthDay round-trip support (Bug #18) and OffsetTime × MILLIS swap asymmetry (Bug #17) — see "API changes" below.

API changes:

  • TemporalFormat.format(...) and parse(...) signatures widened from Temporal to TemporalAccessor:

    • format(Temporal, ZoneId) → format(TemporalAccessor, ZoneId)
    • parse(String, Class<? extends Temporal>, ZoneId) → parse(String, Class<? extends TemporalAccessor>, ZoneId)

    Source-compatible for callers passing Temporal instances (every existing Temporal is a TemporalAccessor). Required so MonthDay (which is a TemporalAccessor but not a Temporal) reaches the dispatch path. Flag in migration guide.

  • New TemporalFormat.isMillisNumeric(Class<? extends TemporalAccessor>) static helper — returns true iff format(value, MILLIS) emits a Long-parseable numeric string for the subtype. Returns false for LocalTime, OffsetTime, and MonthDay (the three subtypes whose MILLIS branch falls back to the per-subtype DEFAULT ISO string form). Single source of truth for the MILLIS wire-type decision; consumed by both temporalSwap and the new temporalAccessorSwap factory.

  • New MarshallingContext capability methods on the binary session base classes (default true; see "Highlights" above for the wire-format implications):

    • OutputStreamSerializerSession.hasNativeBytes()
    • InputStreamParserSession.hasNativeBytes()
  • New MarshalledPropertyPostProcessor internals — sibling temporalAccessorSwap swap factory keyed at TemporalAccessor.class (vs temporalSwap's Temporal.class), and a new isTemporalAccessorType(Class<?>) predicate (negative-space complement: TemporalAccessor.class.isAssignableFrom(c) && ! Temporal.class.isAssignableFrom(c)). Written as the negative-space complement so a future JDK addition of another non-Temporal TemporalAccessor is picked up automatically. Wired symmetrically across the three install paths (applyPropertyFormats, applyClassFormats, applyContextFormats).

  • OffsetTime × TemporalFormat.MILLIS now falls back to ISO_OFFSET_TIME (sibling of the existing LocalTime and MonthDay carve-outs) — OffsetTime has no canonical epoch-millis interpretation. The carve-out is reflected in the updated MILLIS constant Javadoc and consumed via isMillisNumeric.

Performance:

  • IniParserSession and MarkdownParserSession now memoize their internal Json5Parser instance per session instead of constructing a fresh Json5Parser.DEFAULT per parse call. Behavior-preserving; eliminates repeated builder-and-instance construction in tight-loop parsing of INI / Markdown content.

Build-config change (developer-only):

  • New .mvn/maven.config containing --also-make. Affects developers running mvn -pl <module> … from the workspace root: upstream modules are now automatically added to the active reactor instead of being resolved from ~/.m2/repository. Inert without -pl, so root-level builds (mvn clean install, ./scripts/test.py, ./scripts/push.py) and CI are unaffected. Fixes a long-standing developer-loop footgun where in-tree edits to upstream modules wouldn't be picked up by mvn -pl juneau-utest test without an intervening mvn install in the upstream module.

Typed JsonSchema bean generation bridge​

  • Added JsonSchemaBeanGenerator in juneau-bean-jsonschema to generate typed JsonSchema beans from Java types via JsonSchemaGenerator.
  • Added static convenience factories on JsonSchema: JsonSchema.of(Type) and JsonSchema.of(Class<?>).
  • Added JsonSchema bean support for format, $comment, and deprecated so bean output preserves generator metadata.
  • Added tests covering JsonSchemaBeanGenerator output parity with JsonSchemaGenerator map output and round-trip conversion.

Schema validation mode for parsers and serializers​

  • Added an opt-in validateSchema flag on MarshallingContext that validates bean-property values against the constraints declared by @Schema annotations during both parsing (value set on the bean) and serialization (value read from the bean).
  • Backed by a new JsonSchemaValidator in juneau-bean-jsonschema that implements JSON Schema Draft 2020-12 semantics on top of the typed JsonSchema bean.
  • Added new commons-side SPI (PropertyValidator / PropertyValidatorFactory / PropertyValidators) so marshall and juneau-bean-jsonschema stay decoupled. The factory is discovered via ServiceLoader; when juneau-bean-jsonschema is absent, the flag becomes a silent no-op.
  • Added fluent enablers: MarshallingContext.Builder.validateSchema() / validateSchema(boolean), propagated through MarshallingContextable.Builder and every parser/serializer/REST-client builder subclass so chains like JsonParser.create().validateSchema().build() keep their concrete return type.
  • Added @BeanConfig(validateSchema="true") so the flag can be configured by annotation (with VarResolver.DEFAULT interpolation).
  • Validation failures throw SchemaValidationException; parsers surface them as ParseException and serializers as SerializeException.
  • Supported keywords (v1): type, enum, const, numeric (minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf), string (minLength, maxLength, pattern), array (minItems, maxItems, uniqueItems, items), object (minProperties, maxProperties, required, properties).
  • Example:
    public class MyBean {
    @Schema(minLength=2, maxLength=10) public String name;
    @Schema(minimum="0", maximum="150") public int age;
    }
    ReaderParser p = JsonParser.create().validateSchema().build();
    p.parse("{\"name\":\"a\"}", MyBean.class); // throws ParseException - too short
  • Added a summary field (alias su) to @Schema, designed to hold a short, single-line description suitable for AI / LLM consumption, compact docs, and tooltips. Complements (and does not replace) the existing multi-line description field.
  • summary flows through SchemaAnnotation.asMap() and JsonSchemaGenerator into the generated JSON Schema output under the key "summary".
  • Added a paired summary field on companion annotations that already expose description: @Marshalled, @BeanProp, @Response, @Header, @Query, @Path, @FormData, @Content.
  • Added JsonSchema.getSummary() / setSummary(String) (and fluent overrides on JsonSchemaProperty and JsonSchemaRef), so the typed bean and the JsonSchemaBeanGenerator bridge surface the new field automatically.
  • Added Swagger / OpenAPI propagation: summary from @Schema is now copied into the generated Swagger Schema Object alongside description.
  • Example:
    @Schema(
    summary="A pet available for adoption",
    description={
    "Represents a pet in the store's inventory.",
    "Includes details such as name, species, breed, age, and adoption status."
    }
    )
    public class Pet { ... }

Neutral MarshalledMap / MarshalledList collections + Json5Map / Json5List​

A major refactor of the generic-model collection hierarchy. JsonMap / JsonList are no longer the single one-size-fits-all generic-model types: they are now strict-JSON-flavored subclasses of a new neutral, marshaller-agnostic base, with JSON5-flavored siblings in org.apache.juneau.marshall.json5.

Behavioral break. JsonMap.toString() and JsonList.toString() now return strict RFC 8259 JSON (was JSON5), and their (CharSequence) / (Reader) constructors and ofString(...) factories (renamed from ofJson(...)) now default to JsonParser.DEFAULT (was Json5Parser.DEFAULT). See the v10.0 Migration Guide for the full breakdown and the recommended fix paths.

New neutral base in org.apache.juneau.collections​

  • MarshalledMap (extends LinkedHashMap<String,Object>) and MarshalledList (extends LinkedList<Object>) — neutral base classes with no language coupling. Their default toString() is the inherited LinkedHashMap / LinkedList form.
  • Carry the full marshaller-agnostic surface: typed accessors (getInt, getLong, getBoolean, getString, getStringArray, getWithDefault, findKeyIgnoreCase, find, etc.), fluent setters (append, appendIf, appendIfAbsent, appendReverse, etc.), bean integration (cast, setBeanSession, getMarshallingSession), PathTraversal-driven path navigation (getAt, putAt, postAt, deleteAt), copy-with-filter helpers (include, exclude, keepAll, removeAll), inner-map plumbing (inner), nested-collection accessors (getMap, getList), and the standard EMPTY_MAP / EMPTY_LIST / unmodifiable() / modifiable() family.
JsonMap / JsonList retargeted to strict JSON (breaking)​
  • toString() now returns Json.of(this) — strict RFC 8259 JSON (was Json5.of(this)).
  • (CharSequence) / (Reader) constructors and the renamed ofString(...) static factories (previously ofJson(...)) now default to JsonParser.DEFAULT (was Json5Parser.DEFAULT).
  • ofText(...) remains available as a deprecated forwarding alias for compatibility with existing callers. Bare-constructor calls that feed JSON5 input (unquoted keys, single-quoted strings) into new JsonMap(...) will now throw ParseException.
  • The JSON-specific helpers toJson() / toJson5() / toJsonl() / toJcs() / toHjson() / toReadableJson5() stay on JsonMap / JsonList. toJson() is now the synonym for toString(), not toJson5().
New Json5Map / Json5List in org.apache.juneau.marshall.json5​
  • Json5Map (extends MarshalledMap) and Json5List (extends MarshalledList) carry the JSON5 flavor that JsonMap / JsonList used to carry: toString() returns Json5.of(this), (CharSequence) / (Reader) constructors default to Json5Parser.DEFAULT, single-arg ofString(...) static factories (same name as the neutral base), JSON5-flavored putJson5(...).
  • These are the drop-in replacement for callers that today rely on JsonMap.toString() producing JSON5 or on new JsonMap("{unquoted:'json5'}") parsing JSON5.
Parser sessions now produce flavored maps / lists (breaking)​

MarshallingSession now exposes two protected factory hooks — newGenericMap() and newGenericList() — that control the runtime type of maps / lists created during parsing into an unbound Object / Map<String,Object> / Collection<Object> target. The base session returns the neutral MarshalledMap / MarshalledList; per-language parser sessions override to return their flavored type:

Parserparse(text, Object.class) runtime type
JsonParser.DEFAULTJsonMap / JsonList (unchanged)
Json5Parser.DEFAULTJson5Map / Json5List (new — used to be JsonMap / JsonList)
Any other parser (XmlParser, YamlParser, UonParser, UrlEncodingParser, MsgPackParser, CborParser, BsonParser, HtmlParser, HoconParser, JsonlParser, MarkdownParser, HjsonParser, CsvParser, RdfXmlParser, etc.)MarshalledMap / MarshalledList (new — used to be JsonMap / JsonList)

Code that casts the result to JsonMap / JsonList (or uses instanceof JsonMap / instanceof JsonList) will throw ClassCastException against any non-JsonParser parser. Recommended fixes:

  1. Cast to the matching flavored type (Json5Map / Json5List), or
  2. Cast to the neutral base (MarshalledMap / MarshalledList) when flavor doesn't matter at the call site, or
  3. Pass an explicit target class to force the old runtime type: parser.parse(text, JsonMap.class). The hook is only consulted for Object / Map<String,Object> / Collection<Object> targets; explicit targets always win.

Nested maps / lists inside a parsed structure are also produced through the hook, so the flavor propagates uniformly through the parsed tree.

ResolvingJsonMap renamed to ResolvingMarshalledMap​

The SVL-resolving map (which moved into org.apache.juneau.collections as part of an earlier change) has been renamed to ResolvingMarshalledMap and re-parented from JsonMap to the new neutral MarshalledMap base. SVL resolution is language-agnostic, so the strict-JSON parent was no longer the right shape. Hard rename, no deprecation shim. See the SVL "Package Moves" table further down in this file and the v10.0 Migration Guide for the import update.

Migration​

The behavioral break around JsonMap.toString() / JsonList.toString(), the bare-constructor default parser change, and the parser-produced runtime-type change are all called out in detail in the v10.0 Migration Guide. The short version:

Old behaviorNew behaviorRecommended migration
JsonMap.toString() → JSON5strict JSONUse Json5Map (or Json5.of(map)) for JSON5 output.
new JsonMap("{unquoted:'json5'}") parses JSON5parses strict JSON (throws)Use new Json5Map(...) / Json5Map.ofString(...).
JsonMap.ofJson(text) / JsonList.ofJson(text) / Json5Map.ofJson5(text) / Json5List.ofJson5(text)JsonMap.ofString(text) / JsonList.ofString(text) / Json5Map.ofString(text) / Json5List.ofString(text)Single-arg parse factories are now named ofString(...) for consistency with string-based parse APIs across map/list types. ofText(...) is retained as a deprecated compatibility alias. No behavioral change — the subclass single-arg form still uses the language-specific default parser. (Note: JsonList.ofJsonOrCdl(String) and Json5List.ofJson5OrCdl(String) were NOT renamed.)
Json5Parser.DEFAULT.parse(s, Object.class) → JsonMap→ Json5MapCast to Json5Map, or to neutral MarshalledMap, or pass parser.parse(s, JsonMap.class).
Other parsers' parse(s, Object.class) → JsonMap→ MarshalledMapSame fix options.
import org.apache.juneau.collections.ResolvingJsonMap;import org.apache.juneau.collections.ResolvingMarshalledMap;Hard rename; update imports and any new ResolvingJsonMap(...) constructor calls.
Future work (Phase D — not delivered in 10.0)​

Per-language flavored XMap / XList pairs for the remaining marshallers (XmlMap / XmlList, YamlMap / YamlList, UonMap / UonList, HoconMap / HoconList, BsonMap / BsonList, MsgPackMap / MsgPackList, CborMap / CborList, etc.) are planned as a follow-on. Until they land, parser sessions other than JsonParserSession / Json5ParserSession produce the neutral MarshalledMap / MarshalledList.

MessagePack media type normalized to application/msgpack​

  • MsgPackSerializer and MsgPackParser now use the RFC 6838-conforming application/msgpack as the canonical media type. The previous invalid octal/msgpack value (octal is not a registered MIME top-level type) has been replaced.
  • The serializer emits Content-Type: application/msgpack only, and MsgPackSerializer.DEFAULT .getResponseContentType() (and getPrimaryMediaType()) now return application/msgpack.
  • For backward compatibility, both the serializer's Accept media-type list and the parser's consumes list retain octal/msgpack as an alias, so existing wire payloads and clients that still send Content-Type: octal/msgpack or Accept: octal/msgpack continue to be matched and decoded.
  • Updated the RestClient.msgPack() / RestRequest.msgPack() Javadoc to document the new media-type value sent on the Accept and Content-Type request headers.

juneau-marshall bug fixes​

  • Parquet serializer compression codec now fails fast (breaking). An unknown @ParquetConfig(compressionCodec) value (e.g. a typo) or a codec the serializer cannot write (SNAPPY, which is decode-only) now throws an IllegalArgumentException naming the bad value and the valid write codecs (UNCOMPRESSED, GZIP) instead of silently falling back to UNCOMPRESSED. ParquetSerializer.Builder.compressionCodec(CompressionCodec.SNAPPY) throws likewise. The parser still reads SNAPPY-compressed files.

  • Removed the unused @Parquet.parquetType() and @Parquet.logicalType() attributes (breaking). Neither was read by the serializer or parser, so setting them never had any effect. @Parquet no longer declares any attributes; remove them from any source that sets them (otherwise it will no longer compile).

  • @Inject-method and constructor @Value Optional<T> parameters now wrap resolved values correctly. Previously, declaring a parameter as @Value("${maybe}") Optional<String> caused IllegalArgumentException at reflective invoke because ParameterInfo.resolveValue returned the unwrapped scalar. ParameterInfo.resolveValue now mirrors FieldInfo.inject: when the declared parameter type is Optional<T>, a missing or empty-string resolution becomes Optional.empty() and a non-empty resolved value becomes Optional.of(value). The plain-scalar workaround (declare as T + null-check) is no longer needed for Optional-typed parameters.

  • ClassFormatSwap.unswap(...) now consults the session classloader before falling back to the thread-context classloader. Sessions that explicitly set a classloader via MarshallingContext.Builder.classLoader(ClassLoader) — such as OSGi bundle classloaders, webapp classloaders, and plugin systems — now correctly resolve Class<?> values from that classloader during parsing. The same fix was applied to the inline per-property swap installed by MarshalledPropertyPostProcessor. The thread-context classloader is still used as a fallback when no session classloader is configured, preserving historical behavior for existing callers.

  • HoconTokenizer peek/skip invariant hardened. HoconTokenizer.skipWhitespaceAndComments() is now a no-op while a token has already been peeked (peek() / peekNoSkip() has cached a token). Once a token is cached the underlying reader sits after that token, so skipping whitespace would consume characters that semantically follow the cached token — most dangerously a newline that is the element separator of an enclosing array/object. HoconParserSession.parseArray / parseObject previously protected against this with targeted peek-before-skip guards; the invariant is now centralized in the tokenizer, and those guards have been removed. No behavior change for existing inputs (newline- separated nested arrays/objects still stay nested; same-line array/object concatenation still works); the change forecloses a class of latent bugs where a future caller of skipWhitespaceAndComments could silently reintroduce the over-read.

  • Abstract Collection-typed bean properties with no setter and no @BeanProp(type=...) now populate correctly. A field such as Set<MyEnum> tags; (or List / SortedSet / Deque / Queue, generic or raw) with only a public field and no concrete type hint previously could not be parsed from a JSON array — there was no concrete-collection materialization branch (the sibling abstract-Map case was already handled). BeanPropertyMeta.setPropertyValue(...) now materializes a concrete collection using a best-effort-then-fallback strategy: it first tries to instantiate the field's declared type directly (so a concrete AbstractSet / AbstractList subclass with an accessible no-arg constructor is honored as itself), and only falls back to a shape-based default when the declared type is genuinely not instantiable — SortedSet/NavigableSet → TreeSet, Set → LinkedHashSet (insertion order preserved), Deque/Queue → ArrayDeque, List/raw Collection → ArrayList. Element values are coerced to the declared element type. An explicit @BeanProp(type=...) override still wins, but it is no longer required as a workaround for these abstract-collection fields.

  • JSON/JSON5/UON parsers no longer drop U+007F (DEL) characters in strings that also contain escape sequences. A string value containing a literal DEL character alongside at least one escape sequence previously lost the DEL on parse; the character is now preserved.

  • TOML non-scalar lists now round-trip. TomlSerializer previously wrote List<Integer> as nums = ["1", "2"], and lists of beans, maps or lists via toString() (e.g. ["Q$In@74e52303"]), none of which TomlParser could read back. Scalar lists are now native arrays (nums = [1, 2]), lists of lists are nested arrays (grid = [[1], [2, 3]]), lists/arrays of beans or maps are arrays of tables ([[ins]]), maps inside mixed lists are inline tables, and empty lists are []. TomlParser also now resolves dotted table headers that pass through an array of tables (e.g. [[a]] then [a.b]) to the most recent element, per the TOML spec.

juneau-commons​

@Value annotation + ${xxx} shortcut​

  • Added org.apache.juneau.commons.inject.Value annotation for declarative injection of configuration values into BeanInstantiator-created beans (fields, constructor parameters, setter parameters, arbitrary method parameters). Resolves expressions through VarResolver.DEFAULT and coerces the resolved string to the target type via Settings.toType(...).
  • Extended JsrSupport with FQN-based detection of the new Juneau @Value (JUNEAU_VALUE) and Spring's @Value (SPRING_VALUE = "org.springframework.beans.factory.annotation.Value"), keeping juneau-commons compile-time clean of Spring. Either annotation is picked up by the same injection path.
  • Made @Value and @Inject mutually exclusive on the same site — applying both throws BeanCreationException with a clear "site already has @Value / @Inject" message.
  • Added the ${xxx} shortcut in VarResolverSession — the tokenizer rewrites ${...} to $P{...} before resolution, so it inherits the full PropertyVar source stack (system properties, env vars, registered Configs, Spring Environment). The first top-level : in the expression is translated to , so Spring-style ${key:default} defaulting works without further changes.
  • Made Settings.toType(...) public and extended it to also recognise static T parse(CharSequence) methods (in addition to parse(String)), so java.time types like Instant coerce cleanly.
  • Added Settings.removeSource(PropertySource) for explicit deregistration — required so per-context bridges (microservice, RestContext, Spring Environment) can clean up at shutdown without cross-test bleed.
  • Optional<T> field/parameter injection collapses null and empty-string results to Optional.empty() — matching the "missing-key" intuition that consumers expect.

SVL overhaul: #{...} scripting + VarTemplate compilation​

This is a major-release breaking change in the SVL surface. The 11 single-purpose transformation Var classes are removed without @Deprecated shims; their behaviour is recovered through the new #{name(args)} function-call syntax.

New #{...} function-call syntax​

  • Introduced the #{name(arg1, arg2, ...)} script-call form inside any SVL string. Tokenized by the same recursive-descent compiler that handles ${...} and $X{...}, so the three forms nest naturally (#{upper(${user.name})}, ${prefix:#{lower(${value})}}, …).
  • Added the org.apache.juneau.commons.svl.VarFunction SPI plus TypedFunction reflection helper. TypedFunction derives arity and per-arg types from the declared invoke(...) method signature(s) and routes string args through ArgCoercer for coercion to int / long / double / boolean / String / String[] / Object.
  • Boolean truthiness is centralised in ArgCoercer: "true", "yes", "on", "1" are truthy; everything else (including null and empty string) is falsy. #{if(...)}, #{not(...)}, #{switch(...)}, etc. all share the same rule.
  • #{switch(...)} matches each case pattern as a glob (* = any sequence, ? = single char), preserving the legacy $SW{...} Var's matching semantics.
  • #{len(s, delimiter)} adds a two-argument overload that counts parts after split-by-delimiter, preserving the legacy $LN{s, delim} Var's two-arg behaviour. The single-arg form still returns character length.
Built-in function catalog (~68 functions, 10 categories)​
CategoryExamples
Stringupper, lower, len, substring, trim, split, join, format, concat, pathToken
Type conversiontoInt, toLong, toDouble, toBoolean, toString
Arithmeticadd, sub, mul, div, mod, min, max, abs, round, floor, ceil
Booleannot, and, or, xor, eq, ne, lt, gt, le, ge
Conditionalif, switch, coalesce, notEmpty, case
Regexmatches, replace, extract
EncodingurlEncode, urlDecode, base64Encode, base64Decode, htmlEscape, htmlUnescape
Date/timenow, today, formatDate, parseDate
Random/UUIDuuid, randomInt, randomLong, randomDouble, randomString, randomChar
JSON navigationjsonPath, get, keys, values, size

See org.apache.juneau.commons.svl.functions for the per-class function lists with full signatures and arg style notes.

Function discovery​

Three discovery channels, evaluated in order:

  1. Explicit registration — VarResolver.create().functions(MyFn.class).build().
  2. ServiceLoader discovery — declare your implementation in META-INF/services/org.apache.juneau.commons.svl.VarFunction. VarResolver.create().defaultFunctions().build() and VarResolver.DEFAULT both load these automatically.
  3. BeanStore lookup — built-in functions are constructed through the VarResolver.Builder's BeanStore so they can themselves take constructor @Inject/@Value dependencies (rarely needed but available).

Unknown function names fail lazily at resolve time (No such function 'foo') rather than at compile time, matching the existing Var registry behaviour.

Hard-break removal of 11 Var classes​

The following 11 transformation Var classes are removed without @Deprecated shims:

Removed VarRemoved prefixReplacement
IfVar$IF{...}#{if(cond, then, else)}
SwitchVar$SW{...}#{switch(value, pat1:result1, ..., *:default)} (glob semantics preserved)
CoalesceVar$CO{...}#{coalesce(a, b, c, ...)}
NotEmptyVar$NE{...}#{notEmpty(arg)}
PatternMatchVar$PM{...}#{matches(value, pattern)}
PatternReplaceVar$PR{...}#{replace(value, pattern, replacement)}
PatternExtractVar$PE{...}#{extract(value, pattern, groupIndex)}
UpperCaseVar$UC{...}#{upper(arg)}
LowerCaseVar$LC{...}#{lower(arg)}
LenVar$LN{...}#{len(arg)} (single-arg) or #{len(arg, delimiter)} (two-arg parity)
SubstringVar$ST{...}#{substring(arg, start)} or #{substring(arg, start, end)}

VarList.addDefault() no longer registers the removed Vars; the org.apache.juneau.commons.svl.vars package no longer ships their classes. Any user code or template that still references the removed prefixes fails at resolve time with No such var '<prefix>'.

The source-data Vars ($E{}, $S{}, $MF{}, $A{}, $P{} and the shortcut ${}) are unchanged.

New VarTemplate compiled-template API​
  • Added VarResolver.compile(String) → VarTemplate — tokenizes a template once and returns a reusable, threadsafe compiled form. Subsequent template.resolve(session) calls skip tokenization + var-registry lookup, walking the cached segment array directly.
  • Added VarResolver.resolveSupplier(String) → Supplier<String> — opens a fresh session on every .get() call. Safe to share across threads. Recommended default.
  • Added VarResolverSession.compile(String) and VarResolverSession.resolveSupplier(String) — symmetric session-bound variants. The session-bound Supplier inherits the session's threadsafety contract (i.e. not threadsafe by default).
  • Added VarTemplate.isLiteral() — true when the template resolves to a fixed string (empty template, no variables, or all variables fold to literals via stable-value folding). Callers can fast-path purely-literal inputs.
  • Refactored VarResolverSession.resolve(String) to delegate to compile(input).resolve(session) so there is a single tokenizer + segment-resolution path in the codebase. No behaviour change for existing callers.
@Value Supplier<String> field-type autodetect​
  • BeanInstantiator now inspects the declared field/parameter type at injection sites carrying @Value. A Supplier<String> field receives a re-evaluating, threadsafe Supplier (delegates to VarTemplate.asSupplierWithFreshSessions(...)); a bare String field gets the one-shot resolved value as before. No @Value(supplier=true) flag is required — the field type IS the opt-in signal.
  • Literal-only expressions (@Value("plain") Supplier<String>) take a constant-folding fast path that returns the same captured String reference on every .get() call, avoiding the per-call session allocation entirely.
  • Worked patterns for live UUID factories, idempotency keys, jittered delays, and hot-reload config knobs are in the @Value topic page.
Compile-time stable-value folding​
  • New default boolean Var.isStable() { return false; } SPI hook. Opt-in per Var implementation; conservative false default protects third-party Vars from accidental incorrect folding.
  • Four built-in Vars opt in: EnvVariablesVar, SystemPropertiesVar, ManifestFileVar, ArgsVar. Compiled templates eagerly resolve these at compile time and replace the segment with a LiteralSegment — runtime dispatch is eliminated.
  • SystemPropertiesVar's opt-in carries a documented caveat: System.setProperty(...) calls after compile do not propagate to a folded template. In practice this is the desired behaviour for the common "read once at startup" use case.
  • ConfigVar, PropertyVar, DotenvVar, EnvFileVar stay non-stable. Their backing source can mutate at runtime (config reload, file watch, runtime Settings.set), so folding would produce stale reads.
Internal perf​
  • BeanInstantiator's @Value resolution path caches a VarTemplate per distinct expression in ValueResolver.TEMPLATE_CACHE. Repeated bean construction reads the cached form rather than re-tokenizing.
  • RestOpContext.pathMatchers Memoizer now goes through vr.compile(p).resolve(session) explicitly so the framework hot loop exercises the compiled-template seam. If we ever switch to per-request dynamic path resolution, only the .resolve(session) step moves to the request handler — the compile is already done at context-build time.
  • Micro-benchmark (juneau-utest/.../VarResolver_Benchmark_Test): compile-once + resolve-N is ~1.7× faster than compile-per-call for plain ${...} templates and ~1.4× for #{...} script templates on a representative machine. The headline ≥ 2× / ≥ 5× targets from the plan apply to comparisons against the pre-10.0 legacy ad-hoc dispatcher, which is no longer in the tree — the unified compiled-form path is now the baseline.
Migration​

For any SVL template that uses one of the 11 removed Var prefixes, rewrite to #{...}. Cookbook:

BeforeAfter
$UC{$S{user.name}}#{upper(${user.name})}
$IF{$S{prod},prod.key,dev.key}#{if(${prod}, prod.key, dev.key)}
$SW{$S{os},*win*:Windows,*:Unix}#{switch(${os}, *win*:Windows, *:Unix)}
$PR{$S{name},[aeiou],*}#{replace(${name}, [aeiou], *)}
$LN{a,b,c,,}#{len(a,b,c, ,)}
$ST{hello,2,4}#{substring(hello, 2, 4)}

The full ${...} shortcut, $S{}/$E{}/$MF{}/$A{}/$P{} source vars, and $C{} config var keep their existing syntax — no migration required for those.

juneau-commons breaking changes​

  • org.apache.juneau.commons.lang.Value renamed to Holder. The mutable reference-wrapper utility class has been renamed to Holder<T> to resolve an import-name collision with the @Value injection annotation. Update import statements: import org.apache.juneau.commons.lang.Value → import org.apache.juneau.commons.lang.Holder. No behavioral change.

  • XValue primitive-holder companions renamed to XHolder. The nine typed primitive-holder subclasses have been renamed for consistency with the Value → Holder rename: BooleanValue → BooleanHolder, ByteValue → ByteHolder, CharValue → CharHolder, DoubleValue → DoubleHolder, FloatValue → FloatHolder, IntegerValue → IntegerHolder, LongValue → LongHolder, ShortValue → ShortHolder, StringValue → StringHolder. Update import statements and type references accordingly. No behavioral change.

juneau-config​

Classpath-default Config bridge to Settings​

  • Added org.apache.juneau.config.ConfigPropertySourceProvider, a META-INF/services-discovered PropertySourceProvider that bridges Config.getSystemDefault() (i.e. the classpath juneau.cfg / default.cfg / application.cfg / etc. discovered by Config.getCandidateSystemDefaultConfigNames()) into the process-wide Settings singleton.
  • Resolution is lazy — Config.getSystemDefault() is invoked on the first Settings.get(name) lookup rather than at SPI registration time. This avoids a static-initialization cycle (Settings.<clinit> → useServiceLoader() → ConfigPropertySourceProvider.create() → Config.<clinit> → Cache.create() → Utils.env(...) → Settings.<clinit>) and a runtime reentrance cycle (Config.findSystemDefault() looking up "juneau.configFile" through every registered source, including this one). A thread-local guard short-circuits the reentrant lookup; any throwable while Config is mid-initialisation is treated as "not yet available" without memoising the failure.
  • The provider is silent when no classpath Config exists — every lookup returns missing(), no log noise, no exception.
  • The provider's order() is 100, deliberately low — so any programmatic Settings.get().addSource(...) registered later (microservice, RestContext, Spring) shadows the classpath default at lookup time.

YAML config format support​

  • Added optional YAML config support to juneau-config alongside the existing INI/CFG format.
  • Added format selection via config name extension (.yml / .yaml) and explicit builder methods (.yaml() / .ini()).
  • Added path-style section support (e.g. [foo/bar/baz]) to align INI sections with nested YAML mappings.
  • Updated default FileStore extension search order to include cfg,yml,yaml (with cfg remaining first for backward compatibility).

Unified Property Source Hierarchy​

  • Added a new property-source SPI naming model: PropertySource / PropertyStore and functional variants.
  • Added PropertySourceProvider (ServiceLoader SPI) so ambient sources can be auto-registered onto Settings.
  • Added built-in property sources for args, manifest entries, system properties, environment variables, and .env.
  • Added runtime source wiring via Settings.addSource(PropertySource) to allow explicit opt-in sources.
  • Added PropertyVar ($P{key[,default]}) as the SVL unified property-stack resolver.
  • Existing $S / $E / $A / $MF vars remain supported as source-specific resolvers, now aligned with the unified settings model.
  • Breaking: SettingSource, SettingStore, FunctionalSource, and FunctionalStore have been removed outright (no deprecation shims). Migrate to PropertySource, PropertyStore, FunctionalPropertySource, and FunctionalPropertyStore respectively.

Bean→Marshalled Rename​

A comprehensive rename of annotation and engine types to clarify the distinction between Java-bean-structure types (which keep BeanXxx names) and marshalling-process types (which are now MarshallingXxx).

Annotation Renames (breaking)​

OldNew
@Bean (marshall)@Marshalled
@BeanApply@MarshalledApply
@Beanp@MarshalledProp
@BeanpApply@MarshalledPropApply
@Beanc@MarshalledCtor
@BeancApply@MarshalledCtorApply
@BeanIgnore@MarshalledIgnore
@BeanIgnoreApply@MarshalledIgnoreApply
@BeanConfig@MarshalledConfig
Engine Type Renames (breaking)​
OldNew
BeanContextMarshallingContext
BeanContextableMarshallingContextable
BeanSessionMarshallingSession
BeanTraverseContextMarshallingTraverseContext
BeanTraverseSessionMarshallingTraverseSession
BeanRecursionExceptionMarshallingRecursionException
BeanStringSwapMarshallingStringSwap
BeanContextConverterMarshallingContextConverter
BeanDefMapperMarshallingDefMapper
BeanContext.DEFAULTMarshallingContext.DEFAULT
Unchanged Types (clarification)​

The following BeanXxx types model the Java bean structure and are deliberately unchanged: BeanMap, BeanMeta, BeanPropertyMeta, BeanPropertyValue, BeanPropertyConsumer, BeanRegistry, BeanDictionaryMap, BeanDictionaryList, BeanProxyInvocationHandler, BeanDiff.

Package Moves (Phase 5a of bean-layer split)​

As part of the bean-layer split (separating bean-modeling from marshalling), the BeanInterceptor type was moved into the new org.apache.juneau.commons.bean package in juneau-commons:

OldNew
org.apache.juneau.marshall.swap.MarshallingInterceptororg.apache.juneau.commons.bean.BeanInterceptor

This is both a rename (back to the pre-10.0 BeanInterceptor name) and a package move. Source code referencing the type by simple name through wildcard imports may need to update its import from org.apache.juneau.marshall.swap.* to org.apache.juneau.commons.bean.* (or add an explicit import).

New Type: BeanConfigContext (Phase 5b of bean-layer split)​

A new org.apache.juneau.commons.bean.BeanConfigContext POJO carries the runtime bean-modeling settings — visibility thresholds, the beans*Require* toggles, fluent-setter detection, property naming, the bean-type property name, the not-a-bean exclusion sets, the active BeanStore, the active AnnotationProvider, and an optional Predicate<ClassInfo> override for isNotABean. It is the runtime sibling of the @BeanConfig annotation introduced in Phase 3.

MarshallingContext now exposes getBeanConfigContext() returning a memoized snapshot view, so consumers that only need bean-modeling settings can depend on commons.bean without referencing any marshalling-aware types. This is foundational infrastructure for the relocation of BeanMap / BeanMeta / BeanPropertyMeta and friends into commons.bean (completed in Phase 5c below).

Bean-Runtime Cluster Moved to commons.bean (Phase 5c of bean-layer split)​

The seven core bean-runtime types have been physically moved out of org.apache.juneau (in juneau-marshall) into org.apache.juneau.commons.bean (in juneau-commons):

OldNew
org.apache.juneau.BeanMaporg.apache.juneau.commons.bean.BeanMap
org.apache.juneau.BeanMapEntryorg.apache.juneau.commons.bean.BeanMapEntry
org.apache.juneau.BeanMetaorg.apache.juneau.commons.bean.BeanMeta
org.apache.juneau.BeanPropertyMeta (and its Builder inner class)org.apache.juneau.commons.bean.BeanPropertyMeta
org.apache.juneau.BeanPropertyValueorg.apache.juneau.commons.bean.BeanPropertyValue
org.apache.juneau.BeanPropertyConsumerorg.apache.juneau.commons.bean.BeanPropertyConsumer
org.apache.juneau.BeanProxyInvocationHandlerorg.apache.juneau.commons.bean.BeanProxyInvocationHandler

The @Name annotation also moved as part of this work:

OldNew
org.apache.juneau.annotation.Nameorg.apache.juneau.commons.bean.Name

After these moves, juneau-commons compiles standalone with no dependency on juneau-marshall, and the bean-modeling layer in commons.bean is fully self-contained. Source code referencing any of these types by simple name through wildcard imports must update from org.apache.juneau.* (or org.apache.juneau.annotation.*) to org.apache.juneau.commons.bean.*. Fully-qualified @org.apache.juneau.annotation.Name(...) references must be rewritten to @org.apache.juneau.commons.bean.Name(...).

Bean-Modeling Annotation Surface (Phase 5c)​

The bean-modeling attributes have been split out of the marshalling-side @Marshalled family into a parallel set of commons-side annotations. The marshalling-side annotations retain only the marshalling-only attributes; both annotations are still consulted on the marshalling-side path so a class can carry both without conflict.

Commons-side (new)Marshalling-side siblingCarries
@BeanType@MarshalledClass-level bean-modeling attributes: properties / p, excludeProperties / xp, readOnlyProperties / ro, writeOnlyProperties / wo, unsorted, findFluentSetters, propertyNamer, interfaceClass, stopClass, factory.
@BeanProp@MarshalledPropProperty-level bean-modeling attributes: name / value, ro, wo, type, params, elementType, factory.
@BeanCtor@MarshalledCtorConstructor mapping: properties.
@BeanIgnore@MarshalledIgnoreBean-property / class ignore (with ignoreAccessors for fields).
@BeanConfig@MarshalledConfigClass- / method-level bean-modeling config (visibility thresholds, beans*Require* toggles, fluent setters, property naming, not-a-bean exclusions, etc.).

@Marshalled retains the marshalling-only attributes: as, description, dictionary, example, implClass, interceptor, typeName, typePropertyName. @MarshalledProp retains its marshalling-only attributes (e.g. dictionary, format). Contributions from the bean-modeling and marshalling-side annotations are merged when building the bean filter on the marshalling-side path.

commons.bean SPI Surface (Phase 5c)​

To plug the relocated bean-runtime types back into the marshalling layer, several new SPI types have been added to org.apache.juneau.commons.bean:

SPI TypeDescriptionMarshalling-side implementation
BeanInfo<T>Bean-modeling type-info seam. Extends ClassInfoTyped<T>; exposes isBean(), isUri(), getElementType(), getKeyType(), getValueType(), canCreateNewInstance(...), getOptionalDefault(), getBeanConfigContext(), getMarshallingContext().ClassMeta<T> extends this.
BeanSessionBean-modeling session SPI (convertToType, convertToMemberType, toBeanMap, parseToMap(CharSequence), parseToList(CharSequence)).MarshallingSession implements this.
BeanFilterPer-class filter surface (properties / excludeProperties / readOnlyProperties / writeOnlyProperties / typeName / interfaceClass / stopClass / propertyNamer / beanDictionary / interceptor hooks).MarshalledFilter implements this.
BeanRegistryLookupBean-dictionary lookup surface.BeanRegistry implements this.
BeanMetaInitializerMarshalling-side BeanMeta construction hooks (filter discovery, type-name resolution, bean-registry assembly, parent-chain type-name lookup). NOOP default for the commons-side path.MarshalledBeanMetaInitializer implements this.
BeanPropertyPostProcessorMarshalling-side BeanPropertyMeta.Builder post-processing (@MarshalledProp / @Swap / @Uri reads, swap-aware read/write transforms). NOOP default for the commons-side path.MarshalledPropertyPostProcessor implements this.
BeanTypeResolverMarshalling-side type-resolution SPI used by BeanPropertyMeta.Builder.validate(...).MarshallingContext implements this.

BeanConfigContext carries all of these so the bean-modeling layer can drive bean-meta construction with or without a marshalling context.

Type and Method Renames (Phase 5c, breaking)​

The bean-modeling SPI seam was renamed and several accessors aligned with the new SPI surface:

OldNewNotes
BeanTypeInfo<T> (commons.bean)BeanInfo<T>Full type rename. No deprecation shim.
Delegate#getClassMeta()Delegate#getBeanInfo()Returns BeanInfo<T>.
BeanMap#getClassMeta()BeanMap#getBeanInfo()Returns BeanInfo<T>.
BeanMeta#getClassMeta()BeanMeta#getBeanInfo()Returns BeanInfo<T>.
BeanPropertyMeta#getClassMeta()BeanPropertyMeta#getBeanInfo()Returns BeanInfo<?>.
BeanPropertyValue#getClassMeta()BeanPropertyValue#getBeanInfo()Returns BeanInfo<?>.
BeanMap#setMarshallingSession(BeanSession)BeanMap#setBeanSession(BeanSession)No deprecated shim. Existing call sites that pass a MarshallingSession (which implements BeanSession) continue to compile after the rename.

MarshallingContext.getClassMeta(Class) and MarshallingContext.getClassMeta(Type, Type...) are intentionally unchanged — they still resolve ClassMeta directly on the marshalling side.

The BeanPropertyPostProcessor SPI also tightened its signature now that BeanPropertyMeta.Builder lives in commons.bean:

// Before
void process(Object marshallingContext, Object builder);

// After
void process(Object marshallingContext, BeanPropertyMeta.Builder builder);
Removed APIs (Phase 5c)​
  • BeanFilter.getMarshalledClass() — Dead/unused accessor on the bean-filter SPI. Removed along with the backing marshalledClass field on MarshalledFilter and the MarshalledFilter.Builder(Class<?>) / MarshalledFilter.create(Class<T>) overloads. Use BeanFilter.getBeanClass() (returns ClassInfoTyped<?>) instead.
ClassInfo Additions (Phase 5c)​

Two pure class-check helpers were lifted up to org.apache.juneau.commons.reflect.ClassInfo alongside the existing isArray(), isMap(), isBoolean(), and isVoid() siblings:

  • ClassInfo#isObject() — Returns true when this class is exactly java.lang.Object.
  • ClassInfo#isOptional() — Returns true when this class is java.util.Optional.
API Visibility Widening (Phase 5c)​

Several previously protected or package-private members were widened so marshalling-side callers (and juneau-marshall-rdf extensions) can still reach them across the new package boundary:

  • BeanMeta.BeanMetaValue record made public (and its optBeanMeta() accessor).
  • BeanMeta accessors getConstructor(), getConstructorArgs(), hasConstructor(), newBean(Object) widened protected → public.
  • BeanMap constructor (T, BeanMeta<T>) and setBeanSession(BeanSession) widened protected → public.
  • BeanPropertyMeta.Builder fields innerField, getter, setter, rawTypeMeta, swap, readTransform, writeTransform, dictionaryClasses, isUri, and typeMeta widened package-private → public (consumed by MarshalledPropertyPostProcessor).
  • BeanPropertyValue.properties() widened protected → public.
New Feature: @Marshalled(as=STRING) Strategy​

A new MarshalledAs enum with values DETECT (default) and STRING has been added to @Marshalled:

// Force a type to serialize as its toString() value, bypassing bean detection.
@Marshalled(as=STRING)
public class MyType {
@Override public String toString() { return "my-string-form"; }
}

@Marshalled(as=STRING) replaces two earlier patterns:

  • The BeanStringSwap class (now MarshallingStringSwap; kept for custom format overrides)
  • The @BeanIgnore-on-class-then-toString() pattern
Updated @MarshalledIgnore TYPE Semantics (breaking)​

Applying @MarshalledIgnore to a class now skips serialization entirely (outputs null) instead of falling through to toString():

// Before (@BeanIgnore on class → toString())
@BeanIgnore
public class OldClass { @Override public String toString() { return "value"; } }
// Serialized as: "value"

// After (@MarshalledIgnore on class → null)
@MarshalledIgnore
public class NewClass { ... }
// Serialized as: null

// Migration: if you need toString() behavior, use @Marshalled(as=STRING) instead
@Marshalled(as=STRING)
public class NewClass { @Override public String toString() { return "value"; } }
// Serialized as: "value"

Migration: If you previously used @BeanIgnore on a class to force toString() serialization, replace it with @Marshalled(as=STRING). If you truly want to suppress serialization (output null), use @MarshalledIgnore.

Removed @MarshalledProp(properties) Attribute (breaking)​

The @MarshalledProp(properties=...) attribute has been removed. The attribute was used to limit which child properties of a nested bean/map are rendered by serializers:

// Before — only "f1" was rendered on each child.
public class MyClass {
@MarshalledProp(properties="f1")
public MyChildClass x1 = new MyChildClass();
}

// After — the attribute no longer exists; nested beans render in full.
public class MyClass {
public MyChildClass x1 = new MyChildClass();
}

The related internal types are gone too:

  • BeanPropertyMeta.applyChildPropertiesFilter(...), BeanPropertyMeta.getProperties() (override-list accessor), and the underlying properties field/builder setter.
  • BeanMetaFiltered (sole purpose was wrapping a BeanMeta with a filtered property list).

Migration: If you need per-property filtering for a serialization, use a custom BeanFilter/@Marshalled(properties=...) at the bean-class level instead, or configure include/exclude lists on the MarshallingContext.Builder. The marshalling-context-level filtering pre-dated @MarshalledProp(properties) and remains fully supported.

@Beanp("*") on non-Map fields​

On a field whose type is not a Map, @Beanp("*") (or @Beanp(name="*")) no longer tries to register a dyna property. The property name is taken from the field (via the configured PropertyNamer); other @Beanp attributes still apply. Map fields keep the dyna property * as before.

BasicHtmlDocTemplate now treats bare navlink lines like request:?Accept=... or request:/?Accept=... (no label: prefix) as full marshall URIs. Previously the first : split dropped the scheme, producing relative href values that browsers resolved under the current path (e.g. .../request:?Accept=...). The same rejoin applies to servlet: and context: when the part after the colon starts with /, ?, or #.

Labeled navlinks (e.g. JSON: request:?Accept=...) were already correct; the bug affected bare entries where the text before the first : is only the marshall scheme name.

Native TOML Support​

Juneau now includes built-in TOML (Tom's Obvious Minimal Language) v1.0.0 serialization and parsing support, implemented entirely without external library dependencies. TOML is a configuration file format widely used in projects like Cargo (Rust) and Python's pyproject.toml.

New Classes​

  • TomlSerializer - Serializes POJOs to TOML format with tables, key-value pairs, and proper escaping.
  • TomlParser - Parses TOML input into POJOs using a tokenizer-based parser.
  • Toml - Convenience marshaller pairing TomlSerializer and TomlParser.
  • BasicTomlConfig - REST configuration interface for TOML-only resources.
Features​

The implementation conforms to the TOML v1.0.0 specification:

// Serialize a map to TOML
Map<String, Object> config = Map.of("name", "myapp", "port", 8080);
String toml = Toml.of(config);
// Produces: name = "myapp"
// port = 8080

// Parse TOML into JsonMap or beans
JsonMap parsed = Toml.to(toml, JsonMap.class);
MyConfig config = Toml.to(toml, MyConfig.class);
  • Tables - [section] and nested [parent.child] tables
  • Inline tables - {key = "value"} for simple nested structures
  • Arrays - Homogeneous arrays of primitives, strings, and inline tables
  • Strings - Basic, literal, and multi-line variants
  • Dates and times - ISO 8601 (LocalDate, LocalDateTime, OffsetDateTime, etc.)
Media Types​
  • Produces: application/toml
  • Accepts: application/toml, text/toml

TOML is included in BasicUniversalConfig by default, or can be used standalone via BasicTomlConfig.

See TOML Basics for full documentation.

JSONL (JSON Lines) Support​

Juneau now includes built-in JSONL (JSON Lines / NDJSON) serialization and parsing support. JSONL is a text format where each line is a valid JSON value, widely used for LLM fine-tuning datasets, streaming AI inference, log aggregation, and bulk data pipelines.

New Classes​

  • JsonlSerializer - Serializes POJOs to JSONL format with one JSON value per line.
  • JsonlParser - Parses JSONL input into POJOs, reading one line at a time.
  • Jsonl - Convenience marshaller pairing JsonlSerializer and JsonlParser.
Features​
  • Each line is a compact JSON value; collections and arrays serialize one element per line.
  • Full round-trip parity with JSON for beans, maps, collections, scalars, and ObjectSwaps.
  • Supports streaming (Iterator, Iterable, Stream) for lazy serialization.
  • Date/time and Duration types serialized as ISO 8601 strings.
// Serialize a list of beans to JSONL
List<Person> people = List.of(new Person("Alice", 30), new Person("Bob", 25));
String jsonl = Jsonl.of(people);
// Produces:
// {"name":"Alice","age":30}
// {"name":"Bob","age":25}

// Parse JSONL into a list of beans
List<Person> parsed = Jsonl.to(jsonl, List.class, Person.class);
Media Types​
  • Produces: application/jsonl
  • Accepts: application/jsonl, application/x-ndjson, text/jsonl

JSONL is included in BasicUniversalConfig by default.

See JSONL Basics for full documentation.

Server-Sent Events (SSE) Support​

Juneau now ships with a native serializer / parser for the WHATWG Server-Sent Events wire format (text/event-stream). SSE is the standard one-way push channel used by browsers (EventSource) and most LLM streaming APIs (OpenAI, Anthropic, etc.) for progress events, partial responses, and long-lived telemetry feeds.

New Classes (in org.apache.juneau.marshall.sse)​

  • SseEvent — bean carrying the four spec fields (event, data, id, retry). The data payload is a pre-serialized string — callers serialize their typed payload through their marshaller of choice (e.g. Json.of(...)) and assign the resulting string.
  • SseSerializer extends WriterSerializer — produces text/event-stream. Accepts a single SseEvent, an Iterable, a Stream, or an SseEvent[] array. Calls Writer.flush() after every event so events are pushed to the client immediately (essential for server-push and AI streaming).
  • SseParser extends ReaderParser — eager parse of a closed text/event-stream into List<SseEvent>, SseEvent[], or a single SseEvent.
  • SseEventReader — public Iterator<SseEvent> + Closeable for line-driven streaming over a caller-owned Reader. The intended API for long-lived SSE streams that outlive the standard parser-session lifecycle.
  • Sse marshaller (in org.apache.juneau.marshaller) — convenience pairing of SseSerializer and SseParser with of(...) / to(...) shortcuts.
Features​
  • Full WHATWG parse-algorithm compliance: all three line terminators (CR, LF, CR LF), one-time UTF-8 BOM strip, field:value field splitting with optional-space stripping after the colon, multi-line data: concatenation, comment-line (: heartbeat) skipping, and the spec-defined ignore rules for id containing U+0000 and non-digit retry values.
  • Per-event Writer.flush() inside SseSerializerSession.doSerialize(...) so SSE actually works as a push channel (without it, events would buffer and only land at end-of-response).
  • SseSerializer.writeComment(Writer, String) helper for emitting SSE heartbeat / keep-alive comment lines on long-lived connections.
Example​
// Serialize a list of events to SSE wire format
List<SseEvent> events = List.of(
new SseEvent("progress", "step 1").setId("1"),
new SseEvent("progress", "step 2").setId("2")
);
String wire = Sse.of(events);
// Produces:
// event: progress
// data: step 1
// id: 1
//
// event: progress
// data: step 2
// id: 2
//

// Parse SSE wire format back into a list of events
List<SseEvent> parsed = Sse.to(wire, List.class, SseEvent.class);
REST integration​

SSE is not added to BasicUniversalConfig (it's a streaming, one-way push format with different semantics from the regular content-negotiation marshallers). Register it explicitly per-endpoint:

@Rest(serializers = SseSerializer.class)
public class MyStreamResource {
@RestGet("/events")
public List<SseEvent> stream() {
return List.of(
new SseEvent("progress", "step 1"),
new SseEvent("progress", "step 2")
);
}
}
Media Types​
  • Produces / accepts: text/event-stream

See SSE Basics for full documentation.

BSON (Binary JSON) Support​

Juneau now includes built-in BSON (Binary JSON) serialization and parsing support, implemented without external library dependencies. BSON is the binary serialization format used by MongoDB, extending JSON with typed integers (int32/int64), native datetime, decimal128, and binary data.

New Classes​

  • BsonSerializer - Serializes POJOs to BSON format (little-endian, length-prefixed documents).
  • BsonParser - Parses BSON input into POJOs.
  • Bson - Convenience marshaller pairing BsonSerializer and BsonParser.
Features​
  • Full Java type mapping: beans/maps → documents, collections/arrays → BSON arrays, byte[] → binary (0x05), dates → datetime (0x09) or ISO 8601 string, BigDecimal/BigInteger → decimal128 (0x13).
  • REST integration: RestClient.bson(), RestRequest.bson(), MockRestClient.bson(), MockRestRequest.bson().
  • Annotations: @Bson, @BsonConfig (addBeanTypes, writeDatesAsDatetime).
  • Round-trip parity with JSON for all standard data types; included in RoundTripTest_Base.
// Serialize a bean to BSON
Person p = new Person("Alice", 30);
byte[] bson = Bson.of(p);

// Parse BSON into a bean
Person parsed = Bson.to(bson, Person.class);

// REST client with BSON
RestClient client = RestClient.create().uri("http://localhost:8080").bson().build();
byte[] response = client.get("/api/person/1").bson().run().body();
Media Types​
  • Produces: application/bson
  • Accepts: application/bson

BSON is included in BasicUniversalConfig by default.

See BSON Basics for full documentation.

CBOR (Concise Binary Object Representation) Support​

Juneau now includes built-in CBOR per RFC 8949 serialization and parsing support, implemented without external library dependencies. CBOR is a compact binary format designed for extremely small code size and message size, widely used in IoT, constrained environments, COSE (CBOR Object Signing and Encryption), and WebAuthn.

New Classes​

  • CborSerializer - Serializes POJOs to CBOR format (RFC 8949, compact integer encoding, definite-length arrays/maps).
  • CborParser - Parses CBOR input into POJOs.
  • Cbor - Convenience marshaller pairing CborSerializer and CborParser.
Features​
  • Full Java type mapping: beans/maps → CBOR maps (major type 5), collections/arrays → CBOR arrays (major type 4), strings → text strings (major type 3), integers → compact encoding (major types 0/1), floats → IEEE 754 (0xFA/0xFB), byte[] → byte strings (major type 2).
  • SpacedHex and Base64 variants for text-friendly output (debugging, testing).
  • REST integration: CBOR included in BasicUniversalConfig and RestClient.universal().
  • Annotations: @Cbor, @CborConfig (addBeanTypesCbor, useTags).
  • Round-trip parity with JSON for standard data types.
// Serialize a bean to CBOR
Person p = new Person("Alice", 30);
byte[] cbor = Cbor.of(p);

// Parse CBOR into a bean
Person parsed = Cbor.to(cbor, Person.class);
Media Types​
  • Produces: application/cbor
  • Accepts: application/cbor

CBOR is included in BasicUniversalConfig by default.

See CBOR Basics for full documentation.

HOCON Parser Enhancements​

The HoconParser now supports additional HOCON features for configuration files:

  • += array append - list += item appends to an existing array or creates [item] if the key does not exist
  • Self-referential substitution - path = ${path}"/bin" appends to the previous value (e.g. /usr → /usr/bin)
  • Value concatenation - Adjacent values concatenate: path = /usr "/local" → "/usr/local"
  • Array concatenation - [1, 2] [3, 4] → [1, 2, 3, 4]
  • Object concatenation - {a: 1} {b: 2} → {a: 1, b: 2}

See HOCON Basics for full documentation.

JCS (JSON Canonicalization Scheme) Support​

Juneau now includes built-in JCS (JSON Canonicalization Scheme) per RFC 8785. JCS produces a deterministic, byte-for-byte canonical representation of JSON data, enabling reliable cryptographic operations such as hashing and digital signing.

New Classes​

  • JcsSerializer - Serializes POJOs to canonical JSON with sorted properties, ECMAScript number formatting, and no whitespace.
  • Jcs - Convenience marshaller pairing JcsSerializer and JsonParser (JCS output is valid JSON).
Features​
  • Deterministic output - Same input always produces identical output for hashing/signing.
  • No whitespace - Compact single-line output.
  • Sorted properties - UTF-16 code unit order, applied recursively to nested objects.
  • ECMAScript number serialization - Shortest round-trip representation per ECMA-262.
  • Full round-trip - Use JsonParser to parse JCS output.
// Serialize to canonical JSON
Person alice = new Person("Alice", 30);
String canonical = Jcs.of(alice);
// Produces: {"age":30,"name":"Alice"}

// Parse canonical JSON (same as regular JSON)
Person parsed = Jcs.to(canonical, Person.class);
Limitations​
  • NaN and Infinity - Rejected; throws SerializeException.
  • Lone surrogates - Strings with unpaired UTF-16 surrogates throw SerializeException.
  • Media type - Output is application/json; JCS does not define a separate media type.

JCS is included in BasicUniversalConfig by default.

See JCS Basics for full documentation.

JsonSerializer/JsonParser and Json5Serializer/Json5Parser Separation​

JsonSerializer and JsonParser now strictly enforce RFC 8259 JSON (double-quoted strings only). Single-quoted JSON, comments, trailing commas, and unquoted keys are rejected by JsonParser.

For JSON5 syntax (single quotes, comments, trailing commas, unquoted keys), use Json5Serializer and Json5Parser instead.

Changes​

  • JsonParser - Strict RFC 8259 only; single-quoted strings throw ParseException. Use Json5Parser for JSON5 input.
  • JsonSerializer - Produces strict JSON by default. Use Json5Serializer for JSON5-style output (single quotes, etc.).
  • Removed - JsonSerializer.json5() and JsonParser.Strict; use Json5Serializer and Json5Parser directly.
  • BeanContext.beanToStringSerializer - Now uses Json5Serializer for toString() output (more human-readable).
  • ComboRoundTripTest - Framework now distinguishes json vs json5; use json()/json5() builder methods on ComboRoundTrip_Tester accordingly.

See JSON Basics, JSON Parsers, and JSON 5 for full documentation.

Sorted Bean Properties by Default; @Bean(unsorted) Opt-Out​

Bean properties are now serialized in alphabetical order by default across all serializers. Previously, the default was natural JVM order and opting in to sorted output required explicit configuration.

New API:

  • @Bean(unsorted=true) — Opts a specific bean class out of the default sorted order, using natural JVM property order instead.
  • BeanContext.Builder.unsortedProperties() — Disables sorted properties globally for all beans.
  • BeanContext.Builder.unsortedProperties(Class<?>...on) — Disables sorted properties for specific bean classes only.
  • BeanFilter.Builder.unsortedProperties() — Per-bean-filter opt-out of sorted properties.
  • BeanFilter.isUnsortedProperties() — Returns true if this bean filter opts out of sorting.
  • BeanSession.isUnsortedProperties() — Session-level accessor for the unsorted flag.

Migration:

// Before: opt in to sorted output
@Bean(sort=true)
public class MyBean { ... }

// After: sorted is the default, opt out with unsorted=true
@Bean(unsorted=true)
public class MyBean { ... } // opts out of default sorted behavior

// Before: global sorted flag required explicit opt-in
JsonSerializer s = JsonSerializer.create().sortProperties().build();

// After: sorted by default; use unsortedProperties() to opt out
JsonSerializer s = JsonSerializer.create().unsortedProperties().build();

Removed API:

  • @Bean(sort) — Removed. Use @Bean(unsorted=true) with inverted semantics.
  • BeanContext.Builder.sortProperties() / sortProperties(boolean) — Removed. Use unsortedProperties().
  • BeanContext.Builder.sortProperties(Class<?>...on) — Removed. Use unsortedProperties(Class<?>...on).
  • BeanContextable.Builder.sortProperties() / sortProperties(Class<?>...on) — Removed. Use unsortedProperties() / unsortedProperties(Class<?>...on).
  • BeanFilter.Builder.sortProperties() / sortProperties(boolean) — Removed. Use unsortedProperties().
  • BeanFilter.isSortProperties() / BeanSession.isSortProperties() — Removed. Use isUnsortedProperties().
  • BeanMeta.isSortProperties() — Removed. Use isUnsortedProperties().
  • All serializer/parser builder sortProperties() / sortProperties(Class<?>...on) overrides — Removed.

Internal rename:

  • BeanMeta.sortProperties field — Renamed to unsortedProperties with inverted semantics. The field now stores true when the bean opts out of default sorted order, rather than true when sorted is explicitly enabled.

@BeanIgnore(ignoreAccessors=true) on fields​

@BeanIgnore gains ignoreAccessors (default false) for use on fields. When ignoreAccessors = true, BeanMeta also excludes the matching JavaBean accessor pair (getX/setX, or isX for booleans) from bean property discovery, so the property is omitted from serialization, parsing, and BeanMap metadata while accessors can remain for other frameworks.

The default false preserves existing behavior: @BeanIgnore on a field only excludes the field from field-based discovery; accessors can still expose the property (for example when beanFieldVisibility is NONE and only methods are considered).

  • Applies to Juneau’s default introspection and to BeanContext.Builder.useJavaBeanIntrospector() (BeanInfo / PropertyDescriptor discovery).
  • Property names follow the same rules as field discovery: @Beanp / @Name on the field, if present, otherwise the configured PropertyNamer on the field name.

See @BeanIgnore Annotation.

Protobuf Text Format Support​

Juneau now includes built-in Protobuf Text Format serialization and parsing support, implemented without external library dependencies. This is the human-readable text format (not the binary wire format), suitable for configuration, debugging, and interop with tools that use the text format.

New Classes​

  • PrototextSerializer - Serializes POJOs to Protobuf Text Format.
  • PrototextParser - Parses Protobuf Text Format into POJOs.
  • Prototext - Convenience marshaller pairing PrototextSerializer and PrototextParser.
  • BasicPrototextConfig - REST configuration interface for Prototext-only resources.
Features​
  • No .proto schema files required—Java bean metadata provides type information.
  • Supports beans, maps, collections, scalars, repeated fields, and nested messages.
  • Date/time and Duration types serialized as ISO 8601 strings.
  • Annotations: @Prototext (comment), @PrototextConfig (useListSyntaxForBeans, useColonForMessages).
// Serialize a bean to Protobuf Text Format
Person p = new Person("Alice", 30);
String proto = Prototext.of(p);
// Produces: name: "Alice"
// age: 30

// Parse Protobuf Text Format into a bean
Person parsed = Prototext.to(proto, Person.class);
Media Types​
  • Produces: text/protobuf
  • Accepts: text/protobuf, text/x-protobuf

Prototext is included in BasicUniversalConfig by default, or can be used standalone via BasicPrototextConfig.

See Protobuf Basics for full documentation.

Full CSV Parser Support​

The CsvParser is now fully implemented. Previously, parsing always threw "Not implemented.". The parser supports the same range of target types as other Juneau parsers.

New Class​

  • CsvReader - Low-level RFC 4180-compliant CSV reader used internally by CsvParserSession.
Features​

The parser reads a header row to discover column names, then maps each subsequent data row to the target type:

String csv = "name,age\nAlice,30\nBob,25\n";

// Parse into a list of beans
List<Person> people = CsvParser.DEFAULT.parse(csv, List.class, Person.class);

// Parse into a list of maps
List<Map<String,String>> rows = CsvParser.DEFAULT.parse(csv, List.class, Map.class);

// Parse a single bean
Person p = CsvParser.DEFAULT.parse("name,age\nAlice,30\n", Person.class);

Supported target types:

Target TypeBehavior
Collection<Bean> / Bean[]Header row → property names; each data row → one bean.
Collection<Map> / Map[]Header row → map keys; each data row → one map.
Collection<SimpleType> / SimpleType[]value column → each row coerced to element type.
Single BeanHeader row + one data row → one bean.
Single MapHeader row + one data row → one map.
ObjectReturns a JsonList of JsonMap entries.

Type coercion converts string cell values to the target property type, including primitives, enums, date/time types, and any type supported by Juneau's convertToType().

The unquoted literal null in a cell is parsed as Java null.

CSV Serializer Fixes​

  • RFC 4180 Quote Escaping: CsvWriter.writeEntry() now correctly escapes embedded double-quote characters by doubling them (" → ""), as required by RFC 4180. Previously, values containing embedded quotes were written with unescaped quotes, producing invalid CSV.
  • DynaBean Support: The CsvSerializer now serializes DynaBeans (dynamic beans not directly identified as beans by ClassMeta.isBean()) by attempting toBeanMap() on each entry.
  • Round-Trip Tests: CsvSerializer and CsvParser are now included in the RoundTripTest_Base framework (as tester #18), skipping inputs that are not flat collections.

See CSV Basics for full documentation.

Native Markdown Support​

Juneau now includes built-in Markdown serialization and parsing support, implemented entirely without external library dependencies. Markdown output is optimized for both human readability and LLM/AI consumption, with fragment mode (tables, lists) and document mode (headings + tables) for nested structures.

New Classes​

  • MarkdownSerializer - Serializes POJOs to Markdown fragment output (tables, bulleted lists).
  • MarkdownDocSerializer - Extends MarkdownSerializer for full documents with heading-based structure.
  • MarkdownParser - Parses Markdown tables and bulleted lists back into POJOs.
  • MarkdownDocParser - Parses Markdown documents with headings into nested bean structure.
  • Markdown - Convenience marshaller pairing MarkdownSerializer and MarkdownParser.
  • MarkdownDoc - Convenience marshaller pairing MarkdownDocSerializer and MarkdownDocParser.
Features​
  • Fragment mode: Beans and maps as key-value tables; uniform collections as multi-column tables; simple collections as bulleted lists.
  • Document mode: Nested beans under sub-headings (H2, H3, etc.); customizable title, header, and footer content.
  • Inline complex values: Nested beans and collections in table cells rendered as backtick-wrapped JSON5.
Media Types​
  • Produces: text/markdown
  • Accepts: text/markdown, text/x-markdown

Markdown is included in BasicUniversalConfig and RestClient.universal() by default.

See Markdown Basics for full documentation.

Native YAML Support​

Juneau now includes built-in YAML serialization and parsing support, implemented entirely without external library dependencies. Like all other Juneau serializers and parsers, the YAML implementation uses a highly-efficient state machine for both serialization and parsing, converting directly between POJOs and YAML without intermediate DOM objects.

New Classes​

  • YamlSerializer - Serializes POJOs to block-style YAML output.
  • YamlParser - Parses YAML input into POJOs using an indentation-aware state machine.
  • YamlWriter - Specialized writer for YAML output with key quoting and escape handling.
  • Yaml - Convenience marshaller pairing YamlSerializer and YamlParser.
  • @YamlConfig - Annotation for applying YAML configuration to REST resources.
  • BasicYamlConfig - REST configuration interface for YAML-only resources.
Features​

The YAML serializer produces clean, human-readable block-style output:

Person person = new Person("John Smith", 30, List.of(
new Address("100 Main St", "Anytown", "NY", 12345)
));

String yaml = Yaml.of(person);

Produces:

name: John Smith
age: 30
addresses:
- street: 100 Main St
city: Anytown
state: NY
zip: 12345

The parser supports the full range of YAML constructs including block/flow mappings and sequences, quoted and plain scalars, literal and folded block scalars, comments, document markers, and all standard YAML scalar types (null, booleans, numbers).

Media Types​
  • Produces: application/yaml
  • Accepts: application/yaml, text/yaml
REST Integration​

YAML is included in BasicUniversalConfig by default, or can be used standalone via BasicYamlConfig.

See YAML Basics for full documentation.

YAML parser buffer-underflow fix on large documents​
  • Fixed a latent java.io.IOException: Buffer underflow. thrown by YamlParser when round-tripping larger YAML documents whose deeply-indented lines crossed the parser's internal 1024-char read buffer. The shared org.apache.juneau.marshall.parser.ParserReader had a hard-coded 10-char unread lookback budget on the unmarked-refill path, but the YAML parser's indent handling (skipBlanksAndCountIndent + unreadSpaces(...), plus similar patterns in parseBlockScalar and isNullBlockValue) routinely needs to unread an entire line's leading indent at once — 12-20+ chars for deeply-nested OpenAPI 3.1 documents. When the indent read happened to cross the buffer boundary, only the last 10 of those chars survived the refill and the rest of the unreads underflowed.
  • The fix widens that lookback budget from 10 to 256 chars (a new ParserReader.UNMARKED_LOOKBACK_CHARS constant). 256 covers any realistic YAML indent depth (128 levels at 2-space indent) while still leaving 768 chars per refill for forward-progress reads. The change has zero API surface — every parser benefits via the shared seam — and full utest suite (124k+ tests) runs at the same wall-clock as before.
  • Acceptance test: the full BasicRestServlet OpenAPI 3.1 document (six api-docs URLs + full components.schemas set) now round-trips JSON → YAML → JSON cleanly. Previously OpenApiYamlRoundTrip_Test#c01 had to scope to a single OpenApiMixin mount via noInherit={"mixins"} to dodge the underflow; that workaround has been removed.

Direct Iterator/Iterable/Stream/Enumeration Serialization​

Iterator, Iterable (non-Collection), Enumeration, and java.util.stream.Stream types are now serialized directly as arrays by all serializers without requiring intermediate List materialization.

Previously, Iterator and Enumeration objects were handled via IteratorSwap and EnumerationSwap which drained the entire sequence into a LinkedList before serialization began. Iterable (non-Collection) and Stream had no serialization support at all.

Lazy serialization (no intermediate List):

  • JSON, XML, UON, URL Encoding, and OpenAPI serializers write elements directly to the output one at a time via forEachStreamableEntry().

Materialized serialization (collected to List when the format requires it):

  • MsgPack (protocol requires array size in header), HTML (inspects elements for table layout), and CSV (inspects first element for column headers) collect to a List internally.

New API​

  • ClassMeta: New methods isIterator(), isIterable(), isStream(), isStreamable() for detecting these types.
  • ClassMeta: New Category enum values ITERATOR, ITERABLE, STREAM.
  • SerializerSession: New methods forEachStreamableEntry() and toListFromStreamable().
Example​
// Iterators are now serialized directly - no swap needed
Iterator<String> iterator = List.of("foo", "bar", "baz").iterator();
String json = Json5Serializer.DEFAULT.serialize(iterator);
// Produces: ['foo','bar','baz']

// Streams work the same way
Stream<Integer> stream = Stream.of(1, 2, 3);
String json2 = Json5Serializer.DEFAULT.serialize(stream);
// Produces: [1,2,3]

// Non-Collection Iterables are also supported
Iterable<String> iterable = () -> List.of("a", "b").iterator();
String json3 = Json5Serializer.DEFAULT.serialize(iterable);
// Produces: ['a','b']
Removals​
  • IteratorSwap - Removed. Iterators are now handled natively.
  • EnumerationSwap - Removed. Enumerations are now handled natively.
POJO Categories Update​

The POJO categories table has been updated. Iterator, Iterable, Enumeration, and Stream are no longer in the "swapped objects" category (group 4b). They are now in a new group (2c) alongside Collections and arrays as natively serializable sequence types.

Large-Dataset Streaming APIs​

New interfaces and annotations enable streaming serialization and parsing of arbitrarily large datasets without loading all elements into memory. Every serializer and parser format supports these APIs transparently.

New Classes​

  • BeanSupplier<T> — Serialization-side streaming interface (extends Iterable<T>). Provides beans lazily from a cursor or query via begin(), iterator(), onError(), and complete() lifecycle methods.
  • BeanConsumer<T> — Parse-side streaming interface (extends ThrowingConsumer<T>). Receives beans one-at-a-time from any parser for insertion or processing, with the same lifecycle methods.
  • BeanChannel<T> — Combines BeanSupplier and BeanConsumer for round-trip marshalling on a single property. The same object drives both serialization and parsing.
  • ListBeanChannel<T> — Built-in in-memory BeanChannel backed by an ArrayList, useful for testing and small in-memory datasets.
  • BeanFactory<T> — Universal @FunctionalInterface for creating instances of any type, enabling integration with dependency-injection frameworks such as Spring.
New Annotations​
  • @Bean(factory=X.class) — Specifies the BeanFactory class used to instantiate a type during parsing. The factory is resolved from the BeanStore (e.g. a Spring ApplicationContext).
  • @Beanp(factory=X.class) — Property-level factory for a specific bean property value.
  • @Beanp(elementType=Y.class) — Declares the element type for generic streaming properties (Stream<Y>, BeanSupplier<Y>, BeanConsumer<Y>, BeanChannel<Y>) to overcome Java type erasure. Also supports narrowing to concrete implementation types.
New API​
  • BeanContext.Builder.beanStore(BeanStore) — Wires a BeanStore (e.g. SpringBeanStore) into the marshalling context so @Bean(factory=) factories can be resolved from a DI container.
  • ParserSession.parseToBeanConsumer(Object, BeanConsumer<T>, Class<T>) — Parses an input source directly into a BeanConsumer, driving the full lifecycle automatically.
Lifecycle​

All three interfaces share the same three-phase lifecycle: begin() for setup, the transfer method (iterator() or acceptThrows()) for element-by-element data flow, onError() for error handling, and complete() for cleanup. complete() is always called regardless of whether an error occurred.

Key Behaviors​
  • Direction validation: using a BeanConsumer during serialization or a BeanSupplier during parsing throws an informative exception recommending BeanChannel for bidirectional use.
  • Getter-only BeanConsumer properties: parsers reuse a BeanConsumer exposed via a getter (no setter required), calling acceptThrows() on the existing instance — matching the existing behavior for mutable Collection properties.
  • Supplier<T> unwrapping: the standard JDK Supplier<T> (not BeanSupplier) is transparently unwrapped by serializers as a single lazy value, recursively up to depth 10.
  • Stream<T> auto-close: java.util.stream.Stream objects used during serialization are automatically closed via try-with-resources after iteration completes.
Example​
// Serialize a large table from a JDBC cursor — no memory materialization.
@Bean(factory=EmployeeSupplier.Factory.class)
public class EmployeeSupplier implements BeanSupplier<Employee> {
private Connection conn;
private ResultSet rs;

@Override public void begin() throws Exception {
conn = dataSource.getConnection();
rs = conn.prepareStatement("SELECT * FROM employee").executeQuery();
}
@Override public Iterator<Employee> iterator() { return Employee.cursorIterator(rs); }
@Override public void complete() throws Exception { rs.close(); conn.close(); }
}

// Bulk-insert with batch commits — no memory materialization.
@Bean(factory=EmployeeConsumer.Factory.class)
public class EmployeeConsumer implements BeanConsumer<Employee> {
private Connection conn;
private PreparedStatement stmt;
private int count;

@Override public void begin() throws Exception {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
stmt = conn.prepareStatement("INSERT INTO employee (name, dept) VALUES (?,?)");
}
@Override public void acceptThrows(Employee e) throws Exception {
stmt.setString(1, e.getName()); stmt.setString(2, e.getDepartment());
stmt.executeUpdate();
if (++count % 500 == 0) conn.commit();
}
@Override public void onError(Exception e) throws Exception { conn.rollback(); throw e; }
@Override public void complete() throws Exception { conn.commit(); stmt.close(); conn.close(); }
}

See Large-Dataset Streaming for full documentation.

Java Records Support​

Java records are now fully supported for both serialization and parsing across all serializers and parsers.

Records are treated as beans whose properties are derived from the record's components. The canonical constructor is automatically detected for parsing, so no @Beanc annotation is needed.

public record Person(String name, int age) {}

// Serialization
String json = Json5Serializer.DEFAULT.serialize(new Person("John", 30));
// Produces: {age:30,name:'John'}

// Parsing
Person p = JsonParser.DEFAULT.parse("{\"name\":\"John\",\"age\":30}", Person.class);

How It Works​

  • Property discovery: Record component accessor methods (e.g. name() for a name component) are recognized as getters without needing to follow the traditional getX() naming convention.
  • Constructor detection: The canonical constructor is automatically identified. During parsing, property values are cached and then passed to the canonical constructor to create the record instance.
  • Private fields: Record component fields are automatically included as bean properties despite being private final.
Annotation Support​

The following annotations work with records:

  • @Bean(properties) - Control property ordering (all components should be included).
  • @Beanp(name) - Rename properties.
  • @Beanc - Override the constructor used for parsing (e.g. to use a non-canonical constructor with defaults).
  • @Bean(typeName) / @Bean(dictionary) - Polymorphic record types.
Limitations​

Because records are immutable and all components must be provided to the canonical constructor, the following are not supported during parsing:

  • @BeanIgnore on individual record components.
  • @Bean(excludeProperties) excluding record components.
  • @Bean(readOnlyProperties) / @Beanp(ro) on record components.

These annotations will still affect serialization output. Use @Beanc with a non-canonical constructor to provide defaults for omitted components.

Changes​
  • BeanMeta: Added record detection logic in constructor discovery, accessor method recognition, and field introspection. Records are exempt from beansRequireDefaultConstructor and beansRequireSomeProperties checks.
  • BeanPropertyMeta: Constructor-arg properties are now correctly marked as writable.
  • @BeanIgnore, @Bean, @Beanp, @Beanc: Javadocs updated with records-specific notes.

See Java Records Support for full documentation.

Built-in Date/Time and Duration Serialization​

Date, time, and duration types are now serialized and parsed natively as ISO 8601 strings by all serializers and parsers without requiring ObjectSwap registration.

Previously, types such as Calendar, Date, Instant, ZonedDateTime, and other java.time.temporal.Temporal subtypes required default ObjectSwap instances (e.g. TemporalCalendarSwap, TemporalDateSwap) to be serialized as ISO 8601 strings. Without a swap, they would be serialized as beans or via toString().

Now, these types are recognized as first-class citizens by Juneau's type system and are handled directly in every serializer and parser session.

Supported Types​

All java.time.temporal.Temporal subtypes:

  • Instant, ZonedDateTime, OffsetDateTime, LocalDateTime, LocalDate, LocalTime, OffsetTime, Year, YearMonth

Legacy date/time types:

  • java.util.Calendar (and GregorianCalendar)
  • java.util.Date
  • javax.xml.datatype.XMLGregorianCalendar

Duration type:

  • java.time.Duration (serialized as ISO 8601 duration strings, e.g. "PT1H30M")
How It Works​
  • Serialization: All serializers check ClassMeta.isDateOrCalendarOrTemporal() and ClassMeta.isDuration() before bean checks in their serializeAnything() dispatch. Matching objects are formatted via Iso8601Utils.format().
  • Parsing: All parsers check the same conditions in their parseAnything() dispatch. ISO 8601 strings are converted back to the target type via Iso8601Utils.parse().
  • Type conversions: BeanSession.convertToMemberType() and SerializerSession.toString() also use ISO 8601 formatting for these types, ensuring consistent behavior in map keys, bean properties, and other contexts.
New API​
  • ClassMeta: New Category.DURATION enum value for java.time.Duration classification.
  • ClassMeta: New method isDuration().
  • Iso8601Utils: New utility class centralizing ISO 8601 formatting and parsing for all date/time and duration types.
Swap Classes as Overrides​

The existing swap classes (TemporalCalendarSwap, TemporalDateSwap, CalendarSwap, DateSwap, TemporalSwap) remain available but now serve as override mechanisms. If you register one of these swaps, it will take precedence over the built-in behavior, allowing you to customize the output format (e.g. RFC 1123 instead of ISO 8601).

// Default behavior - no swap needed, uses ISO 8601
String json = JsonSerializer.DEFAULT.serialize(Calendar.getInstance());
// Produces: "2026-02-28T12:00:00Z"

// Override with a swap for a different format
JsonSerializer s = JsonSerializer.create()
.swaps(TemporalCalendarSwap.Rfc1123DateTime.class)
.build();
String json2 = s.serialize(Calendar.getInstance());
// Produces: "Sat, 28 Feb 2026 12:00:00 GMT"

// Duration serialization - no swap needed
String json3 = JsonSerializer.DEFAULT.serialize(Duration.ofHours(1).plusMinutes(30));
// Produces: "PT1H30M"
POJO Categories Update​

The POJO categories table has been updated. Calendar, Date, Temporal subtypes, XMLGregorianCalendar, and Duration are no longer in the "swapped objects" category (group 4a). They are now in a new group (2d) as natively serializable date/time types.

See POJO Categories for the updated table.

RDF/THRIFT and RDF/PROTO Binary Format Support​

The juneau-marshall-rdf module now includes full support for RDF/THRIFT and RDF/PROTO binary serialization formats, enabling compact binary representation of RDF graphs for efficient storage and network transfer.

New Classes​

  • RdfThriftSerializer - Serializes POJOs to RDF/THRIFT binary format.
  • RdfThriftParser - Parses RDF/THRIFT binary input into POJOs.
  • RdfThrift - Convenience marshaller pairing RdfThriftSerializer and RdfThriftParser.
  • RdfProtoSerializer - Serializes POJOs to RDF/PROTO binary format.
  • RdfProtoParser - Parses RDF/PROTO binary input into POJOs.
  • RdfProto - Convenience marshaller pairing RdfProtoSerializer and RdfProtoParser.
Features​

Both formats use Apache Jena for underlying RDF model handling, with compact binary encoding via Apache Thrift and Protocol Buffers respectively:

Map<String,String> map = Map.of("foo", "bar");

// RDF/THRIFT
byte[] thriftBytes = RdfThrift.of(map);
Map<String,String> restored = RdfThrift.to(thriftBytes, Map.class, String.class, String.class);

// RDF/PROTO
byte[] protoBytes = RdfProto.of(map);
Map<String,String> restored2 = RdfProto.to(protoBytes, Map.class, String.class, String.class);

The binary formats are semantically equivalent to RDF/XML, RDF/JSON, and other RDF serializations—beans, maps, collections, and primitives are round-tripped correctly. Date/time types are parsed using built-in ISO 8601 support.

Round-Trip Test Coverage​

RDF/XML, RDF/THRIFT, and RDF/PROTO are now included in the RoundTripTest_Base and ComboRoundTrip_Tester frameworks, validating round-trip correctness across a broad set of bean types, collections, and edge cases.

See RDF Basics for full documentation.

ClassInfoTyped​

ClassInfo.of(Class) now returns ClassInfoTyped<T>, a generic subclass of ClassInfo that preserves the type parameter at compile time. The API remains identical—existing code continues to work without changes—but callers can now benefit from stronger typing:

ClassInfoTyped<String> ci = ClassInfo.of(String.class);
// ci carries type information for String

Per-Session Property Overrides (Extended)​

Session.Builder.property(String, Object) and properties(Map) now dispatch to typed session-level setter methods across a broad range of serializer and parser session classes, enabling per-request override of serializer/parser configuration without rebuilding the context.

Both short names (e.g. "escapeSolidus") and fully-qualified names (e.g. "JsonSerializerSession.escapeSolidus") are accepted as keys. BasicConverter handles type coercion so string values (e.g. "true") are automatically converted to the expected target types.

// Per-request JSON settings without rebuilding the serializer
String json = JsonSerializer.DEFAULT
.createSession()
.property("escapeSolidus", true)
.property("addBeanTypes", true)
.build()
.serialize(myBean);

// Per-request parser settings
MyBean bean = JsonParser.DEFAULT
.createSession()
.property("trimStrings", true)
.property("validateEnd", true)
.build()
.parse(json, MyBean.class);

New Session-Level Properties​

Session ClassNew Property Keys
ParserSessiontrimStrings
JsonSerializerSessionescapeSolidus
JsonParserSessionvalidateEnd
UonSerializerSessionencoding, paramFormat
UonParserSessionvalidateEnd
UrlEncodingSerializerSessionexpandedParams
UrlEncodingParserSessionexpandedParams
XmlSerializerSessionaddNamespaceUrisToRoot, autoDetectNamespaces, defaultNamespace, enableNamespaces, textNodeDelimiter
XmlParserSessionpreserveRootElement, validating
HtmlSerializerSessionaddKeyValueTableHeaders, detectLabelParameters, detectLinksInStrings, labelParameter, uriAnchorText

These complement the properties added in the initial release of this feature:

  • BeanTraverseSession: initialDepth
  • SerializerSession: keepNullProperties, trimStrings, addBeanTypes, addRootType, sortCollections, sortMaps, trimEmptyCollections, trimEmptyMaps
  • WriterSerializerSession: maxIndent, quoteChar
  • CsvSerializerSession: byteArrayFormat, allowNestedStructures, nullValue
Bug Fixes — Context Bypass​

Several session classes were overriding accessor methods to return ctx.isAddBeanTypes() directly, bypassing the session-level field. This meant that per-session addBeanTypes overrides set via property("addBeanTypes", true) had no effect for those serializers. The affected classes now correctly delegate to super.isAddBeanTypes():

  • JsonSerializerSession, UonSerializerSession, XmlSerializerSession, HtmlSerializerSession, MsgPackSerializerSession, YamlSerializerSession, CborSerializerSession, BsonSerializerSession, RdfSerializerSession, RdfStreamSerializerSession

Similarly, ParserSession.isTrimStrings() now returns the session-level field (initialized from the context) instead of delegating to ctx.isTrimStrings(). And UonSerializerSession.getQuoteChar() no longer bypasses the WriterSerializerSession session field.

MarshallUtils: Static Serialization and Parsing Helpers​

A new utility class, org.apache.juneau.marshaller.MarshallUtils, provides a single point of static convenience methods for every serialization format supported by Juneau. All methods are designed to be used with a static import so call sites need no explicit marshaller reference.

import static org.apache.juneau.marshaller.MarshallUtils.*;

// Serialize to any text format — returns String
String a = json(myBean);
String b = json5(myBean);
String c = xml(myBean);
String d = html(myBean);
String e = yaml(myBean);
String f = csv(myBean);
String g = toml(myBean);
String h = hocon(myBean);

// Serialize to binary formats — returns byte[]
byte[] m = msgPack(myBean);
byte[] n = cbor(myBean);
byte[] o = bson(myBean);
byte[] p = parquet(myBean);

// Parse — same method names, overloaded by input type
MyBean b1 = json(jsonString, MyBean.class);
MyBean b2 = xml(xmlString, MyBean.class);
MyBean b3 = msgPack(bytes, MyBean.class);
List<MyBean> beans = parquet(bytes, MyBean.class);

Supported Formats​

MethodFormatOutput
json(o)Standard JSON (RFC 8259)String
json5(o)JSON5String
jsonl(o)JSON Lines (NDJSON)String
jcs(o)Canonical JSON (RFC 8785)String
hjson(o)HJSONString
xml(o)XMLString
html(o)HTMLString
uon(o)URL-Encoded Object NotationString
urlEncoding(o)URL EncodingString
yaml(o)YAMLString
csv(o)CSVString
openApi(o)OpenAPIString
plainText(o)Plain textString
markdown(o)Markdown (fragment)String
markdownDoc(o)Markdown (document)String
ini(o)INI / propertiesString
toml(o)TOMLString
hocon(o)HOCONString
proto(o)Protobuf Text FormatString
msgPack(o)MessagePackbyte[]
cbor(o)CBOR (RFC 8949)byte[]
bson(o)BSONbyte[]
parquet(o)Apache Parquetbyte[]
Full Overload Set​

Every format method is overloaded across the following signatures:

SignaturePurpose
format(Object)Serialize to String (text) or byte[] (binary)
format(Object, Object)Serialize to a Writer, OutputStream, File, or StringBuilder
format(Object, Class<T>)Parse from any input (String, byte[], File, Reader, etc.) — throws IOException
format(Object, Type, Type...)Parse from any input with a parameterized type
format(String, Class<T>)Parse from String — wraps exceptions in unchecked runtime exceptions
format(String, Type, Type...)Parse from String with a parameterized type — unchecked
format(byte[], Class<T>)Parse from byte[] (binary formats) — unchecked
format(byte[], Type, Type...)Parse from byte[] with a parameterized type (binary formats) — unchecked

The checked overloads (Object-based input) propagate ParseException and IOException. The String- and byte[]-based overloads wrap checked exceptions in unchecked runtime exceptions so they can be used directly in lambdas and streams without try/catch.

All methods are designed for static import; no explicit marshaller reference is needed at call sites.

Also updated: Jcs and Parquet marshallers​

Both marshaller classes were updated to expose the full static API:

  • Jcs — Added of(Object, Object), to(Object, Class), to(Object, Type, Type...), to(String, Type, Type...) to match the standard char-marshaller contract.
  • Parquet — Added of(Object, Object), to(Object, Class), to(Object, Type), to(byte[], Type, Type...) (Parquet parse methods return List<T> since the format is always collection-oriented).

JsonMap and JsonList: New toX() Serialization Methods​

The serialization helper methods on JsonMap and JsonList have been renamed from the asX() convention to the toX() convention and extended with additional JSON-flavor methods.

Note: in the same release JsonMap.toString() / JsonList.toString() were retargeted from JSON5 to strict JSON as part of an earlier change (see the "Neutral MarshalledMap / MarshalledList" section above). That means toJson() (not toJson5()) is now the synonym for toString(), and the rows below have been updated to reflect the new mapping.

JsonMap​

New MethodDescription
toJson()Serializes to strict RFC 8259 JSON (double-quoted keys/strings). Synonym for toString().
toJson5()Serializes to JSON5 — unquoted keys, single-quoted strings.
toJsonl()Serializes to JSON Lines.
toJcs()Serializes to Canonical JSON (RFC 8785).
toHjson()Serializes to HJSON.
toReadableJson5()Serializes to indented, human-readable JSON5.
toString(WriterSerializer)Serializes using any WriterSerializer.
JsonList​
New MethodDescription
toJson()Serializes to strict RFC 8259 JSON. Synonym for toString().
toJson5()Serializes to JSON5.
toJsonl()Serializes to JSON Lines.
toJcs()Serializes to Canonical JSON (RFC 8785).
toHjson()Serializes to HJSON.
toString(WriterSerializer)Serializes using any WriterSerializer.
Removed Methods​

The following asX() methods have been removed in favor of their toX() equivalents:

RemovedReplacement
asJson()toJson() or toString() (now both strict JSON)
asString()toString() — but note toString() is now strict JSON. Callers that genuinely want the old JSON5 form should switch to Json5Map.toString() / Json5List.toString() (or call toJson5() on JsonMap / JsonList).
asReadableString() (JsonMap only)toReadableJson5()
asString(WriterSerializer)toString(WriterSerializer)

BasicBeanStore precedence-aware affordances​

BasicBeanStore (in org.apache.juneau.cp) gained two new affordances that power the 10.0 RestContext precedence flip (see juneau-rest-server below). Both are opt-in; existing call sites that use only parent(...) and local addBean(...) calls keep their current behavior.

  • Builder.overridingParent(BasicBeanStore) — Registers a parent bean store that is consulted before local entries during getBean(...) / getBeanSupplier(...). The classic parent(...) slot keeps its post-entry fallback role. Useful for bridging an outer scope (Spring ApplicationContext, parent-resource bootstrap, test harness) on top of a child bean store and having the outer scope win without mutating local entries.
  • addDefaultSupplier(Class, Supplier) (and a named variant) — Registers a fallback supplier that fires only after local entries and the regular parent chain have all returned empty. Stored in a separate map from regular entries, so resolution order is unambiguous and addBean(...) calls can override the default at any time without removing the fallback.

Final resolve order on BasicBeanStore is:

overridingParent.getBeanSupplier(type, name)        // outer scope (e.g. Spring)
-> entries[type][name] // addBean(...) / @Bean
-> parent.getBeanSupplier(type, name) // regular fallback
-> defaults[type][name] // addDefaultSupplier(...)

hasBean(...) follows the same precedence; a new hasDefaultSupplier(...) accessor lets callers ask "is there a registered default for this type?" without forcing the supplier to fire.

Removal of Legacy cp Injection Classes​

The following classes in org.apache.juneau.cp have been removed as part of the migration to the org.apache.juneau.commons.inject package:

  • BasicBeanStore (was org.apache.juneau.cp.BasicBeanStore) — replaced by org.apache.juneau.commons.inject.BasicBeanStore (renamed from BasicBeanStore).
  • BeanCreator — replaced by org.apache.juneau.commons.inject.BeanInstantiator.
  • BeanBuilder — replaced by org.apache.juneau.commons.inject.BeanInstantiator (was the base class for domain fluent builders; replaced with native builder fields and BeanInstantiator).
  • BeanCreateMethodFinder — replaced by BeanStore.createBeanFromMethod(...).
  • ContextBeanCreator is not removed — it solves a different problem (persistent Context.Builder holder for repeated annotation application) and is retained as a first-class API.

Migration guide:

Legacy (org.apache.juneau.cp)Replacement (org.apache.juneau.commons.inject)
BasicBeanStore.create().build()new BasicBeanStore()
BasicBeanStore.INSTANCEBasicBeanStore.INSTANCE
BasicBeanStore.of(parent)new BasicBeanStore(parent)
BeanCreator.of(MyBean.class, store)BeanInstantiator.of(MyBean.class, store)
creator.arg(Type.class, value)instantiator.addBean(Type.class, value)
creator.type(Impl.class)instantiator.type(Impl.class)
creator.impl(instance)instantiator.impl(instance)
creator.run()instantiator.run()
creator.orElse(default)instantiator.asOptional().orElse(default)
new BeanCreateMethodFinder(type, obj, bs)bs.createBeanFromMethod(type, obj, predicate)

BeanInstantiator Static Convenience Methods​

Three static factory helpers have been added to BeanInstantiator to eliminate verbose ternary patterns at common call sites:

MethodEquivalent
BeanInstantiator.createOrNull(Class<T>)beanType == null ? null : BeanInstantiator.of(beanType).fallback(() -> null).run()
BeanInstantiator.optionalOf(Class<T>)beanType == null ? Optional.empty() : BeanInstantiator.of(beanType).asOptional()
BeanInstantiator.createOrDefault(Class<T>, T)beanType == null ? defaultValue : BeanInstantiator.of(beanType).run()

All three return null / Optional.empty() / the default when the class argument is null, avoiding null-checks at every call site. Typical usage:

// Before:
var listenerCls = ctx.getListener();
listener = listenerCls == null ? null : BeanInstantiator.of(listenerCls).fallback(() -> null).run();

// After:
listener = BeanInstantiator.createOrNull(ctx.getListener());

CreatableBeanStore Removed​

CreatableBeanStore (in org.apache.juneau.commons.inject) has been removed. It was an unused internal interface with no production callers. WritableBeanStore serves as the write-capable interface for the inject package.

WritableBeanStore → BeanStore in Builder Classes​

The following builder-class constructors and fields have been narrowed from WritableBeanStore to BeanStore because they only perform read operations (bean lookup) on the store; they do not add beans:

  • EncoderSet.Builder
  • SerializerSet.Builder
  • ParserSet.Builder
  • ResponseProcessorList.Builder
  • RestOpArgList.Builder
  • RestMatcherList.Builder
  • RestGuardList.Builder
  • RestConverterList.Builder
  • RestOperations.Builder
  • RestChildren.Builder

Callers that previously passed a WritableBeanStore still compile without changes, since WritableBeanStore extends BeanStore.

JSR-330 Alignment + Spring-Lite Additions in commons.inject​

The org.apache.juneau.commons.inject package now presents a coherent injection story:

Juneau supports JSR-330 (jakarta.inject / javax.inject) annotations as closely as our lightweight model allows, without taking a hard dependency on jakarta.inject-api. Where Spring conventions add value to the lightweight model, we adopt them under our own annotations.

All JSR/Spring annotation recognition is by fully qualified name (FQN), so applications can use jakarta.inject.Inject, javax.inject.Inject, org.springframework.beans.factory.annotation.Autowired, or org.apache.juneau.commons.inject.Inject interchangeably — Juneau picks them up transparently without a compile-time dependency.

New annotations and types​

TypePurpose
@Inject (Juneau-owned)Marks constructor parameters, methods, or fields as injection points. JSR-330 / Spring equivalents (jakarta.inject.Inject, javax.inject.Inject, @Autowired) are recognized by FQN.
@NamedBean qualifier (moved from org.apache.juneau.annotation.Named — see migration row below).
@QualifierMeta-annotation marker used to declare custom qualifier annotations.
@SingletonScope marker. Semantics: a singleton is registered once in a BeanStore and reused; no JVM-wide singleton, no proxying.
@PrimaryDisambiguates when multiple candidates of the same type exist. Unqualified BeanStore.getBean(type) prefers a single @Primary candidate; multiple primaries throw BeanCreationException.
@Order(int) and @Bean#priority()Ordering for BeanStore.getBeansOfType(Class). Lower values are higher precedence. When both are present, @Order wins.
@ConfigurationMarker for types that host @Bean declarations. Supports imports = { ... } for chaining configuration classes (deduped transitively). @Bean members declared on superclasses are inherited.
@PostConstruct / @PreDestroyLifecycle hooks. JSR-250 jakarta.annotation / javax.annotation equivalents are recognized by FQN.
@Conditional(Class<? extends Condition>)Predicate-driven registration guard.
@ConditionalOnClass(String)Skip the annotated configuration / @Bean when a class is absent from the classpath.
@ConditionalOnMissingBean(Class, name)Skip when a matching bean is already registered.
@ConditionalOnProperty(name, havingValue, matchIfMissing)Skip / register based on a Settings property.
Provider<T>Single-method T get() interface. Parameters and fields typed as Provider<T> (Juneau / jakarta.inject / javax.inject) are resolved via a java.lang.reflect.Proxy over a Supplier<T> from the bean store.
Condition / ConditionContextSPI for custom @Conditional predicates with access to the bean store, settings facade, classloader, and annotated element.
JsrSupportSingle utility class centralizing recognized FQN constants (JAKARTA_INJECT, SPRING_AUTOWIRED, etc.) and matcher helpers.
@Configuration registration​

WritableBeanStore.registerConfiguration(Class<?>) and registerConfigurations(Class<?>...) register a @Configuration class with the store:

  • Static and non-static @Bean fields and methods are discovered.
  • imports are processed recursively and deduplicated; the same class registered twice (directly or transitively) is processed only once per bean store.
  • @Bean members declared on superclasses of the configuration type are inherited; the superclass is processed before the subclass so subclass @Bean methods can depend on superclass beans.
  • Duplicate (type, name) registrations within the same store throw BeanCreationException.
  • @Conditional annotations on the configuration class skip the entire class (and any imports it would have pulled in if the configuration class itself is conditional); on individual @Bean members they skip only that member.
Bean lifecycle: AutoCloseable​

WritableBeanStore now extends AutoCloseable. Calling close():

  • Invokes @PreDestroy methods on all beans that were resolved through getBean(...) / getBeansOfType(...), in LIFO order (most-recently resolved first).
  • Aggregates per-bean destroy failures as suppressed exceptions on a single thrown BeanCreationException.
  • Marks the store as closed; subsequent mutating operations throw IllegalStateException.
  • Is idempotent; a second close() is a no-op.

RestContext.destroy() now invokes beanStore.close() after the existing @RestDestroy hooks fire, so REST resources can rely on @PreDestroy for cleanup of bean-store-managed dependencies.

@Inject on @Rest resource fields and methods​

@Rest resources now have @Inject-annotated fields and methods populated from the resource's bean store during context initialization, immediately before @PostConstruct callbacks fire. This applies to all four recognized FQNs (Juneau, jakarta.inject.Inject, javax.inject.Inject, Spring @Autowired). Optional<T>, collection types, and Provider<T> are all supported as field / parameter types.

Constructor visibility for BeanInstantiator​

BeanInstantiator now considers package-private constructors as a final fallback after public and protected, both for the target bean type and for builder discovery. private constructors remain excluded (they signal "do not instantiate"). This means @Configuration classes and other framework-instantiated types no longer need to expose a public (or even protected) constructor purely to satisfy the bean store, as long as the class is reachable in the declaring package.

Migration guide​
TopicAction
org.apache.juneau.annotation.NamedMoved to org.apache.juneau.commons.inject.Named. Update imports — no alias is kept at the old location.
Simple-name annotation matchingRecognition is now strictly FQN-based. Local @interface Inject / @interface Autowired / @interface PostConstruct mocks in tests no longer trigger injection; use the real Juneau-owned types or the canonical JSR-330 / Spring FQNs.
@PostConstruct discoveryNow matches by FQN (org.apache.juneau.commons.inject.PostConstruct, jakarta.annotation.PostConstruct, javax.annotation.PostConstruct). Custom annotations named PostConstruct are no longer picked up.
WritableBeanStore implementationsMust now implement void close() throws BeanCreationException (typically by delegating to a wrapped BasicBeanStore).
@Bean precedence chainUnchanged from earlier in 10.0.0: Spring / overriding-parent → @Bean / local entry → memoizer-backed framework default.

juneau-core​

BctConfiguration defaults migrated to @Value (juneau-bct, org.apache.juneau.bct)​

  • BctConfiguration now exposes a static nested Defaults class whose two boolean fields (sortMaps, sortCollections) are populated by concat-form @Value annotations: @Value("${" + BCT_SORT_MAPS + ":false}") / @Value("${" + BCT_SORT_COLLECTIONS + ":false}"). A BctConfiguration.defaults() static factory routes construction through BeanInstantiator, so the defaults respect the standard Settings precedence chain.
  • The two public constants (BCT_SORT_MAPS, BCT_SORT_COLLECTIONS) are kept, since they have external production consumers and heavy Javadoc as part of the BCT public API. See @Value Framework-Internal Adoption.

ArrayUtils Deprecation​

ArrayUtils has been deprecated in favor of CollectionUtils. All array utility methods have been migrated to CollectionUtils, which now provides a single location for both array and collection operations. ArrayUtils methods delegate to their CollectionUtils counterparts and will remain available during the deprecation period.

Migration: Replace ArrayUtils usage with CollectionUtils. Methods such as last(), append(), combine(), indexOf(), toList(), contains(), and others are available in CollectionUtils with the same signatures.

TupleXFunction Removed​

The Tuple2Function, Tuple3Function, Tuple4Function, and Tuple5Function classes have been removed. Use Function2, Function3, Function4, and Function5 instead. The API is equivalent—replace Tuple2Function with Function2, and so on.

ThrowableUtils Enhancements​

New exception creation methods have been added to ThrowableUtils:

  • unsupportedOp(String msg, Object... args) and overload with cause
  • ioException(String msg, Object... args) and overload with cause
  • Cause overloads for existing methods: illegalArg(Throwable cause, String msg, Object... args) and runtimeException(Throwable cause, String msg, Object... args)

All methods support MessageFormat-style formatting. BasicRuntimeException has been retired in favor of runtimeException().

Value Class Enhancements​

The Value family of classes has been expanded:

  • New classes: StringValue and ByteValue
  • Numeric and character types (IntegerValue, ShortValue, LongValue, CharValue, ByteValue): New methods increment(), decrement(), incrementAndGet(), decrementAndGet(), add(X), addAndGet(X), is(X), isAny(X...). CharValue also has isAny(String). FloatValue and DoubleValue have isAny(X precision, X... values) for precision-based equality.
  • All Value classes: New methods orElse(X), setIf(boolean, value), and update(Function<V,V>) for in-place modification.
  • Value base class: Enhanced to mirror Optional with empty(), filter(Predicate), flatMap(Function), ifPresent(Consumer), map(Function), orElseGet(Supplier), orElseThrow(Supplier).

Console.format Removed​

Console.format() has been removed. It duplicated functionality already provided by Utils.f(). Use Utils.f(String format, Object... args) for MessageFormat-style formatting. Console.err() and Console.out() continue to work as before.

CollectionUtils.treeSet​

CollectionUtils.treeSet(Comparator<? super E>, Collection<? extends E>) creates a modifiable TreeSet with the given comparator and adds all non-null elements from the collection (or none if the collection is null). Use with String.CASE_INSENSITIVE_ORDER for a case-insensitive sorted string set in one call.

StringTokenSet​

StringTokenSet (juneau-commons) builds an insertion-ordered unique set of string tokens. append(String...) splits each argument on commas with StringUtils.split escape rules, then applies mechanical rules (token starting with - and length > 1 removes the trimmed remainder; otherwise the token is added). appendAtomic(String...) applies one mechanical step per argument with no comma splitting. addAllPlain(Iterable) copies members without hyphen rules (used when extending an existing allowlist). toUnmodifiableSet() returns an unmodifiable snapshot.

AnnotationInfo.getAnnotatable()​

AnnotationInfo.getAnnotatable() returns the Annotatable (class, method, field, …) where the annotation was found, for callers that need the annotation’s declaration site.

Simple Variable Language (SVL) Moved to juneau-commons​

The Simple Variable Language (SVL) engine — VarResolver, VarResolverSession, Var, and the bundled vars/* implementations — has been physically moved out of juneau-marshall into juneau-commons so that callers (including juneau-config, juneau-microservice-core, and downstream services) can depend on the variable-resolution engine without pulling in the full marshalling stack.

Package Moves (breaking)​

OldNew
org.apache.juneau.commons.svl.*org.apache.juneau.commons.svl.*
org.apache.juneau.commons.svl.vars.*org.apache.juneau.commons.svl.vars.*
org.apache.juneau.commons.svl.ResolvingJsonMaporg.apache.juneau.collections.ResolvingMarshalledMap

Source code referencing the old packages must update its import statements. The map has also been renamed to ResolvingMarshalledMap and re-parented from JsonMap to the new neutral MarshalledMap base (see the earlier change entry near the top of this file) — its SVL-resolution behavior is language-agnostic. Hard rename; no deprecation shim, so callers must update both the import and any new ResolvingJsonMap(...) constructor calls.

Args and ManifestFile Redesigned and Moved (breaking)​

The legacy org.apache.juneau.collections.Args and org.apache.juneau.utils.ManifestFile classes — both of which extended JsonMap — have been replaced with lean, immutable Optional-returning equivalents in juneau-commons:

OldNew
org.apache.juneau.collections.Args (extends JsonMap)org.apache.juneau.commons.runtime.Args (immutable, no JsonMap dependency)
org.apache.juneau.utils.ManifestFile (extends JsonMap)org.apache.juneau.commons.runtime.ManifestFile (immutable, no JsonMap dependency)

The new Args:

  • Returns Optional<String> from get(int) and get(String); multi-valued options are accessed via getAll(String).
  • Provides positional(), argCount(), optionCount(), has(int), has(String), isEmpty(), and asMap() accessors.
  • Exposes a configurable Args.Builder (allowEquals, allowShortFlags, allowLongFlags, allowSystemPropStyle, caseSensitive, customPrefix) so callers can opt into stricter or different grammars (--key=value, -Dkey=value, etc.).

The new ManifestFile:

  • Returns Optional<String> from get(String) (main attributes) and get(String section, String key) (per-section attributes).
  • Provides sections(), asMap(), and asMap(String section) accessors.
  • Constructors accept Class<?>, Manifest, File, Path, InputStream, and Reader.

Microservice.Builder.args(Args) and Microservice.Builder.manifest(Object) continue to work; their parameter types now resolve to the new commons.runtime classes. Direct callers of args.getArg(...) / args.hasArg(...) / manifest.getString(...) / manifest.containsKey(...) must adopt the new Optional-returning API.

ArgsVar and ManifestFileVar — Supplier Overloads​

ArgsVar and ManifestFileVar retain their static init(Args) / init(ManifestFile) registration for backwards compatibility, and gain new factory methods that accept a Supplier:

VarResolver vr = VarResolver.create()
.defaultVars()
.vars(ArgsVar.create(() -> myArgs), ManifestFileVar.create(() -> myManifest))
.build();

This lets each VarResolver instance carry its own per-resolver Args / ManifestFile state instead of relying on the JVM-wide static. The static init(...) form is targeted for deprecation in a follow-up release.

Removed APIs​
RemovedReplacement
org.apache.juneau.collections.Argsorg.apache.juneau.commons.runtime.Args
org.apache.juneau.utils.ManifestFileorg.apache.juneau.commons.runtime.ManifestFile
Args.getArg(int) / getArg(String) / getArgs(String) / hasArg(String) / size()Args.get(int) / get(String) (returns Optional<String>) / getAll(String) / has(String) / argCount() + optionCount()
ManifestFile.getString(...) / getWithDefault(...) / containsKey(...) (and other JsonMap accessors)ManifestFile.get(String).orElse(...) / get(String, String)

juneau-bct (Bean-Centric Testing)​

BctAssertions API Changes​

The AssertionArgs class has been removed. Assertion methods now use a leading Supplier<String> parameter for custom messages. The default converter can be overridden and reset via setConverter() and resetConverter(). BCT now lives in juneau-bct (package org.apache.juneau.bct):

// Before: AssertionArgs.withConverter(customConverter).assertBean(...)
// After: setConverter(customConverter); assertBean(...); resetConverter();

// Custom message: assertBean(() -> "custom msg", expected, actual, "propertyPath");

The thread-local converter supplier defaults to BasicBeanConverter.DEFAULT (org.apache.juneau.bct) and can be configured per test or per thread.

  • New BctAssertions.assertNotContains(String unexpected, Object actual): asserts that the converted string form of actual does not contain unexpected. A Supplier<String> message overload (assertNotContains(Supplier<String>, String, Object)) is also available.

juneau-bean​

ParamInfo Replaced with ParameterInfo​

The legacy ParamInfo type in the Swagger v2 module has been fully replaced with ParameterInfo. All references have been migrated. If you have code that references ParamInfo, update it to use ParameterInfo.