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). CalldisableDefaultViewInclusion()to reverse. - Union semantics:
@MarshalledProp(view = {"A","B"})makes a property visible whenAorBis 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@MarshalledPropattribute. - 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, ormy-stage.ymlfor 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 viaConfig.Builder.profiles(...)or thejuneau.profiles.activekey (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.activepiggybacks onEnvironment.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/myKeyare satisfied byMY_PROP/MY_SECTION_MY_KEY. The defaultSettingsenv source is wrapped withRelaxedPropertySource(verbatim-first, so exact-match is preserved); it can wrap any otherPropertySourcetoo.
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 inBasicUniversalConfigand theRestClient.universal()set. - Media-type aliases —
application/jsonlines(the type suggested by the JSON Lines spec) is accepted as an alias ofapplication/jsonl, andapplication/json5linesas an alias ofapplication/json5l, for bothAcceptandContent-Typenegotiation. Produced responses keep the canonicalapplication/jsonl/application/json5lContent-Type.ContentType.APPLICATION_JSONLINESandContentType.APPLICATION_JSON5LINESconstants 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— passnullthrough to the setter (current behavior, default).EMPTY— substitute"", an empty mutableCollection/Map, orOptional.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 backingMap/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-copycopy()snapshot, and the RFC 6901 pointer methodsat/find/set/remove. Created viaMarshalledNode.of(Object),MarshalledNode.objectNode(), andMarshalledNode.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 staticencodeToken/decodeToken(~0→~,~1→/). The empty string""addresses the whole document; a non-empty pointer must begin with/.- Convenience —
MarshalledMap.at(String)andMarshalledList.at(String)resolve a pointer and return aMarshalledNode.
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; acceptsapplication/protobufandapplication/x-protobuf(no collision with texttext/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=…)(zigzagsint*,fixed*/sfixed*,uint*, enum-by-name). - Containers: packed repeated scalars, repeated tagged strings/bytes/messages, length-delimited nested
messages, and
Map<K,V>as protomap<>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_MILLISnormalization, so default output is byte-for-byte unchanged. - Write knobs —
pageSizeandrowGroupSizeare 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, …) addto(Reader, Class<T>),to(Reader, Type, Type...), andof(Object, Writer). - Binary facades (
MsgPack,Cbor,Bson,Protobuf,Parquet) addto(InputStream, Class<T>),to(InputStream, Type, Type...), andof(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 byJson5Serializer.DEFAULT_READABLE.IniR— readable (spaced) INI, backed byIniSerializer.DEFAULT_READABLE.HjsonC— compact (single-line) Hjson, backed byHjsonSerializer.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(repeatedkey=valuefor 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):*SpacedHexand*Base64(alternatebyte[]wire representations), plusCborNative/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 revision2025-06-18wire beans (renamed from the withdrawn draft'sjuneau-bean-mcp).juneau-rest-server-mcp-v20250618— the2025-06-18REST-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 onjuneau-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— typedMcpClientfacade for MCP2025-06-18.juneau-rest-client-mcp-v20260728— typedMcpClientfacade for MCP2026-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 threepublic statichelpers used by dispatch implementations:ok(id, result)anderrorResponse(id, code, message[, data])(both response factories), plusnotification(id)(abooleanpredicate testing whether an id represents a JSON-RPC notification, i.e.id == null).JsonRpcError—code,message,data.McpException— aRuntimeExceptioncarrying JSON-RPC error fields (code,data) for handler-side propagation, withtoJsonRpcError()converting it to aJsonRpcError.
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), anderrorCode(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 viaMcpRevision.errorCode(McpErrorKind).McpServerConfig— re-shaped aggregate registry of tools, prompts, resources,name/version,instructions, and pagination strategy (cursor). No longer holdsprotocolVersion(now owned byMcpRevision.protocolVersion()) orcapabilities(now owned by a revision-specific hook — see the2025-06-18adapter below);setProtocolVersion(String)has no replacement.McpRestServlet— abstractBasicRestServletsubclass exposingPOST /. Subclasses implementcreateMcpConfig(); a new abstractrevision()method supplies the bound protocol revision.McpEndpoint— mixin interface exposingPOST /mcpon any@Restresource, re-shaped the same way (getMcpConfig()plus an abstractrevision()).- Neutral model —
McpToolSpec/McpToolOutcome,McpPromptSpec/McpPromptArgument/McpPromptMessage/McpPromptOutcome,McpResourceSpec/McpResourceOutcome,McpContentBlock,McpResourceContents,McpRole, and the unconstrained JSON-object carrierMcpSchema. 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@FunctionalInterfacehandlers, now typed against the neutral spec/outcome types above (previously typed against wire beans). Each keeps its default-throwingdescriptor(), 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) andMcpCursor.fixedSize(n)(opaque integer-offset paging).McpParamUtils— sharedparams-coercion plumbing (asMap/strParam/mapParam) used by revisiondispatch()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 theJsonSchemaargument-schema bean.Tool.outputSchemaandCallToolResult.structuredContent— part of MCP structured tool output since2025-06-18but 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 polymorphicResourceContents(TextResourceContents,BlobResourceContents). - Polymorphic content blocks —
Contentinterface withTextContent,ImageContent,EmbeddedResourceContentdiscriminated by atypeproperty. - Constants —
McpMethods(JSON-RPC method names) andMcpProtocol(version constants, including theJSON_RPC_2_0literal).
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— theMcpRevisionimplementation for protocol revision2025-06-18. Replaces the oldMcpDispatcher/Mcpfaç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-32002in 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 coreMcpRestServlet) and implementcreateMcpConfig(). Exposes aprotected ServerCapabilities capabilities()hook (returnsnullby default) for explicitly overriding theinitializecapabilities 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 withorg.apache.juneau.rest.server.mcp.v20250618.McpRestServlet: implementgetMcpConfig(), and optionally override itsdefault 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 neutralMcpPromptHandlerraw 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, andresources/readresults can each carry a nullable, top-levelttlMs(Integer) andcacheScope(McpCacheScope:public/private) pair, via the newCacheableResult<T>CRTP base thatListToolsResult,ListPromptsResult,ListResourcesResult,ListResourceTemplatesResult, andReadResourceResultall 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) listingResourceTemplatedescriptors (uriTemplate,name,title,description,mimeType) registered via the neutralMcpServerConfig.addResourceTemplate(McpResourceTemplateSpec). Registering at least one resource template auto-derives theresourcescapability, same as resources. The pre-built neutralMcpResourceTemplateSpecdescriptor and this endpoint are designed for reuse by the plannedresources/templates/readcompletion work.- Static, adapter-owned configuration only — a server configures cache hints once, at construction time, via the new
McpCacheConfig/McpCacheHinttypes, mutated in place through thecache(Consumer<McpCacheConfig>)configure-block on theMcpOptionsaggregate (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, forresources/read, adds an exact-URI override map ahead of aresourcesReaddefault. 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/McpCacheScopeall live in the2026-07-28adapter/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 existingMcpResourceTemplateSpecdescriptor with a variable-awareread(String uri, Map<String,String> variables, BeanStore ctx)and an optional per-variablecompleter(String variableName).McpServerConfig's primary resource-template registration is now handler-based (getResourceTemplates()/setResourceTemplates(List<McpResourceTemplateHandler>)/addResourceTemplate(McpResourceTemplateHandler...)); the existing descriptor-onlyaddResourceTemplate(McpResourceTemplateSpec...)registration style remains as a listing-only convenience, now implemented as a built-in handler whoseread/completerboth returnnull. Bulk descriptor replacement is the newsetResourceTemplateSpecs(List<McpResourceTemplateSpec>)(a secondsetResourceTemplates(List<McpResourceTemplateSpec>)overload isn't possible under Java generic erasure). The complete registry is validated at registration time — malformed/duplicate templates fail fast withIllegalArgumentExceptionnaming 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/readresolves 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, andMcpCompletionResultmodel acompletion/completerequest/result independent of either dated wire format.McpPromptArgumentgains a nullable, non-wirecompleterproperty (its existingname/description/requiredfields 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, returnsnull, or returns an invalid result is-32603. At most 100 values are ever emitted, forcinghasMore=trueon truncation — Juneau performs no ranking, filtering, deduplication, or additional pagination of completion values. 2025-06-18backfill — addsResourceTemplateandListResourceTemplatesResultbeans (the same five/two-field shape2026-07-28already had, minus cache fields),McpMethods.RESOURCES_TEMPLATES_LIST, andresources/templates/listdispatch, 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 C2ResourceTemplate/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 reportRESOURCE_NOT_FOUND(-32602).- New completion wire beans (both revisions, independent copies) —
CompleteRequest,CompletionReference(closed polymorphic base withPromptReference/ResourceTemplateReference, discriminatedref/prompt/ref/resource),CompletionArgument,CompletionContext,CompleteResult,Completion, and an emptyCompletionCapabilitymarker under a new nullableServerCapabilities.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. For2026-07-28,completion/completeis deliberately nameless for SEP-2243 (Mcp-Method=completion/complete,Mcp-Name="", since the target is nested underparams.ref); normal per-request_metavalidation still applies. - Included refactor:
McpJsonValueSafetyrelocated tojuneau-commons. The MCP core's bounded-depth/node/deadline JSON-value walker (used for structured tool-output safety) moves tojuneau-commonsasorg.apache.juneau.commons.utils.JsonValueSafety, with an identical API and behavior — a rename-only move (drops theMcpprefix, since the class was already protocol-neutral) bundled into this same change because it touches the same neutral core and both dated adapters.juneau-commonsgains no MCP dependency from this move. - Scope fence, explicitly out of this change — resource subscriptions (
resources/subscribe/resources/unsubscribeon2025-06-18, the newersubscriptions/listenstreaming request on2026-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.
_metawire-compliance reshape. The neutral JSON-RPC envelope beans (JsonRpcRequest/JsonRpcResponse) carry no_metaproperty; it now lives underparams._meta(newRequestMeta, via a newRequestParams<T>CRTP base onCallToolRequest/GetPromptRequest/ReadResourceRequest/CompleteRequest, plusRequestParamsOnlyfor the metadata-only methods) with the exactio.modelcontextprotocol/-prefixed negotiation keys the schema mandates, and underresult._meta(newResultMeta, via a newResult<T>CRTP base every successful result extends) alongside a new requiredresultType("complete") discriminator.ServerDiscoverResultnow extendsCacheableResult<ServerDiscoverResult>and carries its server identity on the inherited_metarather than a top-levelserverInfofield.Implementationgainstitle/description/icons(newIconbean);ClientCapabilities/ServerCapabilitiesgain anextensionsmap; a new typedPingResultreplaces a bareJsonMapforping. This is a correction to the wire shape, not a compatibility mode — there is no old-shape fallback.- Trace-context propagation.
params._metamay additionally carry bare (unprefixed) W3Ctraceparent/tracestate/baggagekeys. When an activeTracerHookis registered, an explicitparams._metavalue wins over the equivalent HTTP header (independently per field); the active trace context is echoed back intoresult._metaon success; and the OpenTelemetry bridge (when present) enriches the span with pinned, experimentalgen_ai.*/mcp.*attributes and maps JSON-RPC errors (which always ride an HTTP200) onto span error status via a newScope.recordRpcError(code, message). Built on three new neutraljuneau-rest-servertypes (TraceContextCarrier,TraceContextExtractor,TraceOperation) plus aTracerHook.startSpan(RestRequest, TraceContextCarrier, TraceOperation)default overload — all source-compatible, so existingTracerHook/Scopeimplementations keep compiling unchanged. Thegen_ai.*/mcp.*attribute names are pinned to a specific OpenTelemetry GenAI semantic-conventions snapshot and marked experimental. SpringMcpRestServletconvenience class (both revisions). A newSpringMcpRestServlet(org.apache.juneau.rest.server.mcp.v20250618.SpringMcpRestServletand.v20260728.SpringMcpRestServlet) combinesBasicSpringRestServletwith the revision'sMcpEndpointmixin — the simplest way to expose MCP from a Spring Boot resource with full Spring dependency-injection support in tool/prompt/resource handlers; implement onlygetMcpConfig(). Don't subclass the plainMcpRestServletunder Spring Boot instead — it's a sibling ofSpringRestServlet, not a parent ofBasicSpringRestServlet, 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 neutralMcpServerConfigand the nestedMcpCacheConfig/McpMrtrConfig/McpSubscriptionsConfigtypes it wraps. PlaingetXxx()/setXxx(...)forcapabilities/instructions/subscriptionBroker, plusConsumer-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.
McpRestServletgainscreateMcpOptions()(mirroringcreateMcpConfig());McpEndpointgainsgetMcpOptions()(mirroringgetMcpConfig()). The prior six hooks (capabilities(),instructions(),cacheConfig()/createCacheConfig(),mrtrConfig()/createMrtrConfig(),subscriptionsConfig()/createSubscriptionsConfig(), the v2subscriptionBroker()override) and the wiring seamsubscriptionsConfigBean()are gone from the user-visible surface. - Uniform per-binding memoization.
McpOptionsis 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 McpOptionsin the application context (Spring users inject their configuration into thecreateMcpOptions()/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-deletedSharedMrtrConfig/SharedSubscriptionBrokersingletons) while the servlet path was already per-instance. Both paths are now uniformly per-binding — two distinct bindings always get distinctmrtrkeys and broker registries. - Subscription broker folded in.
McpOptions.getSubscriptionBroker()defaults tonull("framework-derived"): the binding lazily derives and memoizes aBasicMcpSubscriptionBrokersized fromsubscriptions'queueSize. CallsetSubscriptionBroker(...)to supply a custom broker instead. - Client-side elicitation naming split, for symmetry.
org.apache.juneau.rest.client.mcp.v20260728.ElicitationAccessis replaced by anElicitationRequests(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-neutralsubscriptionBroker()/getSubscriptionBroker()seam is unchanged; only the v2 adapter delegates throughMcpOptions.
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
KeyProviderSPI (org.apache.juneau.rest.server.mcp.v20260728) —currentKey(): KeyedSecret(the key to seal new tokens with, plus itskeyId) andresolveKey(keyId): Optional<SecretKey>(never throws; empty on an unknown/retiredkeyId).KeyedSecretis a smallkeyId/SecretKeyrecord with non-blank/length-boundedkeyIdvalidation. - Two built-ins.
EphemeralKeyProviderpreserves today's exact zero-config behavior (a random AES-256 key and a randomkeyIdper instance, still not restart-durable or cross-instance-shareable).StaticKeyProvideris an immutable, statically-configured keyring for the shared/rotating case — aStaticKeyProvider.of(keyId, key)one-liner, or a builder (create().addKey(...).current(...).build()) supporting overlapping keys during a rollover — plus anaesKey(byte[] | String)helper for building theSecretKeyfrom raw config/secret material. AeadRequestStateCodecnow takes aKeyProvider. The no-arg constructor is unchanged behaviorally (this(new EphemeralKeyProvider())); a newAeadRequestStateCodec(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)— withkeyIdauthenticated (folded into the AEAD's AAD) but not encrypted (it must be readable before decryption selects the key). Because MCP2026-07-28support is itself unshipped, there is no legacy two-segment fallback:unsealaccepts only the new format.McpMrtrConfig.setKeyProvider(KeyProvider)(new convenience). Sugar forsetCodec(new AeadRequestStateCodec(value)), reachable through theMcpOptionsaggregate vianew McpOptions().mrtr(m -> m.setKeyProvider(kp)). Last-wins againstsetCodec(...); no getter for the provider (getCodec()remains the sole accessor).- Error mapping unchanged. The never-throw
unsealcontract and the existing-32602/-32022/-32023error codes are untouched — an unknown/retiredkeyIdflows through the sameOptional.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/-32002missing-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
requestStatetoken is unchanged — a multi-use bearer credential, replayable any number of times within its TTL. A newReplayCacheSPI (org.apache.juneau.rest.server.mcp.v20260728) — a single atomiccheckAndRecord(String jti, long expiresAtMs): boolean— narrows that default to single-use once wired viaMcpMrtrConfig.setReplayCache(ReplayCache); unset (the default) means no replay check at all. The built-inInMemoryReplayCacheis a per-process, self-evicting seen-jtiset — not shareable across instances, mirroringKeyProvider/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 returnsfalsefrom their owncheckAndRecordinstead of throwing. A detected replay is rejected with the newCODE_REQUEST_STATE_REPLAYED(-32025). - Argument-hash sealing, always-on. The original
tools/call/prompts/getargumentsare now canonicalized (RFC 8785 JSON Canonicalization Scheme) and SHA-256-hashed at PAUSE time, then re-verified against the RESUME request's ownargumentson 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 newCODE_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 (noarguments) is unaffected. - Bounded-JSON safety now covers
argumentstoo. Because the always-on hash above canonicalizes the fullargumentsvalue, Juneau's existing bounded-JSON safety checks (max depth, max node count) now also apply to tool/promptargumentson every PAUSE and RESUME — including schemalesstools/callandprompts/get, which previously had no input-shape guard at all — so pathologically large or deepargumentsare rejected with-32602(invalid params) before the handler ever runs. McpRequestStategains two additive fields (jti,argumentsHash) alongside its existingcontinuation/method/round/expiresAtMs. Both ride inside the AEAD-sealed plaintext, so neither is forgeable without breaking the existing integrity check; no change toRequestStateCodec/AeadRequestStateCodecor 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(packageorg.apache.juneau.rest.client.mcp.auth, on the Nimbus OAuth 2.0 SDK) — feeds the existingMcpAuthInterceptortoken seam (aSupplier<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 viacurrentRefreshToken()and terminal-invalid_grantlatching). The dynamic modes requireresource(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 aLoopbackRedirectReceiver, launches the authorization URL (pluggablebrowserLauncher), validates the callback (single-usestateCSRF, SEP-2468iss—expectedIssuer(URI)required unlessskipIssuerValidation(true)), and exchanges the code.offlineAccess(true)requests a refresh token.McpProtectedResourceMetadataClient— RFC 9728 PRM discovery with anexpectedResource(URI)§3.3 identity check and anhttps-by-default scheme requirement (loopback exempt), plusdiscoverAuthorizationServer(...);WwwAuthenticateChallengeparses the401/403Bearerchallenge to recover theresource_metadatapointer.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 intoMcpOptionsas theresourceServer(Consumer<McpResourceServerConfig>)nested config. When enabled it requires a valid RFC 6750 bearer token on the MCPPOSTendpoint (validation mode chosen by the suppliedTokenValidator:OAuthIntrospectionValidatorfor opaque tokens,JwtTokenValidatorfor JWTs), serves an RFC 9728 PRM document from.well-known/oauth-protected-resource, answers missing/invalid tokens with401/403+ aWWW-Authenticate: Bearer …challenge carryingresource_metadata, and enforces RFC 8707 audience matching (requireAudienceClaimdefaults totrue, rejecting a claims-less principal). Bearer extraction reuses theOAuthFilterfromjuneau-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_metadatachallenge — 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 a500carrying anorigin-rootdiagnostic (on both thePOSTgate 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).
McpDynamicClientRegistrarPOSTs client metadata to an ASregistration_endpointand returns an immutable, secret-redactingMcpClientRegistration.McpApplicationTypeis a Juneau-ownedNATIVE/WEBenum (native is the SEP-837 SHOULD for CLI/desktop/localhostclients) so the public API does not leak the Nimbusprovidedtype. Loopback redirects are built withLoopbackRedirectUris:portAgnostic(path)is the RFC 8252 §7.3 spec-blessed easy default (http://127.0.0.1/callback+http://localhost/callback, any port matches), andforPort(port, path)is the bind-first strategy for strict exact-match ASes — now genuinely usable end to end becauseLoopbackRedirectReceivercan bind a caller-chosen fixed port,McpAuthorizationCodeAcquirer.Builder.redirectPort(int)opens the receiver on it, andMcpClientRegistrations.configure(...)carries the registered port (not just the path) onto the acquirer.McpClientRegistrationsbridges a registration into the F1 flows unchanged (credential provenance only). The registration endpoint must behttps(loopback exempt) — theBuilder.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
issuerand never reused across authorization servers.McpClientRegistrationStoreis the persistence SPI;InMemoryMcpClientRegistrationStoreis the thread-safe, secret-redacting default.McpClientRegistrationManagerorchestrates mechanism selection, issuer-keying, and migration: on-demand (no store), store-hit reuse, or re-registration when discovery indicates a different issuer — surfacing anMcpAuthExceptionrather 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 newMcpTokenProviderfor 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 with403 insufficient_scope+ aWWW-Authenticatescope=hint, and the baseline401now also carries thescopehint 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,McpScopeAccumulatorcomputes the order-preserving{previously-requested} ∪ {challenge}union andMcpStepUpAuthorizer.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, andreset(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 aresources/updatedchange to subscribers. - tool + elicitation/MRTR —
deleteNote(title), which pauses withinput_requiredto confirm before resuming to delete. - prompt + completion —
summarize(title), whosetitleargument offerscompletion/completesuggestions from existing note titles. - resource — the fixed
note:///indexresource, 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/listenheld-open SSE, delivering theresources/updatedchangepublishNotefires.
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 onecreateMcpOptions()override layeringMcpResourceServerConfigon top of the inherited notes surface: RS auth enabled,setResource(...)(the RFC 9728 resource identity / RFC 8707 default audience), aJwtTokenValidatorcheckingiss/aud/exp/nbfagainst the offline AS,addAuthorizationServer(...), a baselineaddRequiredScope("mcp.read"), and SEP-2350 per-operation step-up (addOperationScope("tools/call", "publishNote", "mcp.write"), likewise fordeleteNote).OfflineAuthorizationServer— a clearly-marked DEMO-ONLY stand-in AS: a fresh self-generated RSA signing key per run, an offlineJWKSourcehanded straight to the validator (no JWKS HTTP fetch), a real RFC 6749 §4.4 client-credentials/tokenendpoint gated by a fixed scope allowlist (mcp.read/mcp.write, elseinvalid_scope), and an RFC 8414/.well-known/oauth-authorization-servermetadata document (issuer = its own loopback base URL, plustoken_endpoint) enabling genuine discovery.SecuredExampleServer/SecuredExampleClient— a launcher plus a real, discovery-driven end-to-end client walkthrough: an unauthenticated call rejected with401+WWW-Authenticate: Bearer, RFC 9728 PRM discovery (authorization_servers,scopes_supported), RFC 8414 AS-metadata discovery of thetoken_endpoint, a client-credentials token acquisition viajuneau-rest-client-mcp-auth'sMcpTokenProvider, a successfulmcp.readread, the same token rejected with403 insufficient_scopeon a write, and a step-up tomcp.read mcp.writethat 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 firstTextContentblock found while scanning the result'scontentlist in order, ornullif the list is empty/unset or contains noTextContentblock. 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 forcallTool(...)followed byCallToolResult.firstText(); null-safe ifcallTool(...)itself returns anullresult.ServerDiscoverResult.getServerInfo()(juneau-bean-mcp-v20260728) — null-safe shortcut forgetMeta() == null ? null : getMeta().getServerInfo(), so a caller doesn't have to null-check the intermediate_metaenvelope just to reach the server'sImplementationidentity.@BeanIgnored so it does not add a second, top-levelserverInfomember to theserver/discoverwire format.McpMrtrResumeContext.continuationAsString()(juneau-rest-server-mcp-v20260728) — shorthand forcontinuationAs(String.class), for the common case of a plainStringcontinuation.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-ACCEPTaction (declined/cancelled) all resolve to a safe default (falseforgetBoolean,nullforgetString) instead of requiring the caller to null-checkget(...)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
LoginStateStoreinterface —store(state, PendingLogin)+ atomic single-useconsume(state), with a framework-ownedMAX_TTLceiling (30 min).PendingLogin(moved ontoLoginStateStore) now carries a framework-mintedexpiresAtand atoString()that redacts the secret-bearingnonce/code_verifier. Builder.loginStateStore(...)— inject a shared (Redis / JDBC) store for clustered deployments; thestateNonceTtlcap(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-validatesredirectTargetviasafeRelativePath(a writable shared store cannot smuggle in an off-site redirect) and re-checksexpiresAtagainst its own clock (anull-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, thePendingLoginmove ontoLoginStateStore, and the newstore(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
Durationserialization now usesISO_8601_WITH_DAYS(for exampleDuration.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(defaultISO_OFFSET_DATE_TIME; ISO variants,RFC_1123_DATE_TIME,MILLIS, plus an opt-inXML_FORMATforCalendar/GregorianCalendar).DateFormat(defaultISO_LOCAL_DATE_TIME; ISO variants,RFC_1123_DATE_TIME,MILLIS).TemporalFormat(defaultDEFAULT= per-subtype default; ISO variants,RFC_1123_DATE_TIME,ISO_YEAR,ISO_YEAR_MONTH,MILLIS).TimeZoneFormat(defaultID; shared byTimeZoneandZoneId;OFFSET,NAME_LONG,NAME_SHORT).LocaleFormat(defaultBCP_47; alsoUNDERSCORE).
- Added global marshalling-context settings:
MarshallingContext.Builder.calendarFormat(...)/dateFormat(...)/temporalFormat(...)/timeZoneFormat(...)/localeFormat(...).- Mirrored on
MarshallingContextable.Builderso 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>MarshallingContextsetting > enum default. Resolution installsStringSwapinstances built directly from the enum'sformat/parsemethods on the matching property type (Calendar/GregorianCalendar,Date,TemporalAccessorsubtypes,TimeZone/ZoneId,Locale).XMLGregorianCalendaris hard-wired to XML lexical format regardless of any configuredCalendarFormat; theCalendarFormat.XML_FORMATconstant is the opt-in path forCalendar/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.
Iso8601Utilsgained format-aware overloads that route through the session's resolved format. - Downgraded
TimeZoneSwap,ZoneIdSwap, andLocaleSwapto thin delegators that read the resolvedTimeZoneFormat/LocaleFormatfrom the active session, so root-level (de)serialization ofTimeZone/ZoneId/Localefollows the same context configuration as bean properties.
Removed (hard break, no @Deprecated):
org.apache.juneau.marshall.swaps.TemporalCalendarSwapand all 17 inner-class variants (IsoOffsetDateTime,IsoInstant,IsoLocalDateTime, etc.).org.apache.juneau.marshall.swaps.TemporalDateSwapand all 17 inner-class variants.org.apache.juneau.marshall.swaps.TemporalSwapand all 18 inner-class variants.org.apache.juneau.marshall.swaps.XMLGregorianCalendarSwap(its always-XML behavior is now built into the post-processor forXMLGregorianCalendarproperties; theXML_FORMATCalendarFormatconstant covers the opt-in case forCalendar).
Notable behavior — MILLIS for TemporalFormat:
Instant,OffsetDateTime,ZonedDateTimeusetoInstant().toEpochMilli().LocalDateTimeusesldt.toInstant(ZoneOffset.UTC).toEpochMilli().LocalDateusesld.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli().YearMonthandYearuse the first day at midnight UTC.LocalTimeandMonthDayfall back to the per-subtypeDEFAULTISO string form.- This asymmetry is documented in the
MILLISconstant'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 withxxxFormat(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.BinaryFormatwith 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>Formatenum).BASE64_URL— RFC 4648 §5 URL-safe Base64 encoding (-/_instead of+//, no padding on the wire but accepted on input).
BinaryFormatnow exposesformat(byte[])andparse(String)methods so a single enum drives both serialize and parse paths. Theparse(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 byformat(byte[])regardless of the configured constant.- Added new
org.apache.juneau.EnumFormatwith the constantsNOT_SET,TO_STRING(default; callsEnum.toString()),NAME(callsEnum.name()),LOWER_HYPHEN(e.g.MY_VALUE→my-value),UPPER_HYPHEN,LOWER_UNDERSCORE,LOWER,UPPER, andORDINAL(numeric on the wire).format(Enum)andparse(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)(defaultNOT_SET).enumFormat(EnumFormat)(defaultTO_STRING).- Mirrored on
MarshallingContextable.Builderso 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>MarshallingContextsetting > enum default. Resolution installs the appropriate session-awareObjectSwapon eachbyte[]/ enum bean property. ForEnumFormat.ORDINALthe swap produces aNumberso 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 resolvedBinaryFormatfrom the activeMarshallingContextat swap time. Skips binary serializers (BSON / CBOR / MsgPack / Proto / Parquet emit native bytes), CSV (defers tocsv.CsvByteArrayCellFormat), and OpenAPI (uses its schema-directed BYTE / BINARY / BINARY_SPACED encoding). When the resolved format isNOT_SET, the swap is a no-op sobyte[]falls back to the surrounding serializer's native array representation (e.g. JSON emits[1,2,3]). OutputStreamSerializerSession#serializeToString(Object)now always emitsBinaryFormat.HEXregardless of the configuredbinaryFormat, 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.ByteArraySwapand theBase64,Hex,SpacedHexinner-class variants. Replace@Swap(ByteArraySwap.Base64.class)etc. with@MarshalledProp(binaryFormat=BASE64)etc.OutputStreamSerializer.Builder.binaryFormat(BinaryFormat)andInputStreamParser.Builder.binaryFormat(BinaryFormat). The setting is now onMarshallingContext.Builder(and inherited by every serializer / parser builder viaMarshallingContextable.Builder) so the same wire-format setting drives both binary and textual sessions.BeanContext.Builder.useEnumNames(boolean)and theuseEnumNamesfield onMarshallingContext. ReplaceuseEnumNames()withenumFormat(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-levelBinaryFormat.
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 inheritedbinaryFormat(...)fromMarshallingContextable.Builder(no source change needed for builder chains; the inherited setter has the same signature). - Replace
useEnumNames()/useEnumNames(true)withenumFormat(EnumFormat.NAME).
UUID + BigInteger/BigDecimal wire format controls
- Added new enum strategies under
org.apache.juneau:UuidFormat(defaultSTANDARD; alsoNO_DASHESfor the 32-hex-character compact form, andURNfor the RFC 4122urn:uuid:namespace).BigNumberFormat(defaultNUMBER; alsoSTRINGfor always-quoted output, andAUTOwhich 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(...)andparse(...)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)(defaultSTANDARD).bigNumberFormat(BigNumberFormat)(defaultNUMBER).- Mirrored on
MarshallingContextable.Builderso 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>MarshallingContextsetting > enum default. Resolution installs anObjectSwap<T, Object>onUUID/BigInteger/BigDecimalproperties whoseswap(...)returns either aNumber(bare numeric token) or aString(quoted), so binary serializers continue to see native types and text serializers branch on the formatted value. ForBigNumberFormat.AUTOthe swap dynamically picksNumbervsStringper 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(defaultTRUE_FALSE; alsoZERO_ONEfor numeric0/1tokens,YES_NO/Y_N/ON_OFFfor textual forms).ZERO_ONEemits a bare numeric token (not a quoted string) on JSON.FloatFormat(NaN_AS_NULL(default per spec, but install-skipped on primitivefloat/double— see note below),NaN_AS_STRINGfor quoted"NaN"/"Infinity"/"-Infinity",NaN_AS_NUMBERfor the bare token (non-strict JSON),NaN_AS_ERRORfor fail-fast).CurrencyFormat(defaultISO_CODE— locale-independent, round-trip-safe; alsoSYMBOLandDISPLAY_NAMEwhich are locale-sensitive and best-effort on parse).ClassFormat(defaultFQCN—Class.getCanonicalName()withgetName()fallback for local/anonymous classes; alsoBINARY_NAMEforClass.getName()with$separators, andSIMPLE_NAMEforClass.getSimpleName()which is serialize-only — parsingSIMPLE_NAMEthrowsUnsupportedOperationException).
- 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)(defaultTRUE_FALSE).floatFormat(FloatFormat)(defaultNaN_AS_NULL).currencyFormat(CurrencyFormat)(defaultISO_CODE).classFormat(ClassFormat)(defaultFQCN).- Mirrored on
MarshallingContextable.Builderand overridden onSerializer.Builder/Parser.Builderso 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>MarshallingContextsetting > enum default. Resolution installs anObjectSwaponBoolean/Float/Double/Currencyproperties as appropriate. Binary serializers continue to see native primitives via the swap'sswap(...)short-circuit onOutputStreamSerializerSession.
Breaking changes:
ClassSwapdeleted outright. The legacyorg.apache.juneau.marshall.swaps.ClassSwapis removed in 9.6. Its responsibilities split into two replacements:- For bean properties, an
ObjectSwap<Class, String>is installed byMarshalledPropertyPostProcessorwhen@MarshalledProp(classFormat=...)or@Marshalled(classFormat=...)is configured. - For standalone
Class<?>values (root-level, map keys, list elements), a neworg.apache.juneau.marshall.swaps.ClassFormatSwapis registered inDefaultSwapsand readsMarshallingContext.classFormatat swap-time. - Default behavior (
FQCN≈Class.getCanonicalName()) is functionally compatible with the oldClassSwap.getName()for top-level classes. Differences:FQCNreturnsint[]forint[].class(vs.[IfromClass.getName()); useBINARY_NAMEif you need the JVM binary form. Callers that explicitly wantedgetSimpleName()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).
- For bean properties, an
Notable caveats:
FloatFormat.NaN_AS_NUMBERemits the bareNaN/Infinity/-Infinitytoken, 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.FloatFormaton primitivefloat/doublefields: Juneau's bean machinery applies a null-to-primitive-default convention (e.g.m.put("d1", null)→d1 = 0.0) that conflicts with swap-drivenNaN_AS_NULLsemantics. To preserve this contract, the post-processor installs the float swap only on boxedFloat/Doubleproperties for context-level format settings. Explicit@MarshalledProp(floatFormat=...)or@Marshalled(floatFormat=...)overrides install for the explicit type the user declared. Net effect: declaringpublic Double nrather thanpublic double nis required when you want the wire format to differ from the bare-token default.CurrencyFormat.SYMBOL/DISPLAY_NAMEround-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 throwsIllegalArgumentExceptionwhen no match is found.ClassFormat.SIMPLE_NAMEis serialize-only —parse(...)throwsUnsupportedOperationException. UseFQCN(default) orBINARY_NAMEfor 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:
TemporalFormatcarryover — all 10 supportedTemporalAccessorsubtypes (Instant,LocalDateTime,LocalDate,OffsetDateTime,ZonedDateTime,Year,YearMonth,LocalTime,OffsetTime,MonthDay) now round-trip across everyTemporalFormatvalue. Date-bearing-only / time-bearing-only formats are handled lossily via the existingDefaultingTemporalAccessormechanism (year defaults to 1970, time defaults to 00:00:00, etc.).MonthDaysupport —java.time.MonthDayis aTemporalAccessorbut not aTemporal, 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 siblingTemporalAccessor-keyed swap factory; configuredTemporalFormatvalues are silently ignored forMonthDayproperties (the only stable wire shape is the native--MM-DDtoString()/parse()form, since noDateTimeFormatterfrom the standard ISO family matches that pattern).hasNativeBytes()capability check — new public methods onOutputStreamSerializerSessionandInputStreamParserSession(defaulttrue) consulted byBinarySwap.matchandMarshalledPropertyPostProcessor.binarySwap. Overridden tofalseonParquetSerializerSession/ParquetParserSession/RdfStreamSerializerSession/RdfStreamParserSession. Wire-format change: at any non-NOT_SETBinaryFormat, Parquet and the two binary RDF serializers (RDF/Thrift, RDF/Proto) now routebyte[]through the configured text wire form (UTF-8 BASE64 / HEX / SPACED_HEX string column or literal) instead of raw bytes /xsd:base64Binarytyped literals. MsgPack / CBOR / BSON are unchanged at every format. AtBinaryFormat.NOT_SET, behavior is unchanged (Parquet uses its nativebyte[]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 (negativePeriod/BigIntegervalues no longer come back as unsigned magnitudes); Proto text-formatFloat/Doubledecimal-literal sign loss and0-prefix tokenization for0.x/0eN/0fshapes; Protonan/infidentifier-vs-token disambiguation. - JSON-family precision tier (Bug #5) —
StringUtils.parseNumberno longer auto-classifies bare decimals asFloatwhen targetingNumber/Object/Double. JSON / JSON5 / JSONL / XML / HTML / UON / UrlEncoding / JCS parsers now returnDoubleforparse("3.14", Object.class)and preserve full double precision inDoublebean properties. Caller-visible if you were instance-checking the parsedNumberforFloat— see migration guide. - Bean-property
Map<Enum, V>key coercion (Bug #7b) — Hjson / Hocon / Proto / Bson parsers now thread the bean property'sClassMeta<K>for the map key into the key-coercion step, so typedMap<MyEnum, V>properties round-trip cleanly at everyEnumFormatvalue. 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(viaLOGICAL_TYPE_UUID),Class<?>(via the default-swap dispatch),EnumatEnumFormat.ORDINAL(viaINT32column), andbyte[](via theTYPE_BYTE_ARRAYschema element withoutconvertedType). - Parser dispatch for collection-element / top-level
byte[](Bug #12) — Toml / Proto / Hjson / Hocon parsers now consultBinarySwap.unswapforbyte[]collection elements and top-level values at non-NOT_SETformats, so wire forms like"0001ff807f102030"(HEX) decode to the correct 8-byte array instead of a 16-byte ASCII char array. BinaryFormat.BASE64_URLhint (Bug #9) —BinaryFormat.parse(String)now honors theBASE64_URLconstant directly (routing toBase64.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_CHARSextended to cover=(BASE64 padding),+(+NNNNtimezone offsets), and$(nested-classBINARY_NAME);HoconTokenizerrecursion guard now throws a cleanIOExceptioninstead ofStackOverflowError;HoconParserSession.parseArray/parseObjectarray-flatten bug fixed (nested arrays no longer collapse via spurious array-concatenation). HoconTokenizer=+lookahead bug —HoconTokenizer.readTokenImmediateincorrectly guarded theEQUALSemission withpeekChar() != '+', causing any=+character sequence in the input to throwIOException("Unexpected character '='")instead of emittingEQUALS. 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 asEQUALS.ClassFormat.FQCNnested-types / arrays parser-fragility (Bug #16) —ClassFormat.parsenow 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).Currencydefault-swap registration (Bug #6) —Currencyadded toDefaultSwaps(sibling of the existingLocale/TimeZone/ZoneIdentries) soCurrencybean properties round-trip at the defaultCurrencyFormat.ISO_CODE/NOT_SETlevels.- UrlEncoding-expanded empty
byte[](Bug #13) — emptybyte[]properties now round-trip asbyte[0]instead ofnullthrough the expanded-params UrlEncoding flavor. MonthDayround-trip support (Bug #18) andOffsetTime × MILLISswap asymmetry (Bug #17) — see "API changes" below.
- Format-hint correctness across binary serializers — MsgPack
API changes:
-
TemporalFormat.format(...)andparse(...)signatures widened fromTemporaltoTemporalAccessor:format(Temporal, ZoneId)→format(TemporalAccessor, ZoneId)parse(String, Class<? extends Temporal>, ZoneId)→parse(String, Class<? extends TemporalAccessor>, ZoneId)
Source-compatible for callers passing
Temporalinstances (every existingTemporalis aTemporalAccessor). Required soMonthDay(which is aTemporalAccessorbut not aTemporal) reaches the dispatch path. Flag in migration guide. -
New
TemporalFormat.isMillisNumeric(Class<? extends TemporalAccessor>)static helper — returnstrueiffformat(value, MILLIS)emits aLong-parseable numeric string for the subtype. ReturnsfalseforLocalTime,OffsetTime, andMonthDay(the three subtypes whoseMILLISbranch falls back to the per-subtypeDEFAULTISO string form). Single source of truth for theMILLISwire-type decision; consumed by bothtemporalSwapand the newtemporalAccessorSwapfactory. -
New
MarshallingContextcapability methods on the binary session base classes (defaulttrue; see "Highlights" above for the wire-format implications):OutputStreamSerializerSession.hasNativeBytes()InputStreamParserSession.hasNativeBytes()
-
New
MarshalledPropertyPostProcessorinternals — siblingtemporalAccessorSwapswap factory keyed atTemporalAccessor.class(vstemporalSwap'sTemporal.class), and a newisTemporalAccessorType(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-TemporalTemporalAccessoris picked up automatically. Wired symmetrically across the three install paths (applyPropertyFormats,applyClassFormats,applyContextFormats). -
OffsetTime × TemporalFormat.MILLISnow falls back toISO_OFFSET_TIME(sibling of the existingLocalTimeandMonthDaycarve-outs) —OffsetTimehas no canonical epoch-millis interpretation. The carve-out is reflected in the updatedMILLISconstant Javadoc and consumed viaisMillisNumeric.
Performance:
IniParserSessionandMarkdownParserSessionnow memoize their internalJson5Parserinstance per session instead of constructing a freshJson5Parser.DEFAULTper 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.configcontaining--also-make. Affects developers runningmvn -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 bymvn -pl juneau-utest testwithout an interveningmvn installin the upstream module.
Typed JsonSchema bean generation bridge
- Added
JsonSchemaBeanGeneratorinjuneau-bean-jsonschemato generate typedJsonSchemabeans from Java types viaJsonSchemaGenerator. - Added static convenience factories on
JsonSchema:JsonSchema.of(Type)andJsonSchema.of(Class<?>). - Added
JsonSchemabean support forformat,$comment, anddeprecatedso bean output preserves generator metadata. - Added tests covering
JsonSchemaBeanGeneratoroutput parity withJsonSchemaGeneratormap output and round-trip conversion.
Schema validation mode for parsers and serializers
- Added an opt-in
validateSchemaflag onMarshallingContextthat validates bean-property values against the constraints declared by@Schemaannotations during both parsing (value set on the bean) and serialization (value read from the bean). - Backed by a new
JsonSchemaValidatorinjuneau-bean-jsonschemathat implements JSON Schema Draft 2020-12 semantics on top of the typedJsonSchemabean. - Added new commons-side SPI (
PropertyValidator/PropertyValidatorFactory/PropertyValidators) so marshall andjuneau-bean-jsonschemastay decoupled. The factory is discovered viaServiceLoader; whenjuneau-bean-jsonschemais absent, the flag becomes a silent no-op. - Added fluent enablers:
MarshallingContext.Builder.validateSchema()/validateSchema(boolean), propagated throughMarshallingContextable.Builderand every parser/serializer/REST-client builder subclass so chains likeJsonParser.create().validateSchema().build()keep their concrete return type. - Added
@BeanConfig(validateSchema="true")so the flag can be configured by annotation (withVarResolver.DEFAULTinterpolation). - Validation failures throw
SchemaValidationException; parsers surface them asParseExceptionand serializers asSerializeException. - 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
AI-friendly summary field on @Schema and related annotations
- Added a
summaryfield (aliassu) 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-linedescriptionfield. summaryflows throughSchemaAnnotation.asMap()andJsonSchemaGeneratorinto the generated JSON Schema output under the key"summary".- Added a paired
summaryfield on companion annotations that already exposedescription:@Marshalled,@BeanProp,@Response,@Header,@Query,@Path,@FormData,@Content. - Added
JsonSchema.getSummary()/setSummary(String)(and fluent overrides onJsonSchemaPropertyandJsonSchemaRef), so the typed bean and theJsonSchemaBeanGeneratorbridge surface the new field automatically. - Added Swagger / OpenAPI propagation:
summaryfrom@Schemais now copied into the generated Swagger Schema Object alongsidedescription. - 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(extendsLinkedHashMap<String,Object>) andMarshalledList(extendsLinkedList<Object>) — neutral base classes with no language coupling. Their defaulttoString()is the inheritedLinkedHashMap/LinkedListform.- 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 standardEMPTY_MAP/EMPTY_LIST/unmodifiable()/modifiable()family.
JsonMap / JsonList retargeted to strict JSON (breaking)
toString()now returnsJson.of(this)— strict RFC 8259 JSON (wasJson5.of(this)).(CharSequence)/(Reader)constructors and the renamedofString(...)static factories (previouslyofJson(...)) now default toJsonParser.DEFAULT(wasJson5Parser.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) intonew JsonMap(...)will now throwParseException.- The JSON-specific helpers
toJson()/toJson5()/toJsonl()/toJcs()/toHjson()/toReadableJson5()stay onJsonMap/JsonList.toJson()is now the synonym fortoString(), nottoJson5().
New Json5Map / Json5List in org.apache.juneau.marshall.json5
Json5Map(extendsMarshalledMap) andJson5List(extendsMarshalledList) carry the JSON5 flavor thatJsonMap/JsonListused to carry:toString()returnsJson5.of(this),(CharSequence)/(Reader)constructors default toJson5Parser.DEFAULT, single-argofString(...)static factories (same name as the neutral base), JSON5-flavoredputJson5(...).- These are the drop-in replacement for callers that today rely on
JsonMap.toString()producing JSON5 or onnew 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:
| Parser | parse(text, Object.class) runtime type |
|---|---|
JsonParser.DEFAULT | JsonMap / JsonList (unchanged) |
Json5Parser.DEFAULT | Json5Map / 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:
- Cast to the matching flavored type (
Json5Map/Json5List), or - Cast to the neutral base (
MarshalledMap/MarshalledList) when flavor doesn't matter at the call site, or - Pass an explicit target class to force the old runtime type:
parser.parse(text, JsonMap.class). The hook is only consulted forObject/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 behavior | New behavior | Recommended migration |
|---|---|---|
JsonMap.toString() → JSON5 | strict JSON | Use Json5Map (or Json5.of(map)) for JSON5 output. |
new JsonMap("{unquoted:'json5'}") parses JSON5 | parses 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 | → Json5Map | Cast to Json5Map, or to neutral MarshalledMap, or pass parser.parse(s, JsonMap.class). |
Other parsers' parse(s, Object.class) → JsonMap | → MarshalledMap | Same 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
MsgPackSerializerandMsgPackParsernow use the RFC 6838-conformingapplication/msgpackas the canonical media type. The previous invalidoctal/msgpackvalue (octalis not a registered MIME top-level type) has been replaced.- The serializer emits
Content-Type: application/msgpackonly, andMsgPackSerializer.DEFAULT .getResponseContentType()(andgetPrimaryMediaType()) now returnapplication/msgpack. - For backward compatibility, both the serializer's
Acceptmedia-type list and the parser'sconsumeslist retainoctal/msgpackas an alias, so existing wire payloads and clients that still sendContent-Type: octal/msgpackorAccept: octal/msgpackcontinue to be matched and decoded. - Updated the
RestClient.msgPack()/RestRequest.msgPack()Javadoc to document the new media-type value sent on theAcceptandContent-Typerequest 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 anIllegalArgumentExceptionnaming the bad value and the valid write codecs (UNCOMPRESSED,GZIP) instead of silently falling back toUNCOMPRESSED.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.@Parquetno 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>causedIllegalArgumentExceptionat reflective invoke becauseParameterInfo.resolveValuereturned the unwrapped scalar.ParameterInfo.resolveValuenow mirrorsFieldInfo.inject: when the declared parameter type isOptional<T>, a missing or empty-string resolution becomesOptional.empty()and a non-empty resolved value becomesOptional.of(value). The plain-scalar workaround (declare asT+ 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 viaMarshallingContext.Builder.classLoader(ClassLoader)— such as OSGi bundle classloaders, webapp classloaders, and plugin systems — now correctly resolveClass<?>values from that classloader during parsing. The same fix was applied to the inline per-property swap installed byMarshalledPropertyPostProcessor. The thread-context classloader is still used as a fallback when no session classloader is configured, preserving historical behavior for existing callers. -
HoconTokenizerpeek/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/parseObjectpreviously 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 ofskipWhitespaceAndCommentscould silently reintroduce the over-read. -
Abstract
Collection-typed bean properties with no setter and no@BeanProp(type=...)now populate correctly. A field such asSet<MyEnum> tags;(orList/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-Mapcase 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 concreteAbstractSet/AbstractListsubclass 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/rawCollection→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.
TomlSerializerpreviously wroteList<Integer>asnums = ["1", "2"], and lists of beans, maps or lists viatoString()(e.g.["Q$In@74e52303"]), none of whichTomlParsercould 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[].TomlParseralso 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.Valueannotation for declarative injection of configuration values intoBeanInstantiator-created beans (fields, constructor parameters, setter parameters, arbitrary method parameters). Resolves expressions throughVarResolver.DEFAULTand coerces the resolved string to the target type viaSettings.toType(...). - Extended
JsrSupportwith FQN-based detection of the new Juneau@Value(JUNEAU_VALUE) and Spring's@Value(SPRING_VALUE = "org.springframework.beans.factory.annotation.Value"), keepingjuneau-commonscompile-time clean of Spring. Either annotation is picked up by the same injection path. - Made
@Valueand@Injectmutually exclusive on the same site — applying both throwsBeanCreationExceptionwith a clear "site already has @Value / @Inject" message. - Added the
${xxx}shortcut inVarResolverSession— the tokenizer rewrites${...}to$P{...}before resolution, so it inherits the fullPropertyVarsource stack (system properties, env vars, registeredConfigs, SpringEnvironment). 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 recognisestatic T parse(CharSequence)methods (in addition toparse(String)), sojava.timetypes likeInstantcoerce cleanly. - Added
Settings.removeSource(PropertySource)for explicit deregistration — required so per-context bridges (microservice,RestContext, SpringEnvironment) can clean up at shutdown without cross-test bleed. Optional<T>field/parameter injection collapsesnulland empty-string results toOptional.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.VarFunctionSPI plusTypedFunctionreflection helper.TypedFunctionderives arity and per-arg types from the declaredinvoke(...)method signature(s) and routes string args throughArgCoercerfor coercion toint/long/double/boolean/String/String[]/Object. - Boolean truthiness is centralised in
ArgCoercer:"true","yes","on","1"are truthy; everything else (includingnulland 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)
| Category | Examples |
|---|---|
| String | upper, lower, len, substring, trim, split, join, format, concat, pathToken |
| Type conversion | toInt, toLong, toDouble, toBoolean, toString |
| Arithmetic | add, sub, mul, div, mod, min, max, abs, round, floor, ceil |
| Boolean | not, and, or, xor, eq, ne, lt, gt, le, ge |
| Conditional | if, switch, coalesce, notEmpty, case |
| Regex | matches, replace, extract |
| Encoding | urlEncode, urlDecode, base64Encode, base64Decode, htmlEscape, htmlUnescape |
| Date/time | now, today, formatDate, parseDate |
| Random/UUID | uuid, randomInt, randomLong, randomDouble, randomString, randomChar |
| JSON navigation | jsonPath, 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:
- Explicit registration —
VarResolver.create().functions(MyFn.class).build(). ServiceLoaderdiscovery — declare your implementation inMETA-INF/services/org.apache.juneau.commons.svl.VarFunction.VarResolver.create().defaultFunctions().build()andVarResolver.DEFAULTboth load these automatically.BeanStorelookup — built-in functions are constructed through theVarResolver.Builder'sBeanStoreso they can themselves take constructor@Inject/@Valuedependencies (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 Var | Removed prefix | Replacement |
|---|---|---|
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. Subsequenttemplate.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)andVarResolverSession.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()—truewhen 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 tocompile(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
BeanInstantiatornow inspects the declared field/parameter type at injection sites carrying@Value. ASupplier<String>field receives a re-evaluating, threadsafe Supplier (delegates toVarTemplate.asSupplierWithFreshSessions(...)); a bareStringfield 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 capturedStringreference 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
@Valuetopic page.
Compile-time stable-value folding
- New
default boolean Var.isStable() { return false; }SPI hook. Opt-in perVarimplementation; conservativefalsedefault protects third-partyVars 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 aLiteralSegment— 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,EnvFileVarstay non-stable. Their backing source can mutate at runtime (config reload, file watch, runtimeSettings.set), so folding would produce stale reads.
Internal perf
BeanInstantiator's@Valueresolution path caches aVarTemplateper distinct expression inValueResolver.TEMPLATE_CACHE. Repeated bean construction reads the cached form rather than re-tokenizing.RestOpContext.pathMatchersMemoizer now goes throughvr.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:
| Before | After |
|---|---|
$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.Valuerenamed toHolder. The mutable reference-wrapper utility class has been renamed toHolder<T>to resolve an import-name collision with the@Valueinjection annotation. Update import statements:import org.apache.juneau.commons.lang.Value→import org.apache.juneau.commons.lang.Holder. No behavioral change. -
XValueprimitive-holder companions renamed toXHolder. The nine typed primitive-holder subclasses have been renamed for consistency with theValue→Holderrename: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, aMETA-INF/services-discoveredPropertySourceProviderthat bridgesConfig.getSystemDefault()(i.e. the classpathjuneau.cfg/default.cfg/application.cfg/ etc. discovered byConfig.getCandidateSystemDefaultConfigNames()) into the process-wideSettingssingleton. - Resolution is lazy —
Config.getSystemDefault()is invoked on the firstSettings.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 whileConfigis mid-initialisation is treated as "not yet available" without memoising the failure. - The provider is silent when no classpath
Configexists — every lookup returnsmissing(), no log noise, no exception. - The provider's
order()is100, deliberately low — so any programmaticSettings.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-configalongside 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
FileStoreextension search order to includecfg,yml,yaml(withcfgremaining first for backward compatibility).
Unified Property Source Hierarchy
- Added a new property-source SPI naming model:
PropertySource/PropertyStoreand functional variants. - Added
PropertySourceProvider(ServiceLoader SPI) so ambient sources can be auto-registered ontoSettings. - 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/$MFvars remain supported as source-specific resolvers, now aligned with the unified settings model. - Breaking:
SettingSource,SettingStore,FunctionalSource, andFunctionalStorehave been removed outright (no deprecation shims). Migrate toPropertySource,PropertyStore,FunctionalPropertySource, andFunctionalPropertyStorerespectively.
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)
| Old | New |
|---|---|
@Bean (marshall) | @Marshalled |
@BeanApply | @MarshalledApply |
@Beanp | @MarshalledProp |
@BeanpApply | @MarshalledPropApply |
@Beanc | @MarshalledCtor |
@BeancApply | @MarshalledCtorApply |
@BeanIgnore | @MarshalledIgnore |
@BeanIgnoreApply | @MarshalledIgnoreApply |
@BeanConfig | @MarshalledConfig |
Engine Type Renames (breaking)
| Old | New |
|---|---|
BeanContext | MarshallingContext |
BeanContextable | MarshallingContextable |
BeanSession | MarshallingSession |
BeanTraverseContext | MarshallingTraverseContext |
BeanTraverseSession | MarshallingTraverseSession |
BeanRecursionException | MarshallingRecursionException |
BeanStringSwap | MarshallingStringSwap |
BeanContextConverter | MarshallingContextConverter |
BeanDefMapper | MarshallingDefMapper |
BeanContext.DEFAULT | MarshallingContext.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:
| Old | New |
|---|---|
org.apache.juneau.marshall.swap.MarshallingInterceptor | org.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):
| Old | New |
|---|---|
org.apache.juneau.BeanMap | org.apache.juneau.commons.bean.BeanMap |
org.apache.juneau.BeanMapEntry | org.apache.juneau.commons.bean.BeanMapEntry |
org.apache.juneau.BeanMeta | org.apache.juneau.commons.bean.BeanMeta |
org.apache.juneau.BeanPropertyMeta (and its Builder inner class) | org.apache.juneau.commons.bean.BeanPropertyMeta |
org.apache.juneau.BeanPropertyValue | org.apache.juneau.commons.bean.BeanPropertyValue |
org.apache.juneau.BeanPropertyConsumer | org.apache.juneau.commons.bean.BeanPropertyConsumer |
org.apache.juneau.BeanProxyInvocationHandler | org.apache.juneau.commons.bean.BeanProxyInvocationHandler |
The @Name annotation also moved as part of this work:
| Old | New |
|---|---|
org.apache.juneau.annotation.Name | org.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 sibling | Carries |
|---|---|---|
@BeanType | @Marshalled | Class-level bean-modeling attributes: properties / p, excludeProperties / xp, readOnlyProperties / ro, writeOnlyProperties / wo, unsorted, findFluentSetters, propertyNamer, interfaceClass, stopClass, factory. |
@BeanProp | @MarshalledProp | Property-level bean-modeling attributes: name / value, ro, wo, type, params, elementType, factory. |
@BeanCtor | @MarshalledCtor | Constructor mapping: properties. |
@BeanIgnore | @MarshalledIgnore | Bean-property / class ignore (with ignoreAccessors for fields). |
@BeanConfig | @MarshalledConfig | Class- / 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 Type | Description | Marshalling-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. |
BeanSession | Bean-modeling session SPI (convertToType, convertToMemberType, toBeanMap, parseToMap(CharSequence), parseToList(CharSequence)). | MarshallingSession implements this. |
BeanFilter | Per-class filter surface (properties / excludeProperties / readOnlyProperties / writeOnlyProperties / typeName / interfaceClass / stopClass / propertyNamer / beanDictionary / interceptor hooks). | MarshalledFilter implements this. |
BeanRegistryLookup | Bean-dictionary lookup surface. | BeanRegistry implements this. |
BeanMetaInitializer | Marshalling-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. |
BeanPropertyPostProcessor | Marshalling-side BeanPropertyMeta.Builder post-processing (@MarshalledProp / @Swap / @Uri reads, swap-aware read/write transforms). NOOP default for the commons-side path. | MarshalledPropertyPostProcessor implements this. |
BeanTypeResolver | Marshalling-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:
| Old | New | Notes |
|---|---|---|
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 backingmarshalledClassfield onMarshalledFilterand theMarshalledFilter.Builder(Class<?>)/MarshalledFilter.create(Class<T>)overloads. UseBeanFilter.getBeanClass()(returnsClassInfoTyped<?>) 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()— Returnstruewhen this class is exactlyjava.lang.Object.ClassInfo#isOptional()— Returnstruewhen this class isjava.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.BeanMetaValuerecord madepublic(and itsoptBeanMeta()accessor).BeanMetaaccessorsgetConstructor(),getConstructorArgs(),hasConstructor(),newBean(Object)widenedprotected→public.BeanMapconstructor(T, BeanMeta<T>)andsetBeanSession(BeanSession)widenedprotected→public.BeanPropertyMeta.BuilderfieldsinnerField,getter,setter,rawTypeMeta,swap,readTransform,writeTransform,dictionaryClasses,isUri, andtypeMetawidened package-private →public(consumed byMarshalledPropertyPostProcessor).BeanPropertyValue.properties()widenedprotected→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
BeanStringSwapclass (nowMarshallingStringSwap; 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 underlyingpropertiesfield/builder setter.BeanMetaFiltered(sole purpose was wrapping aBeanMetawith 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.
HTML documentation: bare request: / servlet: / context: navlinks
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 pairingTomlSerializerandTomlParser.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 pairingJsonlSerializerandJsonlParser.
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). Thedatapayload is a pre-serialized string — callers serialize their typed payload through their marshaller of choice (e.g.Json.of(...)) and assign the resulting string.SseSerializerextendsWriterSerializer— producestext/event-stream. Accepts a singleSseEvent, anIterable, aStream, or anSseEvent[]array. CallsWriter.flush()after every event so events are pushed to the client immediately (essential for server-push and AI streaming).SseParserextendsReaderParser— eager parse of a closedtext/event-streamintoList<SseEvent>,SseEvent[], or a singleSseEvent.SseEventReader— publicIterator<SseEvent>+Closeablefor line-driven streaming over a caller-ownedReader. The intended API for long-lived SSE streams that outlive the standard parser-session lifecycle.Ssemarshaller (inorg.apache.juneau.marshaller) — convenience pairing ofSseSerializerandSseParserwithof(...)/to(...)shortcuts.
Features
- Full WHATWG parse-algorithm compliance: all three line terminators (
CR,LF,CR LF), one-time UTF-8 BOM strip,field:valuefield splitting with optional-space stripping after the colon, multi-linedata:concatenation, comment-line (: heartbeat) skipping, and the spec-defined ignore rules foridcontaining U+0000 and non-digitretryvalues. - Per-event
Writer.flush()insideSseSerializerSession.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 pairingBsonSerializerandBsonParser.
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 pairingCborSerializerandCborParser.
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
BasicUniversalConfigandRestClient.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 += itemappends 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 pairingJcsSerializerandJsonParser(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
JsonParserto 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 throwParseException. UseJson5Parserfor JSON5 input.JsonSerializer- Produces strict JSON by default. UseJson5Serializerfor JSON5-style output (single quotes, etc.).- Removed -
JsonSerializer.json5()andJsonParser.Strict; useJson5SerializerandJson5Parserdirectly. BeanContext.beanToStringSerializer- Now usesJson5SerializerfortoString()output (more human-readable).- ComboRoundTripTest - Framework now distinguishes
jsonvsjson5; usejson()/json5()builder methods onComboRoundTrip_Testeraccordingly.
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()— Returnstrueif 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. UseunsortedProperties().BeanContext.Builder.sortProperties(Class<?>...on)— Removed. UseunsortedProperties(Class<?>...on).BeanContextable.Builder.sortProperties()/sortProperties(Class<?>...on)— Removed. UseunsortedProperties()/unsortedProperties(Class<?>...on).BeanFilter.Builder.sortProperties()/sortProperties(boolean)— Removed. UseunsortedProperties().BeanFilter.isSortProperties()/BeanSession.isSortProperties()— Removed. UseisUnsortedProperties().BeanMeta.isSortProperties()— Removed. UseisUnsortedProperties().- All serializer/parser builder
sortProperties()/sortProperties(Class<?>...on)overrides — Removed.
Internal rename:
BeanMeta.sortPropertiesfield — Renamed tounsortedPropertieswith inverted semantics. The field now storestruewhen the bean opts out of default sorted order, rather thantruewhen 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/PropertyDescriptordiscovery). - Property names follow the same rules as field discovery:
@Beanp/@Nameon the field, if present, otherwise the configuredPropertyNameron the field name.
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 pairingPrototextSerializerandPrototextParser.BasicPrototextConfig- REST configuration interface for Prototext-only resources.
Features
- No
.protoschema 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 byCsvParserSession.
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 Type | Behavior |
|---|---|
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 Bean | Header row + one data row → one bean. |
Single Map | Header row + one data row → one map. |
Object | Returns 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
CsvSerializernow serializes DynaBeans (dynamic beans not directly identified as beans byClassMeta.isBean()) by attemptingtoBeanMap()on each entry. - Round-Trip Tests:
CsvSerializerandCsvParserare now included in theRoundTripTest_Baseframework (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- ExtendsMarkdownSerializerfor 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 pairingMarkdownSerializerandMarkdownParser.MarkdownDoc- Convenience marshaller pairingMarkdownDocSerializerandMarkdownDocParser.
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 pairingYamlSerializerandYamlParser.@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 byYamlParserwhen round-tripping larger YAML documents whose deeply-indented lines crossed the parser's internal 1024-char read buffer. The sharedorg.apache.juneau.marshall.parser.ParserReaderhad 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 inparseBlockScalarandisNullBlockValue) 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_CHARSconstant). 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
BasicRestServletOpenAPI 3.1 document (six api-docs URLs + full components.schemas set) now round-trips JSON → YAML → JSON cleanly. PreviouslyOpenApiYamlRoundTrip_Test#c01had to scope to a singleOpenApiMixinmount vianoInherit={"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 methodsisIterator(),isIterable(),isStream(),isStreamable()for detecting these types.ClassMeta: NewCategoryenum valuesITERATOR,ITERABLE,STREAM.SerializerSession: New methodsforEachStreamableEntry()andtoListFromStreamable().
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 (extendsIterable<T>). Provides beans lazily from a cursor or query viabegin(),iterator(),onError(), andcomplete()lifecycle methods.BeanConsumer<T>— Parse-side streaming interface (extendsThrowingConsumer<T>). Receives beans one-at-a-time from any parser for insertion or processing, with the same lifecycle methods.BeanChannel<T>— CombinesBeanSupplierandBeanConsumerfor round-trip marshalling on a single property. The same object drives both serialization and parsing.ListBeanChannel<T>— Built-in in-memoryBeanChannelbacked by anArrayList, useful for testing and small in-memory datasets.BeanFactory<T>— Universal@FunctionalInterfacefor creating instances of any type, enabling integration with dependency-injection frameworks such as Spring.
New Annotations
@Bean(factory=X.class)— Specifies theBeanFactoryclass used to instantiate a type during parsing. The factory is resolved from theBeanStore(e.g. a SpringApplicationContext).@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 aBeanStore(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 aBeanConsumer, 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
BeanConsumerduring serialization or aBeanSupplierduring parsing throws an informative exception recommendingBeanChannelfor bidirectional use. - Getter-only
BeanConsumerproperties: parsers reuse aBeanConsumerexposed via a getter (no setter required), callingacceptThrows()on the existing instance — matching the existing behavior for mutableCollectionproperties. Supplier<T>unwrapping: the standard JDKSupplier<T>(notBeanSupplier) is transparently unwrapped by serializers as a single lazy value, recursively up to depth 10.Stream<T>auto-close:java.util.stream.Streamobjects 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 anamecomponent) are recognized as getters without needing to follow the traditionalgetX()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:
@BeanIgnoreon 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 frombeansRequireDefaultConstructorandbeansRequireSomePropertieschecks.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(andGregorianCalendar)java.util.Datejavax.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()andClassMeta.isDuration()before bean checks in theirserializeAnything()dispatch. Matching objects are formatted viaIso8601Utils.format(). - Parsing: All parsers check the same conditions in their
parseAnything()dispatch. ISO 8601 strings are converted back to the target type viaIso8601Utils.parse(). - Type conversions:
BeanSession.convertToMemberType()andSerializerSession.toString()also use ISO 8601 formatting for these types, ensuring consistent behavior in map keys, bean properties, and other contexts.
New API
ClassMeta: NewCategory.DURATIONenum value forjava.time.Durationclassification.ClassMeta: New methodisDuration().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 pairingRdfThriftSerializerandRdfThriftParser.RdfProtoSerializer- Serializes POJOs to RDF/PROTO binary format.RdfProtoParser- Parses RDF/PROTO binary input into POJOs.RdfProto- Convenience marshaller pairingRdfProtoSerializerandRdfProtoParser.
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 Class | New Property Keys |
|---|---|
ParserSession | trimStrings |
JsonSerializerSession | escapeSolidus |
JsonParserSession | validateEnd |
UonSerializerSession | encoding, paramFormat |
UonParserSession | validateEnd |
UrlEncodingSerializerSession | expandedParams |
UrlEncodingParserSession | expandedParams |
XmlSerializerSession | addNamespaceUrisToRoot, autoDetectNamespaces, defaultNamespace, enableNamespaces, textNodeDelimiter |
XmlParserSession | preserveRootElement, validating |
HtmlSerializerSession | addKeyValueTableHeaders, detectLabelParameters, detectLinksInStrings, labelParameter, uriAnchorText |
These complement the properties added in the initial release of this feature:
BeanTraverseSession:initialDepthSerializerSession:keepNullProperties,trimStrings,addBeanTypes,addRootType,sortCollections,sortMaps,trimEmptyCollections,trimEmptyMapsWriterSerializerSession:maxIndent,quoteCharCsvSerializerSession: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
| Method | Format | Output |
|---|---|---|
json(o) | Standard JSON (RFC 8259) | String |
json5(o) | JSON5 | String |
jsonl(o) | JSON Lines (NDJSON) | String |
jcs(o) | Canonical JSON (RFC 8785) | String |
hjson(o) | HJSON | String |
xml(o) | XML | String |
html(o) | HTML | String |
uon(o) | URL-Encoded Object Notation | String |
urlEncoding(o) | URL Encoding | String |
yaml(o) | YAML | String |
csv(o) | CSV | String |
openApi(o) | OpenAPI | String |
plainText(o) | Plain text | String |
markdown(o) | Markdown (fragment) | String |
markdownDoc(o) | Markdown (document) | String |
ini(o) | INI / properties | String |
toml(o) | TOML | String |
hocon(o) | HOCON | String |
proto(o) | Protobuf Text Format | String |
msgPack(o) | MessagePack | byte[] |
cbor(o) | CBOR (RFC 8949) | byte[] |
bson(o) | BSON | byte[] |
parquet(o) | Apache Parquet | byte[] |
Full Overload Set
Every format method is overloaded across the following signatures:
| Signature | Purpose |
|---|---|
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— Addedof(Object, Object),to(Object, Class),to(Object, Type, Type...),to(String, Type, Type...)to match the standard char-marshaller contract.Parquet— Addedof(Object, Object),to(Object, Class),to(Object, Type),to(byte[], Type, Type...)(Parquet parse methods returnList<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 Method | Description |
|---|---|
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 Method | Description |
|---|---|
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:
| Removed | Replacement |
|---|---|
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 duringgetBean(...)/getBeanSupplier(...). The classicparent(...)slot keeps its post-entry fallback role. Useful for bridging an outer scope (SpringApplicationContext, 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 andaddBean(...)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(wasorg.apache.juneau.cp.BasicBeanStore) — replaced byorg.apache.juneau.commons.inject.BasicBeanStore(renamed fromBasicBeanStore).BeanCreator— replaced byorg.apache.juneau.commons.inject.BeanInstantiator.BeanBuilder— replaced byorg.apache.juneau.commons.inject.BeanInstantiator(was the base class for domain fluent builders; replaced with native builder fields andBeanInstantiator).BeanCreateMethodFinder— replaced byBeanStore.createBeanFromMethod(...).ContextBeanCreatoris not removed — it solves a different problem (persistentContext.Builderholder 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.INSTANCE | BasicBeanStore.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:
| Method | Equivalent |
|---|---|
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.BuilderSerializerSet.BuilderParserSet.BuilderResponseProcessorList.BuilderRestOpArgList.BuilderRestMatcherList.BuilderRestGuardList.BuilderRestConverterList.BuilderRestOperations.BuilderRestChildren.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 onjakarta.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
| Type | Purpose |
|---|---|
@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. |
@Named | Bean qualifier (moved from org.apache.juneau.annotation.Named — see migration row below). |
@Qualifier | Meta-annotation marker used to declare custom qualifier annotations. |
@Singleton | Scope marker. Semantics: a singleton is registered once in a BeanStore and reused; no JVM-wide singleton, no proxying. |
@Primary | Disambiguates 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. |
@Configuration | Marker for types that host @Bean declarations. Supports imports = { ... } for chaining configuration classes (deduped transitively). @Bean members declared on superclasses are inherited. |
@PostConstruct / @PreDestroy | Lifecycle 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 / ConditionContext | SPI for custom @Conditional predicates with access to the bean store, settings facade, classloader, and annotated element. |
JsrSupport | Single 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
@Beanfields and methods are discovered. importsare processed recursively and deduplicated; the same class registered twice (directly or transitively) is processed only once per bean store.@Beanmembers declared on superclasses of the configuration type are inherited; the superclass is processed before the subclass so subclass@Beanmethods can depend on superclass beans.- Duplicate
(type, name)registrations within the same store throwBeanCreationException. @Conditionalannotations 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@Beanmembers they skip only that member.
Bean lifecycle: AutoCloseable
WritableBeanStore now extends AutoCloseable. Calling close():
- Invokes
@PreDestroymethods on all beans that were resolved throughgetBean(...)/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
| Topic | Action |
|---|---|
org.apache.juneau.annotation.Named | Moved to org.apache.juneau.commons.inject.Named. Update imports — no alias is kept at the old location. |
| Simple-name annotation matching | Recognition 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 discovery | Now 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 implementations | Must now implement void close() throws BeanCreationException (typically by delegating to a wrapped BasicBeanStore). |
@Bean precedence chain | Unchanged 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)
BctConfigurationnow exposes a static nestedDefaultsclass whose two boolean fields (sortMaps,sortCollections) are populated by concat-form@Valueannotations:@Value("${" + BCT_SORT_MAPS + ":false}")/@Value("${" + BCT_SORT_COLLECTIONS + ":false}"). ABctConfiguration.defaults()static factory routes construction throughBeanInstantiator, so the defaults respect the standardSettingsprecedence 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 causeioException(String msg, Object... args)and overload with cause- Cause overloads for existing methods:
illegalArg(Throwable cause, String msg, Object... args)andruntimeException(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:
StringValueandByteValue - Numeric and character types (
IntegerValue,ShortValue,LongValue,CharValue,ByteValue): New methodsincrement(),decrement(),incrementAndGet(),decrementAndGet(),add(X),addAndGet(X),is(X),isAny(X...).CharValuealso hasisAny(String).FloatValueandDoubleValuehaveisAny(X precision, X... values)for precision-based equality. - All Value classes: New methods
orElse(X),setIf(boolean, value), andupdate(Function<V,V>)for in-place modification. Valuebase class: Enhanced to mirrorOptionalwithempty(),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)
| Old | New |
|---|---|
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.ResolvingJsonMap | org.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:
| Old | New |
|---|---|
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>fromget(int)andget(String); multi-valued options are accessed viagetAll(String). - Provides
positional(),argCount(),optionCount(),has(int),has(String),isEmpty(), andasMap()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>fromget(String)(main attributes) andget(String section, String key)(per-section attributes). - Provides
sections(),asMap(), andasMap(String section)accessors. - Constructors accept
Class<?>,Manifest,File,Path,InputStream, andReader.
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
| Removed | Replacement |
|---|---|
org.apache.juneau.collections.Args | org.apache.juneau.commons.runtime.Args |
org.apache.juneau.utils.ManifestFile | org.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 ofactualdoes not containunexpected. ASupplier<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.