You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/05/20 13:10:15 UTC

(juneau) branch master updated: chore: SSE + HAL/JsonApi/JsonPatch/RFC7807 bean modules, msgpack media-type fix, SonarLint cleanups, MockConsole test isolation

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 7d55bc7f4d chore: SSE + HAL/JsonApi/JsonPatch/RFC7807 bean modules, msgpack media-type fix, SonarLint cleanups, MockConsole test isolation
7d55bc7f4d is described below

commit 7d55bc7f4dc73c5e9416ad9b5566faf64f7996b2
Author: James Bognar <ja...@jamesbog-ltmjqf2.internal.salesforce.com>
AuthorDate: Wed May 20 09:10:07 2026 -0400

    chore: SSE + HAL/JsonApi/JsonPatch/RFC7807 bean modules, msgpack media-type fix, SonarLint cleanups, MockConsole test isolation
    
    - juneau-marshall: new org.apache.juneau.sse package (SseSerializer/SseParser/SseEvent/SseEventReader + sessions); marshaller.Sse convenience; per-event Writer.flush; SonarLint cleanups (S2095/S115/UncheckedIOException refactor in SseSerializerSession)
    - juneau-marshall msgpack: normalize canonical media type to application/msgpack (retain octal/msgpack alias for back-compat)
    - juneau-marshall collections: assertArgNotNull in MarshalledList/MarshalledMap parse ctors; java:S110/S2386/S2160 suppressions in JsonMap; package-info touch-ups
    - juneau-bean: new juneau-bean-hal, juneau-bean-jsonapi, juneau-bean-jsonpatch, juneau-bean-rfc7807 modules with round-trip tests
    - juneau-rest-client-classic: RestClient/RestRequest/RestResponse touch-ups and new RestClient_Logging_StreamClosed_Test (FULL-detail logging on consumed entity)
    - juneau-rest-common BasicHttpException: java:S3008/S1948/S1165 field suppressions
    - juneau-rest-mock MockConsole: per-instance ByteArrayOutputStream (fixes order-dependent RestClient logging test)
    - juneau-examples-rest: SseDemoResource + RootResources wiring
    - juneau-utest: new SSE/HAL/JsonApi/JsonPatch/RFC7807/MsgPackMediaType tests plus broad test refresh
    - todo: archive TODO-45/46/47 as FINISHED; add TODO-39 (sonarqube command), TODO-4 (duration format control), TODO-48 (marshalled-collection empty-return), TODO-49 (SchemaUtils null-returns); trim TODO-43 from TODO.md
---
 juneau-bean/juneau-bean-hal/pom.xml                | 102 +++
 .../java/org/apache/juneau/bean/hal/HalLink.java   | 236 +++++++
 .../org/apache/juneau/bean/hal/HalLinkArray.java   |  58 ++
 .../apache/juneau/bean/hal/HalLinkOrArraySwap.java |  74 +++
 .../org/apache/juneau/bean/hal/HalResource.java    | 278 ++++++++
 .../apache/juneau/bean/hal/HalResourceArray.java   |  58 ++
 .../juneau/bean/hal/HalResourceOrArraySwap.java    |  71 ++
 .../org/apache/juneau/bean/hal/package-info.java   |  68 ++
 juneau-bean/juneau-bean-jsonapi/pom.xml            | 102 +++
 .../juneau/bean/jsonapi/JsonApiDocument.java       | 259 ++++++++
 .../apache/juneau/bean/jsonapi/JsonApiError.java   | 211 ++++++
 .../juneau/bean/jsonapi/JsonApiErrorSource.java    | 100 +++
 .../apache/juneau/bean/jsonapi/JsonApiLink.java    | 198 ++++++
 .../bean/jsonapi/JsonApiLinkOrStringSwap.java      |  67 ++
 .../juneau/bean/jsonapi/JsonApiRelationship.java   | 146 +++++
 .../juneau/bean/jsonapi/JsonApiResource.java       | 231 +++++++
 .../bean/jsonapi/JsonApiResourceIdentifier.java    | 118 ++++
 .../apache/juneau/bean/jsonapi/JsonApiVersion.java |  96 +++
 .../apache/juneau/bean/jsonapi/package-info.java   |  86 +++
 juneau-bean/juneau-bean-jsonpatch/pom.xml          | 102 +++
 .../org/apache/juneau/bean/jsonpatch/AddOp.java    |  66 ++
 .../org/apache/juneau/bean/jsonpatch/CopyOp.java   |  66 ++
 .../apache/juneau/bean/jsonpatch/JsonPatch.java    |  60 ++
 .../juneau/bean/jsonpatch/JsonPatchOperation.java  |  88 +++
 .../org/apache/juneau/bean/jsonpatch/MoveOp.java   |  66 ++
 .../org/apache/juneau/bean/jsonpatch/RemoveOp.java |  44 ++
 .../apache/juneau/bean/jsonpatch/ReplaceOp.java    |  66 ++
 .../org/apache/juneau/bean/jsonpatch/TestOp.java   |  66 ++
 .../apache/juneau/bean/jsonpatch/package-info.java |  67 ++
 juneau-bean/juneau-bean-rfc7807/pom.xml            | 102 +++
 .../org/apache/juneau/bean/rfc7807/Problem.java    | 274 ++++++++
 .../apache/juneau/bean/rfc7807/package-info.java   |  27 +
 juneau-bean/pom.xml                                |   4 +
 .../apache/juneau/commons/bean/BeanSession.java    |   2 +-
 .../juneau/commons/utils/ThrowableUtils.java       |   7 +-
 .../main/java/org/apache/juneau/config/Entry.java  |   4 +-
 .../main/java/org/apache/juneau/BeanMapLoader.java |   4 +-
 .../java/org/apache/juneau/MarshallingContext.java |   6 +-
 .../java/org/apache/juneau/MarshallingSession.java |   8 +-
 .../main/java/org/apache/juneau/UriContext.java    |   2 +-
 .../org/apache/juneau/collections/JsonList.java    |  18 +-
 .../org/apache/juneau/collections/JsonMap.java     |  24 +-
 .../apache/juneau/collections/MarshalledList.java  |  10 +-
 .../apache/juneau/collections/MarshalledMap.java   |  10 +-
 .../apache/juneau/collections/package-info.java    |   4 +-
 .../java/org/apache/juneau/json5/Json5List.java    |  10 +-
 .../java/org/apache/juneau/json5/Json5Map.java     |  10 +-
 .../org/apache/juneau/jsonschema/SchemaUtils.java  |   4 +-
 .../java/org/apache/juneau/marshaller/Sse.java     | 154 +++++
 .../org/apache/juneau/msgpack/MsgPackParser.java   |   9 +-
 .../apache/juneau/msgpack/MsgPackSerializer.java   |  12 +-
 .../main/java/org/apache/juneau/sse/SseEvent.java  | 193 ++++++
 .../java/org/apache/juneau/sse/SseEventReader.java | 270 ++++++++
 .../main/java/org/apache/juneau/sse/SseParser.java | 133 ++++
 .../org/apache/juneau/sse/SseParserSession.java    | 108 +++
 .../java/org/apache/juneau/sse/SseSerializer.java  | 172 +++++
 .../apache/juneau/sse/SseSerializerSession.java    | 158 +++++
 .../java/org/apache/juneau/sse/package-info.java   |  39 ++
 .../apache/juneau/examples/rest/RootResources.java |   1 +
 .../juneau/examples/rest/SseDemoResource.java      |  92 +++
 .../juneau/rest/client/classic/RestClient.java     |   6 +-
 .../juneau/rest/client/classic/RestRequest.java    |   4 +-
 .../juneau/rest/client/classic/RestResponse.java   |  67 +-
 .../juneau/http/response/BasicHttpException.java   |   1 +
 .../juneau/rest/mock/classic/MockConsole.java      |   7 +-
 .../rest/swagger/BasicSwaggerProviderSession.java  |   4 +-
 juneau-utest/pom.xml                               |  20 +
 .../test/java/org/apache/juneau/BeanMap_Test.java  | 104 +--
 .../org/apache/juneau/DataConversion_Test.java     |   4 +-
 .../test/java/org/apache/juneau/JsonList_Test.java |   2 +-
 .../test/java/org/apache/juneau/JsonMap_Test.java  | 224 +++----
 .../org/apache/juneau/MarshalledConfig_Test.java   |   4 +-
 .../java/org/apache/juneau/ObjectSwap_Test.java    |   2 +-
 .../juneau/a/rttests/JsonMaps_RoundTripTest.java   |   6 +-
 .../a/rttests/SimpleObjects_RoundTripTest.java     |   2 +-
 .../a/rttests/TrimStrings_RoundTripTest.java       |  12 +-
 .../apache/juneau/bean/hal/Hal_RoundTrip_Test.java | 350 ++++++++++
 .../bean/jsonapi/JsonApi_RoundTrip_Test.java       | 446 +++++++++++++
 .../bean/jsonpatch/JsonPatch_RoundTrip_Test.java   | 236 +++++++
 .../jsonschema/JsonSchemaBeanGenerator_Test.java   |   2 +-
 .../bean/rfc7807/Problem_RoundTrip_Test.java       | 276 ++++++++
 .../apache/juneau/cbor/CborCompliance_Test.java    |   2 +-
 .../apache/juneau/cbor/CborOutputStream_Test.java  |   4 +-
 .../org/apache/juneau/cbor/CborRoundTrip_Test.java |   2 +-
 .../apache/juneau/cbor/CborSerializer_Test.java    |   6 +-
 .../juneau/collections/MarshalledList_Test.java    |  14 +-
 .../juneau/collections/MarshalledMap_Test.java     |  18 +-
 .../httppart/OpenApiPartSerializer_Test.java       |  68 +-
 .../java/org/apache/juneau/json/Json_Test.java     |   4 +-
 .../org/apache/juneau/json5/Json5List_Test.java    |  16 +-
 .../org/apache/juneau/json5/Json5Map_Test.java     |  18 +-
 .../juneau/msgpack/MsgPackMediaType_Test.java      |  90 +++
 .../juneau/msgpack/MsgPackSerializerTest.java      |   8 +-
 .../apache/juneau/objecttools/ObjectRest_Test.java |  16 +-
 .../RestClient_Logging_StreamClosed_Test.java      | 244 +++++++
 .../client/classic/RestClient_Marshalls_Test.java  |   4 +-
 .../test/java/org/apache/juneau/sse/Sse_Test.java  | 728 +++++++++++++++++++++
 .../ReaderObjectSwap_ComboSerializeTest.java       |   4 +-
 .../SwapsAnnotation_ComboSerializeTest.java        |   2 +-
 .../org/apache/juneau/uon/UonSerializer_Test.java  |  58 +-
 .../urlencoding/UrlEncodingSerializer_Test.java    |  58 +-
 .../org/apache/juneau/utils/FilteredMapTest.java   |   2 +-
 ...c7807.md => FINISHED-45-juneau-bean-rfc7807.md} |  26 +-
 ...l-sse.md => FINISHED-46-juneau-marshall-sse.md} |  26 +-
 ...s.md => FINISHED-47-additional-bean-modules.md} |  34 +-
 todo/TODO-39-sonarqube-command.md                  | 292 +++++++++
 todo/TODO-4-duration-format-control.md             | 305 +++++++++
 .../TODO-48-empty-return-marshalled-collections.md | 271 ++++++++
 todo/TODO-49-schemautils-null-returns.md           |  38 ++
 todo/TODO.md                                       |  15 -
 110 files changed, 8828 insertions(+), 445 deletions(-)

diff --git a/juneau-bean/juneau-bean-hal/pom.xml b/juneau-bean/juneau-bean-hal/pom.xml
new file mode 100644
index 0000000000..e1f1975a59
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.juneau</groupId>
+		<artifactId>juneau-bean</artifactId>
+		<version>9.5.0-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>juneau-bean-hal</artifactId>
+	<name>Apache Juneau HAL Beans</name>
+	<description>Apache Juneau Serializable Beans - HAL (Hypertext Application Language)</description>
+	<packaging>bundle</packaging>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.apache.juneau</groupId>
+			<artifactId>juneau-marshall</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.felix</groupId>
+				<artifactId>maven-bundle-plugin</artifactId>
+				<extensions>true</extensions>
+				<configuration>
+					<supportIncrementalBuild>true</supportIncrementalBuild>
+				</configuration>
+				<executions>
+					<execution>
+						<id>bundle-manifest</id>
+						<phase>process-classes</phase>
+						<goals>
+							<goal>manifest</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-source-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>attach-sources</id>
+						<phase>verify</phase>
+						<goals>
+							<goal>jar-no-fork</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+			</plugin>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>default-prepare-agent</id>
+						<goals>
+							<goal>prepare-agent</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-report</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>report</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLink.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLink.java
new file mode 100644
index 0000000000..94da4d890d
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLink.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.hal;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a HAL Link Object as defined by
+ * <a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08#section-5">draft-kelly-json-hal-08 &#167; 5</a>.
+ *
+ * <p>
+ * A Link Object describes a hyperlink from the containing resource to another resource. The {@code href} property is
+ * the only required field; all other fields are optional.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08#section-5">HAL Link Object</a>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanHal">juneau-bean-hal</a>
+ * </ul>
+ */
+@Marshalled
+@SuppressWarnings({
+	"java:S116" // Field names mirror HAL spec.
+})
+public class HalLink {
+
+	private String href;
+	private Boolean templated;
+	private String type;
+	private String deprecation;
+	private String name;
+	private String profile;
+	private String title;
+	private String hreflang;
+
+	/**
+	 * Default constructor.
+	 */
+	public HalLink() {}
+
+	/**
+	 * Convenience constructor with the required {@code href} field.
+	 *
+	 * @param href The link URI or URI template.
+	 */
+	public HalLink(String href) {
+		this.href = href;
+	}
+
+	/**
+	 * Bean property getter:  <property>href</property>.
+	 *
+	 * <p>
+	 * The link's target URI or URI template (per RFC 6570). This is the only required HAL Link field.
+	 *
+	 * @return The value of the <property>href</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getHref() { return href; }
+
+	/**
+	 * Bean property setter:  <property>href</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public HalLink setHref(String value) {
+		href = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>templated</property>.
+	 *
+	 * <p>
+	 * Indicates whether {@link #getHref() href} is a URI template per RFC 6570.
+	 *
+	 * @return The value of the <property>templated</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Boolean getTemplated() { return templated; }
+
+	/**
+	 * Bean property setter:  <property>templated</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public HalLink setTemplated(Boolean value) {
+		templated = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>type</property>.
+	 *
+	 * <p>
+	 * A hint about the media type expected when dereferencing the target resource.
+	 *
+	 * @return The value of the <property>type</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getType() { return type; }
+
+	/**
+	 * Bean property setter:  <property>type</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public HalLink setType(String value) {
+		type = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>deprecation</property>.
+	 *
+	 * <p>
+	 * A URL providing information about deprecation of the link.
+	 *
+	 * @return The value of the <property>deprecation</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getDeprecation() { return deprecation; }
+
+	/**
+	 * Bean property setter:  <property>deprecation</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public HalLink setDeprecation(String value) {
+		deprecation = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>name</property>.
+	 *
+	 * <p>
+	 * A secondary key for selecting a link from a set sharing the same relation.
+	 *
+	 * @return The value of the <property>name</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getName() { return name; }
+
+	/**
+	 * Bean property setter:  <property>name</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public HalLink setName(String value) {
+		name = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>profile</property>.
+	 *
+	 * <p>
+	 * A URI hint about the profile (additional semantics) of the target resource.
+	 *
+	 * @return The value of the <property>profile</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getProfile() { return profile; }
+
+	/**
+	 * Bean property setter:  <property>profile</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public HalLink setProfile(String value) {
+		profile = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>title</property>.
+	 *
+	 * <p>
+	 * A human-readable identifier for the link, suitable for display.
+	 *
+	 * @return The value of the <property>title</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getTitle() { return title; }
+
+	/**
+	 * Bean property setter:  <property>title</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public HalLink setTitle(String value) {
+		title = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>hreflang</property>.
+	 *
+	 * <p>
+	 * The language of the target resource (RFC 5646 language tag).
+	 *
+	 * @return The value of the <property>hreflang</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getHreflang() { return hreflang; }
+
+	/**
+	 * Bean property setter:  <property>hreflang</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public HalLink setHreflang(String value) {
+		hreflang = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkArray.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkArray.java
new file mode 100644
index 0000000000..ccbc5aec60
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkArray.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.hal;
+
+import java.util.*;
+
+/**
+ * Represents a list of {@link HalLink} objects.
+ *
+ * <p>
+ * Serializes as a top-level JSON array when used as a value inside a HAL {@code _links} map. Mirrors the
+ * top-level-array pattern used by {@code JsonSchemaArray}.
+ *
+ * @serial exclude
+ */
+public class HalLinkArray extends LinkedList<HalLink> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * Default constructor.
+	 */
+	public HalLinkArray() {}
+
+	/**
+	 * Constructor with predefined links to add to this array.
+	 *
+	 * @param value The list of links in this array.
+	 */
+	public HalLinkArray(HalLink...value) {
+		addAll(value);
+	}
+
+	/**
+	 * Convenience method for adding one or more {@link HalLink} objects to this array.
+	 *
+	 * @param value The {@link HalLink} objects to add to this array.
+	 * @return This object.
+	 */
+	public HalLinkArray addAll(HalLink...value) {
+		Collections.addAll(this, value);
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkOrArraySwap.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkOrArraySwap.java
new file mode 100644
index 0000000000..4a4ef3c14e
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkOrArraySwap.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.hal;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.swap.*;
+
+/**
+ * Per-property {@link ObjectSwap} applied to HAL {@code _links} maps so that each map entry round-trips
+ * as either a single {@link HalLink} (JSON object) or a {@link HalLinkArray} (JSON array) per the spec.
+ *
+ * <p>
+ * The swap operates on the whole {@code Map<String,Object>} bean property and walks the entries:
+ * <ul class='spaced-list'>
+ *   <li>On <b>serialize</b> ({@link #swap}): identity - values that are {@link HalLink} or
+ *     {@link HalLinkArray} already serialize as the correct JSON shape.
+ *   <li>On <b>parse</b> ({@link #unswap}): each value that came back as a parsed list is converted to
+ *     {@link HalLinkArray}; each value that came back as a parsed map is converted to {@link HalLink}.
+ *   <li>{@code null} input ⇒ {@code null} output.
+ * </ul>
+ *
+ * <p>
+ * Mirrors {@code BooleanOrSchemaSwap} in {@code juneau-bean-jsonschema} (the entry-disambiguation idea), with
+ * iteration over map entries instead of operating on a single value.
+ */
+public class HalLinkOrArraySwap extends ObjectSwap<Object,Object> {
+
+	@Override /* Overridden from ObjectSwap */
+	public Object swap(MarshallingSession session, Object o) throws SerializeException {
+		return o;
+	}
+
+	@Override /* Overridden from ObjectSwap */
+	public Object unswap(MarshallingSession session, Object o, ClassMeta<?> hint) throws ParseException {
+		if (o == null)
+			return null;
+		if (o instanceof Map<?,?> raw) {
+			var out = new LinkedHashMap<String,Object>();
+			var linkCm = session.getClassMeta(HalLink.class);
+			var arrCm = session.getClassMeta(HalLinkArray.class);
+			for (var e : raw.entrySet()) {
+				var key = String.valueOf(e.getKey());
+				var v = e.getValue();
+				if (v instanceof Collection<?>) {
+					out.put(key, session.convertToType(v, arrCm));
+				} else if (v instanceof Map<?,?>) {
+					out.put(key, session.convertToType(v, linkCm));
+				} else {
+					out.put(key, v);
+				}
+			}
+			return out;
+		}
+		return o;
+	}
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResource.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResource.java
new file mode 100644
index 0000000000..a20d753ab5
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResource.java
@@ -0,0 +1,278 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.hal;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.swap.*;
+
+/**
+ * Represents a HAL Resource Object as defined by
+ * <a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08#section-4">draft-kelly-json-hal-08 &#167; 4</a>.
+ *
+ * <p>
+ * A HAL Resource carries three pieces of state:
+ * <ul class='spaced-list'>
+ *   <li>The reserved {@code _links} member - a map of relation name to either a single {@link HalLink}
+ *     or a {@link HalLinkArray}.
+ *   <li>The reserved {@code _embedded} member - a map of relation name to either a single {@link HalResource}
+ *     or a {@link HalResourceArray} (sub-resources). HAL embeddings are conceptually a <i>tree</i>; cyclic
+ *     {@code _embedded} graphs are rejected by Juneau's {@code BeanTraverseContext} by default.
+ *   <li>Arbitrary payload fields, set via the dynamic {@link #set(String,Object)} method (uses the
+ *     {@code @BeanProp("*")} extra-properties triplet, the same pattern used by {@code OpenApiElement} and
+ *     {@code SwaggerElement}).
+ * </ul>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * 	HalResource <jv>order</jv> = <jk>new</jk> HalResource()
+ * 		.addLink(<js>"self"</js>, <jk>new</jk> HalLink().setHref(<js>"/orders/123"</js>))
+ * 		.addLinks(<js>"curies"</js>,
+ * 			<jk>new</jk> HalLink().setName(<js>"acme"</js>).setHref(<js>"https://acme.example/{rel}"</js>).setTemplated(<jk>true</jk>))
+ * 		.set(<js>"total"</js>, 99.50);
+ *
+ * 	String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>order</jv>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a href="https://stateless.group/hal_specification.html">HAL Specification</a>
+ *   <li class='link'><a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08">draft-kelly-json-hal-08</a>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanHal">juneau-bean-hal</a>
+ * </ul>
+ */
+@Marshalled
+@SuppressWarnings({
+	"java:S116" // Field names mirror HAL spec.
+})
+public class HalResource {
+
+	private static final String ARG_property = "property";
+	private static final String ARG_relation = "relation";
+
+	private Map<String,Object> _links;
+	private Map<String,Object> _embedded;
+	private Map<String,Object> properties;
+
+	/**
+	 * Default constructor.
+	 */
+	public HalResource() {}
+
+	/**
+	 * Bean property getter:  <property>_links</property>.
+	 *
+	 * <p>
+	 * The reserved {@code _links} member. Each map value is either a single {@link HalLink} (rendered as a JSON
+	 * object on the wire) or a {@link HalLinkArray} (rendered as a JSON array).
+	 *
+	 * @return The value of the <property>_links</property> property, or <jk>null</jk> if it is not set.
+	 */
+	@BeanProp("_links")
+	@Swap(HalLinkOrArraySwap.class)
+	public Map<String,Object> getLinks() {
+		return _links;
+	}
+
+	/**
+	 * Bean property setter:  <property>_links</property>.
+	 *
+	 * @param value The new value for this property. Can be <jk>null</jk> to unset.
+	 * @return This object.
+	 */
+	@BeanProp("_links")
+	@Swap(HalLinkOrArraySwap.class)
+	public HalResource setLinks(Map<String,Object> value) {
+		_links = value;
+		return this;
+	}
+
+	/**
+	 * Convenience method that adds a single link under the given relation.
+	 *
+	 * <p>
+	 * If a value already exists for {@code relation}, it is replaced with the supplied single link. To accumulate
+	 * multiple links under the same relation use {@link #addLinks(String, HalLink...)} instead.
+	 *
+	 * @param relation The link relation (e.g. {@code "self"}, {@code "next"}, {@code "curies"}).
+	 * @param value The link to set.
+	 * @return This object.
+	 */
+	public HalResource addLink(String relation, HalLink value) {
+		assertArgNotNull(ARG_relation, relation);
+		if (_links == null)
+			_links = map();
+		_links.put(relation, value);
+		return this;
+	}
+
+	/**
+	 * Convenience method that appends one or more links to a {@link HalLinkArray} stored under {@code relation}.
+	 *
+	 * <p>
+	 * If the current value at {@code relation} is a single {@link HalLink}, it is promoted to a {@link HalLinkArray}
+	 * containing the original link and the appended ones.
+	 *
+	 * @param relation The link relation.
+	 * @param values The links to append.
+	 * @return This object.
+	 */
+	public HalResource addLinks(String relation, HalLink...values) {
+		assertArgNotNull(ARG_relation, relation);
+		if (_links == null)
+			_links = map();
+		var existing = _links.get(relation);
+		HalLinkArray arr;
+		if (existing instanceof HalLinkArray a) {
+			arr = a;
+		} else if (existing instanceof HalLink l) {
+			arr = new HalLinkArray(l);
+			_links.put(relation, arr);
+		} else {
+			arr = new HalLinkArray();
+			_links.put(relation, arr);
+		}
+		arr.addAll(values);
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>_embedded</property>.
+	 *
+	 * <p>
+	 * The reserved {@code _embedded} member. Each map value is either a single {@link HalResource} or a
+	 * {@link HalResourceArray}.
+	 *
+	 * @return The value of the <property>_embedded</property> property, or <jk>null</jk> if it is not set.
+	 */
+	@BeanProp("_embedded")
+	@Swap(HalResourceOrArraySwap.class)
+	public Map<String,Object> getEmbedded() {
+		return _embedded;
+	}
+
+	/**
+	 * Bean property setter:  <property>_embedded</property>.
+	 *
+	 * <p>
+	 * Note: HAL embeddings are conceptually a tree, not a graph. Juneau's {@code BeanTraverseContext} throws on
+	 * cyclic references by default, which matches the spec.
+	 *
+	 * @param value The new value for this property. Can be <jk>null</jk> to unset.
+	 * @return This object.
+	 */
+	@BeanProp("_embedded")
+	@Swap(HalResourceOrArraySwap.class)
+	public HalResource setEmbedded(Map<String,Object> value) {
+		_embedded = value;
+		return this;
+	}
+
+	/**
+	 * Convenience method that embeds a single sub-resource under the given relation.
+	 *
+	 * @param relation The relation name.
+	 * @param value The sub-resource.
+	 * @return This object.
+	 */
+	public HalResource addEmbedded(String relation, HalResource value) {
+		assertArgNotNull(ARG_relation, relation);
+		if (_embedded == null)
+			_embedded = map();
+		_embedded.put(relation, value);
+		return this;
+	}
+
+	/**
+	 * Convenience method that appends one or more sub-resources to a {@link HalResourceArray} stored under
+	 * {@code relation}.
+	 *
+	 * @param relation The relation name.
+	 * @param values The sub-resources to append.
+	 * @return This object.
+	 */
+	public HalResource addEmbedded(String relation, HalResource...values) {
+		assertArgNotNull(ARG_relation, relation);
+		if (_embedded == null)
+			_embedded = map();
+		var existing = _embedded.get(relation);
+		HalResourceArray arr;
+		if (existing instanceof HalResourceArray a) {
+			arr = a;
+		} else if (existing instanceof HalResource r) {
+			arr = new HalResourceArray(r);
+			_embedded.put(relation, arr);
+		} else {
+			arr = new HalResourceArray();
+			_embedded.put(relation, arr);
+		}
+		arr.addAll(values);
+		return this;
+	}
+
+	/**
+	 * Generic property keyset.
+	 *
+	 * @return All the payload (non-{@code _links}, non-{@code _embedded}) property names on this resource. Never <jk>null</jk>.
+	 */
+	@BeanProp("*")
+	public Set<String> extraKeys() {
+		return properties == null ? Collections.emptySet() : properties.keySet();
+	}
+
+	/**
+	 * Generic property getter.
+	 *
+	 * @param property The property name. Must not be <jk>null</jk>.
+	 * @return The property value, or <jk>null</jk> if the property is not set.
+	 */
+	@BeanProp("*")
+	public Object get(String property) {
+		assertArgNotNull(ARG_property, property);
+		return opt(properties).map(x -> x.get(property)).orElse(null);
+	}
+
+	/**
+	 * Generic property setter.
+	 *
+	 * <p>
+	 * Use this to set arbitrary payload fields alongside {@code _links} and {@code _embedded}.
+	 *
+	 * @param property The property name. Must not be <jk>null</jk>.
+	 * @param value The new value for the property.
+	 * @return This object.
+	 */
+	@BeanProp("*")
+	public HalResource set(String property, Object value) {
+		assertArgNotNull(ARG_property, property);
+		if (properties == null)
+			properties = map();
+		properties.put(property, value);
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceArray.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceArray.java
new file mode 100644
index 0000000000..f71dd06865
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceArray.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.hal;
+
+import java.util.*;
+
+/**
+ * Represents a list of {@link HalResource} objects.
+ *
+ * <p>
+ * Serializes as a top-level JSON array when used as a value inside a HAL {@code _embedded} map. Mirrors the
+ * top-level-array pattern used by {@code JsonSchemaArray}.
+ *
+ * @serial exclude
+ */
+public class HalResourceArray extends LinkedList<HalResource> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * Default constructor.
+	 */
+	public HalResourceArray() {}
+
+	/**
+	 * Constructor with predefined resources to add to this array.
+	 *
+	 * @param value The list of resources in this array.
+	 */
+	public HalResourceArray(HalResource...value) {
+		addAll(value);
+	}
+
+	/**
+	 * Convenience method for adding one or more {@link HalResource} objects to this array.
+	 *
+	 * @param value The {@link HalResource} objects to add to this array.
+	 * @return This object.
+	 */
+	public HalResourceArray addAll(HalResource...value) {
+		Collections.addAll(this, value);
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceOrArraySwap.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceOrArraySwap.java
new file mode 100644
index 0000000000..58be476399
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceOrArraySwap.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.hal;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.swap.*;
+
+/**
+ * Per-property {@link ObjectSwap} applied to HAL {@code _embedded} maps so that each map entry round-trips
+ * as either a single {@link HalResource} (JSON object) or a {@link HalResourceArray} (JSON array) per the
+ * spec.
+ *
+ * <p>
+ * The swap operates on the whole {@code Map<String,Object>} bean property and walks the entries:
+ * <ul class='spaced-list'>
+ *   <li>On <b>serialize</b> ({@link #swap}): identity - values that are {@link HalResource} or
+ *     {@link HalResourceArray} already serialize as the correct JSON shape.
+ *   <li>On <b>parse</b> ({@link #unswap}): each value that came back as a parsed list is converted to
+ *     {@link HalResourceArray}; each value that came back as a parsed map is converted to {@link HalResource}.
+ *   <li>{@code null} input ⇒ {@code null} output.
+ * </ul>
+ */
+public class HalResourceOrArraySwap extends ObjectSwap<Object,Object> {
+
+	@Override /* Overridden from ObjectSwap */
+	public Object swap(MarshallingSession session, Object o) throws SerializeException {
+		return o;
+	}
+
+	@Override /* Overridden from ObjectSwap */
+	public Object unswap(MarshallingSession session, Object o, ClassMeta<?> hint) throws ParseException {
+		if (o == null)
+			return null;
+		if (o instanceof Map<?,?> raw) {
+			var out = new LinkedHashMap<String,Object>();
+			var resCm = session.getClassMeta(HalResource.class);
+			var arrCm = session.getClassMeta(HalResourceArray.class);
+			for (var e : raw.entrySet()) {
+				var key = String.valueOf(e.getKey());
+				var v = e.getValue();
+				if (v instanceof Collection<?>) {
+					out.put(key, session.convertToType(v, arrCm));
+				} else if (v instanceof Map<?,?>) {
+					out.put(key, session.convertToType(v, resCm));
+				} else {
+					out.put(key, v);
+				}
+			}
+			return out;
+		}
+		return o;
+	}
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/package-info.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/package-info.java
new file mode 100644
index 0000000000..f83de48694
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/package-info.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Juneau {@link org.apache.juneau.annotation.Marshalled @Marshalled} types for the
+ * <a href="https://stateless.group/hal_specification.html">HAL (Hypertext Application Language)</a>
+ * hypermedia format, also covered by
+ * <a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08">draft-kelly-json-hal-08</a>.
+ *
+ * <h5 class='topic'>Overview</h5>
+ *
+ * <p>
+ * HAL is a JSON-based hypermedia format used by RESTful APIs to express links and embedded resources alongside the
+ * payload itself. This module provides typed Juneau beans modeling the HAL wire format — no marshaller is needed; the
+ * existing {@link org.apache.juneau.json.JsonSerializer} / {@link org.apache.juneau.json.JsonParser} already produce
+ * and consume {@code application/hal+json}. The matching {@code ContentType} constant is
+ * {@code org.apache.juneau.http.header.ContentType#APPLICATION_HAL_JSON} (in {@code juneau-rest-common}).
+ *
+ * <h5 class='topic'>Bean Classes</h5>
+ * <ul class='spaced-list'>
+ *   <li>{@link org.apache.juneau.bean.hal.HalResource} - Top-level HAL resource. Carries
+ *     {@code _links}, {@code _embedded}, and arbitrary payload fields.
+ *   <li>{@link org.apache.juneau.bean.hal.HalLink} - HAL Link Object with the 8 spec fields.
+ *   <li>{@link org.apache.juneau.bean.hal.HalLinkArray} - {@code LinkedList&lt;HalLink&gt;} used as
+ *     the multi-link branch inside an {@code _links} map.
+ *   <li>{@link org.apache.juneau.bean.hal.HalResourceArray} - {@code LinkedList&lt;HalResource&gt;}
+ *     used as the multi-resource branch inside an {@code _embedded} map.
+ *   <li>{@link org.apache.juneau.bean.hal.HalLinkOrArraySwap} - {@link org.apache.juneau.swap.ObjectSwap}
+ *     that materializes each {@code _links} value as either a single {@link org.apache.juneau.bean.hal.HalLink}
+ *     (JSON object) or a {@link org.apache.juneau.bean.hal.HalLinkArray} (JSON array) per spec.
+ *   <li>{@link org.apache.juneau.bean.hal.HalResourceOrArraySwap} - The {@code _embedded} analogue.
+ * </ul>
+ *
+ * <h5 class='topic'>Example</h5>
+ *
+ * <p class='bjava'>
+ * 	<jc>// Build a HAL resource with a self link, a curies array, and embedded items.</jc>
+ * 	HalResource <jv>order</jv> = <jk>new</jk> HalResource()
+ * 		.addLink(<js>"self"</js>, <jk>new</jk> HalLink().setHref(<js>"/orders/123"</js>))
+ * 		.addLinks(<js>"curies"</js>,
+ * 			<jk>new</jk> HalLink().setName(<js>"acme"</js>).setHref(<js>"https://acme.example/{rel}"</js>).setTemplated(<jk>true</jk>))
+ * 		.set(<js>"total"</js>, 99.50);
+ *
+ * 	String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>order</jv>);
+ * 	HalResource <jv>back</jv> = JsonParser.<jsf>DEFAULT</jsf>.parse(<jv>json</jv>, HalResource.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a href="https://stateless.group/hal_specification.html">HAL Specification</a>
+ *   <li class='link'><a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08">draft-kelly-json-hal-08</a>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanHal">juneau-bean-hal</a>
+ * </ul>
+ */
+package org.apache.juneau.bean.hal;
diff --git a/juneau-bean/juneau-bean-jsonapi/pom.xml b/juneau-bean/juneau-bean-jsonapi/pom.xml
new file mode 100644
index 0000000000..c3ce7e648e
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.juneau</groupId>
+		<artifactId>juneau-bean</artifactId>
+		<version>9.5.0-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>juneau-bean-jsonapi</artifactId>
+	<name>Apache Juneau JSON:API Beans</name>
+	<description>Apache Juneau Serializable Beans - JSON:API v1.1</description>
+	<packaging>bundle</packaging>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.apache.juneau</groupId>
+			<artifactId>juneau-marshall</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.felix</groupId>
+				<artifactId>maven-bundle-plugin</artifactId>
+				<extensions>true</extensions>
+				<configuration>
+					<supportIncrementalBuild>true</supportIncrementalBuild>
+				</configuration>
+				<executions>
+					<execution>
+						<id>bundle-manifest</id>
+						<phase>process-classes</phase>
+						<goals>
+							<goal>manifest</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-source-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>attach-sources</id>
+						<phase>verify</phase>
+						<goals>
+							<goal>jar-no-fork</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+			</plugin>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>default-prepare-agent</id>
+						<goals>
+							<goal>prepare-agent</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-report</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>report</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiDocument.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiDocument.java
new file mode 100644
index 0000000000..86f145311b
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiDocument.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.illegalState;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents the top-level JSON:API document as defined by
+ * <a href="https://jsonapi.org/format/#document-top-level">JSON:API v1.1 &#167; Document Structure</a>.
+ *
+ * <p>
+ * Carries the standard six top-level members: {@code data}, {@code errors}, {@code meta}, {@code jsonapi},
+ * {@code links}, {@code included}.
+ *
+ * <h5 class='topic'>Mutual exclusion</h5>
+ *
+ * <p>
+ * Per spec, a document MUST contain at least one of {@code data}/{@code errors}/{@code meta}, and {@code data}
+ * and {@code errors} MUST NOT coexist. The {@link #validate()} helper checks both rules and throws
+ * {@link IllegalStateException} on violation.
+ *
+ * <h5 class='topic'>The polymorphic {@code data} member</h5>
+ *
+ * <p>
+ * The top-level {@code data} can be {@code null}, a single {@link JsonApiResource}, or an array of them. We
+ * model {@code data} as a plain {@code Object} property; the writer chooses single-vs-array shape based on
+ * which value was set. Typed setters {@link #setData(JsonApiResource)} and {@link #setData(List)} are provided
+ * for convenience.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiDocument {
+
+	private Object data;
+	private List<JsonApiError> errors;
+	private Map<String,Object> meta;
+	private JsonApiVersion jsonapi;
+	private Map<String,Object> links;
+	private List<JsonApiResource> included;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonApiDocument() {}
+
+	/**
+	 * Bean property getter:  <property>data</property>.
+	 *
+	 * <p>
+	 * Top-level resource linkage. May be {@code null}, a single {@link JsonApiResource}, or a
+	 * {@code List&lt;JsonApiResource&gt;}.
+	 *
+	 * @return The value of the <property>data</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Object getData() { return data; }
+
+	/**
+	 * Bean property setter:  <property>data</property>.
+	 *
+	 * @param value The new value (single resource, list of resources, or {@code null} to unset).
+	 * @return This object.
+	 */
+	public JsonApiDocument setData(Object value) {
+		data = value;
+		return this;
+	}
+
+	/**
+	 * Typed setter for single-resource primary data.
+	 *
+	 * @param value The resource.
+	 * @return This object.
+	 */
+	public JsonApiDocument setData(JsonApiResource value) {
+		data = value;
+		return this;
+	}
+
+	/**
+	 * Typed setter for to-many primary data.
+	 *
+	 * @param value The list of resources.
+	 * @return This object.
+	 */
+	public JsonApiDocument setData(List<JsonApiResource> value) {
+		data = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>errors</property>.
+	 *
+	 * @return The value of the <property>errors</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public List<JsonApiError> getErrors() { return errors; }
+
+	/**
+	 * Bean property setter:  <property>errors</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiDocument setErrors(List<JsonApiError> value) {
+		errors = value;
+		return this;
+	}
+
+	/**
+	 * Bean property appender:  <property>errors</property>.
+	 *
+	 * @param value The error objects to append.
+	 * @return This object.
+	 */
+	public JsonApiDocument addErrors(JsonApiError...value) {
+		if (errors == null)
+			errors = list();
+		Collections.addAll(errors, value);
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>meta</property>.
+	 *
+	 * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,Object> getMeta() { return meta; }
+
+	/**
+	 * Bean property setter:  <property>meta</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiDocument setMeta(Map<String,Object> value) {
+		meta = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>jsonapi</property>.
+	 *
+	 * @return The value of the <property>jsonapi</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public JsonApiVersion getJsonapi() { return jsonapi; }
+
+	/**
+	 * Bean property setter:  <property>jsonapi</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiDocument setJsonapi(JsonApiVersion value) {
+		jsonapi = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>links</property>.
+	 *
+	 * <p>
+	 * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
+	 *
+	 * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
+	 */
+	@BeanProp(value="links")
+	@Swap(JsonApiLinkOrStringSwap.class)
+	public Map<String,Object> getLinks() { return links; }
+
+	/**
+	 * Bean property setter:  <property>links</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	@BeanProp(value="links")
+	@Swap(JsonApiLinkOrStringSwap.class)
+	public JsonApiDocument setLinks(Map<String,Object> value) {
+		links = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>included</property>.
+	 *
+	 * @return The value of the <property>included</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public List<JsonApiResource> getIncluded() { return included; }
+
+	/**
+	 * Bean property setter:  <property>included</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiDocument setIncluded(List<JsonApiResource> value) {
+		included = value;
+		return this;
+	}
+
+	/**
+	 * Bean property appender:  <property>included</property>.
+	 *
+	 * @param value The resource objects to append.
+	 * @return This object.
+	 */
+	public JsonApiDocument addIncluded(JsonApiResource...value) {
+		if (included == null)
+			included = list();
+		Collections.addAll(included, value);
+		return this;
+	}
+
+	/**
+	 * Validates document-level invariants per the JSON:API spec.
+	 *
+	 * <p>
+	 * Throws if {@code data} and {@code errors} are both present, or if none of {@code data}/{@code errors}/{@code meta}
+	 * is present.
+	 *
+	 * @return This object.
+	 * @throws IllegalStateException If the document violates a top-level invariant.
+	 */
+	public JsonApiDocument validate() {
+		if (data != null && errors != null)
+			throw illegalState("JSON:API document must not contain both 'data' and 'errors'.");
+		if (data == null && errors == null && meta == null)
+			throw illegalState("JSON:API document must contain at least one of 'data', 'errors', or 'meta'.");
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiError.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiError.java
new file mode 100644
index 0000000000..680454bd43
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiError.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Error Object as defined by
+ * <a href="https://jsonapi.org/format/#error-objects">JSON:API v1.1 &#167; Error Objects</a>.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiError {
+
+	private String id;
+	private Map<String,Object> links;
+	private String status;
+	private String code;
+	private String title;
+	private String detail;
+	private JsonApiErrorSource source;
+	private Map<String,Object> meta;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonApiError() {}
+
+	/**
+	 * Bean property getter:  <property>id</property>.
+	 *
+	 * @return The value of the <property>id</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getId() { return id; }
+
+	/**
+	 * Bean property setter:  <property>id</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiError setId(String value) {
+		id = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>links</property>.
+	 *
+	 * <p>
+	 * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
+	 *
+	 * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
+	 */
+	@BeanProp(value="links")
+	@Swap(JsonApiLinkOrStringSwap.class)
+	public Map<String,Object> getLinks() { return links; }
+
+	/**
+	 * Bean property setter:  <property>links</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	@BeanProp(value="links")
+	@Swap(JsonApiLinkOrStringSwap.class)
+	public JsonApiError setLinks(Map<String,Object> value) {
+		links = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>status</property>.
+	 *
+	 * <p>
+	 * Per spec, the HTTP status code as a string.
+	 *
+	 * @return The value of the <property>status</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getStatus() { return status; }
+
+	/**
+	 * Bean property setter:  <property>status</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiError setStatus(String value) {
+		status = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>code</property>.
+	 *
+	 * <p>
+	 * An application-specific error code.
+	 *
+	 * @return The value of the <property>code</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getCode() { return code; }
+
+	/**
+	 * Bean property setter:  <property>code</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiError setCode(String value) {
+		code = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>title</property>.
+	 *
+	 * @return The value of the <property>title</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getTitle() { return title; }
+
+	/**
+	 * Bean property setter:  <property>title</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiError setTitle(String value) {
+		title = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>detail</property>.
+	 *
+	 * @return The value of the <property>detail</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getDetail() { return detail; }
+
+	/**
+	 * Bean property setter:  <property>detail</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiError setDetail(String value) {
+		detail = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>source</property>.
+	 *
+	 * @return The value of the <property>source</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public JsonApiErrorSource getSource() { return source; }
+
+	/**
+	 * Bean property setter:  <property>source</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiError setSource(JsonApiErrorSource value) {
+		source = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>meta</property>.
+	 *
+	 * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,Object> getMeta() { return meta; }
+
+	/**
+	 * Bean property setter:  <property>meta</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiError setMeta(Map<String,Object> value) {
+		meta = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiErrorSource.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiErrorSource.java
new file mode 100644
index 0000000000..1a14cc59d7
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiErrorSource.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents the {@code source} sub-object of a JSON:API Error Object.
+ *
+ * <p>
+ * Per <a href="https://jsonapi.org/format/#error-objects">JSON:API v1.1 &#167; Error Objects</a>, the
+ * {@code source} object can carry a {@code pointer} (JSON Pointer to the offending element), a
+ * {@code parameter} (the URI query parameter name), and/or a {@code header} (the request header name).
+ */
+@Marshalled
+public class JsonApiErrorSource {
+
+	private String pointer;
+	private String parameter;
+	private String header;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonApiErrorSource() {}
+
+	/**
+	 * Bean property getter:  <property>pointer</property>.
+	 *
+	 * @return The value of the <property>pointer</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getPointer() { return pointer; }
+
+	/**
+	 * Bean property setter:  <property>pointer</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiErrorSource setPointer(String value) {
+		pointer = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>parameter</property>.
+	 *
+	 * @return The value of the <property>parameter</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getParameter() { return parameter; }
+
+	/**
+	 * Bean property setter:  <property>parameter</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiErrorSource setParameter(String value) {
+		parameter = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>header</property>.
+	 *
+	 * @return The value of the <property>header</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getHeader() { return header; }
+
+	/**
+	 * Bean property setter:  <property>header</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiErrorSource setHeader(String value) {
+		header = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLink.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLink.java
new file mode 100644
index 0000000000..66810ab176
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLink.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Link Object as defined by
+ * <a href="https://jsonapi.org/format/#document-links">JSON:API v1.1 &#167; Links</a>.
+ *
+ * <p>
+ * Per the spec, each {@code links} entry may be either a JSON string URL or a JSON object with these members:
+ * {@code href} (required URI), {@code rel}, {@code describedby}, {@code title}, {@code type} (media-type hint),
+ * {@code hreflang} (language tag), and {@code meta}. The single-vs-object union is handled by
+ * {@link JsonApiLinkOrStringSwap}, applied via field-level {@code @BeanProp("links") @Swap(...)} on every type
+ * carrying a {@code links} map.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a href="https://jsonapi.org/format/#document-links">JSON:API v1.1 &gt; Links</a>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiLink {
+
+	private String href;
+	private String rel;
+	private String describedby;
+	private String title;
+	private String type;
+	private String hreflang;
+	private Map<String,Object> meta;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonApiLink() {}
+
+	/**
+	 * Convenience constructor with the required {@code href} field.
+	 *
+	 * @param href The link target URI.
+	 */
+	public JsonApiLink(String href) {
+		this.href = href;
+	}
+
+	/**
+	 * Bean property getter:  <property>href</property>.
+	 *
+	 * @return The value of the <property>href</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getHref() { return href; }
+
+	/**
+	 * Bean property setter:  <property>href</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiLink setHref(String value) {
+		href = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>rel</property>.
+	 *
+	 * @return The value of the <property>rel</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getRel() { return rel; }
+
+	/**
+	 * Bean property setter:  <property>rel</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiLink setRel(String value) {
+		rel = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>describedby</property>.
+	 *
+	 * @return The value of the <property>describedby</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getDescribedby() { return describedby; }
+
+	/**
+	 * Bean property setter:  <property>describedby</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiLink setDescribedby(String value) {
+		describedby = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>title</property>.
+	 *
+	 * @return The value of the <property>title</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getTitle() { return title; }
+
+	/**
+	 * Bean property setter:  <property>title</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiLink setTitle(String value) {
+		title = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>type</property>.
+	 *
+	 * <p>
+	 * Media-type hint for the target resource.
+	 *
+	 * @return The value of the <property>type</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getType() { return type; }
+
+	/**
+	 * Bean property setter:  <property>type</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiLink setType(String value) {
+		type = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>hreflang</property>.
+	 *
+	 * @return The value of the <property>hreflang</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getHreflang() { return hreflang; }
+
+	/**
+	 * Bean property setter:  <property>hreflang</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiLink setHreflang(String value) {
+		hreflang = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>meta</property>.
+	 *
+	 * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,Object> getMeta() { return meta; }
+
+	/**
+	 * Bean property setter:  <property>meta</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiLink setMeta(Map<String,Object> value) {
+		meta = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLinkOrStringSwap.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLinkOrStringSwap.java
new file mode 100644
index 0000000000..b9befc3cf0
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLinkOrStringSwap.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.swap.*;
+
+/**
+ * Per-property {@link ObjectSwap} applied to JSON:API {@code links} maps so that each map entry round-trips
+ * as either a JSON string URL or a {@link JsonApiLink} JSON object per the spec.
+ *
+ * <p>
+ * The swap operates on the whole {@code Map<String,Object>} bean property and walks the entries:
+ * <ul class='spaced-list'>
+ *   <li>On <b>serialize</b> ({@link #swap}): identity - {@code String} values and {@link JsonApiLink} values
+ *     serialize directly as JSON strings and JSON objects respectively.
+ *   <li>On <b>parse</b> ({@link #unswap}): each value that came back as a parsed map (i.e. originally a JSON
+ *     object) is converted to a {@link JsonApiLink}; all other values pass through unchanged.
+ *   <li>{@code null} input ⇒ {@code null} output.
+ * </ul>
+ */
+public class JsonApiLinkOrStringSwap extends ObjectSwap<Object,Object> {
+
+	@Override /* Overridden from ObjectSwap */
+	public Object swap(MarshallingSession session, Object o) throws SerializeException {
+		return o;
+	}
+
+	@Override /* Overridden from ObjectSwap */
+	public Object unswap(MarshallingSession session, Object o, ClassMeta<?> hint) throws ParseException {
+		if (o == null)
+			return null;
+		if (o instanceof Map<?,?> raw) {
+			var out = new LinkedHashMap<String,Object>();
+			var linkCm = session.getClassMeta(JsonApiLink.class);
+			for (var e : raw.entrySet()) {
+				var key = String.valueOf(e.getKey());
+				var v = e.getValue();
+				if (v instanceof Map<?,?>) {
+					out.put(key, session.convertToType(v, linkCm));
+				} else {
+					out.put(key, v);
+				}
+			}
+			return out;
+		}
+		return o;
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiRelationship.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiRelationship.java
new file mode 100644
index 0000000000..0b6777165e
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiRelationship.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Relationship Object as defined by
+ * <a href="https://jsonapi.org/format/#document-resource-object-relationships">JSON:API v1.1 &#167; Relationships</a>.
+ *
+ * <p>
+ * A relationship carries:
+ * <ul class='spaced-list'>
+ *   <li>{@code data} - resource linkage. Per spec, this can be {@code null}, a single
+ *     {@link JsonApiResourceIdentifier}, or a JSON array of identifiers. Modeled here as {@code Object}.
+ *   <li>{@code links} - related URLs. Each value can be either a JSON string URL or a {@link JsonApiLink} object;
+ *     the field-level {@link JsonApiLinkOrStringSwap} swap handles the union.
+ *   <li>{@code meta} - free-form meta bag.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiRelationship {
+
+	private Object data;
+	private Map<String,Object> links;
+	private Map<String,Object> meta;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonApiRelationship() {}
+
+	/**
+	 * Bean property getter:  <property>data</property>.
+	 *
+	 * <p>
+	 * Resource linkage. Per spec, this may be {@code null}, a single {@link JsonApiResourceIdentifier}, or a
+	 * {@code List&lt;JsonApiResourceIdentifier&gt;}.
+	 *
+	 * @return The value of the <property>data</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Object getData() { return data; }
+
+	/**
+	 * Bean property setter:  <property>data</property>.
+	 *
+	 * @param value The new value (single identifier, list of identifiers, or {@code null} to unset).
+	 * @return This object.
+	 */
+	public JsonApiRelationship setData(Object value) {
+		data = value;
+		return this;
+	}
+
+	/**
+	 * Typed setter for single-identifier linkage.
+	 *
+	 * @param value The identifier.
+	 * @return This object.
+	 */
+	public JsonApiRelationship setData(JsonApiResourceIdentifier value) {
+		data = value;
+		return this;
+	}
+
+	/**
+	 * Typed setter for to-many linkage.
+	 *
+	 * @param value The list of identifiers.
+	 * @return This object.
+	 */
+	public JsonApiRelationship setData(List<JsonApiResourceIdentifier> value) {
+		data = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>links</property>.
+	 *
+	 * <p>
+	 * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
+	 *
+	 * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
+	 */
+	@BeanProp(value="links")
+	@Swap(JsonApiLinkOrStringSwap.class)
+	public Map<String,Object> getLinks() { return links; }
+
+	/**
+	 * Bean property setter:  <property>links</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	@BeanProp(value="links")
+	@Swap(JsonApiLinkOrStringSwap.class)
+	public JsonApiRelationship setLinks(Map<String,Object> value) {
+		links = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>meta</property>.
+	 *
+	 * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,Object> getMeta() { return meta; }
+
+	/**
+	 * Bean property setter:  <property>meta</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiRelationship setMeta(Map<String,Object> value) {
+		meta = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResource.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResource.java
new file mode 100644
index 0000000000..119f07eb76
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResource.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Resource Object as defined by
+ * <a href="https://jsonapi.org/format/#document-resource-objects">JSON:API v1.1 &#167; Resource Objects</a>.
+ *
+ * <p>
+ * Carries the standard six members: {@code type}, {@code id}, {@code attributes}, {@code relationships},
+ * {@code links}, {@code meta}.
+ *
+ * <h5 class='topic'>Why {@code type} is a plain String</h5>
+ *
+ * <p>
+ * JSON:API uses {@code type} as an open-ended string field on resource objects (the entity-type name, e.g.
+ * {@code "articles"}); the value is chosen by the API and is <b>not</b> a closed Java class hierarchy.
+ * Juneau's {@code @Marshalled(typePropertyName=...)} discriminator defaults to {@code _type} (so there is also
+ * no literal name clash). <b>Do NOT</b> annotate {@code JsonApiResource} with
+ * {@code @Marshalled(typePropertyName="type", dictionary=...)} — that would conflate JSON:API resource typing
+ * with Juneau bean dispatch, break round-tripping of unknown {@code type} values, and force every API to
+ * enumerate its resource types in a Java dictionary up front.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiResource {
+
+	private String type;
+	private String id;
+	private Map<String,Object> attributes;
+	private Map<String,JsonApiRelationship> relationships;
+	private Map<String,Object> links;
+	private Map<String,Object> meta;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonApiResource() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param type The entity-type name.
+	 * @param id The identifier value.
+	 */
+	public JsonApiResource(String type, String id) {
+		this.type = type;
+		this.id = id;
+	}
+
+	/**
+	 * Bean property getter:  <property>type</property>.
+	 *
+	 * <p>
+	 * Plain {@code String} entity-type name. See class-level Javadoc for why this is not a Juneau polymorphic
+	 * discriminator.
+	 *
+	 * @return The value of the <property>type</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getType() { return type; }
+
+	/**
+	 * Bean property setter:  <property>type</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiResource setType(String value) {
+		type = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>id</property>.
+	 *
+	 * @return The value of the <property>id</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getId() { return id; }
+
+	/**
+	 * Bean property setter:  <property>id</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiResource setId(String value) {
+		id = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>attributes</property>.
+	 *
+	 * <p>
+	 * The spec lets {@code attributes} be any JSON object. Modeled as {@code Map&lt;String,Object&gt;} (a nested
+	 * object on the wire — not flattened).
+	 *
+	 * @return The value of the <property>attributes</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,Object> getAttributes() { return attributes; }
+
+	/**
+	 * Bean property setter:  <property>attributes</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiResource setAttributes(Map<String,Object> value) {
+		attributes = value;
+		return this;
+	}
+
+	/**
+	 * Convenience method to add a single attribute.
+	 *
+	 * @param name The attribute name.
+	 * @param value The attribute value.
+	 * @return This object.
+	 */
+	public JsonApiResource putAttribute(String name, Object value) {
+		if (attributes == null)
+			attributes = map();
+		attributes.put(name, value);
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>relationships</property>.
+	 *
+	 * @return The value of the <property>relationships</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,JsonApiRelationship> getRelationships() { return relationships; }
+
+	/**
+	 * Bean property setter:  <property>relationships</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiResource setRelationships(Map<String,JsonApiRelationship> value) {
+		relationships = value;
+		return this;
+	}
+
+	/**
+	 * Convenience method to add a single named relationship.
+	 *
+	 * @param name The relationship name.
+	 * @param value The relationship object.
+	 * @return This object.
+	 */
+	public JsonApiResource putRelationship(String name, JsonApiRelationship value) {
+		if (relationships == null)
+			relationships = map();
+		relationships.put(name, value);
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>links</property>.
+	 *
+	 * <p>
+	 * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
+	 *
+	 * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
+	 */
+	@BeanProp(value="links")
+	@Swap(JsonApiLinkOrStringSwap.class)
+	public Map<String,Object> getLinks() { return links; }
+
+	/**
+	 * Bean property setter:  <property>links</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	@BeanProp(value="links")
+	@Swap(JsonApiLinkOrStringSwap.class)
+	public JsonApiResource setLinks(Map<String,Object> value) {
+		links = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>meta</property>.
+	 *
+	 * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,Object> getMeta() { return meta; }
+
+	/**
+	 * Bean property setter:  <property>meta</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiResource setMeta(Map<String,Object> value) {
+		meta = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResourceIdentifier.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResourceIdentifier.java
new file mode 100644
index 0000000000..084c8f7068
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResourceIdentifier.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Resource Identifier Object as defined by
+ * <a href="https://jsonapi.org/format/#document-resource-identifier-objects">JSON:API v1.1 &#167; Resource Identifier Objects</a>.
+ *
+ * <p>
+ * A slim form of {@link JsonApiResource} used inside relationship {@code data} linkage. Carries only {@code type},
+ * {@code id}, and an optional {@code meta} bag. {@code type} is a plain {@code String} for the same reasons
+ * documented on {@link JsonApiResource#getType()}.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiResourceIdentifier {
+
+	private String type;
+	private String id;
+	private Map<String,Object> meta;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonApiResourceIdentifier() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param type The entity-type name.
+	 * @param id The identifier value.
+	 */
+	public JsonApiResourceIdentifier(String type, String id) {
+		this.type = type;
+		this.id = id;
+	}
+
+	/**
+	 * Bean property getter:  <property>type</property>.
+	 *
+	 * @return The value of the <property>type</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getType() { return type; }
+
+	/**
+	 * Bean property setter:  <property>type</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiResourceIdentifier setType(String value) {
+		type = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>id</property>.
+	 *
+	 * @return The value of the <property>id</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getId() { return id; }
+
+	/**
+	 * Bean property setter:  <property>id</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiResourceIdentifier setId(String value) {
+		id = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>meta</property>.
+	 *
+	 * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,Object> getMeta() { return meta; }
+
+	/**
+	 * Bean property setter:  <property>meta</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiResourceIdentifier setMeta(Map<String,Object> value) {
+		meta = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiVersion.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiVersion.java
new file mode 100644
index 0000000000..370a21bd48
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiVersion.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents the JSON:API top-level {@code jsonapi} member.
+ *
+ * <p>
+ * Per <a href="https://jsonapi.org/format/#document-jsonapi-object">JSON:API v1.1 &#167; JSON:API Object</a>,
+ * this object carries the optional protocol {@code version} string (e.g. {@code "1.1"}) and an optional
+ * {@code meta} bag.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiVersion {
+
+	private String version;
+	private Map<String,Object> meta;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonApiVersion() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param version The JSON:API protocol version (e.g. {@code "1.1"}).
+	 */
+	public JsonApiVersion(String version) {
+		this.version = version;
+	}
+
+	/**
+	 * Bean property getter:  <property>version</property>.
+	 *
+	 * @return The value of the <property>version</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getVersion() { return version; }
+
+	/**
+	 * Bean property setter:  <property>version</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiVersion setVersion(String value) {
+		version = value;
+		return this;
+	}
+
+	/**
+	 * Bean property getter:  <property>meta</property>.
+	 *
+	 * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Map<String,Object> getMeta() { return meta; }
+
+	/**
+	 * Bean property setter:  <property>meta</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonApiVersion setMeta(Map<String,Object> value) {
+		meta = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/package-info.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/package-info.java
new file mode 100644
index 0000000000..57d5adfb51
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/package-info.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Juneau {@link org.apache.juneau.annotation.Marshalled @Marshalled} types for the
+ * <a href="https://jsonapi.org/format/">JSON:API v1.1</a> wire format.
+ *
+ * <h5 class='topic'>Overview</h5>
+ *
+ * <p>
+ * JSON:API is a media-type ({@code application/vnd.api+json}) for building APIs with shared conventions for
+ * resources, relationships, links, errors, and metadata. This module provides typed Juneau beans modeling the
+ * JSON:API wire format - no marshaller is needed; the existing {@link org.apache.juneau.json.JsonSerializer} /
+ * {@link org.apache.juneau.json.JsonParser} already produce and consume the wire format. The matching
+ * {@code ContentType} constant is {@code org.apache.juneau.http.header.ContentType#APPLICATION_VND_API_JSON}.
+ *
+ * <h5 class='topic'>Bean Classes</h5>
+ * <ul class='spaced-list'>
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiDocument} - Top-level container with
+ *     {@code data}/{@code errors}/{@code meta}/{@code jsonapi}/{@code links}/{@code included}.
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiResource} - Resource object with
+ *     {@code type}/{@code id}/{@code attributes}/{@code relationships}/{@code links}/{@code meta}.
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiResourceIdentifier} - Slim form used inside relationship linkage.
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiRelationship} - Relationship object
+ *     ({@code data} linkage, {@code links}, {@code meta}).
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiError} - Error object with
+ *     {@code id}/{@code links}/{@code status}/{@code code}/{@code title}/{@code detail}/{@code source}/{@code meta}.
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiErrorSource} - The {@code source} sub-object
+ *     ({@code pointer}/{@code parameter}/{@code header}).
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiLink} - Link object with
+ *     {@code href}/{@code rel}/{@code describedby}/{@code title}/{@code type}/{@code hreflang}/{@code meta}.
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiVersion} - The {@code jsonapi} member
+ *     ({@code version}/{@code meta}).
+ *   <li>{@link org.apache.juneau.bean.jsonapi.JsonApiLinkOrStringSwap} -
+ *     {@link org.apache.juneau.swap.ObjectSwap} for {@code links} map values; each value can be either a JSON
+ *     string URL or a {@link org.apache.juneau.bean.jsonapi.JsonApiLink} object per the spec.
+ * </ul>
+ *
+ * <h5 class='topic'>Important: {@code JsonApiResource.type} is a plain String</h5>
+ *
+ * <p>
+ * JSON:API uses {@code type} as an open-ended string field on resource objects (the entity-type name, e.g.
+ * {@code "articles"}); the value is chosen by the API and is <b>not</b> a closed Java class hierarchy. Therefore
+ * {@code JsonApiResource} models {@code type} as a plain {@code String} and is <b>not</b> annotated with
+ * {@code @Marshalled(typePropertyName="type", dictionary=...)}. Conflating JSON:API resource typing with Juneau
+ * bean dispatch would break round-tripping of unknown {@code type} values and force every API to enumerate its
+ * resource types in a Java dictionary up front.
+ *
+ * <h5 class='topic'>Document-level mutual exclusion</h5>
+ *
+ * <p>
+ * Per the JSON:API spec, a document MUST contain at least one of {@code data}, {@code errors}, or {@code meta};
+ * {@code data} and {@code errors} MUST NOT coexist. {@link org.apache.juneau.bean.jsonapi.JsonApiDocument}
+ * documents this constraint and offers an optional {@code validate()} helper.
+ *
+ * <h5 class='topic'>Example</h5>
+ *
+ * <p class='bjava'>
+ * 	<jc>// Build a compound document with included resources.</jc>
+ * 	JsonApiResource <jv>article</jv> = <jk>new</jk> JsonApiResource()
+ * 		.setType(<js>"articles"</js>).setId(<js>"1"</js>)
+ * 		.putAttribute(<js>"title"</js>, <js>"JSON:API paints my bikeshed!"</js>);
+ * 	JsonApiDocument <jv>doc</jv> = <jk>new</jk> JsonApiDocument().setData(<jv>article</jv>);
+ * 	String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>doc</jv>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a href="https://jsonapi.org/format/">JSON:API v1.1</a>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+package org.apache.juneau.bean.jsonapi;
diff --git a/juneau-bean/juneau-bean-jsonpatch/pom.xml b/juneau-bean/juneau-bean-jsonpatch/pom.xml
new file mode 100644
index 0000000000..c8cde6fa67
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.juneau</groupId>
+		<artifactId>juneau-bean</artifactId>
+		<version>9.5.0-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>juneau-bean-jsonpatch</artifactId>
+	<name>Apache Juneau JSON Patch Beans</name>
+	<description>Apache Juneau Serializable Beans - JSON Patch (RFC 6902)</description>
+	<packaging>bundle</packaging>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.apache.juneau</groupId>
+			<artifactId>juneau-marshall</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.felix</groupId>
+				<artifactId>maven-bundle-plugin</artifactId>
+				<extensions>true</extensions>
+				<configuration>
+					<supportIncrementalBuild>true</supportIncrementalBuild>
+				</configuration>
+				<executions>
+					<execution>
+						<id>bundle-manifest</id>
+						<phase>process-classes</phase>
+						<goals>
+							<goal>manifest</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-source-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>attach-sources</id>
+						<phase>verify</phase>
+						<goals>
+							<goal>jar-no-fork</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+			</plugin>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>default-prepare-agent</id>
+						<goals>
+							<goal>prepare-agent</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-report</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>report</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/AddOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/AddOp.java
new file mode 100644
index 0000000000..4fe79d6ee1
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/AddOp.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "add"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.1">RFC 6902 &#167; 4.1</a>.
+ *
+ * <p>
+ * Adds {@link #getValue() value} at {@link #getPath() path}.
+ */
+@Marshalled(typeName = "add")
+public class AddOp extends JsonPatchOperation {
+
+	private Object value;
+
+	/**
+	 * Default constructor.
+	 */
+	public AddOp() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param path The JSON Pointer target.
+	 * @param value The value to add.
+	 */
+	public AddOp(String path, Object value) {
+		super(path);
+		this.value = value;
+	}
+
+	/**
+	 * Bean property getter:  <property>value</property>.
+	 *
+	 * @return The value of the <property>value</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Object getValue() { return value; }
+
+	/**
+	 * Bean property setter:  <property>value</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public AddOp setValue(Object value) {
+		this.value = value;
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/CopyOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/CopyOp.java
new file mode 100644
index 0000000000..8480b8057b
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/CopyOp.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "copy"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.5">RFC 6902 &#167; 4.5</a>.
+ *
+ * <p>
+ * Copies the value at {@link #getFrom() from} to {@link #getPath() path}.
+ */
+@Marshalled(typeName = "copy")
+public class CopyOp extends JsonPatchOperation {
+
+	private String from;
+
+	/**
+	 * Default constructor.
+	 */
+	public CopyOp() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param path The JSON Pointer destination.
+	 * @param from The JSON Pointer source.
+	 */
+	public CopyOp(String path, String from) {
+		super(path);
+		this.from = from;
+	}
+
+	/**
+	 * Bean property getter:  <property>from</property>.
+	 *
+	 * @return The value of the <property>from</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getFrom() { return from; }
+
+	/**
+	 * Bean property setter:  <property>from</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public CopyOp setFrom(String value) {
+		from = value;
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatch.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatch.java
new file mode 100644
index 0000000000..823a625859
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatch.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import java.util.*;
+
+/**
+ * Represents a JSON Patch document per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>.
+ *
+ * <p>
+ * A JSON Patch document is a JSON array of operation objects; this type therefore extends
+ * {@link LinkedList} of {@link JsonPatchOperation}. Mirrors the top-level-array pattern used by
+ * {@code JsonSchemaArray} - no {@code @Json(wrapperAttr=...)} wrapping.
+ *
+ * @serial exclude
+ */
+public class JsonPatch extends LinkedList<JsonPatchOperation> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * Default constructor.
+	 */
+	public JsonPatch() {}
+
+	/**
+	 * Constructor with predefined operations to add to this patch.
+	 *
+	 * @param value The operations in this patch.
+	 */
+	public JsonPatch(JsonPatchOperation...value) {
+		append(value);
+	}
+
+	/**
+	 * Convenience method for appending one or more {@link JsonPatchOperation} objects to this patch.
+	 *
+	 * @param value The operations to append.
+	 * @return This object.
+	 */
+	public JsonPatch append(JsonPatchOperation...value) {
+		Collections.addAll(this, value);
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatchOperation.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatchOperation.java
new file mode 100644
index 0000000000..e58d02d0f5
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatchOperation.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Abstract base for a JSON Patch operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>.
+ *
+ * <p>
+ * Polymorphic dispatch is wired here via
+ * {@code @Marshalled(typePropertyName="op", dictionary={...})}: on read, the parser inspects the {@code op}
+ * member and instantiates the matching concrete subclass; on write, the serializer emits {@code op} as the
+ * discriminator and the operation's own members alongside.
+ *
+ * <p>
+ * <b>Not</b> declared {@code sealed} - keeping it open lets downstream callers add their own ops if needed
+ * (e.g. JSON Patch extensions or vendor-specific test operators) by adding the new subclass to a custom
+ * dictionary.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonPatch">juneau-bean-jsonpatch</a>
+ * </ul>
+ */
+@Marshalled(
+	typePropertyName = "op",
+	dictionary = { AddOp.class, RemoveOp.class, ReplaceOp.class, MoveOp.class, CopyOp.class, TestOp.class }
+)
+public abstract class JsonPatchOperation {
+
+	private String path;
+
+	/**
+	 * Default constructor.
+	 */
+	protected JsonPatchOperation() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param path The JSON Pointer target of the operation.
+	 */
+	protected JsonPatchOperation(String path) {
+		this.path = path;
+	}
+
+	/**
+	 * Bean property getter:  <property>path</property>.
+	 *
+	 * <p>
+	 * The target JSON Pointer (RFC 6901) of this operation.
+	 *
+	 * @return The value of the <property>path</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getPath() { return path; }
+
+	/**
+	 * Bean property setter:  <property>path</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public JsonPatchOperation setPath(String value) {
+		path = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return JsonSerializer.DEFAULT.toString(this);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/MoveOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/MoveOp.java
new file mode 100644
index 0000000000..90f40fd33a
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/MoveOp.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "move"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.4">RFC 6902 &#167; 4.4</a>.
+ *
+ * <p>
+ * Moves the value at {@link #getFrom() from} to {@link #getPath() path}.
+ */
+@Marshalled(typeName = "move")
+public class MoveOp extends JsonPatchOperation {
+
+	private String from;
+
+	/**
+	 * Default constructor.
+	 */
+	public MoveOp() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param path The JSON Pointer destination.
+	 * @param from The JSON Pointer source.
+	 */
+	public MoveOp(String path, String from) {
+		super(path);
+		this.from = from;
+	}
+
+	/**
+	 * Bean property getter:  <property>from</property>.
+	 *
+	 * @return The value of the <property>from</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public String getFrom() { return from; }
+
+	/**
+	 * Bean property setter:  <property>from</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public MoveOp setFrom(String value) {
+		from = value;
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/RemoveOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/RemoveOp.java
new file mode 100644
index 0000000000..d01bc2bf82
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/RemoveOp.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "remove"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.2">RFC 6902 &#167; 4.2</a>.
+ *
+ * <p>
+ * Removes the value at {@link #getPath() path}. Carries no extra members beyond {@code op} and {@code path}.
+ */
+@Marshalled(typeName = "remove")
+public class RemoveOp extends JsonPatchOperation {
+
+	/**
+	 * Default constructor.
+	 */
+	public RemoveOp() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param path The JSON Pointer target.
+	 */
+	public RemoveOp(String path) {
+		super(path);
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/ReplaceOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/ReplaceOp.java
new file mode 100644
index 0000000000..701828b3a1
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/ReplaceOp.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "replace"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.3">RFC 6902 &#167; 4.3</a>.
+ *
+ * <p>
+ * Replaces the value at {@link #getPath() path} with {@link #getValue() value}.
+ */
+@Marshalled(typeName = "replace")
+public class ReplaceOp extends JsonPatchOperation {
+
+	private Object value;
+
+	/**
+	 * Default constructor.
+	 */
+	public ReplaceOp() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param path The JSON Pointer target.
+	 * @param value The replacement value.
+	 */
+	public ReplaceOp(String path, Object value) {
+		super(path);
+		this.value = value;
+	}
+
+	/**
+	 * Bean property getter:  <property>value</property>.
+	 *
+	 * @return The value of the <property>value</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Object getValue() { return value; }
+
+	/**
+	 * Bean property setter:  <property>value</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public ReplaceOp setValue(Object value) {
+		this.value = value;
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/TestOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/TestOp.java
new file mode 100644
index 0000000000..5d422d237a
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/TestOp.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "test"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.6">RFC 6902 &#167; 4.6</a>.
+ *
+ * <p>
+ * Tests that the value at {@link #getPath() path} equals {@link #getValue() value}.
+ */
+@Marshalled(typeName = "test")
+public class TestOp extends JsonPatchOperation {
+
+	private Object value;
+
+	/**
+	 * Default constructor.
+	 */
+	public TestOp() {}
+
+	/**
+	 * Convenience constructor.
+	 *
+	 * @param path The JSON Pointer target.
+	 * @param value The value to test for.
+	 */
+	public TestOp(String path, Object value) {
+		super(path);
+		this.value = value;
+	}
+
+	/**
+	 * Bean property getter:  <property>value</property>.
+	 *
+	 * @return The value of the <property>value</property> property, or <jk>null</jk> if it is not set.
+	 */
+	public Object getValue() { return value; }
+
+	/**
+	 * Bean property setter:  <property>value</property>.
+	 *
+	 * @param value The new value for this property.
+	 * @return This object.
+	 */
+	public TestOp setValue(Object value) {
+		this.value = value;
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/package-info.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/package-info.java
new file mode 100644
index 0000000000..7d9d6285c1
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/package-info.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Juneau {@link org.apache.juneau.annotation.Marshalled @Marshalled} types for
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902">JSON Patch (RFC 6902)</a>.
+ *
+ * <h5 class='topic'>Overview</h5>
+ *
+ * <p>
+ * A JSON Patch document is a JSON array of operation objects describing changes to apply to a target JSON
+ * document. Each operation object has an {@code op} member naming one of the six operation types
+ * ({@code add}, {@code remove}, {@code replace}, {@code move}, {@code copy}, {@code test}), a {@code path}
+ * member (JSON Pointer per RFC 6901), and op-specific additional members. The matching {@code ContentType}
+ * constant is {@code org.apache.juneau.http.header.ContentType#APPLICATION_JSON_PATCH}.
+ *
+ * <h5 class='topic'>Bean Classes</h5>
+ * <ul class='spaced-list'>
+ *   <li>{@link org.apache.juneau.bean.jsonpatch.JsonPatchOperation} - Abstract base. Polymorphic dispatch
+ *     wired via {@code @Marshalled(typePropertyName="op", dictionary={...})}, so the parser materializes the
+ *     correct concrete subclass keyed off the {@code op} member.
+ *   <li>{@link org.apache.juneau.bean.jsonpatch.AddOp} - {@code op:"add"} + {@code value}.
+ *   <li>{@link org.apache.juneau.bean.jsonpatch.RemoveOp} - {@code op:"remove"}.
+ *   <li>{@link org.apache.juneau.bean.jsonpatch.ReplaceOp} - {@code op:"replace"} + {@code value}.
+ *   <li>{@link org.apache.juneau.bean.jsonpatch.MoveOp} - {@code op:"move"} + {@code from}.
+ *   <li>{@link org.apache.juneau.bean.jsonpatch.CopyOp} - {@code op:"copy"} + {@code from}.
+ *   <li>{@link org.apache.juneau.bean.jsonpatch.TestOp} - {@code op:"test"} + {@code value}.
+ *   <li>{@link org.apache.juneau.bean.jsonpatch.JsonPatch} - {@code LinkedList&lt;JsonPatchOperation&gt;};
+ *     the JSON Patch <i>document</i> serializes as a top-level JSON array (mirrors {@code JsonSchemaArray}).
+ * </ul>
+ *
+ * <h5 class='topic'>Example</h5>
+ *
+ * <p class='bjava'>
+ * 	JsonPatch <jv>patch</jv> = <jk>new</jk> JsonPatch()
+ * 		.append(<jk>new</jk> AddOp(<js>"/a/b/c"</js>, <js>"foo"</js>))
+ * 		.append(<jk>new</jk> RemoveOp(<js>"/a/b/c"</js>))
+ * 		.append(<jk>new</jk> ReplaceOp(<js>"/a/b/c"</js>, 42))
+ * 		.append(<jk>new</jk> MoveOp(<js>"/a/b/c"</js>, <js>"/a/b/d"</js>))
+ * 		.append(<jk>new</jk> CopyOp(<js>"/a/b/c"</js>, <js>"/a/b/e"</js>))
+ * 		.append(<jk>new</jk> TestOp(<js>"/a/b/c"</js>, <js>"foo"</js>));
+ *
+ * 	String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>patch</jv>);
+ * 	JsonPatch <jv>back</jv> = JsonParser.<jsf>DEFAULT</jsf>.parse(<jv>json</jv>, JsonPatch.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *   <li class='link'><a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902 - JSON Patch</a>
+ *   <li class='link'><a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901 - JSON Pointer</a>
+ *   <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonPatch">juneau-bean-jsonpatch</a>
+ * </ul>
+ */
+package org.apache.juneau.bean.jsonpatch;
diff --git a/juneau-bean/juneau-bean-rfc7807/pom.xml b/juneau-bean/juneau-bean-rfc7807/pom.xml
new file mode 100644
index 0000000000..2159996b2f
--- /dev/null
+++ b/juneau-bean/juneau-bean-rfc7807/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.apache.juneau</groupId>
+		<artifactId>juneau-bean</artifactId>
+		<version>9.5.0-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>juneau-bean-rfc7807</artifactId>
+	<name>Apache Juneau RFC 7807 Beans</name>
+	<description>Apache Juneau Serializable Beans - RFC 7807 Problem Details for HTTP APIs (application/problem+json)</description>
+	<packaging>bundle</packaging>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.apache.juneau</groupId>
+			<artifactId>juneau-marshall</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.felix</groupId>
+				<artifactId>maven-bundle-plugin</artifactId>
+				<extensions>true</extensions>
+				<configuration>
+					<supportIncrementalBuild>true</supportIncrementalBuild>
+				</configuration>
+				<executions>
+					<execution>
+						<id>bundle-manifest</id>
+						<phase>process-classes</phase>
+						<goals>
+							<goal>manifest</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-source-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>attach-sources</id>
+						<phase>verify</phase>
+						<goals>
+							<goal>jar-no-fork</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+			</plugin>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>default-prepare-agent</id>
+						<goals>
+							<goal>prepare-agent</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-report</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>report</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/Problem.java b/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/Problem.java
new file mode 100644
index 0000000000..1772d29602
--- /dev/null
+++ b/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/Problem.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.rfc7807;
+
+import java.net.*;
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+
+/**
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807">RFC 7807 &mdash; Problem Details for HTTP APIs</a>
+ * bean.
+ *
+ * <p>
+ * Represents the canonical {@code application/problem+json} document. The five standard members defined by
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">&sect;3.1</a> are modelled as typed
+ * properties; arbitrary <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">&sect;3.2</a>
+ * extension members flatten into the top-level JSON object via a {@link BeanProp @BeanProp("*")} triplet.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * 	Problem <jv>problem</jv> = <jk>new</jk> Problem()
+ * 		.setType(<jk>new</jk> URI(<js>"https://example.com/probs/out-of-credit"</js>))
+ * 		.setTitle(<js>"You do not have enough credit."</js>)
+ * 		.setStatus(403)
+ * 		.setDetail(<js>"Your current balance is 30, but that costs 50."</js>)
+ * 		.setInstance(<jk>new</jk> URI(<js>"/account/12345/msgs/abc"</js>))
+ * 		.set(<js>"balance"</js>, 30)
+ * 		.set(<js>"accounts"</js>, <jsm>list</jsm>(<js>"/account/12345"</js>, <js>"/account/67890"</js>));
+ *
+ * 	String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>problem</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5>
+ * <ul class='spaced-list'>
+ * 	<li>The {@code type} field is nullable. {@link #getType()} returns the raw value (possibly {@code null});
+ * 		{@link #getTypeOrDefault()} returns the spec default {@code "about:blank"} per
+ * 		<a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 &sect;3.1</a> when the
+ * 		field is unset. The field is never eagerly defaulted, so a fresh {@code Problem} round-trips without a
+ * 		synthetic {@code "type":"about:blank"} on the wire.
+ * 	<li>RFC 7807 was obsoleted by <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457</a> in
+ * 		July 2023, but the data model and the {@code application/problem+json} IANA registration are unchanged.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanRfc7807">juneau-bean-rfc7807</a>
+ * </ul>
+ */
+@Marshalled
+public class Problem {
+
+	/**
+	 * The default {@code type} URI defined by
+	 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 &sect;3.1</a> for problem
+	 * documents that omit the {@code type} member.
+	 */
+	public static final URI DEFAULT_TYPE = URI.create("about:blank");
+
+	private URI type;
+	private String title;
+	private Integer status;
+	private String detail;
+	private URI instance;
+	private Map<String,Object> extra;
+
+	/**
+	 * Convenience factory for the common &quot;build a problem from a status code + title + detail&quot; call site.
+	 *
+	 * <p>
+	 * Populates only the {@code status}, {@code title}, and {@code detail} members. Callers wire from
+	 * {@link Throwable}s or HTTP exception types themselves &mdash; for example:
+	 * <p class='bjava'>
+	 * 	Problem.<jsm>fromStatus</jsm>(<jv>e</jv>.getStatusCode(), <jv>e</jv>.getStatusLine().getReasonPhrase(), <jv>e</jv>.getMessage());
+	 * </p>
+	 *
+	 * @param status The HTTP status code (e.g. {@code 404}).
+	 * @param title A short, human-readable summary of the problem type. Can be <jk>null</jk>.
+	 * @param detail A human-readable explanation specific to this occurrence. Can be <jk>null</jk>.
+	 * @return A new {@link Problem} populated with the supplied values.
+	 */
+	public static Problem fromStatus(int status, String title, String detail) {
+		return new Problem().setStatus(status).setTitle(title).setDetail(detail);
+	}
+
+	/**
+	 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">type</a> URI that identifies
+	 * the problem type.
+	 *
+	 * <p>
+	 * Returns the raw field value, which may be <jk>null</jk> when the {@code type} member is absent. Use
+	 * {@link #getTypeOrDefault()} to get the spec default {@code "about:blank"} for absent values.
+	 *
+	 * @return The {@code type} URI, or <jk>null</jk> if not set.
+	 */
+	public URI getType() {
+		return type;
+	}
+
+	/**
+	 * Returns the {@code type} URI, defaulting to {@link #DEFAULT_TYPE} ({@code "about:blank"}) per
+	 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 &sect;3.1</a> when the
+	 * field is <jk>null</jk>.
+	 *
+	 * <p>
+	 * The underlying field is left untouched, so this method never causes a synthetic {@code "type":"about:blank"}
+	 * to appear on the wire.
+	 *
+	 * @return The {@code type} URI, never <jk>null</jk>.
+	 */
+	@BeanIgnore
+	public URI getTypeOrDefault() {
+		return type == null ? DEFAULT_TYPE : type;
+	}
+
+	/**
+	 * Sets the {@code type} URI.
+	 *
+	 * @param value The new value. Can be <jk>null</jk> to unset the property.
+	 * @return This object.
+	 */
+	public Problem setType(URI value) {
+		type = value;
+		return this;
+	}
+
+	/**
+	 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">title</a> &mdash; a short,
+	 * human-readable summary of the problem type.
+	 *
+	 * @return The title, or <jk>null</jk> if not set.
+	 */
+	public String getTitle() {
+		return title;
+	}
+
+	/**
+	 * Sets the {@code title}.
+	 *
+	 * @param value The new value. Can be <jk>null</jk> to unset the property.
+	 * @return This object.
+	 */
+	public Problem setTitle(String value) {
+		title = value;
+		return this;
+	}
+
+	/**
+	 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">status</a> &mdash; the HTTP
+	 * status code originally generated by the origin server for this occurrence.
+	 *
+	 * <p>
+	 * Modelled as {@link Integer} (boxed) because RFC 7807 declares {@code status} as OPTIONAL.
+	 *
+	 * @return The status code, or <jk>null</jk> if not set.
+	 */
+	public Integer getStatus() {
+		return status;
+	}
+
+	/**
+	 * Sets the {@code status} HTTP status code.
+	 *
+	 * @param value The new value. Can be <jk>null</jk> to unset the property.
+	 * @return This object.
+	 */
+	public Problem setStatus(Integer value) {
+		status = value;
+		return this;
+	}
+
+	/**
+	 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">detail</a> &mdash; a
+	 * human-readable explanation specific to this occurrence of the problem.
+	 *
+	 * @return The detail, or <jk>null</jk> if not set.
+	 */
+	public String getDetail() {
+		return detail;
+	}
+
+	/**
+	 * Sets the {@code detail}.
+	 *
+	 * @param value The new value. Can be <jk>null</jk> to unset the property.
+	 * @return This object.
+	 */
+	public Problem setDetail(String value) {
+		detail = value;
+		return this;
+	}
+
+	/**
+	 * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">instance</a> URI that
+	 * identifies the specific occurrence of the problem.
+	 *
+	 * @return The {@code instance} URI, or <jk>null</jk> if not set.
+	 */
+	public URI getInstance() {
+		return instance;
+	}
+
+	/**
+	 * Sets the {@code instance} URI.
+	 *
+	 * @param value The new value. Can be <jk>null</jk> to unset the property.
+	 * @return This object.
+	 */
+	public Problem setInstance(URI value) {
+		instance = value;
+		return this;
+	}
+
+	/**
+	 * Generic <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">extension-member</a>
+	 * keyset.
+	 *
+	 * @return All extension keys on this problem. Never <jk>null</jk>.
+	 */
+	@BeanProp("*")
+	public Set<String> extraKeys() {
+		return extra == null ? Collections.emptySet() : extra.keySet();
+	}
+
+	/**
+	 * Generic extension-member getter.
+	 *
+	 * <p>
+	 * Used by the bean framework to read arbitrary
+	 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">&sect;3.2</a> extension members
+	 * that flatten into the top-level JSON object.
+	 *
+	 * @param property The property name to retrieve. Must not be <jk>null</jk>.
+	 * @return The property value, or <jk>null</jk> if the property does not exist.
+	 */
+	@BeanProp("*")
+	public Object get(String property) {
+		return extra == null ? null : extra.get(property);
+	}
+
+	/**
+	 * Generic extension-member setter.
+	 *
+	 * <p>
+	 * Used by the bean framework to set arbitrary
+	 * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">&sect;3.2</a> extension members.
+	 * The extension map is lazily initialised on first use.
+	 *
+	 * @param property The property name to set. Must not be <jk>null</jk>.
+	 * @param value The new value for the property.
+	 * @return This object.
+	 */
+	@BeanProp("*")
+	public Problem set(String property, Object value) {
+		if (extra == null)
+			extra = new LinkedHashMap<>();
+		extra.put(property, value);
+		return this;
+	}
+}
diff --git a/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/package-info.java b/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/package-info.java
new file mode 100644
index 0000000000..a89914d5b0
--- /dev/null
+++ b/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Juneau {@link org.apache.juneau.annotation.Marshalled @Marshalled} beans for
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807">RFC 7807 &mdash; Problem Details for HTTP APIs</a>
+ * (media type {@code application/problem+json}).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanRfc7807">juneau-bean-rfc7807</a>
+ * </ul>
+ */
+package org.apache.juneau.bean.rfc7807;
diff --git a/juneau-bean/pom.xml b/juneau-bean/pom.xml
index 7e7c6d0b88..9269640e0b 100644
--- a/juneau-bean/pom.xml
+++ b/juneau-bean/pom.xml
@@ -33,10 +33,14 @@
 	<modules>
 		<module>juneau-bean-atom</module>
 		<module>juneau-bean-common</module>
+		<module>juneau-bean-hal</module>
 		<module>juneau-bean-html5</module>
+		<module>juneau-bean-jsonapi</module>
+		<module>juneau-bean-jsonpatch</module>
 		<module>juneau-bean-jsonschema</module>
 		<module>juneau-bean-mcp</module>
 		<module>juneau-bean-openapi-v3</module>
+		<module>juneau-bean-rfc7807</module>
 		<module>juneau-bean-swagger-v2</module>
 	</modules>
 
diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanSession.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanSession.java
index 359c4595bb..b0353d6fdc 100644
--- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanSession.java
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanSession.java
@@ -84,7 +84,7 @@ public interface BeanSession {
 	 * directly, so the parse is delegated to the session via this SPI.
 	 *
 	 * <p>
-	 * Implementations typically route through {@code JsonMap.ofText(value).session(this)}.
+	 * Implementations typically route through {@code JsonMap.ofString(value).session(this)}.
 	 *
 	 * @param value The JSON-formatted character sequence to parse.  Must not be <jk>null</jk>.
 	 * @return The parsed map.
diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ThrowableUtils.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ThrowableUtils.java
index 878833e222..4e48a3bada 100644
--- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ThrowableUtils.java
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ThrowableUtils.java
@@ -313,8 +313,11 @@ public class ThrowableUtils {
 	 * @param maxDepth The maximum number of stack trace elements to print. If <jk>null</jk> or negative, prints all elements.
 	 */
 	public static void printStackTrace(Throwable t, Integer maxDepth) {
-		LOG.warning(t, "Stack trace");
-		printStackTrace(t, new PrintWriter(System.err, true), maxDepth);
+		var sw = new StringWriter();
+		try (var pw = new PrintWriter(sw)) {
+			printStackTrace(t, pw, maxDepth);
+		}
+		LOG.warning(t, sw.toString());
 	}
 
 	/**
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Entry.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Entry.java
index ad32643437..cca1d4820f 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Entry.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Entry.java
@@ -351,7 +351,7 @@ public class Entry {
 			if (s1 != '[' && ! "null".equals(s))
 				s = '[' + s + ']';
 		}
-		return opt(JsonList.ofText(s, parser));
+		return opt(JsonList.ofString(s, parser));
 	}
 
 	/**
@@ -425,7 +425,7 @@ public class Entry {
 			if (s1 != '{' && ! "null".equals(s))
 				s = '{' + s + '}';
 		}
-		return opt(JsonMap.ofText(s, parser));
+		return opt(JsonMap.ofString(s, parser));
 	}
 
 	/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java
index 27824e84b7..6ba1641c91 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java
@@ -50,7 +50,7 @@ public final class BeanMapLoader {
 	 * @throws ParseException Malformed input encountered.
 	 */
 	public static <T> BeanMap<T> load(BeanMap<T> m, String input) throws ParseException {
-		m.putAll(Json5Map.ofText(input));
+		m.putAll(Json5Map.ofString(input));
 		return m;
 	}
 
@@ -69,7 +69,7 @@ public final class BeanMapLoader {
 	 * @throws IOException Thrown by the reader.
 	 */
 	public static <T> BeanMap<T> load(BeanMap<T> m, Reader r, ReaderParser p) throws ParseException, IOException {
-		m.putAll(JsonMap.ofText(r, p));
+		m.putAll(JsonMap.ofString(r, p));
 		return m;
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java
index 712839a4f4..dd94a70848 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java
@@ -4118,7 +4118,7 @@ public class MarshallingContext extends Context implements ConversionFinder, Bea
 					var str = in.toString();
 					Json5List list;
 					if (startsWith(str, '['))
-						list = Json5List.ofText(str).setBeanSession(bs);
+						list = Json5List.ofString(str).setBeanSession(bs);
 					else
 						list = new Json5List((Object[]) splita(str)).setBeanSession(bs);
 					return bs.convertToType(list, outType);
@@ -4133,7 +4133,7 @@ public class MarshallingContext extends Context implements ConversionFinder, Bea
 			return (in, memberOf, session, args) -> {
 				try {
 					var bs = beanSessionOrDefault(session);
-					var m = Json5Map.ofText(in.toString());
+					var m = Json5Map.ofString(in.toString());
 					m.setBeanSession(bs);
 					return bs.convertToType(m, outType);
 				} catch (Exception e) {
@@ -4197,7 +4197,7 @@ public class MarshallingContext extends Context implements ConversionFinder, Bea
 					if (!isProbablyJsonArray(str, false))
 						throw rex("Cannot convert string to {0}: {1}", outType.getName(), str);
 					var elemType = args.length > 0 ? args[0] : null;
-					var l2 = Json5List.ofText(str).setBeanSession(bs);
+					var l2 = Json5List.ofString(str).setBeanSession(bs);
 					var result = newCollection(outType);
 					l2.forEach(x -> result.add(elemType != null && x != null ? converter.to(x, elemType) : x));
 					return result;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java
index 3e8b3f9241..88396d36c5 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java
@@ -1374,7 +1374,7 @@ public class MarshallingSession extends ContextSession implements ConverterSessi
 
 	/**
 	 * Bridge implementation of {@link BeanSession#parseToMap(CharSequence)} that delegates to
-	 * {@link Json5Map#ofText(CharSequence)} paired with this session.
+	 * {@link Json5Map#ofString(CharSequence)} paired with this session.
 	 *
 	 * <p>
 	 * Used by {@link BeanPropertyMeta#setPropertyValue} to parse a {@link CharSequence} into a {@link Map} when
@@ -1390,8 +1390,8 @@ public class MarshallingSession extends ContextSession implements ConverterSessi
 	 * @return The parsed {@link Json5Map}.
 	 */
 	@Override /* BeanSession */
-	public final java.util.Map<?,?> parseToMap(CharSequence value) {
-		return Json5Map.ofText(value).session(this);
+	public final Map<?,?> parseToMap(CharSequence value) {
+		return Json5Map.ofString(value).session(this);
 	}
 
 	/**
@@ -1411,7 +1411,7 @@ public class MarshallingSession extends ContextSession implements ConverterSessi
 	 * @return The parsed {@link Json5List}.
 	 */
 	@Override /* BeanSession */
-	public final java.util.Collection<?> parseToList(CharSequence value) {
+	public final Collection<?> parseToList(CharSequence value) {
 		return new Json5List(value).setBeanSession(this);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriContext.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriContext.java
index 1673177574..d073fd4746 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriContext.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriContext.java
@@ -159,7 +159,7 @@ public class UriContext {
 	 * 	If input string is not a valid JSON object.
 	 */
 	public UriContext(String s) throws ParseException {
-		var m = Json5Map.ofText(s);
+		var m = Json5Map.ofString(s);
 		this.authority = nullIfEmpty(trimSlashes(m.getString(PROP_authority)));
 		this.contextRoot = nullIfEmpty(trimSlashes(m.getString(PROP_contextRoot)));
 		this.servletPath = nullIfEmpty(trimSlashes(m.getString(PROP_servletPath)));
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java
index e413047a7e..87ff0377d6 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java
@@ -64,24 +64,24 @@ import org.apache.juneau.serializer.*;
  * 	<jv>list</jv> = JsonList.<jsm>of</jsm>(<js>"foo"</js>, 123, <jk>true</jk>);  <jc>// Equivalent</jc>
  *
  * 	<jc>// Construct a list of integers from JSON</jc>
- * 	<jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[1,2,3]"</js>);
+ * 	<jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[1,2,3]"</js>);
  *
  * 	<jc>// Construct a list of generic JsonMap objects from JSON</jc>
- * 	<jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
+ * 	<jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
  *
  * 	<jc>// Construct JSON from JsonList</jc>
- * 	<jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
+ * 	<jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
  * 	String <jv>json</jv> = <jv>list</jv>.toString();  <jc>// Produces "[{foo:'bar'},{baz:'bing'}]"</jc>
  * 	<jv>json</jv> = <jv>list</jv>.toString(JsonSerializer.<jsf>DEFAULT</jsf>);  <jc>// Equivalent</jc>
  * 	<jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>list</jv>);  <jc>// Equivalent</jc>
  *
  * 	<jc>// Get one of the entries in the list as an Integer</jc>
- * 	<jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[1,2,3]"</js>);
+ * 	<jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[1,2,3]"</js>);
  * 	Integer <jv>integer</jv> = <jv>list</jv>.getInt(1);
  * 	<jv>integer</jv> = <jv>list</jv>.get(Integer.<jk>class</jk>, 1);  <jc>// Equivalent</jc>
  *
  * 	<jc>// Iterate over a list of beans using the elements() method</jc>
- * 	<jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[{name:'John Smith',age:45}]"</js>);
+ * 	<jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[{name:'John Smith',age:45}]"</js>);
  * 	<jk>for</jk> (Person <jv>person</jv> : <jv>list</jv>.elements(Person.<jk>class</jk>) {
  * 		<jc>// Do something with p</jc>
  * 	}
@@ -236,7 +236,7 @@ public class JsonList extends MarshalledList {
 	 * @return A new list or <jk>null</jk> if the string was null.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static JsonList ofText(CharSequence json) throws ParseException {
+	public static JsonList ofString(CharSequence json) throws ParseException {
 		return json == null ? null : new JsonList(json);
 	}
 
@@ -252,7 +252,7 @@ public class JsonList extends MarshalledList {
 	@SuppressWarnings({
 		"java:S1168"     // TODO: null input = null output by design. Consider empty JsonList.
 	})
-	public static JsonList ofText(Reader json) throws ParseException {
+	public static JsonList ofString(Reader json) throws ParseException {
 		return json == null ? null : new JsonList(json);
 	}
 
@@ -289,7 +289,7 @@ public class JsonList extends MarshalledList {
 	 * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static JsonList ofText(CharSequence in, Parser p) throws ParseException {
+	public static JsonList ofString(CharSequence in, Parser p) throws ParseException {
 		return in == null ? null : new JsonList(in, p);
 	}
 
@@ -308,7 +308,7 @@ public class JsonList extends MarshalledList {
 	@SuppressWarnings({
 		"java:S1172" // Parameter reserved for future parser context support
 	})
-	public static JsonList ofText(Reader in, Parser p) throws ParseException {
+	public static JsonList ofString(Reader in, Parser p) throws ParseException {
 		return in == null ? null : new JsonList(in);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java
index b69dddf231..7496223153 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java
@@ -58,7 +58,7 @@ import org.apache.juneau.serializer.*;
  * 	JsonMap <jv>map</jv> = JsonMap.<jsm>of</jsm>();
  *
  * 	<jc>// Construct a Map from JSON</jc>
- * 	<jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
+ * 	<jv>map</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
  *
  * 	<jc>// Construct a Map using the append method</jc>
  * 	<jv>map</jv> = JsonMap.<jsm>of</jsm>().a(<js>"foo"</js>,<js>"x"</js>).a(<js>"bar"</js>,123).a(<js>"baz"</js>,<jk>true</jk>);
@@ -72,19 +72,19 @@ import org.apache.juneau.serializer.*;
  * 	<jv>map</jv> = JsonMap.<jsm>of</jsm>(<jv>urlParams</jv>, UrlEncodingParser.<jsf>DEFAULT</jsf>);
  *
  * 	<jc>// Construct JSON from JsonMap</jc>
- * 	<jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{foo:'bar'},{baz:[123,true]}"</js>);
+ * 	<jv>map</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{foo:'bar'},{baz:[123,true]}"</js>);
  * 	String <jv>json</jv> = <jv>map</jv>.toString();  <jc>// Produces "{foo:'bar'},{baz:[123,true]}"</jc>
  * 	<jv>json</jv> = <jv>map</jv>.toString(JsonSerializer.<jsf>DEFAULT</jsf>);  <jc>// Equivalent</jc>
  * 	<jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>map</jv>);  <jc>// Equivalent</jc>
  *
  * 	<jc>// Get a map entry as an Integer</jc>
- * 	<jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{foo:123}"</js>);
+ * 	<jv>map</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{foo:123}"</js>);
  * 	Integer <jv>integer</jv> = <jv>map</jv>.getInt(<js>"foo"</js>);
  * 	<jv>integer</jv> = <jv>map</jv>.get(Integer.<jk>class</jk>, <js>"foo"</js>);  <jc>// Equivalent</jc>
  *
  * 	<jc>// Add an inner map</jc>
- * 	JsonMap <jv>map1</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{a:1}"</js>);
- * 	JsonMap <jv>map2</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{b:2}"</js>).setInner(<jv>map1</jv>);
+ * 	JsonMap <jv>map1</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{a:1}"</js>);
+ * 	JsonMap <jv>map2</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{b:2}"</js>).setInner(<jv>map1</jv>);
  * 	<jk>int</jk> <jv>_int</jv> = <jv>map2</jv>.getInt(<js>"a"</js>);  <jc>// a == 1 </jc>
  * </p>
  *
@@ -98,7 +98,8 @@ import org.apache.juneau.serializer.*;
 public class JsonMap extends MarshalledMap {
 
 	@SuppressWarnings({
-		"java:S2160" // equals() / hashCode() inherited from JsonMap; map equality is element-based
+		"java:S2160", // equals() / hashCode() inherited from JsonMap; map equality is element-based
+		"java:S110" // Inheritance depth is intentional in the marshalled-map hierarchy.
 	})
 	private static class UnmodifiableJsonMap extends JsonMap {
 		private static final long serialVersionUID = 1L;
@@ -133,7 +134,8 @@ public class JsonMap extends MarshalledMap {
 	 * @serial exclude
 	 */
 	@SuppressWarnings({
-		"java:S2386" // Public static final field accessed externally, cannot be protected
+		"java:S2386", // Public static final field accessed externally, cannot be protected
+		"java:S110" // Anonymous JsonMap subtype intentionally inherits deep collection hierarchy.
 	})
 	public static final JsonMap EMPTY_MAP = new JsonMap() {
 
@@ -233,7 +235,7 @@ public class JsonMap extends MarshalledMap {
 	 * @return A new map or <jk>null</jk> if the string was null.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static JsonMap ofText(CharSequence json) throws ParseException {
+	public static JsonMap ofString(CharSequence json) throws ParseException {
 		return json == null ? null : new JsonMap(json);
 	}
 
@@ -246,7 +248,7 @@ public class JsonMap extends MarshalledMap {
 	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static JsonMap ofText(Reader json) throws ParseException {
+	public static JsonMap ofString(Reader json) throws ParseException {
 		return json == null ? null : new JsonMap(json);
 	}
 
@@ -262,7 +264,7 @@ public class JsonMap extends MarshalledMap {
 	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static JsonMap ofText(CharSequence in, Parser p) throws ParseException {
+	public static JsonMap ofString(CharSequence in, Parser p) throws ParseException {
 		return in == null ? null : new JsonMap(in, p);
 	}
 
@@ -278,7 +280,7 @@ public class JsonMap extends MarshalledMap {
 	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static JsonMap ofText(Reader in, Parser p) throws ParseException {
+	public static JsonMap ofString(Reader in, Parser p) throws ParseException {
 		return in == null ? null : new JsonMap(in, p);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java
index f38d86f5b0..2d28ea0364 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java
@@ -197,7 +197,7 @@ public class MarshalledList extends LinkedList<Object> {
 	 * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static MarshalledList ofText(CharSequence in, Parser p) throws ParseException {
+	public static MarshalledList ofString(CharSequence in, Parser p) throws ParseException {
 		return in == null ? null : new MarshalledList(in, p);
 	}
 
@@ -212,7 +212,7 @@ public class MarshalledList extends LinkedList<Object> {
 	 * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static MarshalledList ofText(java.io.Reader in, Parser p) throws ParseException {
+	public static MarshalledList ofString(java.io.Reader in, Parser p) throws ParseException {
 		return in == null ? null : new MarshalledList(in, p);
 	}
 
@@ -246,8 +246,7 @@ public class MarshalledList extends LinkedList<Object> {
 	 * @throws ParseException Malformed input encountered.
 	 */
 	public MarshalledList(CharSequence in, Parser p) throws ParseException {
-		this(p == null ? MarshallingContext.DEFAULT_SESSION : p.getMarshallingContext().getSession());
-		assertArg(p != null, "Parser must not be null.");
+		this(assertArgNotNull("p", p).getMarshallingContext().getSession());
 		if (nn(in))
 			p.parseIntoCollection(in, this, bs().object());
 	}
@@ -283,8 +282,7 @@ public class MarshalledList extends LinkedList<Object> {
 	 * @throws ParseException Malformed input encountered.
 	 */
 	public MarshalledList(java.io.Reader in, Parser p) throws ParseException {
-		this(p == null ? MarshallingContext.DEFAULT_SESSION : p.getMarshallingContext().getSession());
-		assertArg(p != null, "Parser must not be null.");
+		this(assertArgNotNull("p", p).getMarshallingContext().getSession());
 		p.parseIntoCollection(in, this, bs().object());
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java
index 7c44257f42..cea0087ea5 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java
@@ -190,7 +190,7 @@ public class MarshalledMap extends LinkedHashMap<String,Object> {
 	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static MarshalledMap ofText(CharSequence in, Parser p) throws ParseException {
+	public static MarshalledMap ofString(CharSequence in, Parser p) throws ParseException {
 		return in == null ? null : new MarshalledMap(in, p);
 	}
 
@@ -205,7 +205,7 @@ public class MarshalledMap extends LinkedHashMap<String,Object> {
 	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static MarshalledMap ofText(java.io.Reader in, Parser p) throws ParseException {
+	public static MarshalledMap ofString(java.io.Reader in, Parser p) throws ParseException {
 		return in == null ? null : new MarshalledMap(in, p);
 	}
 
@@ -252,8 +252,7 @@ public class MarshalledMap extends LinkedHashMap<String,Object> {
 	 * @throws ParseException Malformed input encountered.
 	 */
 	public MarshalledMap(CharSequence in, Parser p) throws ParseException {
-		this(p == null ? MarshallingContext.DEFAULT_SESSION : p.getMarshallingContext().getSession());
-		assertArg(p != null, "Parser must not be null.");
+		this(assertArgNotNull("p", p).getMarshallingContext().getSession());
 		if (ne(in))
 			p.parseIntoMap(in, this, bs().string(), bs().object());
 	}
@@ -294,8 +293,7 @@ public class MarshalledMap extends LinkedHashMap<String,Object> {
 	 * @throws ParseException Malformed input encountered.
 	 */
 	public MarshalledMap(java.io.Reader in, Parser p) throws ParseException {
-		this(p == null ? MarshallingContext.DEFAULT_SESSION : p.getMarshallingContext().getSession());
-		assertArg(p != null, "Parser must not be null.");
+		this(assertArgNotNull("p", p).getMarshallingContext().getSession());
 		p.parseIntoMap(in, this, bs().string(), bs().object());
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/package-info.java
index 8e3354f86c..5076072034 100755
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/package-info.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/package-info.java
@@ -28,13 +28,13 @@
  * 		&mdash; strict-JSON-flavored subclasses. Their {@code toString()} renders as strict
  * 		<a href="https://www.rfc-editor.org/rfc/rfc8259" target="_blank">RFC&nbsp;8259</a> JSON via
  * 		{@link org.apache.juneau.marshaller.Json#of(Object)}, and their {@code (CharSequence)} /
- * 		{@code (Reader)} constructors and {@code ofText(...)} factories default to
+ * 		{@code (Reader)} constructors and {@code ofString(...)} factories default to
  * 		{@link org.apache.juneau.json.JsonParser#DEFAULT}.
  * 	<li>{@link org.apache.juneau.json5.Json5Map} / {@link org.apache.juneau.json5.Json5List}
  * 		(in {@link org.apache.juneau.json5}) &mdash; JSON5-flavored siblings of
  * 		{@code JsonMap} / {@code JsonList}. Their {@code toString()} renders as JSON5 via
  * 		{@link org.apache.juneau.marshaller.Json5#of(Object)}, and their {@code (CharSequence)} /
- * 		{@code (Reader)} constructors and {@code ofText(...)} factories default to
+ * 		{@code (Reader)} constructors and {@code ofString(...)} factories default to
  * 		{@link org.apache.juneau.json5.Json5Parser#DEFAULT}. These are the drop-in replacement
  * 		for callers that previously relied on {@code JsonMap.toString()} producing JSON5 or on
  * 		{@code new JsonMap("{unquoted:'json5'}")} parsing JSON5.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java
index 8b6c4422d3..d886af6cbe 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java
@@ -49,7 +49,7 @@ import org.apache.juneau.serializer.*;
  * 	Json5List <jv>list</jv> = Json5List.<jsm>of</jsm>();
  *
  * 	<jc>// Construct a list of generic Json5Map objects from JSON5</jc>
- * 	<jv>list</jv> = Json5List.<jsm>ofText</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
+	 * 	<jv>list</jv> = Json5List.<jsm>ofString</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
  *
  * 	<jc>// JSON5 round-trip via toString()</jc>
  * 	String <jv>s</jv> = <jv>list</jv>.toString();  <jc>// "[{foo:'bar'},{baz:'bing'}]"</jc>
@@ -200,7 +200,7 @@ public class Json5List extends MarshalledList {
 	 * @return A new list or <jk>null</jk> if the string was null.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static Json5List ofText(CharSequence json5) throws ParseException {
+	public static Json5List ofString(CharSequence json5) throws ParseException {
 		return json5 == null ? null : new Json5List(json5);
 	}
 
@@ -214,7 +214,7 @@ public class Json5List extends MarshalledList {
 	@SuppressWarnings({
 		"java:S1168"     // TODO: null input = null output by design. Consider empty Json5List.
 	})
-	public static Json5List ofText(Reader json5) throws ParseException {
+	public static Json5List ofString(Reader json5) throws ParseException {
 		return json5 == null ? null : new Json5List(json5);
 	}
 
@@ -247,7 +247,7 @@ public class Json5List extends MarshalledList {
 	 * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static Json5List ofText(CharSequence in, Parser p) throws ParseException {
+	public static Json5List ofString(CharSequence in, Parser p) throws ParseException {
 		return in == null ? null : new Json5List(in, p);
 	}
 
@@ -262,7 +262,7 @@ public class Json5List extends MarshalledList {
 	@SuppressWarnings({
 		"java:S1172" // Parameter reserved for future parser context support
 	})
-	public static Json5List ofText(Reader in, Parser p) throws ParseException {
+	public static Json5List ofString(Reader in, Parser p) throws ParseException {
 		return in == null ? null : new Json5List(in);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java
index df777e52f3..3ba3320268 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java
@@ -49,7 +49,7 @@ import org.apache.juneau.serializer.*;
  * 	Json5Map <jv>map</jv> = Json5Map.<jsm>of</jsm>();
  *
  * 	<jc>// Construct a Map from JSON5</jc>
- * 	<jv>map</jv> = Json5Map.<jsm>ofText</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
+ * 	<jv>map</jv> = Json5Map.<jsm>ofString</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
  *
  * 	<jc>// JSON5 round-trip via toString()</jc>
  * 	String <jv>s</jv> = <jv>map</jv>.toString();  <jc>// "{a:'A',b:{c:'C',d:123}}"</jc>
@@ -201,7 +201,7 @@ public class Json5Map extends MarshalledMap {
 	 * @return A new map or <jk>null</jk> if the string was null.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static Json5Map ofText(CharSequence json5) throws ParseException {
+	public static Json5Map ofString(CharSequence json5) throws ParseException {
 		return json5 == null ? null : new Json5Map(json5);
 	}
 
@@ -213,7 +213,7 @@ public class Json5Map extends MarshalledMap {
 	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static Json5Map ofText(Reader json5) throws ParseException {
+	public static Json5Map ofString(Reader json5) throws ParseException {
 		return json5 == null ? null : new Json5Map(json5);
 	}
 
@@ -229,7 +229,7 @@ public class Json5Map extends MarshalledMap {
 	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static Json5Map ofText(CharSequence in, Parser p) throws ParseException {
+	public static Json5Map ofString(CharSequence in, Parser p) throws ParseException {
 		return in == null ? null : new Json5Map(in, p);
 	}
 
@@ -244,7 +244,7 @@ public class Json5Map extends MarshalledMap {
 	 * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
 	 * @throws ParseException Malformed input encountered.
 	 */
-	public static Json5Map ofText(Reader in, Parser p) throws ParseException {
+	public static Json5Map ofString(Reader in, Parser p) throws ParseException {
 		return in == null ? null : new Json5Map(in, p);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java
index fed5269fe1..9fc1bf6342 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java
@@ -78,7 +78,7 @@ public class SchemaUtils {
 				return JsonMap.of("ignore", true);
 			if (! isProbablyJsonObject(s, true))
 				s = "{" + s + "}";
-			return new JsonMap(Json5Map.ofText(s));
+			return new JsonMap(Json5Map.ofString(s));
 		}
 		if (o instanceof JsonMap o2)
 			return o2;
@@ -105,7 +105,7 @@ public class SchemaUtils {
 			return null;
 		if (! isProbablyJsonObject(s, true))
 			s = "{" + s + "}";
-		return new JsonMap(Json5Map.ofText(s));
+		return new JsonMap(Json5Map.ofString(s));
 	}
 
 	/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Sse.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Sse.java
new file mode 100644
index 0000000000..9123e59494
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Sse.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshaller;
+
+import java.io.*;
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.ParseException;
+import org.apache.juneau.serializer.SerializeException;
+import org.apache.juneau.sse.*;
+
+/**
+ * Pairs {@link SseSerializer} and {@link SseParser} into a single class with convenience
+ * read/write methods for the <c>text/event-stream</c> wire format.
+ *
+ * <h5 class='figure'>Examples:</h5>
+ * <p class='bjava'>
+ * 	<jc>// Serialize a list of events using the DEFAULT instance</jc>
+ * 	List&lt;SseEvent&gt; <jv>events</jv> = List.<jsm>of</jsm>(
+ * 		<jk>new</jk> SseEvent(<js>"progress"</js>, <js>"step 1"</js>),
+ * 		<jk>new</jk> SseEvent(<js>"progress"</js>, <js>"step 2"</js>)
+ * 	);
+ * 	String <jv>wire</jv> = Sse.<jsm>of</jsm>(<jv>events</jv>);
+ *
+ * 	<jc>// Parse a wire string back into a list of events</jc>
+ * 	List&lt;SseEvent&gt; <jv>parsed</jv> = Sse.<jsm>to</jsm>(<jv>wire</jv>, List.<jk>class</jk>, SseEvent.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Marshallers">Marshallers</a>
+ * </ul>
+ */
+public class Sse extends CharMarshaller {
+
+	/**
+	 * Default reusable instance.
+	 */
+	public static final Sse DEFAULT = new Sse();
+
+	/**
+	 * Serializes a Java object to an SSE wire string.
+	 *
+	 * @param object The object to serialize.
+	 * @return The serialized output.
+	 * @throws SerializeException If a problem occurred trying to convert the output.
+	 */
+	public static String of(Object object) throws SerializeException {
+		return DEFAULT.write(object);
+	}
+
+	/**
+	 * Serializes a Java object to an SSE output.
+	 *
+	 * @param object The object to serialize.
+	 * @param output The output object.
+	 * @return The output object.
+	 * @throws SerializeException If a problem occurred trying to convert the output.
+	 * @throws IOException Thrown by underlying stream.
+	 */
+	public static Object of(Object object, Object output) throws SerializeException, IOException {
+		DEFAULT.write(object, output);
+		return output;
+	}
+
+	/**
+	 * Parses an SSE input object to the specified Java type.
+	 *
+	 * @param <T> The class type of the object being created.
+	 * @param input The input.
+	 * @param type The object type to create.
+	 * @return The parsed object.
+	 * @throws ParseException Malformed input encountered.
+	 * @throws IOException Thrown by underlying stream.
+	 */
+	public static <T> T to(Object input, Class<T> type) throws ParseException, IOException {
+		return DEFAULT.read(input, type);
+	}
+
+	/**
+	 * Parses an SSE input object to the specified Java type.
+	 *
+	 * @param <T> The class type of the object to create.
+	 * @param input The input.
+	 * @param type The object type to create.
+	 * @param args The type arguments of the class if it's a collection or map.
+	 * @return The parsed object.
+	 * @throws ParseException Malformed input encountered.
+	 * @throws IOException Thrown by underlying stream.
+	 * @see MarshallingSession#getClassMeta(Type,Type...) for argument syntax for maps and collections.
+	 */
+	public static <T> T to(Object input, Type type, Type...args) throws ParseException, IOException {
+		return DEFAULT.read(input, type, args);
+	}
+
+	/**
+	 * Parses an SSE input string to the specified type.
+	 *
+	 * @param <T> The class type of the object being created.
+	 * @param input The input.
+	 * @param type The object type to create.
+	 * @return The parsed object.
+	 * @throws ParseException Malformed input encountered.
+	 */
+	public static <T> T to(String input, Class<T> type) throws ParseException {
+		return DEFAULT.read(input, type);
+	}
+
+	/**
+	 * Parses an SSE input string to the specified Java type.
+	 *
+	 * @param <T> The class type of the object to create.
+	 * @param input The input.
+	 * @param type The object type to create.
+	 * @param args The type arguments of the class if it's a collection or map.
+	 * @return The parsed object.
+	 * @throws ParseException Malformed input encountered.
+	 * @see MarshallingSession#getClassMeta(Type,Type...) for argument syntax for maps and collections.
+	 */
+	public static <T> T to(String input, Type type, Type...args) throws ParseException {
+		return DEFAULT.read(input, type, args);
+	}
+
+	/**
+	 * Constructor.
+	 */
+	public Sse() {
+		this(SseSerializer.DEFAULT, SseParser.DEFAULT);
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param s The serializer.
+	 * @param p The parser.
+	 */
+	public Sse(SseSerializer s, SseParser p) {
+		super(s, p);
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
index d010f1a721..a27bdc8c1d 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
@@ -35,7 +35,12 @@ import org.apache.juneau.commons.bean.BeanPropertyMeta;
  *
  * <h5 class='topic'>Media types</h5>
  *
- * Handles <c>Content-Type</c> types:  <bc>octal/msgpack</bc>
+ * Handles <c>Content-Type</c> types:  <bc>application/msgpack, octal/msgpack</bc>
+ *
+ * <p>
+ * The legacy media type <bc>octal/msgpack</bc> is retained as a backward-compatibility alias so
+ * that existing wire values still decode. New consumers should prefer the RFC-standard
+ * <bc>application/msgpack</bc> value.
  *
  * <h5 class='topic'>Description</h5>
  * <p>
@@ -93,7 +98,7 @@ public class MsgPackParser extends InputStreamParser implements MsgPackMetaProvi
 		 * Constructor, default settings.
 		 */
 		protected Builder() {
-			consumes("octal/msgpack");
+			consumes("application/msgpack,octal/msgpack");
 		}
 
 		/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
index 801fd22061..14f2c99e8f 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
@@ -38,9 +38,14 @@ import org.apache.juneau.commons.bean.BeanPropertyMeta;
  *
  * <h5 class='section'>Media types:</h5>
  *
- * Handles <c>Accept</c> types:  <bc>octal/msgpack</bc>
+ * Handles <c>Accept</c> types:  <bc>application/msgpack, octal/msgpack</bc>
  * <p>
- * Produces <c>Content-Type</c> types: <bc>octal/msgpack</bc>
+ * Produces <c>Content-Type</c> types: <bc>application/msgpack</bc>
+ *
+ * <p>
+ * The legacy media type <bc>octal/msgpack</bc> is retained in the <c>Accept</c> list as a
+ * backward-compatibility alias. The <c>Content-Type</c> emitted by the serializer is always the
+ * RFC-standard <bc>application/msgpack</bc> value.
  *
  * <h5 class='topic'>Description</h5>
  * <p>
@@ -107,7 +112,8 @@ public class MsgPackSerializer extends OutputStreamSerializer implements MsgPack
 		 * Constructor, default settings.
 		 */
 		protected Builder() {
-			produces("octal/msgpack");
+			produces("application/msgpack");
+			accept("application/msgpack,octal/msgpack");
 			addBeanTypesMsgPack = env("MsgPackSerializer.addBeanTypesMsgPack", false);
 		}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEvent.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEvent.java
new file mode 100644
index 0000000000..2cb01c705a
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEvent.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.sse;
+
+import java.util.*;
+
+import org.apache.juneau.commons.bean.*;
+
+/**
+ * Represents a single <a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">Server-Sent Event</a>
+ * (SSE) frame on the wire.
+ *
+ * <p>
+ * Each event maps to a sequence of <c>field: value</c> lines terminated by a blank line in the
+ * <c>text/event-stream</c> wire format:
+ * <p class='bcode'>
+ * event: progress
+ * data: {"step":4,"total":10}
+ * id: 42
+ * retry: 5000
+ *
+ * </p>
+ *
+ * <p>
+ * The payload field {@link #getData() data} is a <b>pre-serialized</b> {@link String} — the SSE
+ * serializer does not compose with a delegate marshaller. Callers that need a typed payload should
+ * serialize it themselves (e.g. via {@link org.apache.juneau.marshaller.Json#of(Object) Json.of(x)})
+ * and assign the resulting string to {@link #setData(String) setData(...)}. The serializer will
+ * split that string on {@code \n} into multiple <c>data:</c> lines per spec.
+ *
+ * <h5 class='figure'>Example: build and serialize an event:</h5>
+ * <p class='bjava'>
+ * 	SseEvent <jv>event</jv> = <jk>new</jk> SseEvent()
+ * 		.setEvent(<js>"progress"</js>)
+ * 		.setData(Json.<jsm>of</jsm>(Map.of(<js>"step"</js>, 4, <js>"total"</js>, 10)))
+ * 		.setId(<js>"42"</js>)
+ * 		.setRetry(5000L);
+ *
+ * 	String <jv>wire</jv> = Sse.<jsm>of</jsm>(<jv>event</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='note'>This class is not thread safe.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * 	<li class='link'><a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG HTML §9.2 — Server-sent events</a>
+ * </ul>
+ */
+@BeanType(properties = "event,data,id,retry")
+public class SseEvent {
+
+	/** Default event-type name dispatched by the WHATWG <c>EventSource</c> when no <c>event:</c> field is present. */
+	public static final String DEFAULT_EVENT = "message";
+
+	private String event;
+	private String data;
+	private String id;
+	private Long retry;
+
+	/**
+	 * Constructor.
+	 */
+	public SseEvent() {}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param event The event-type name. Can be <jk>null</jk> (dispatches as {@value #DEFAULT_EVENT}).
+	 * @param data The payload string. Can be <jk>null</jk> (no <c>data:</c> lines emitted).
+	 */
+	public SseEvent(String event, String data) {
+		this.event = event;
+		this.data = data;
+	}
+
+	/**
+	 * Returns the event-type name.
+	 *
+	 * @return The event-type name, or <jk>null</jk> if not set.
+	 */
+	public String getEvent() {
+		return event;
+	}
+
+	/**
+	 * Sets the event-type name.
+	 *
+	 * @param value The event-type name. Can be <jk>null</jk> to unset.
+	 * @return This object.
+	 */
+	public SseEvent setEvent(String value) {
+		event = value;
+		return this;
+	}
+
+	/**
+	 * Returns the data payload (pre-serialized string).
+	 *
+	 * @return The data payload, or <jk>null</jk> if not set.
+	 */
+	public String getData() {
+		return data;
+	}
+
+	/**
+	 * Sets the data payload.
+	 *
+	 * @param value The pre-serialized payload string. Can be <jk>null</jk> to unset.
+	 * @return This object.
+	 */
+	public SseEvent setData(String value) {
+		data = value;
+		return this;
+	}
+
+	/**
+	 * Returns the last-event-id value.
+	 *
+	 * @return The id, or <jk>null</jk> if not set.
+	 */
+	public String getId() {
+		return id;
+	}
+
+	/**
+	 * Sets the last-event-id value.
+	 *
+	 * @param value The id. Can be <jk>null</jk> to unset.
+	 * @return This object.
+	 */
+	public SseEvent setId(String value) {
+		id = value;
+		return this;
+	}
+
+	/**
+	 * Returns the reconnect-delay hint in milliseconds.
+	 *
+	 * @return The retry value, or <jk>null</jk> if not set.
+	 */
+	public Long getRetry() {
+		return retry;
+	}
+
+	/**
+	 * Sets the reconnect-delay hint in milliseconds.
+	 *
+	 * @param value The retry value. Can be <jk>null</jk> to unset.
+	 * @return This object.
+	 */
+	public SseEvent setRetry(Long value) {
+		retry = value;
+		return this;
+	}
+
+	@Override /* Overridden from Object */
+	public boolean equals(Object o) {
+		if (this == o)
+			return true;
+		if (!(o instanceof SseEvent o2))
+			return false;
+		return Objects.equals(event, o2.event)
+			&& Objects.equals(data, o2.data)
+			&& Objects.equals(id, o2.id)
+			&& Objects.equals(retry, o2.retry);
+	}
+
+	@Override /* Overridden from Object */
+	public int hashCode() {
+		return Objects.hash(event, data, id, retry);
+	}
+
+	@Override /* Overridden from Object */
+	public String toString() {
+		return "SseEvent[event=" + event + ",data=" + data + ",id=" + id + ",retry=" + retry + "]";
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEventReader.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEventReader.java
new file mode 100644
index 0000000000..8e606eff49
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEventReader.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Line-driven {@link Iterator} over the events in a <c>text/event-stream</c>.
+ *
+ * <p>
+ * Wraps a {@link Reader} the caller owns and dispatches one {@link SseEvent} at a time using the
+ * WHATWG parse state machine (<a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">HTML §9.2</a>).
+ * Designed for long-lived SSE streams that outlive any reasonable per-call buffer.
+ *
+ * <h5 class='section'>Why a separate class:</h5>
+ * <p>
+ * No other Juneau marshaller ships a separate iterator-style helper. SSE warrants the deviation
+ * because SSE responses are open until the client disconnects or the server pushes its last event;
+ * forcing callers through the eager {@code List<SseEvent>} path would defeat the entire point of
+ * using SSE.
+ *
+ * <h5 class='figure'>Example:</h5>
+ * <p class='bjava'>
+ * 	<jk>try</jk> (SseEventReader <jv>r</jv> = <jk>new</jk> SseEventReader(<jv>responseReader</jv>)) {
+ * 		<jk>while</jk> (<jv>r</jv>.hasNext()) {
+ * 			SseEvent <jv>e</jv> = <jv>r</jv>.next();
+ * 			<jc>// Dispatch...</jc>
+ * 		}
+ * 	}
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='warn'>This class is not thread safe.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+	"java:S6541" // Brain method acceptable for readLine / nextEvent — they encode the SSE lexical state machine
+})
+public class SseEventReader implements Iterator<SseEvent>, Closeable {
+
+	@SuppressWarnings("java:S115") // Match AssertionUtils arg-name style used throughout Juneau.
+	private static final String ARG_in = "in";
+
+	/** UTF-8 BOM character that must be stripped exactly once from the start of an SSE stream. */
+	private static final int BOM = 0xFEFF;
+
+	private final Reader in;
+
+	private boolean bomChecked;
+	private boolean eof;
+	private SseEvent peeked;
+
+	private final StringBuilder dataBuf = new StringBuilder();
+	private String eventName;
+	private String lastEventId;
+	private Long retryValue;
+	private boolean haveAnyField;
+
+	private int pendingChar = -1;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param in The reader to consume events from. Must not be <jk>null</jk>. Caller retains ownership;
+	 * 	{@link #close()} closes this reader.
+	 */
+	@SuppressWarnings({ "java:S2095", "resource" }) // Reader ownership is transferred to this Closeable and released by close().
+	public SseEventReader(Reader in) {
+		this.in = assertArgNotNull(ARG_in, in);
+	}
+
+	@Override /* Overridden from Iterator */
+	public boolean hasNext() {
+		if (peeked != null)
+			return true;
+		if (eof)
+			return false;
+		try {
+			peeked = nextEvent();
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+		return peeked != null;
+	}
+
+	@Override /* Overridden from Iterator */
+	public SseEvent next() {
+		if (!hasNext())
+			throw new NoSuchElementException();
+		var out = peeked;
+		peeked = null;
+		return out;
+	}
+
+	@Override /* Overridden from Closeable */
+	public void close() throws IOException {
+		in.close();
+	}
+
+	/**
+	 * Drains the remaining events into a list, then closes this reader.
+	 *
+	 * @return The list of events in order.
+	 * @throws IOException If an I/O error occurs while reading.
+	 */
+	public List<SseEvent> toList() throws IOException {
+		var out = new ArrayList<SseEvent>();
+		try {
+			while (hasNext())
+				out.add(next());
+		} catch (UncheckedIOException e) {
+			throw e.getCause();
+		} finally {
+			close();
+		}
+		return out;
+	}
+
+	private SseEvent nextEvent() throws IOException {
+		while (true) {
+			var line = readLine();
+			if (line == null) {
+				eof = true;
+				if (haveAnyField)
+					return dispatch();
+				return null;
+			}
+			if (line.isEmpty()) {
+				if (haveAnyField)
+					return dispatch();
+				continue;
+			}
+			processField(line);
+		}
+	}
+
+	private SseEvent dispatch() {
+		var out = new SseEvent();
+		out.setEvent(eventName);
+		if (!dataBuf.isEmpty()) {
+			if (dataBuf.charAt(dataBuf.length() - 1) == '\n')
+				dataBuf.deleteCharAt(dataBuf.length() - 1);
+			out.setData(dataBuf.toString());
+		}
+		out.setId(lastEventId);
+		out.setRetry(retryValue);
+		resetEventBuffers();
+		return out;
+	}
+
+	private void resetEventBuffers() {
+		dataBuf.setLength(0);
+		eventName = null;
+		lastEventId = null;
+		retryValue = null;
+		haveAnyField = false;
+	}
+
+	private void processField(String line) {
+		if (line.charAt(0) == ':') {
+			haveAnyField = true;
+			return;
+		}
+		String fieldName;
+		String fieldValue;
+		var colon = line.indexOf(':');
+		if (colon < 0) {
+			fieldName = line;
+			fieldValue = "";
+		} else {
+			fieldName = line.substring(0, colon);
+			fieldValue = line.substring(colon + 1);
+			if (!fieldValue.isEmpty() && fieldValue.charAt(0) == ' ')
+				fieldValue = fieldValue.substring(1);
+		}
+		haveAnyField = true;
+		applyField(fieldName, fieldValue);
+	}
+
+	private void applyField(String fieldName, String fieldValue) {
+		switch (fieldName) {
+			case "event":
+				eventName = fieldValue;
+				break;
+			case "data":
+				dataBuf.append(fieldValue).append('\n');
+				break;
+			case "id":
+				if (fieldValue.indexOf('\u0000') < 0)
+					lastEventId = fieldValue;
+				break;
+			case "retry":
+				retryValue = parseRetry(fieldValue);
+				break;
+			default:
+				break;
+		}
+	}
+
+	private static Long parseRetry(String s) {
+		if (s.isEmpty())
+			return null;
+		for (var i = 0; i < s.length(); i++) {
+			var c = s.charAt(i);
+			if (c < '0' || c > '9')
+				return null;
+		}
+		try {
+			return Long.valueOf(s);
+		} catch (@SuppressWarnings("unused") NumberFormatException e) {
+			return null;
+		}
+	}
+
+	@SuppressWarnings("java:S3776") // SSE line parsing is a deliberate lexical state machine.
+	private String readLine() throws IOException {
+		var sb = new StringBuilder();
+		int c;
+		var sawAny = false;
+		while (true) {
+			if (pendingChar != -1) {
+				c = pendingChar;
+				pendingChar = -1;
+			} else {
+				c = in.read();
+			}
+			if (c == -1) {
+				if (!sawAny)
+					return null;
+				return sb.toString();
+			}
+			if (!bomChecked) {
+				bomChecked = true;
+				if (c == BOM)
+					continue;
+			}
+			sawAny = true;
+			if (c == '\n')
+				return sb.toString();
+			if (c == '\r') {
+				var next = in.read();
+				if (next != -1 && next != '\n')
+					pendingChar = next;
+				return sb.toString();
+			}
+			sb.append((char) c);
+		}
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParser.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParser.java
new file mode 100644
index 0000000000..7f3c28d858
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParser.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Parses <c>text/event-stream</c> input (WHATWG Server-Sent Events) into Java objects.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Content-Type</c> types: <bc>text/event-stream</bc>.
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * Eager parse path — drains the stream into a {@link java.util.List List&lt;SseEvent&gt;}.
+ * For true line-driven streaming over long-lived connections, use {@link SseEventReader} directly.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='note'>This class is thread safe and reusable.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+	"java:S110" // Inheritance depth acceptable for parser hierarchy
+})
+public class SseParser extends ReaderParser {
+
+	/** Default parser, all default settings. */
+	public static final SseParser DEFAULT = new SseParser(create());
+
+	/**
+	 * Creates a new builder for this object.
+	 *
+	 * @return A new builder.
+	 */
+	public static Builder create() {
+		return new Builder();
+	}
+
+	/**
+	 * Builder class.
+	 */
+	public static class Builder extends ReaderParser.Builder {
+
+		private static final Cache<HashKey,SseParser> CACHE = Cache.of(HashKey.class, SseParser.class).build();
+
+		/**
+		 * Constructor, default settings.
+		 */
+		protected Builder() {
+			consumes(SseSerializer.MEDIA_TYPE);
+			type(SseParser.class);
+		}
+
+		/**
+		 * Copy constructor.
+		 *
+		 * @param copyFrom The builder to copy from. Must not be <jk>null</jk>.
+		 */
+		protected Builder(Builder copyFrom) {
+			super(assertArgNotNull("copyFrom", copyFrom));
+		}
+
+		/**
+		 * Copy constructor.
+		 *
+		 * @param copyFrom The parser to copy from. Must not be <jk>null</jk>.
+		 */
+		protected Builder(SseParser copyFrom) {
+			super(assertArgNotNull("copyFrom", copyFrom));
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public HashKey hashKey() {
+			return HashKey.of(super.hashKey());
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public SseParser build() {
+			return cache(CACHE).build(SseParser.class);
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public Builder copy() {
+			return new Builder(this);
+		}
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param builder The builder for this object.
+	 */
+	public SseParser(Builder builder) {
+		super(builder);
+	}
+
+	@Override /* Overridden from Context */
+	public Builder copy() {
+		return new Builder(this);
+	}
+
+	@Override /* Overridden from Context */
+	public SseParserSession.Builder createSession() {
+		return SseParserSession.create(this);
+	}
+
+	@Override /* Overridden from Context */
+	public SseParserSession getSession() {
+		return createSession().build();
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParserSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParserSession.java
new file mode 100644
index 0000000000..be0e30cc1e
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParserSession.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link SseParser}.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='warn'>This class is not thread safe and is typically discarded after one use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+	"java:S110",   // Inheritance depth acceptable for session hierarchy
+	"unchecked"    // Type erasure: SseEvent/List<SseEvent> returned via Object cast
+})
+public class SseParserSession extends ReaderParserSession {
+
+	@SuppressWarnings("java:S115") // Match AssertionUtils arg-name style used throughout Juneau.
+	private static final String ARG_ctx = "ctx";
+
+	/**
+	 * Builder class.
+	 */
+	public static class Builder extends ReaderParserSession.Builder {
+
+		/**
+		 * Constructor.
+		 *
+		 * @param ctx The context creating this session. Cannot be <jk>null</jk>.
+		 */
+		protected Builder(SseParser ctx) {
+			super(assertArgNotNull(ARG_ctx, ctx));
+		}
+
+		@Override
+		public SseParserSession build() {
+			return new SseParserSession(this);
+		}
+	}
+
+	/**
+	 * Creates a new builder for this object.
+	 *
+	 * @param ctx The context creating this session. Cannot be <jk>null</jk>.
+	 * @return A new builder.
+	 */
+	public static Builder create(SseParser ctx) {
+		return new Builder(assertArgNotNull(ARG_ctx, ctx));
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param builder The builder for this object.
+	 */
+	protected SseParserSession(Builder builder) {
+		super(builder);
+	}
+
+	@Override /* Overridden from ParserSession */
+	@SuppressWarnings({ "java:S2095", "resource" }) // Reader is owned by ParserPipe and closed via SseEventReader in try-with-resources.
+	protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException {
+		var r = pipe.getReader();
+		if (r == null)
+			return null;
+		List<SseEvent> events;
+		try (var r2 = new SseEventReader(r)) {
+			events = r2.toList();
+		}
+		if (type == null || type.isObject() || type.isCollectionOrArray()) {
+			if (type != null && type.isArray())
+				return (T) events.toArray(new SseEvent[0]);
+			return (T) events;
+		}
+		if (SseEvent.class.equals(type.inner())) {
+			return events.isEmpty() ? null : (T) events.get(0);
+		}
+		if (List.class.isAssignableFrom(type.inner()))
+			return (T) events;
+		throw new ParseException(this, "SseParser cannot produce target type ''{0}''.", type);
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializer.java
new file mode 100644
index 0000000000..44c742dedd
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializer.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Serializes {@link SseEvent} objects to the <c>text/event-stream</c> wire format defined by the
+ * <a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG Server-Sent Events spec</a>.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Produces <c>Content-Type</c> types: <bc>text/event-stream</bc>.
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * Accepts any of the following as input and emits one wire event per element followed by a blank
+ * line (the spec's "dispatch" boundary):
+ * <ul class='spaced-list'>
+ * 	<li>A single {@link SseEvent}.
+ * 	<li>{@link Iterable Iterable&lt;SseEvent&gt;}.
+ * 	<li>An array {@code SseEvent[]}.
+ * 	<li>{@link java.util.stream.Stream Stream&lt;SseEvent&gt;}.
+ * </ul>
+ *
+ * <p>
+ * After each event is written, the session calls {@link Writer#flush() Writer.flush()} so that downstream
+ * REST infrastructure flushes the underlying TCP socket and the event is delivered immediately to the
+ * client.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='note'>This class is thread safe and reusable.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+	"java:S110" // Inheritance depth acceptable for serializer hierarchy
+})
+public class SseSerializer extends WriterSerializer {
+
+	@SuppressWarnings("java:S115") // Match AssertionUtils arg-name style used throughout Juneau.
+	private static final String ARG_w = "w";
+
+	/**
+	 * Wire-format media type produced by this serializer: <c>text/event-stream</c>.
+	 */
+	public static final String MEDIA_TYPE = "text/event-stream";
+
+	/** Default serializer, all default settings. */
+	public static final SseSerializer DEFAULT = new SseSerializer(create());
+
+	/**
+	 * Creates a new builder for this object.
+	 *
+	 * @return A new builder.
+	 */
+	public static Builder create() {
+		return new Builder();
+	}
+
+	/**
+	 * Builder class.
+	 */
+	public static class Builder extends WriterSerializer.Builder {
+
+		private static final Cache<HashKey,SseSerializer> CACHE = Cache.of(HashKey.class, SseSerializer.class).build();
+
+		/**
+		 * Constructor, default settings.
+		 */
+		protected Builder() {
+			produces(MEDIA_TYPE);
+			type(SseSerializer.class);
+		}
+
+		/**
+		 * Copy constructor.
+		 *
+		 * @param copyFrom The builder to copy from. Must not be <jk>null</jk>.
+		 */
+		protected Builder(Builder copyFrom) {
+			super(assertArgNotNull("copyFrom", copyFrom));
+		}
+
+		/**
+		 * Copy constructor.
+		 *
+		 * @param copyFrom The serializer to copy from. Must not be <jk>null</jk>.
+		 */
+		protected Builder(SseSerializer copyFrom) {
+			super(assertArgNotNull("copyFrom", copyFrom));
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public HashKey hashKey() {
+			return HashKey.of(super.hashKey());
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public SseSerializer build() {
+			return cache(CACHE).build(SseSerializer.class);
+		}
+
+		@Override /* Overridden from Context.Builder */
+		public Builder copy() {
+			return new Builder(this);
+		}
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param builder The builder for this object.
+	 */
+	public SseSerializer(Builder builder) {
+		super(builder);
+	}
+
+	@Override /* Overridden from Context */
+	public Builder copy() {
+		return new Builder(this);
+	}
+
+	@Override /* Overridden from Context */
+	public SseSerializerSession.Builder createSession() {
+		return SseSerializerSession.create(this);
+	}
+
+	@Override /* Overridden from Context */
+	public SseSerializerSession getSession() {
+		return createSession().build();
+	}
+
+	/**
+	 * Writes a comment line (heartbeat) to the supplied writer in the SSE wire format and flushes it.
+	 *
+	 * @param w The writer. Must not be <jk>null</jk>.
+	 * @param comment The comment text. {@code null} is treated as an empty string.
+	 * @throws IOException If a problem occurred writing to the underlying stream.
+	 */
+	@SuppressWarnings({ "java:S2095", "resource" }) // Writer ownership is the caller's; we only write/flush.
+	public static void writeComment(Writer w, String comment) throws IOException {
+		assertArgNotNull(ARG_w, w);
+		var c = comment == null ? "" : comment;
+		for (var line : c.split("\n", -1))
+			w.write(": " + line + "\n");
+		w.write("\n");
+		w.flush();
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializerSession.java
new file mode 100644
index 0000000000..898695e7b7
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializerSession.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.serializer.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link SseSerializer}.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * 	<li class='warn'>This class is not thread safe and is typically discarded after one use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+	"java:S110" // Inheritance depth acceptable for session hierarchy
+})
+public class SseSerializerSession extends WriterSerializerSession {
+
+	@SuppressWarnings("java:S115") // Match AssertionUtils arg-name style used throughout Juneau.
+	private static final String ARG_ctx = "ctx";
+
+	/**
+	 * Builder class.
+	 */
+	public static class Builder extends WriterSerializerSession.Builder {
+
+		/**
+		 * Constructor.
+		 *
+		 * @param ctx The context creating this session. Cannot be <jk>null</jk>.
+		 */
+		protected Builder(SseSerializer ctx) {
+			super(assertArgNotNull(ARG_ctx, ctx));
+		}
+
+		@Override
+		public SseSerializerSession build() {
+			return new SseSerializerSession(this);
+		}
+	}
+
+	/**
+	 * Creates a new builder for this object.
+	 *
+	 * @param ctx The context creating this session. Cannot be <jk>null</jk>.
+	 * @return A new builder.
+	 */
+	public static Builder create(SseSerializer ctx) {
+		return new Builder(assertArgNotNull(ARG_ctx, ctx));
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param builder The builder for this object.
+	 */
+	protected SseSerializerSession(Builder builder) {
+		super(builder);
+	}
+
+	@Override /* Overridden from SerializerSession */
+	@SuppressWarnings({ "java:S2095", "resource" }) // Writer is owned by SerializerPipe/caller and must not be closed by this session.
+	protected void doSerialize(SerializerPipe pipe, Object o) throws IOException, SerializeException {
+		var w = pipe.getWriter();
+		if (o == null)
+			return;
+		if (o instanceof SseEvent e) {
+			writeEvent(w, e);
+		} else if (o instanceof Iterable<?> i) {
+			for (var item : i)
+				writeIfEvent(w, item);
+		} else if (o instanceof Stream<?> s) {
+			try (var s2 = s) {
+				s2.forEach(item -> writeIfEventUnchecked(w, item));
+			}
+		} else if (o instanceof SseEvent[] arr) {
+			for (var e : arr)
+				writeEvent(w, e);
+		} else if (o.getClass().isArray()) {
+			var arr = (Object[]) o;
+			for (var item : arr)
+				writeIfEvent(w, item);
+		} else {
+			throw new SerializeException("SseSerializer cannot serialize objects of type ''{0}''.", o.getClass().getName());
+		}
+	}
+
+	private static void writeIfEvent(Writer w, Object item) throws IOException, SerializeException {
+		if (item == null)
+			return;
+		if (!(item instanceof SseEvent e))
+			throw new SerializeException("SseSerializer encountered non-SseEvent element of type ''{0}''.", item.getClass().getName());
+		writeEvent(w, e);
+	}
+
+	private static void writeIfEventUnchecked(Writer w, Object item) {
+		try {
+			writeIfEvent(w, item);
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		} catch (SerializeException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	private static void writeEvent(Writer w, SseEvent e) throws IOException {
+		if (e == null)
+			return;
+		if (e.getEvent() != null && !e.getEvent().isEmpty())
+			writeField(w, "event", e.getEvent());
+		if (e.getData() != null) {
+			for (var line : splitData(e.getData()))
+				writeField(w, "data", line);
+		}
+		if (e.getId() != null)
+			writeField(w, "id", e.getId());
+		if (e.getRetry() != null)
+			w.write("retry: " + e.getRetry() + "\n");
+		w.write("\n");
+		w.flush();
+	}
+
+	private static void writeField(Writer w, String name, String value) throws IOException {
+		w.write(name);
+		w.write(": ");
+		w.write(value);
+		w.write('\n');
+	}
+
+	static List<String> splitData(String s) {
+		var parts = s.split("\n", -1);
+		return Arrays.asList(parts);
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/package-info.java
new file mode 100644
index 0000000000..6f493d86c2
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/package-info.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Server-Sent Events (SSE) marshalling support — {@code text/event-stream}.
+ *
+ * <p>
+ * Provides {@link org.apache.juneau.sse.SseSerializer} and {@link org.apache.juneau.sse.SseParser}
+ * for the
+ * <a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG Server-Sent Events</a>
+ * wire format. The serializer accepts a single {@link org.apache.juneau.sse.SseEvent}, an
+ * {@link java.lang.Iterable Iterable}, a {@link java.util.stream.Stream Stream}, or an
+ * {@code SseEvent[]} array and emits one event per element with a {@link java.io.Writer#flush()
+ * flush()} after each one — that is what makes server push actually work behind
+ * {@code SerializedPojoProcessor}.
+ *
+ * <p>
+ * SSE streams are typically long-lived. For true line-driven streaming over a {@link java.io.Reader}
+ * that the caller owns, use the {@link org.apache.juneau.sse.SseEventReader} iterator.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * 	<li class='link'><a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG HTML §9.2 — Server-sent events</a>
+ * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+package org.apache.juneau.sse;
diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
index b86b2fa876..a3836b161f 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
@@ -38,6 +38,7 @@ import org.apache.juneau.serializer.annotation.*;
 		DtoExamples.class,
 		UtilityBeansResource.class,
 		HtmlBeansResource.class,
+		SseDemoResource.class,
 		ConfigResource.class,
 		LogsResource.class,
 		ShutdownResource.class
diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/SseDemoResource.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/SseDemoResource.java
new file mode 100644
index 0000000000..abca46bada
--- /dev/null
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/SseDemoResource.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.rest;
+
+import java.time.*;
+import java.time.format.*;
+import java.util.*;
+
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.sse.*;
+
+/**
+ * Sample REST resource that emits a stream of Server-Sent Events.
+ *
+ * <p>
+ * The {@link #stream()} endpoint returns a lazy {@link Iterable} of {@link SseEvent} objects whose
+ * iterator sleeps {@value #SLEEP_MILLIS} milliseconds between calls to {@code next()}. Combined with the
+ * per-event {@code Writer.flush()} performed by {@link SseSerializer}, this allows the per-event
+ * arrival timing to be observed with a streaming HTTP client such as {@code curl -N}.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * 	$ curl -N -H 'Accept: text/event-stream' http://localhost:10000/sseDemo/stream
+ * 	event: tick-1
+ * 	data: 2026-05-20T11:08:00.123Z
+ *
+ * 	event: tick-2
+ * 	data: 2026-05-20T11:08:00.624Z
+ * 	...
+ * </p>
+ */
+@Rest(
+	path = "/sseDemo",
+	title = "SSE demo",
+	description = "Emits a stream of Server-Sent Events with a short delay between events."
+)
+public class SseDemoResource extends BasicRestObject {
+
+	private static final int EVENT_COUNT = 10;
+	private static final long SLEEP_MILLIS = 500L;
+
+	/**
+	 * Streams {@value #EVENT_COUNT} SSE events with a {@value #SLEEP_MILLIS}-ms delay between
+	 * successive events. Each event carries a UTC timestamp in its {@code data:} field so that
+	 * per-event flush behavior is observable from the client's wall clock.
+	 *
+	 * @return A lazy iterable of {@link SseEvent} objects.
+	 */
+	@RestGet(path = "/stream", serializers = SseSerializer.class)
+	public Iterable<SseEvent> stream() {
+		return () -> new Iterator<>() {
+			private int i = 0;
+
+			@Override
+			public boolean hasNext() {
+				return i < EVENT_COUNT;
+			}
+
+			@Override
+			public SseEvent next() {
+				if (i >= EVENT_COUNT)
+					throw new NoSuchElementException();
+				if (i > 0) {
+					try {
+						Thread.sleep(SLEEP_MILLIS);
+					} catch (InterruptedException e) {
+						Thread.currentThread().interrupt();
+						throw new IllegalStateException("Interrupted while sleeping between SSE events", e);
+					}
+				}
+				i++;
+				var ts = DateTimeFormatter.ISO_INSTANT.format(Instant.now());
+				return new SseEvent("tick-" + i, ts).setId(String.valueOf(i));
+			}
+		};
+	}
+}
diff --git a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
index 95ef320b5c..3be0721db8 100644
--- a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
+++ b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
@@ -3703,10 +3703,10 @@ public class RestClient extends MarshallingContextable implements HttpClient, Cl
 		 * 			bean context property setters (e.g. {@link #swaps(Class...)}) defined on this builder class.
 		 * 	</ul>
 		 * <p>
-		 * 	<c>Accept</c> request header will be set to <js>"octal/msgpack"</js> unless overridden
+		 * 	<c>Accept</c> request header will be set to <js>"application/msgpack"</js> unless overridden
 		 * 		via {@link #headers()}, or per-request via {@link RestRequest#header(Header)}.
 		 * <p>
-		 * 	<c>Content-Type</c> request header will be set to <js>"octal/msgpack"</js> unless overridden
+		 * 	<c>Content-Type</c> request header will be set to <js>"application/msgpack"</js> unless overridden
 		 * 		via {@link #headers()}, or per-request via {@link RestRequest#header(Header)}.
 		 * <p>
 		 * 	Can be combined with other marshaller setters such as {@link #json()} to provide support for multiple languages.
@@ -6582,7 +6582,7 @@ public class RestClient extends MarshallingContextable implements HttpClient, Cl
 		try {
 			var req = request(method, uri, ne(content));
 			if (nn(headers))
-				Json5Map.ofText(headers).forEach((k, v) -> req.header(stringHeader(k, s(v))));
+				Json5Map.ofString(headers).forEach((k, v) -> req.header(stringHeader(k, s(v))));
 			if (ne(content))
 				req.contentString(content);
 			return req;
diff --git a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestRequest.java b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestRequest.java
index faa9cab2c5..d9c85f739b 100644
--- a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestRequest.java
+++ b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestRequest.java
@@ -1445,10 +1445,10 @@ public class RestRequest extends MarshallingSession implements HttpUriRequest, C
 	 * 			bean context property setters (e.g. {@link RestClient.Builder#swaps(Class...)}) defined on this builder class.
 	 * 	</ul>
 	 * <p>
-	 * 	<c>Accept</c> request header will be set to <js>"octal/msgpack"</js> unless overridden
+	 * 	<c>Accept</c> request header will be set to <js>"application/msgpack"</js> unless overridden
 	 * 		by {@link RestRequest#header(String,Object)} or {@link RestRequest#accept(String)}.
 	 * <p>
-	 * 	<c>Content-Type</c> request header will be set to <js>"octal/msgpack"</js> unless overridden
+	 * 	<c>Content-Type</c> request header will be set to <js>"application/msgpack"</js> unless overridden
 	 * 		by {@link RestRequest#header(String,Object)} or {@link RestRequest#contentType(String)}.
 	 * <p>
 	 * 	Identical to calling <c>serializer(MsgPackSerializer.<jk>class</jk>).parser(MsgPackParser.<jk>class</jk>)</c>.
diff --git a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestResponse.java b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestResponse.java
index 4a368915cc..5e7207ea23 100644
--- a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestResponse.java
+++ b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestResponse.java
@@ -401,36 +401,49 @@ public class RestResponse implements HttpResponse, AutoCloseable {
 		isClosed = true;
 
 		try {
-			EntityUtils.consumeQuietly(response.getEntity());
-
-			if (! request.isLoggingSuppressed() && (request.isDebug() || client.logRequestsPredicate.test(request, this))) {
-				if (client.logRequests == DetailLevel.SIMPLE) {
-					client.log(client.logRequestsLevel, "HTTP {0} {1}, {2}", request.getMethod(), request.getURI(), this.getStatusLine());
-				} else if (request.isDebug() || client.logRequests == DetailLevel.FULL) {
-					var output = getContent().asString();
-					var sb = new StringBuilder();
-					sb.append("\n=== HTTP Call (outgoing) ======================================================");
-					sb.append("\n=== REQUEST ===\n");
-					sb.append(request.getMethod()).append(" ").append(request.getURI());
-					sb.append("\n---request headers---");
-					request.getHeaders().forEach(x -> sb.append("\n\t").append(x));
-					if (request.hasHttpEntity()) {
-						sb.append("\n---request entity---");
-						var e = request.getHttpEntity();
-						if (nn(e.getContentType()))
-							sb.append("\n\t").append(e.getContentType());
-						if (e.isRepeatable()) {
-							appendRequestContent(sb, e);
+			// Render FULL/SIMPLE request logging BEFORE consuming the underlying entity.
+			//
+			// FULL logging reads the response body via getContent().asString(), which reads
+			// from the same HttpEntity returned by response.getEntity().  If consumeQuietly()
+			// runs first it closes the underlying network input stream, leaving the entity in
+			// a state where any subsequent read throws "Stream closed".  Running logging first
+			// lets it cache the body bytes into ResponseContent.body so both this log render
+			// and any later caller (e.g. user-level getContent().asString()) succeed.
+			//
+			// EntityUtils.consumeQuietly() is invoked in the finally block so the entity is
+			// still drained on the non-logged and exceptional paths.
+			try {
+				if (! request.isLoggingSuppressed() && (request.isDebug() || client.logRequestsPredicate.test(request, this))) {
+					if (client.logRequests == DetailLevel.SIMPLE) {
+						client.log(client.logRequestsLevel, "HTTP {0} {1}, {2}", request.getMethod(), request.getURI(), this.getStatusLine());
+					} else if (request.isDebug() || client.logRequests == DetailLevel.FULL) {
+						var output = getContent().asString();
+						var sb = new StringBuilder();
+						sb.append("\n=== HTTP Call (outgoing) ======================================================");
+						sb.append("\n=== REQUEST ===\n");
+						sb.append(request.getMethod()).append(" ").append(request.getURI());
+						sb.append("\n---request headers---");
+						request.getHeaders().forEach(x -> sb.append("\n\t").append(x));
+						if (request.hasHttpEntity()) {
+							sb.append("\n---request entity---");
+							var e = request.getHttpEntity();
+							if (nn(e.getContentType()))
+								sb.append("\n\t").append(e.getContentType());
+							if (e.isRepeatable()) {
+								appendRequestContent(sb, e);
+							}
 						}
+						sb.append("\n=== RESPONSE ===\n").append(getStatusLine());
+						sb.append("\n---response headers---");
+						for (var h : getAllHeaders())
+							sb.append("\n\t").append(h);
+						sb.append("\n---response content---\n").append(output);
+						sb.append("\n=== END =======================================================================");
+						client.log(client.logRequestsLevel, sb.toString());
 					}
-					sb.append("\n=== RESPONSE ===\n").append(getStatusLine());
-					sb.append("\n---response headers---");
-					for (var h : getAllHeaders())
-						sb.append("\n\t").append(h);
-					sb.append("\n---response content---\n").append(output);
-					sb.append("\n=== END =======================================================================");
-					client.log(client.logRequestsLevel, sb.toString());
 				}
+			} finally {
+				EntityUtils.consumeQuietly(response.getEntity());
 			}
 
 			for (var r : request.interceptors) {
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BasicHttpException.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BasicHttpException.java
index 852c6d1101..e3fc9ca0b4 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BasicHttpException.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BasicHttpException.java
@@ -47,6 +47,7 @@ import org.apache.juneau.http.header.*;
  *
  * @since 9.2.1
  */
+@SuppressWarnings({"java:S3008", "java:S1948", "java:S1165"})
 public class BasicHttpException extends RuntimeException implements HttpResponseMessage {
 
 	private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/classic/MockConsole.java b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/classic/MockConsole.java
index 3b95bb36ad..ce2a283c14 100644
--- a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/classic/MockConsole.java
+++ b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/classic/MockConsole.java
@@ -87,7 +87,7 @@ import org.apache.juneau.assertions.*;
 })
 public class MockConsole extends PrintStream {
 
-	private static final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+	private final ByteArrayOutputStream baos;
 
 	/**
 	 * Creator.
@@ -102,7 +102,12 @@ public class MockConsole extends PrintStream {
 	 * Constructor.
 	 */
 	public MockConsole() {
+		this(new ByteArrayOutputStream());
+	}
+
+	private MockConsole(ByteArrayOutputStream baos) {
 		super(baos);
+		this.baos = baos;
 	}
 
 	/**
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
index 6d03fee97d..2ba55b9197 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
@@ -1025,7 +1025,7 @@ public class BasicSwaggerProviderSession {
 			s = resolve(s);
 			if (! isProbablyJsonArray(s, true))
 				s = "[" + s + "]";
-			return Json5List.ofText(s);
+			return Json5List.ofString(s);
 		} catch (ParseException e) {
 			throw new SwaggerException(e, "Malformed swagger JSON array encountered in " + location + ".", locationArgs);
 		}
@@ -1058,7 +1058,7 @@ public class BasicSwaggerProviderSession {
 				return Json5Map.of(SWAGGER_ignore, true);
 			if (! isProbablyJsonObject(o2, true))
 				o2 = "{" + o2 + "}";
-			return Json5Map.ofText(o2);
+			return Json5Map.ofString(o2);
 		}
 		if (o instanceof Json5Map o2)
 			return o2;
diff --git a/juneau-utest/pom.xml b/juneau-utest/pom.xml
index 7af9bd1459..91d7ae1ba4 100644
--- a/juneau-utest/pom.xml
+++ b/juneau-utest/pom.xml
@@ -65,11 +65,26 @@
 			<artifactId>juneau-bean-common</artifactId>
 			<version>${project.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>org.apache.juneau</groupId>
+			<artifactId>juneau-bean-hal</artifactId>
+			<version>${project.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>org.apache.juneau</groupId>
 			<artifactId>juneau-bean-html5</artifactId>
 			<version>${project.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>org.apache.juneau</groupId>
+			<artifactId>juneau-bean-jsonapi</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.juneau</groupId>
+			<artifactId>juneau-bean-jsonpatch</artifactId>
+			<version>${project.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>org.apache.juneau</groupId>
 			<artifactId>juneau-bean-jsonschema</artifactId>
@@ -90,6 +105,11 @@
 			<artifactId>juneau-bean-mcp</artifactId>
 			<version>${project.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>org.apache.juneau</groupId>
+			<artifactId>juneau-bean-rfc7807</artifactId>
+			<version>${project.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>org.apache.juneau</groupId>
 			<artifactId>juneau-rest-mock</artifactId>
diff --git a/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
index 8599808d2c..0a69fcb4ed 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
@@ -223,11 +223,11 @@ class BeanMap_Test extends TestBase {
 		var m = bc.toBeanMap(t);
 
 		// Non-initialized list fields.
-		m.put("l1", JsonList.ofText("[1,2,3]"));
-		m.put("al1", JsonList.ofText("[1,2,3]"));
-		m.put("ll1", JsonList.ofText("[1,2,3]"));
-		m.put("c1", JsonList.ofText("[1,2,3]"));
-		m.put("jl1", JsonList.ofText("[1,2,3]"));
+		m.put("l1", JsonList.ofString("[1,2,3]"));
+		m.put("al1", JsonList.ofString("[1,2,3]"));
+		m.put("ll1", JsonList.ofString("[1,2,3]"));
+		m.put("c1", JsonList.ofString("[1,2,3]"));
+		m.put("jl1", JsonList.ofString("[1,2,3]"));
 
 		// al1 should be initialized with an ArrayList, since it's not a superclass of JsonList.
 		// The rest are proper superclasses of JsonList.
@@ -238,10 +238,10 @@ class BeanMap_Test extends TestBase {
 		);
 
 		// Non-initialized map fields.
-		m.put("m1", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("hm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("jm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("tm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
+		m.put("m1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("hm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("jm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("tm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
 
 		// tm1 should be initialized with TreeMap, since it's not a superclass of JsonMap.
 		// The rest are proper superclasses of JsonMap
@@ -252,15 +252,15 @@ class BeanMap_Test extends TestBase {
 		);
 
 		// Initialized fields should reuse existing field value.
-		m.put("l2", JsonList.ofText("[1,2,3]"));
-		m.put("al2", JsonList.ofText("[1,2,3]"));
-		m.put("ll2", JsonList.ofText("[1,2,3]"));
-		m.put("c2", JsonList.ofText("[1,2,3]"));
-		m.put("m2", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("hm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("tm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("jm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("jl2", JsonList.ofText("[1,2,3]"));
+		m.put("l2", JsonList.ofString("[1,2,3]"));
+		m.put("al2", JsonList.ofString("[1,2,3]"));
+		m.put("ll2", JsonList.ofString("[1,2,3]"));
+		m.put("c2", JsonList.ofString("[1,2,3]"));
+		m.put("m2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("hm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("tm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("jm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("jl2", JsonList.ofString("[1,2,3]"));
 
 		assertBean(
 			m,
@@ -298,11 +298,11 @@ class BeanMap_Test extends TestBase {
 		var m = bc.toBeanMap(t);
 
 		// Non-initialized list fields.
-		m.put("l1", JsonList.ofText("[1,2,3]"));
-		m.put("al1", JsonList.ofText("[1,2,3]"));
-		m.put("ll1", JsonList.ofText("[1,2,3]"));
-		m.put("c1", JsonList.ofText("[1,2,3]"));
-		m.put("jl1", JsonList.ofText("[1,2,3]"));
+		m.put("l1", JsonList.ofString("[1,2,3]"));
+		m.put("al1", JsonList.ofString("[1,2,3]"));
+		m.put("ll1", JsonList.ofString("[1,2,3]"));
+		m.put("c1", JsonList.ofString("[1,2,3]"));
+		m.put("jl1", JsonList.ofString("[1,2,3]"));
 
 		// al1 should be initialized with an ArrayList, since it's not a superclass of JsonList.
 		// The rest are proper superclasses of JsonList.
@@ -313,10 +313,10 @@ class BeanMap_Test extends TestBase {
 		);
 
 		// Non-initialized map fields.
-		m.put("m1", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("hm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("jm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("tm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
+		m.put("m1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("hm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("jm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("tm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
 
 		// tm1 should be initialized with TreeMap, since it's not a superclass of JsonMap.
 		// The rest are proper superclasses of JsonMap
@@ -327,15 +327,15 @@ class BeanMap_Test extends TestBase {
 		);
 
 		// Initialized fields should reuse existing field value.
-		m.put("l2", JsonList.ofText("[1,2,3]"));
-		m.put("al2", JsonList.ofText("[1,2,3]"));
-		m.put("ll2", JsonList.ofText("[1,2,3]"));
-		m.put("c2", JsonList.ofText("[1,2,3]"));
-		m.put("m2", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("hm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("tm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("jm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
-		m.put("jl2", JsonList.ofText("[1,2,3]"));
+		m.put("l2", JsonList.ofString("[1,2,3]"));
+		m.put("al2", JsonList.ofString("[1,2,3]"));
+		m.put("ll2", JsonList.ofString("[1,2,3]"));
+		m.put("c2", JsonList.ofString("[1,2,3]"));
+		m.put("m2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("hm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("tm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("jm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+		m.put("jl2", JsonList.ofString("[1,2,3]"));
 
 		assertBean(
 			m,
@@ -424,7 +424,7 @@ class BeanMap_Test extends TestBase {
 	@Test void a05_arrayProperties() {
 		var t = new D1();
 		var m = bc.toBeanMap(t);
-		m.put("b", JsonMap.ofText("{\"s\":\"foo\"}"));
+		m.put("b", JsonMap.ofString("{\"s\":\"foo\"}"));
 		assertNotNull(t.b);
 		assertEquals("foo", t.b.s);
 
@@ -438,16 +438,16 @@ class BeanMap_Test extends TestBase {
 		assertEquals("default", t.b.s);
 
 		var p = Json5Parser.create().beanDictionary(D2.class).build();
-		m.put("lb1", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+		m.put("lb1", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
 		assertBean(t, "lb1{class{simpleName}},lb1{0{class{simpleName}}},lb1{0{s}}", "{{ArrayList}},{{{D2}}},{{foobar}}");
 
-		m.put("lb2", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+		m.put("lb2", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
 		assertBean(t, "lb2{class{simpleName}},lb2{0{class{simpleName}}},lb2{0{s}}", "{{ArrayList}},{{{D2}}},{{foobar}}");
 
-		m.put("ab1", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+		m.put("ab1", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
 		assertBean(t, "ab1{class{simpleName}},ab1{0{class{simpleName},s}}", "{{D2[]}},{{{D2},foobar}}");
 
-		m.put("ab2", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+		m.put("ab2", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
 		assertBean(t, "ab2{class{simpleName}},ab2{0{class{simpleName},s}}", "{{D2[]}},{{{D2},foobar}}");
 	}
 
@@ -467,7 +467,7 @@ class BeanMap_Test extends TestBase {
 	@Test void a06_arrayProperties_usingConfig() {
 		var t = new D1c();
 		var m = bc.toBeanMap(t);
-		m.put("b", JsonMap.ofText("{\"s\":\"foo\"}"));
+		m.put("b", JsonMap.ofString("{\"s\":\"foo\"}"));
 		assertNotNull(t.b);
 		assertBean(t.b, "s", "foo");
 
@@ -481,16 +481,16 @@ class BeanMap_Test extends TestBase {
 		assertBean(t.b, "s", "default");
 
 		var p = Json5Parser.create().beanDictionary(D2c.class).applyAnnotations(D1cConfig.class).build();
-		m.put("lb1", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+		m.put("lb1", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
 		assertBean(t, "lb1{class{simpleName}},lb1{0{class{simpleName}}},lb1{0{s}}", "{{ArrayList}},{{{D2c}}},{{foobar}}");
 
-		m.put("lb2", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+		m.put("lb2", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
 		assertBean(t, "lb2{class{simpleName}},lb2{0{class{simpleName}}},lb2{0{s}}", "{{ArrayList}},{{{D2c}}},{{foobar}}");
 
-		m.put("ab1", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+		m.put("ab1", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
 		assertBean(t, "ab1{class{simpleName}},ab1{0{class{simpleName},s}}", "{{D2c[]}},{{{D2c},foobar}}");
 
-		m.put("ab2", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+		m.put("ab2", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
 		assertBean(t, "ab2{class{simpleName}},ab2{0{class{simpleName},s}}", "{{D2c[]}},{{{D2c},foobar}}");
 	}
 
@@ -517,13 +517,13 @@ class BeanMap_Test extends TestBase {
 	@Test void a07_arrayPropertiesInJsonList() {
 		var t = new E();
 		var m = bc.toBeanMap(t);
-		m.put("s", JsonList.ofText("[\"foo\"]"));
-		m.put("s2", JsonList.ofText("[[\"foo\"]]"));
-		m.put("i", JsonList.ofText("[1,2,3]"));
-		m.put("i2", JsonList.ofText("[[1,2,3],[4,5,6]]"));
+		m.put("s", JsonList.ofString("[\"foo\"]"));
+		m.put("s2", JsonList.ofString("[[\"foo\"]]"));
+		m.put("i", JsonList.ofString("[1,2,3]"));
+		m.put("i2", JsonList.ofString("[[1,2,3],[4,5,6]]"));
 		assertEquals("{s:['foo'],s2:[['foo']],i:[1,2,3],i2:[[1,2,3],[4,5,6]]}", Json5Serializer.DEFAULT.serialize(t));
-		m.put("i", JsonList.ofText("[null,null,null]"));
-		m.put("i2", JsonList.ofText("[[null,null,null],[null,null,null]]"));
+		m.put("i", JsonList.ofString("[null,null,null]"));
+		m.put("i2", JsonList.ofString("[[null,null,null],[null,null,null]]"));
 		assertEquals("{s:['foo'],s2:[['foo']],i:[0,0,0],i2:[[0,0,0],[0,0,0]]}", Json5Serializer.DEFAULT.serialize(t));
 	}
 
diff --git a/juneau-utest/src/test/java/org/apache/juneau/DataConversion_Test.java b/juneau-utest/src/test/java/org/apache/juneau/DataConversion_Test.java
index 363d650506..7026cfcee7 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/DataConversion_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/DataConversion_Test.java
@@ -71,7 +71,7 @@ class DataConversion_Test extends TestBase {
 		assertEquals("{}", m.getString("x"));
 
 		// *** JsonMap ***
-		m.put("x", Json5Map.ofText("{foo:123}"));
+		m.put("x", Json5Map.ofString("{foo:123}"));
 		assertEquals("{foo:123}", m.getString("x"));
 
 		// *** Collection ***
@@ -81,7 +81,7 @@ class DataConversion_Test extends TestBase {
 		assertEquals("[123]", m.getString("x"));
 
 		// *** JsonList ***
-		m.put("x", JsonList.ofText("[123]"));
+		m.put("x", JsonList.ofString("[123]"));
 		assertEquals("[123]", m.getString("x"));
 		assertSize(1, m.getList("x"));
 
diff --git a/juneau-utest/src/test/java/org/apache/juneau/JsonList_Test.java b/juneau-utest/src/test/java/org/apache/juneau/JsonList_Test.java
index d375b74c79..b34ff0836e 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/JsonList_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/JsonList_Test.java
@@ -92,7 +92,7 @@ class JsonList_Test extends TestBase {
 		r = l.getAt("0/foo", String.class);
 		assertEquals("bing", r);
 
-		l.postAt("", Json5Map.ofText("{a:'b'}"));
+		l.postAt("", Json5Map.ofString("{a:'b'}"));
 		r = l.getAt("2/a", String.class);
 		assertEquals("b", r);
 
diff --git a/juneau-utest/src/test/java/org/apache/juneau/JsonMap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/JsonMap_Test.java
index 33d6812a0f..4a1f926c2f 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/JsonMap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/JsonMap_Test.java
@@ -37,207 +37,207 @@ class JsonMap_Test extends TestBase {
 	@Test void a01_basic() throws Exception {
 		var in = "{A:'asdf'}";
 
-		checkStep(1, in, Json5Map.ofText(in).getString("A"), "asdf");
+		checkStep(1, in, Json5Map.ofString(in).getString("A"), "asdf");
 
 		in = "{A:{B:'asdf'}}";
-		checkStep(2, in, getDeepString(Json5Map.ofText(in), "A/B"), "asdf");
-		checkStep(3, in, Json5Map.ofText(in).getString("A"), "{B:'asdf'}");
+		checkStep(2, in, getDeepString(Json5Map.ofString(in), "A/B"), "asdf");
+		checkStep(3, in, Json5Map.ofString(in).getString("A"), "{B:'asdf'}");
 
 		in = "{A:{B:'asdf'+\"asdf\"}}";
-		checkStep(4, in, getDeepString(Json5Map.ofText(in), "A/B"), "asdfasdf");
-		checkStep(5, in, Json5Map.ofText(in).getString("A"), "{B:'asdfasdf'}");
+		checkStep(4, in, getDeepString(Json5Map.ofString(in), "A/B"), "asdfasdf");
+		checkStep(5, in, Json5Map.ofString(in).getString("A"), "{B:'asdfasdf'}");
 
 		in = "{A:{B:'asdf' + \n\t \"asdf\"}}";
-		checkStep(6, in, getDeepString(Json5Map.ofText(in), "A/B"), "asdfasdf");
-		checkStep(7, in, Json5Map.ofText(in).getString("A"), "{B:'asdfasdf'}");
+		checkStep(6, in, getDeepString(Json5Map.ofString(in), "A/B"), "asdfasdf");
+		checkStep(7, in, Json5Map.ofString(in).getString("A"), "{B:'asdfasdf'}");
 
 		in = "{A:{B:'asdf\"asdf', C:\"asdf'asdf\", D : \"asdf\\\"asdf\", E: 'asdf\\\'asdf', F:\"asdf\\\'asdf\", G:'asdf\\\"asdf'}}";
-		checkStep(8, in, getDeepString(Json5Map.ofText(in), "A/B"), "asdf\"asdf");
-		checkStep(9, in, getDeepString(Json5Map.ofText(in), "A/C"), "asdf'asdf");
-		checkStep(10, in, getDeepString(Json5Map.ofText(in), "A/D"), "asdf\"asdf");
-		checkStep(11, in, getDeepString(Json5Map.ofText(in), "A/E"), "asdf'asdf");
-		checkStep(12, in, getDeepString(Json5Map.ofText(in), "A/F"), "asdf'asdf");
-		checkStep(13, in, getDeepString(Json5Map.ofText(in), "A/G"), "asdf\"asdf");
+		checkStep(8, in, getDeepString(Json5Map.ofString(in), "A/B"), "asdf\"asdf");
+		checkStep(9, in, getDeepString(Json5Map.ofString(in), "A/C"), "asdf'asdf");
+		checkStep(10, in, getDeepString(Json5Map.ofString(in), "A/D"), "asdf\"asdf");
+		checkStep(11, in, getDeepString(Json5Map.ofString(in), "A/E"), "asdf'asdf");
+		checkStep(12, in, getDeepString(Json5Map.ofString(in), "A/F"), "asdf'asdf");
+		checkStep(13, in, getDeepString(Json5Map.ofString(in), "A/G"), "asdf\"asdf");
 
 		in = "{A:123, B: 123}";
-		checkStep(16, in, Json5Map.ofText(in).getInt("A").toString(), "123");
-		checkStep(17, in, Json5Map.ofText(in).getInt("B").toString(), "123");
+		checkStep(16, in, Json5Map.ofString(in).getInt("A").toString(), "123");
+		checkStep(17, in, Json5Map.ofString(in).getInt("B").toString(), "123");
 
 		in = "{A:true, B: true, C:false, D: false}";
-		checkStep(18, in, Json5Map.ofText(in).getBoolean("A").toString(), "true");
-		checkStep(19, in, Json5Map.ofText(in).getBoolean("B").toString(), "true");
-		checkStep(20, in, Json5Map.ofText(in).getBoolean("C").toString(), "false");
-		checkStep(21, in, Json5Map.ofText(in).getBoolean("D").toString(), "false");
+		checkStep(18, in, Json5Map.ofString(in).getBoolean("A").toString(), "true");
+		checkStep(19, in, Json5Map.ofString(in).getBoolean("B").toString(), "true");
+		checkStep(20, in, Json5Map.ofString(in).getBoolean("C").toString(), "false");
+		checkStep(21, in, Json5Map.ofString(in).getBoolean("D").toString(), "false");
 
 		in = "{'AAA':{\"BBB\":\"CCC\",'DDD':false}}";
-		checkStep(31, in, getDeepString(Json5Map.ofText(in), "AAA/BBB"), "CCC");
-		checkStep(32, in, getDeepBoolean(Json5Map.ofText(in), "AAA/DDD").toString(), "false");
+		checkStep(31, in, getDeepString(Json5Map.ofString(in), "AAA/BBB"), "CCC");
+		checkStep(32, in, getDeepBoolean(Json5Map.ofString(in), "AAA/DDD").toString(), "false");
 
 		in = " \n\n\t {  'AAA' : { \"BBB\" : \"CCC\" , 'DDD' : false } } \n\t";
-		checkStep(33, in, getDeepString(Json5Map.ofText(in), "AAA/BBB"), "CCC");
-		checkStep(34, in, getDeepBoolean(Json5Map.ofText(in), "AAA/DDD").toString(), "false");
+		checkStep(33, in, getDeepString(Json5Map.ofString(in), "AAA/BBB"), "CCC");
+		checkStep(34, in, getDeepBoolean(Json5Map.ofString(in), "AAA/DDD").toString(), "false");
 
 		in = "/*x*/{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(100, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(100, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{/*x*/A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(101, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(101, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{A/*x*/:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(102, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(102, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{A:/*x*/'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(103, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(103, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{A:'/*x*/B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(104, in, Json5Map.ofText(in).getString("A"), "/*x*/B");
+		checkStep(104, in, Json5Map.ofString(in).getString("A"), "/*x*/B");
 		in = "{A:'B/*x*/','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(105, in, Json5Map.ofText(in).getString("A"), "B/*x*/");
+		checkStep(105, in, Json5Map.ofString(in).getString("A"), "B/*x*/");
 		in = "{A:'B'/*x*/,'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(106, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(106, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{A:'B',/*x*/'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(107, in, Json5Map.ofText(in).getString("C"), "1");
+		checkStep(107, in, Json5Map.ofString(in).getString("C"), "1");
 		in = "{A:'B','C':/*x*/1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(108, in, Json5Map.ofText(in).getString("C"), "1");
+		checkStep(108, in, Json5Map.ofString(in).getString("C"), "1");
 		in = "{A:'B','C':1/*x*/,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(109, in, Json5Map.ofText(in).getString("C"), "1");
+		checkStep(109, in, Json5Map.ofString(in).getString("C"), "1");
 		in = "{A:'B','C':1,/*x*/\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(110, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(110, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"/*x*/E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(111, in, Json5Map.ofText(in).getList("/*x*/E").getString(0), "1");
+		checkStep(111, in, Json5Map.ofString(in).getList("/*x*/E").getString(0), "1");
 		in = "{A:'B','C':1,\"E/*x*/\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(112, in, Json5Map.ofText(in).getList("E/*x*/").getString(0), "1");
+		checkStep(112, in, Json5Map.ofString(in).getList("E/*x*/").getString(0), "1");
 		in = "{A:'B','C':1,\"E\"/*x*/:[1,2,3],G:['g1','g2','g3']}";
-		checkStep(113, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(113, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"E\":/*x*/[1,2,3],G:['g1','g2','g3']}";
-		checkStep(114, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(114, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"E\":[/*x*/1,2,3],G:['g1','g2','g3']}";
-		checkStep(115, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(115, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"E\":[1/*x*/,2,3],G:['g1','g2','g3']}";
-		checkStep(116, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(116, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"E\":[1,/*x*/2,3],G:['g1','g2','g3']}";
-		checkStep(117, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+		checkStep(117, in, Json5Map.ofString(in).getList("E").getString(1), "2");
 		in = "{A:'B','C':1,\"E\":[1,2/*x*/,3],G:['g1','g2','g3']}";
-		checkStep(118, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+		checkStep(118, in, Json5Map.ofString(in).getList("E").getString(1), "2");
 		in = "{A:'B','C':1,\"E\":[1,2,/*x*/3],G:['g1','g2','g3']}";
-		checkStep(119, in, Json5Map.ofText(in).getList("E").getString(2), "3");
+		checkStep(119, in, Json5Map.ofString(in).getList("E").getString(2), "3");
 		in = "{A:'B','C':1,\"E\":[1,2,3]/*x*/,G:['g1','g2','g3']}";
-		checkStep(120, in, Json5Map.ofText(in).getList("E").getString(2), "3");
+		checkStep(120, in, Json5Map.ofString(in).getList("E").getString(2), "3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],/*x*/G:['g1','g2','g3']}";
-		checkStep(121, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+		checkStep(121, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:[/*x*/'g1','g2','g3']}";
-		checkStep(122, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+		checkStep(122, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['/*x*/g1','g2','g3']}";
-		checkStep(123, in, Json5Map.ofText(in).getList("G").getString(0), "/*x*/g1");
+		checkStep(123, in, Json5Map.ofString(in).getList("G").getString(0), "/*x*/g1");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1'/*x*/,'g2','g3']}";
-		checkStep(124, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+		checkStep(124, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1',/*x*/'g2','g3']}";
-		checkStep(125, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+		checkStep(125, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2'/*x*/,'g3']}";
-		checkStep(126, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+		checkStep(126, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2',/*x*/'g3']}";
-		checkStep(127, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+		checkStep(127, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3'/*x*/]}";
-		checkStep(128, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+		checkStep(128, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']/*x*/}";
-		checkStep(129, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+		checkStep(129, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}/*x*/";
-		checkStep(130, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+		checkStep(130, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
 
 		in = "/*\tx\t*///\tx\t\n\t/*\tx\t*/{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(201, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(201, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{/*\tx\t*///\tx\t\n\t/*\tx\t*/A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(202, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(202, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{A/*\tx\t*///\tx\t\n\t/*\tx\t*/:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(203, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(203, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{A:/*\tx\t*///\tx\t\n\t/*\tx\t*/'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(204, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(204, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{A:'/*\tx\t*///\tx\t\n\t/*\tx\t*/B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(205, in, Json5Map.ofText(in).getString("A"), "/*\tx\t*///\tx\t\n\t/*\tx\t*/B");
+		checkStep(205, in, Json5Map.ofString(in).getString("A"), "/*\tx\t*///\tx\t\n\t/*\tx\t*/B");
 		in = "{A:'B/*\tx\t*///\tx\t\n\t/*\tx\t*/','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(206, in, Json5Map.ofText(in).getString("A"), "B/*\tx\t*///\tx\t\n\t/*\tx\t*/");
+		checkStep(206, in, Json5Map.ofString(in).getString("A"), "B/*\tx\t*///\tx\t\n\t/*\tx\t*/");
 		in = "{A:'B'/*\tx\t*///\tx\t\n\t/*\tx\t*/,'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(207, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(207, in, Json5Map.ofString(in).getString("A"), "B");
 		in = "{A:'B',/*\tx\t*///\tx\t\n\t/*\tx\t*/'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(208, in, Json5Map.ofText(in).getString("C"), "1");
+		checkStep(208, in, Json5Map.ofString(in).getString("C"), "1");
 		in = "{A:'B','C':/*\tx\t*///\tx\t\n\t/*\tx\t*/1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(209, in, Json5Map.ofText(in).getString("C"), "1");
+		checkStep(209, in, Json5Map.ofString(in).getString("C"), "1");
 		in = "{A:'B','C':1/*\tx\t*///\tx\t\n\t/*\tx\t*/,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(210, in, Json5Map.ofText(in).getString("C"), "1");
+		checkStep(210, in, Json5Map.ofString(in).getString("C"), "1");
 		in = "{A:'B','C':1,/*\tx\t*///\tx\t\n\t/*\tx\t*/\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(211, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(211, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"/*\tx\t*///\tx\t\n\t/*\tx\t*/E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(212, in, Json5Map.ofText(in).getList("/*\tx\t*///\tx\t\n\t/*\tx\t*/E").getString(0), "1");
+		checkStep(212, in, Json5Map.ofString(in).getList("/*\tx\t*///\tx\t\n\t/*\tx\t*/E").getString(0), "1");
 		in = "{A:'B','C':1,\"E/*\tx\t*///\tx\t\n\t/*\tx\t*/\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(213, in, Json5Map.ofText(in).getList("E/*\tx\t*///\tx\t\n\t/*\tx\t*/").getString(0), "1");
+		checkStep(213, in, Json5Map.ofString(in).getList("E/*\tx\t*///\tx\t\n\t/*\tx\t*/").getString(0), "1");
 		in = "{A:'B','C':1,\"E\"/*\tx\t*///\tx\t\n\t/*\tx\t*/:[1,2,3],G:['g1','g2','g3']}";
-		checkStep(214, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(214, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"E\":/*\tx\t*///\tx\t\n\t/*\tx\t*/[1,2,3],G:['g1','g2','g3']}";
-		checkStep(215, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(215, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"E\":[/*\tx\t*///\tx\t\n\t/*\tx\t*/1,2,3],G:['g1','g2','g3']}";
-		checkStep(216, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(216, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"E\":[1/*\tx\t*///\tx\t\n\t/*\tx\t*/,2,3],G:['g1','g2','g3']}";
-		checkStep(217, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+		checkStep(217, in, Json5Map.ofString(in).getList("E").getString(0), "1");
 		in = "{A:'B','C':1,\"E\":[1,/*\tx\t*///\tx\t\n\t/*\tx\t*/2,3],G:['g1','g2','g3']}";
-		checkStep(218, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+		checkStep(218, in, Json5Map.ofString(in).getList("E").getString(1), "2");
 		in = "{A:'B','C':1,\"E\":[1,2/*\tx\t*///\tx\t\n\t/*\tx\t*/,3],G:['g1','g2','g3']}";
-		checkStep(219, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+		checkStep(219, in, Json5Map.ofString(in).getList("E").getString(1), "2");
 		in = "{A:'B','C':1,\"E\":[1,2,/*\tx\t*///\tx\t\n\t/*\tx\t*/3],G:['g1','g2','g3']}";
-		checkStep(220, in, Json5Map.ofText(in).getList("E").getString(2), "3");
+		checkStep(220, in, Json5Map.ofString(in).getList("E").getString(2), "3");
 		in = "{A:'B','C':1,\"E\":[1,2,3]/*\tx\t*///\tx\t\n\t/*\tx\t*/,G:['g1','g2','g3']}";
-		checkStep(221, in, Json5Map.ofText(in).getList("E").getString(2), "3");
+		checkStep(221, in, Json5Map.ofString(in).getList("E").getString(2), "3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],/*\tx\t*///\tx\t\n\t/*\tx\t*/G:['g1','g2','g3']}";
-		checkStep(222, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+		checkStep(222, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:[/*\tx\t*///\tx\t\n\t/*\tx\t*/'g1','g2','g3']}";
-		checkStep(223, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+		checkStep(223, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['/*\tx\t*///\tx\t\n\t/*\tx\t*/g1','g2','g3']}";
-		checkStep(224, in, Json5Map.ofText(in).getList("G").getString(0), "/*\tx\t*///\tx\t\n\t/*\tx\t*/g1");
+		checkStep(224, in, Json5Map.ofString(in).getList("G").getString(0), "/*\tx\t*///\tx\t\n\t/*\tx\t*/g1");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1'/*\tx\t*///\tx\t\n\t/*\tx\t*/,'g2','g3']}";
-		checkStep(225, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+		checkStep(225, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1',/*\tx\t*///\tx\t\n\t/*\tx\t*/'g2','g3']}";
-		checkStep(226, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+		checkStep(226, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2'/*\tx\t*///\tx\t\n\t/*\tx\t*/,'g3']}";
-		checkStep(227, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+		checkStep(227, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2',/*\tx\t*///\tx\t\n\t/*\tx\t*/'g3']}";
-		checkStep(228, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+		checkStep(228, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3'/*\tx\t*///\tx\t\n\t/*\tx\t*/]}";
-		checkStep(229, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+		checkStep(229, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']/*\tx\t*///\tx\t\n\t/*\tx\t*/}";
-		checkStep(230, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+		checkStep(230, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}/*\tx\t*///\tx\t\n\t/*\tx\t*/";
-		checkStep(231, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+		checkStep(231, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
 
 		in = "{  /*  x  */  //  x  \n  /*  x  */  A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(240, in, Json5Map.ofText(in).getString("A"), "B");
+		checkStep(240, in, Json5Map.ofString(in).getString("A"), "B");
 
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(301, in, Json5Map.ofText(in).getString("A", "default"), "B");
+		checkStep(301, in, Json5Map.ofString(in).getString("A", "default"), "B");
 		in = "{/*A:'B',*/'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(302, in, Json5Map.ofText(in).getString("A", "default"), "default");
+		checkStep(302, in, Json5Map.ofString(in).getString("A", "default"), "default");
 		in = "{A:'B',/*'C':1,*/\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(303, in, Json5Map.ofText(in).getString("C", "default"), "default");
+		checkStep(303, in, Json5Map.ofString(in).getString("C", "default"), "default");
 		in = "{A:'B','C':1,/*\"E\":[1,2,3],*/G:['g1','g2','g3']}";
-		checkStep(304, in, Json5Map.ofText(in).getString("E", "default"), "default");
+		checkStep(304, in, Json5Map.ofString(in).getString("E", "default"), "default");
 		in = "{A:'B','C':1,\"E\":[/*1,*/2,3],G:['g1','g2','g3']}";
-		checkStep(305, in, Json5Map.ofText(in).getList("E").getString(0), "2");
+		checkStep(305, in, Json5Map.ofString(in).getList("E").getString(0), "2");
 		in = "{A:'B','C':1,\"E\":[1,/*2,*/3],G:['g1','g2','g3']}";
-		checkStep(306, in, Json5Map.ofText(in).getList("E").getString(1), "3");
+		checkStep(306, in, Json5Map.ofString(in).getList("E").getString(1), "3");
 		in = "{A:'B','C':1,\"E\":[1,2/*,3*/],G:['g1','g2','g3']}";
-		checkStep(307, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+		checkStep(307, in, Json5Map.ofString(in).getList("E").getString(1), "2");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:[/*'g1',*/'g2','g3']}";
-		checkStep(308, in, Json5Map.ofText(in).getList("G").getString(0), "g2");
+		checkStep(308, in, Json5Map.ofString(in).getList("G").getString(0), "g2");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1'/*,'g2'*/,'g3']}";
-		checkStep(309, in, Json5Map.ofText(in).getList("G").getString(1), "g3");
+		checkStep(309, in, Json5Map.ofString(in).getList("G").getString(1), "g3");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2'/*,'g3'*/]}";
-		checkStep(310, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+		checkStep(310, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
 		in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
-		checkStep(310, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+		checkStep(310, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
 
 		// Check keys that contain array indexes
 		in = "{A:{B:[{C:'c0'},{C:'c1'},{C:'c2'}]}}";
-		checkStep(401, in, getDeepString(Json5Map.ofText(in), "A/B/0/C"), "c0");
-		checkStep(402, in, getDeepString(Json5Map.ofText(in), "A/B/1/C"), "c1");
-		checkStep(403, in, getDeepString(Json5Map.ofText(in), "A/B/2/C"), "c2");
+		checkStep(401, in, getDeepString(Json5Map.ofString(in), "A/B/0/C"), "c0");
+		checkStep(402, in, getDeepString(Json5Map.ofString(in), "A/B/1/C"), "c1");
+		checkStep(403, in, getDeepString(Json5Map.ofString(in), "A/B/2/C"), "c2");
 
 		// Check extended unicode characters.
 		in = "{'𤭢𤭢':'𤭢𤭢'}";
-		checkStep(1, in, Json5Map.ofText(in).getString("𤭢𤭢"), "𤭢𤭢");
+		checkStep(1, in, Json5Map.ofString(in).getString("𤭢𤭢"), "𤭢𤭢");
 	}
 
 	private static String getDeepString(Json5Map m, String url) {
@@ -261,8 +261,8 @@ class JsonMap_Test extends TestBase {
 	// testComparison
 	//====================================================================================================
 	@Test void a02_comparison() throws Exception {
-		var m1 = Json5Map.ofText("{ firstName:'John', lastName:'Smith', age:123, isDeceased:false }");
-		var m2 = Json5Map.ofText("{ age:123, isDeceased:false, lastName:'Smith', firstName:'John' }");
+		var m1 = Json5Map.ofString("{ firstName:'John', lastName:'Smith', age:123, isDeceased:false }");
+		var m2 = Json5Map.ofString("{ age:123, isDeceased:false, lastName:'Smith', firstName:'John' }");
 
 		assertEquals(m1, m2);
 	}
@@ -271,8 +271,8 @@ class JsonMap_Test extends TestBase {
 	// testParent
 	//====================================================================================================
 	@Test void a03_parent() throws Exception {
-		var m1 = Json5Map.ofText("{a:1}");
-		var m2 = Json5Map.ofText("{b:2}").inner(m1);
+		var m1 = Json5Map.ofString("{a:1}");
+		var m2 = Json5Map.ofString("{b:2}").inner(m1);
 
 		assertEquals(Integer.valueOf(1), m2.getInt("a"));
 	}
@@ -281,13 +281,13 @@ class JsonMap_Test extends TestBase {
 	// testUpdatability
 	//====================================================================================================
 	@Test void a04_updatability() throws Exception {
-		var m = Json5Map.ofText("{a:[{b:'c'}]}");
+		var m = Json5Map.ofString("{a:[{b:'c'}]}");
 		var l = m.getList("a");
 		var m2 = l.getMap(0);
 		m2.put("b", "x");
 		assertBean(m, "a", "[{b=x}]");
 
-		m = Json5Map.ofText("{a:[{b:'c'}]}");
+		m = Json5Map.ofString("{a:[{b:'c'}]}");
 		for (var m3 : m.getList("a").elements(Json5Map.class))
 			m3.put("b", "y");
 
@@ -298,7 +298,7 @@ class JsonMap_Test extends TestBase {
 	// testAtMethods
 	//====================================================================================================
 	@Test void a05_atMethods() throws Exception {
-		var m = Json5Map.ofText("{a:[{b:'c'}]}");
+		var m = Json5Map.ofString("{a:[{b:'c'}]}");
 		var r = m.getAt("a/0/b", String.class);
 
 		assertEquals("c", r);
@@ -319,14 +319,14 @@ class JsonMap_Test extends TestBase {
 	// JsonMap(Reader)
 	//====================================================================================================
 	@Test void a06_fromReader() throws Exception {
-		assertBean(Json5Map.ofText(reader("{foo:'bar'}")), "foo", "bar");
+		assertBean(Json5Map.ofString(reader("{foo:'bar'}")), "foo", "bar");
 	}
 
 	//====================================================================================================
 	// testGetMap
 	//====================================================================================================
 	@Test void a07_getMap() throws Exception {
-		var m = Json5Map.ofText("{a:{1:'true',2:'false'}}");
+		var m = Json5Map.ofString("{a:{1:'true',2:'false'}}");
 		var m2 = m.getMap("a", Integer.class, Boolean.class, null);
 		assertJson("{'1':true,'2':false}", m2);
 		assertEquals(Integer.class, m2.keySet().iterator().next().getClass());
@@ -348,7 +348,7 @@ class JsonMap_Test extends TestBase {
 	// testGetList
 	//====================================================================================================
 	@Test void a08_getList() throws Exception {
-		var m = Json5Map.ofText("{a:['123','456']}");
+		var m = Json5Map.ofString("{a:['123','456']}");
 		var l2 = m.getList("a", Integer.class, null);
 		assertList(l2, "123", "456");
 		assertEquals(Integer.class, l2.iterator().next().getClass());
@@ -368,7 +368,7 @@ class JsonMap_Test extends TestBase {
 	// toX serialization methods
 	//====================================================================================================
 	@Test void a09_toX() {
-		var m = Json5Map.ofText("{b:'2',a:'1'}");
+		var m = Json5Map.ofString("{b:'2',a:'1'}");
 
 		// toJson5 — JSON5 (unquoted keys where possible), same as toString()
 		assertString("{b:'2',a:'1'}", m.toJson5());
diff --git a/juneau-utest/src/test/java/org/apache/juneau/MarshalledConfig_Test.java b/juneau-utest/src/test/java/org/apache/juneau/MarshalledConfig_Test.java
index e9f866d09b..f6af8c070b 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/MarshalledConfig_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/MarshalledConfig_Test.java
@@ -314,7 +314,7 @@ class MarshalledConfig_Test extends TestBase {
 		assertEquals("{name:'x',age:123}", bc.convertToType(o, String.class));
 
 		// List of Maps to array of beans.
-		var o2 = JsonList.of(JsonMap.ofText("{\"name\":\"x\",\"age\":1}"), JsonMap.ofText("{\"name\":\"y\",\"age\":2}"));
+		var o2 = JsonList.of(JsonMap.ofString("{\"name\":\"x\",\"age\":1}"), JsonMap.ofString("{\"name\":\"y\",\"age\":2}"));
 		assertEquals(1, bc.convertToType(o2, ReadOnlyPerson[].class)[0].getAge());
 	}
 
@@ -346,7 +346,7 @@ class MarshalledConfig_Test extends TestBase {
 		assertEquals("{name:'x',age:123}", bc.convertToType(o, String.class));
 
 		// List of Maps to array of beans.
-		var o2 = JsonList.of(JsonMap.ofText("{\"name\":\"x\",\"age\":1}"), JsonMap.ofText("{\"name\":\"y\",\"age\":2}"));
+		var o2 = JsonList.of(JsonMap.ofString("{\"name\":\"x\",\"age\":1}"), JsonMap.ofString("{\"name\":\"y\",\"age\":2}"));
 		assertEquals(1, bc.convertToType(o2, ReadOnlyPerson2[].class)[0].getAge());
 	}
 
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ObjectSwap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ObjectSwap_Test.java
index 3f1ad2ba59..3bd15c6556 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/ObjectSwap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ObjectSwap_Test.java
@@ -39,7 +39,7 @@ class ObjectSwap_Test extends TestBase {
 		r = p.parse(r, String.class);
 		assertEquals("foobar", r);
 
-		var m = Json5Map.ofText("{foo:'bar'}");
+		var m = Json5Map.ofString("{foo:'bar'}");
 		r = s.serialize(m);
 		assertEquals("{xfoox:'xbarx'}", r);
 	}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/JsonMaps_RoundTripTest.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/JsonMaps_RoundTripTest.java
index 26ddf62bad..90ed463215 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/JsonMaps_RoundTripTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/JsonMaps_RoundTripTest.java
@@ -42,7 +42,7 @@ class JsonMaps_RoundTripTest extends RoundTripTest_Base {
 	@ParameterizedTest
 	@MethodSource("testers")
 	void a01_basic(RoundTrip_Tester t) throws Exception {
-		var x1 = new A(JsonMap.ofText("{\"f1\":\"a\",\"f2\":2}"));
+		var x1 = new A(JsonMap.ofString("{\"f1\":\"a\",\"f2\":2}"));
 		x1 = t.roundTrip(x1, A.class);
 		assertBean(x1, "f1,f2", "a,2");
 
@@ -50,11 +50,11 @@ class JsonMaps_RoundTripTest extends RoundTripTest_Base {
 		x2 = t.roundTrip(x2, A[].class);
 		assertBean(x2, "length,#{f1,f2}", "1,[{a,2}]");
 
-		var x3 = l(new A(JsonMap.ofText("{\"f1\":\"a\",\"f2\":2}")));
+		var x3 = l(new A(JsonMap.ofString("{\"f1\":\"a\",\"f2\":2}")));
 		x3 = t.roundTrip(x3, List.class, A.class);
 		assertBean(x3, "size,0{f1,f2}", "1,{a,2}");
 
-		var x4 = m("a",new A(JsonMap.ofText("{\"f1\":\"a\",\"f2\":2}")));
+		var x4 = m("a",new A(JsonMap.ofString("{\"f1\":\"a\",\"f2\":2}")));
 		x4 = t.roundTrip(x4, Map.class, String.class, A.class);
 		assertBean(x4, "size,a{f1,f2}", "1,{a,2}");
 	}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/SimpleObjects_RoundTripTest.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/SimpleObjects_RoundTripTest.java
index e0b2b494d0..f3d3511339 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/SimpleObjects_RoundTripTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/SimpleObjects_RoundTripTest.java
@@ -450,7 +450,7 @@ class SimpleObjects_RoundTripTest extends RoundTripTest_Base {
 	void a51_jsonMap(RoundTrip_Tester t) throws Exception {
 		if (isRdf(t))
 			return;
-		var x = JsonMap.ofText("{\"a\":\"b\",\"c\":123,\"d\":false,\"e\":null,\"f\":[123,\"abc\",true,false,null]}");
+		var x = JsonMap.ofString("{\"a\":\"b\",\"c\":123,\"d\":false,\"e\":null,\"f\":[123,\"abc\",true,false,null]}");
 		x = t.roundTrip(x);
 		assertEquals("b", x.get("a"));
 		assertEquals(123, ((Number)x.get("c")).intValue());
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/TrimStrings_RoundTripTest.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/TrimStrings_RoundTripTest.java
index 9c7faafe7a..56d4df68c5 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/TrimStrings_RoundTripTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/TrimStrings_RoundTripTest.java
@@ -54,8 +54,8 @@ class TrimStrings_RoundTripTest extends RoundTripTest_Base {
 		a = p2.parse(s.serialize(in), String.class);
 		assertEquals(json(a), json(e));
 
-		in = JsonMap.ofText("{\" foo \": \" bar \"}");
-		e = JsonMap.ofText("{\"foo\":\"bar\"}");
+		in = JsonMap.ofString("{\" foo \": \" bar \"}");
+		e = JsonMap.ofString("{\"foo\":\"bar\"}");
 		a = p.parse(s2.serialize(in), JsonMap.class);
 		assertEquals(json(a), json(e));
 		a = p2.parse(s.serialize(in), JsonMap.class);
@@ -89,16 +89,16 @@ class TrimStrings_RoundTripTest extends RoundTripTest_Base {
 		public A init1() throws Exception {
 			f1 = " f1 ";
 			f2 = a(" f2a ", " f2b ");
-			f3 = JsonList.ofText("[\" f3a \",\" f3b \"]");
-			f4 = JsonMap.ofText("{\" foo \":\" bar \"}");
+			f3 = JsonList.ofString("[\" f3a \",\" f3b \"]");
+			f4 = JsonMap.ofString("{\" foo \":\" bar \"}");
 			return this;
 		}
 
 		public A init2() throws Exception {
 			f1 = "f1";
 			f2 = a("f2a", "f2b");
-			f3 = JsonList.ofText("[\"f3a\",\"f3b\"]");
-			f4 = JsonMap.ofText("{\"foo\":\"bar\"}");
+			f3 = JsonList.ofString("[\"f3a\",\"f3b\"]");
+			f4 = JsonMap.ofString("{\"foo\":\"bar\"}");
 			return this;
 		}
 	}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/hal/Hal_RoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/hal/Hal_RoundTrip_Test.java
new file mode 100644
index 0000000000..576c2158b8
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/hal/Hal_RoundTrip_Test.java
@@ -0,0 +1,350 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.hal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@code juneau-bean-hal} wire beans.
+ */
+class Hal_RoundTrip_Test {
+
+	private static final JsonSerializer HAL_JSON = JsonSerializer.DEFAULT;
+	private static final JsonParser HAL_PARSER = JsonParser.DEFAULT;
+
+	private static void assertJsonRoundTrip(Object bean, Class<?> type) {
+		var j1 = HAL_JSON.serialize(bean);
+		var copy = HAL_PARSER.parse(j1, type);
+		var j2 = HAL_JSON.serialize(copy);
+		assertEquals(j1, j2, () -> "Round-trip JSON mismatch for " + type.getName() + ": " + j1);
+	}
+
+	@Nested
+	class A_HalLink {
+
+		@Test
+		void allFields_roundTrip() {
+			var link = new HalLink()
+				.setHref("/orders/123")
+				.setTemplated(false)
+				.setType("application/hal+json")
+				.setDeprecation("https://api.example.com/deprecations/links")
+				.setName("acme")
+				.setProfile("https://api.example.com/profiles/order")
+				.setTitle("Order #123")
+				.setHreflang("en");
+			assertJsonRoundTrip(link, HalLink.class);
+		}
+
+		@Test
+		void hrefConstructor_andToStringWorks() {
+			var link = new HalLink("/u");
+			assertEquals("/u", link.getHref());
+			assertNotNull(link.toString());
+		}
+
+		@Test
+		void emptyConstructor_fieldsAreNull() {
+			var link = new HalLink();
+			assertNull(link.getHref());
+			assertNull(link.getTemplated());
+			assertNull(link.getType());
+			assertNull(link.getDeprecation());
+			assertNull(link.getName());
+			assertNull(link.getProfile());
+			assertNull(link.getTitle());
+			assertNull(link.getHreflang());
+		}
+	}
+
+	@Nested
+	class B_HalResource_links {
+
+		@Test
+		void singleLink_roundTripsAsObject() {
+			var r = new HalResource().addLink("self", new HalLink("/orders/123"));
+			var j = HAL_JSON.serialize(r);
+			// Single-link form ⇒ JSON object, not array.
+			assertTrue(j.contains("\"self\":{"), () -> "Expected single-object form for self link: " + j);
+			assertJsonRoundTrip(r, HalResource.class);
+		}
+
+		@Test
+		void multiLink_roundTripsAsArray() {
+			var r = new HalResource()
+				.addLinks("curies",
+					new HalLink("https://acme.example/{rel}").setName("acme").setTemplated(true),
+					new HalLink("https://acme.example/v2/{rel}").setName("acme2").setTemplated(true)
+				);
+			var j = HAL_JSON.serialize(r);
+			assertTrue(j.contains("\"curies\":["), () -> "Expected multi-link array form for curies: " + j);
+			assertJsonRoundTrip(r, HalResource.class);
+		}
+
+		@Test
+		void addLinks_promotesSingleToArray() {
+			var r = new HalResource()
+				.addLink("alt", new HalLink("/a"))
+				.addLinks("alt", new HalLink("/b"));
+			var raw = r.getLinks().get("alt");
+			assertInstanceOf(HalLinkArray.class, raw);
+			assertEquals(2, ((HalLinkArray) raw).size());
+			assertJsonRoundTrip(r, HalResource.class);
+		}
+
+		@Test
+		void addLinks_extendsExistingArray() {
+			var r = new HalResource()
+				.addLinks("alt", new HalLink("/a"), new HalLink("/b"))
+				.addLinks("alt", new HalLink("/c"));
+			var raw = r.getLinks().get("alt");
+			assertInstanceOf(HalLinkArray.class, raw);
+			assertEquals(3, ((HalLinkArray) raw).size());
+		}
+
+		@Test
+		void setLinks_replacesContent() {
+			var r = new HalResource()
+				.addLink("self", new HalLink("/x"))
+				.setLinks(new LinkedHashMap<>(Map.of("self", new HalLink("/y"))));
+			assertEquals("/y", ((HalLink) r.getLinks().get("self")).getHref());
+		}
+	}
+
+	@Nested
+	class C_HalResource_embedded {
+
+		@Test
+		void singleEmbedded_roundTripsAsObject() {
+			var inner = new HalResource()
+				.addLink("self", new HalLink("/items/1"))
+				.set("name", "widget");
+			var r = new HalResource()
+				.addLink("self", new HalLink("/orders/123"))
+				.addEmbedded("item", inner);
+			var j = HAL_JSON.serialize(r);
+			assertTrue(j.contains("\"item\":{"), () -> "Expected single-embedded object form: " + j);
+			assertJsonRoundTrip(r, HalResource.class);
+		}
+
+		@Test
+		void multiEmbedded_roundTripsAsArray() {
+			var r = new HalResource()
+				.addLink("self", new HalLink("/orders/123"))
+				.addEmbedded("items",
+					new HalResource().addLink("self", new HalLink("/items/1")).set("name", "a"),
+					new HalResource().addLink("self", new HalLink("/items/2")).set("name", "b")
+				);
+			var j = HAL_JSON.serialize(r);
+			assertTrue(j.contains("\"items\":["), () -> "Expected multi-embedded array form: " + j);
+			assertJsonRoundTrip(r, HalResource.class);
+		}
+
+		@Test
+		void addEmbedded_promotesSingleToArray() {
+			var r = new HalResource()
+				.addEmbedded("item", new HalResource().set("k", "v1"))
+				.addEmbedded("item", new HalResource[] { new HalResource().set("k", "v2") });
+			var raw = r.getEmbedded().get("item");
+			assertInstanceOf(HalResourceArray.class, raw);
+			assertEquals(2, ((HalResourceArray) raw).size());
+		}
+
+		@Test
+		void addEmbedded_extendsExistingArray() {
+			var r = new HalResource()
+				.addEmbedded("items", new HalResource[] { new HalResource().set("k", "a") })
+				.addEmbedded("items", new HalResource[] { new HalResource().set("k", "b") });
+			var raw = r.getEmbedded().get("items");
+			assertInstanceOf(HalResourceArray.class, raw);
+			assertEquals(2, ((HalResourceArray) raw).size());
+		}
+
+		@Test
+		void addEmbeddedSingle_twiceUnderDifferentRelations_keepsBothInExistingMap() {
+			var r = new HalResource()
+				.addEmbedded("a", new HalResource().set("k", "va"))
+				.addEmbedded("b", new HalResource().set("k", "vb"));
+			assertEquals(2, r.getEmbedded().size());
+		}
+
+		@Test
+		void setEmbedded_replacesContent() {
+			var r = new HalResource()
+				.addEmbedded("a", new HalResource().set("k", "v"))
+				.setEmbedded(new LinkedHashMap<>(Map.of("b", new HalResource().set("k", "w"))));
+			assertNull(r.getEmbedded().get("a"));
+			assertNotNull(r.getEmbedded().get("b"));
+		}
+	}
+
+	@Nested
+	class D_HalResource_extras {
+
+		@Test
+		void payload_isFlat_notNestedUnderProperties() {
+			var r = new HalResource()
+				.addLink("self", new HalLink("/orders/123"))
+				.set("total", 99.50)
+				.set("currency", "USD");
+			var j = HAL_JSON.serialize(r);
+			assertTrue(j.contains("\"total\":99.5"), () -> j);
+			assertTrue(j.contains("\"currency\":\"USD\""), () -> j);
+			assertJsonRoundTrip(r, HalResource.class);
+		}
+
+		@Test
+		void getOnUnknownReturnsNull() {
+			var r = new HalResource();
+			assertNull(r.get("nope"));
+			assertTrue(r.extraKeys().isEmpty());
+		}
+
+		@Test
+		void getAfterSetReturnsValue() {
+			var r = new HalResource().set("k", "v");
+			assertEquals("v", r.get("k"));
+			assertTrue(r.extraKeys().contains("k"));
+		}
+	}
+
+	@Nested
+	class E_TopLevelArrayBeans {
+
+		@Test
+		void halLinkArray_constructionAndAddAll() {
+			var arr = new HalLinkArray(new HalLink("/a"));
+			arr.addAll(new HalLink("/b"), new HalLink("/c"));
+			assertEquals(3, arr.size());
+		}
+
+		@Test
+		void halResourceArray_constructionAndAddAll() {
+			var arr = new HalResourceArray(new HalResource());
+			arr.addAll(new HalResource(), new HalResource());
+			assertEquals(3, arr.size());
+		}
+
+		@Test
+		void halLinkArray_serializesAsJsonArray() {
+			var arr = new HalLinkArray(new HalLink("/x"), new HalLink("/y"));
+			var j = HAL_JSON.serialize(arr);
+			assertTrue(j.startsWith("["), j);
+			assertJsonRoundTrip(arr, HalLinkArray.class);
+		}
+	}
+
+	@Nested
+	class F_Swaps_directInvocation {
+
+		@Test
+		void linkOrArraySwap_nullIsNull() {
+			var s = new HalLinkOrArraySwap();
+			assertNull(s.unswap(null, null, null));
+		}
+
+		@Test
+		void linkOrArraySwap_swapIsIdentity() {
+			var s = new HalLinkOrArraySwap();
+			var input = new Object();
+			assertSame(input, s.swap(null, input));
+		}
+
+		@Test
+		void linkOrArraySwap_nonMapPassesThrough() {
+			var s = new HalLinkOrArraySwap();
+			var input = "not-a-map";
+			assertSame(input, s.unswap(null, input, null));
+		}
+
+		@Test
+		void linkOrArraySwap_mapWithScalarPassesValueThrough() {
+			// Round-trip via parser to exercise the "else" branch (scalar entries inside a links map).
+			var json = "{\"_links\":{\"x\":\"a-string-not-a-link\"}}";
+			var r = JsonParser.DEFAULT.parse(json, HalResource.class);
+			assertEquals("a-string-not-a-link", r.getLinks().get("x"));
+		}
+
+		@Test
+		void resourceOrArraySwap_nullIsNull() {
+			var s = new HalResourceOrArraySwap();
+			assertNull(s.unswap(null, null, null));
+		}
+
+		@Test
+		void resourceOrArraySwap_swapIsIdentity() {
+			var s = new HalResourceOrArraySwap();
+			var input = new Object();
+			assertSame(input, s.swap(null, input));
+		}
+
+		@Test
+		void resourceOrArraySwap_nonMapPassesThrough() {
+			var s = new HalResourceOrArraySwap();
+			var input = "not-a-map";
+			assertSame(input, s.unswap(null, input, null));
+		}
+
+		@Test
+		void resourceOrArraySwap_mapWithScalarPassesValueThrough() {
+			var json = "{\"_embedded\":{\"x\":\"a-string-not-a-resource\"}}";
+			var r = JsonParser.DEFAULT.parse(json, HalResource.class);
+			assertEquals("a-string-not-a-resource", r.getEmbedded().get("x"));
+		}
+	}
+
+	@Nested
+	class G_Resource_toString {
+
+		@Test
+		void toString_doesNotThrow() {
+			assertNotNull(new HalResource().addLink("self", new HalLink("/a")).toString());
+		}
+
+		@Test
+		void halLink_toString_doesNotThrow() {
+			assertNotNull(new HalLink("/a").toString());
+		}
+	}
+
+	@Nested
+	class H_Resource_CompoundExample {
+
+		@Test
+		void compoundDocument_roundTrip() {
+			var order = new HalResource()
+				.addLink("self", new HalLink("/orders/123"))
+				.addLink("warehouse", new HalLink("/warehouse/56"))
+				.addLink("invoice", new HalLink("/invoices/873"))
+				.addLinks("curies", new HalLink("https://acme.example/{rel}").setName("acme").setTemplated(true))
+				.set("currency", "USD")
+				.set("status", "shipped")
+				.set("total", 30.00)
+				.addEmbedded("customer",
+					new HalResource()
+						.addLink("self", new HalLink("/customers/7"))
+						.set("name", "Kelsey")
+				);
+			assertJsonRoundTrip(order, HalResource.class);
+		}
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/jsonapi/JsonApi_RoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonapi/JsonApi_RoundTrip_Test.java
new file mode 100644
index 0000000000..10c90872ba
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonapi/JsonApi_RoundTrip_Test.java
@@ -0,0 +1,446 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonapi;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.list;
+import static org.apache.juneau.commons.utils.CollectionUtils.map;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.collections.*;
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@code juneau-bean-jsonapi} wire beans.
+ */
+class JsonApi_RoundTrip_Test {
+
+	private static final JsonSerializer SER = JsonSerializer.DEFAULT;
+	private static final JsonParser PAR = JsonParser.DEFAULT;
+
+	private static void assertJsonRoundTrip(Object bean, Class<?> type) {
+		var j1 = SER.serialize(bean);
+		var copy = PAR.parse(j1, type);
+		var j2 = SER.serialize(copy);
+		assertEquals(j1, j2, () -> "Round-trip JSON mismatch for " + type.getName() + ": " + j1);
+	}
+
+	@Nested
+	class A_Link {
+
+		@Test
+		void allFields_roundTrip() {
+			var link = new JsonApiLink()
+				.setHref("https://example.com/articles/1")
+				.setRel("canonical")
+				.setDescribedby("https://example.com/schemas/article")
+				.setTitle("Article 1")
+				.setType("application/vnd.api+json")
+				.setHreflang("en")
+				.setMeta(map("k", "v"));
+			assertJsonRoundTrip(link, JsonApiLink.class);
+		}
+
+		@Test
+		void hrefConstructor_andToString() {
+			var link = new JsonApiLink("/u");
+			assertEquals("/u", link.getHref());
+			assertNotNull(link.toString());
+		}
+
+		@Test
+		void emptyConstructor_allFieldsNull() {
+			var l = new JsonApiLink();
+			assertNull(l.getHref());
+			assertNull(l.getRel());
+			assertNull(l.getDescribedby());
+			assertNull(l.getTitle());
+			assertNull(l.getType());
+			assertNull(l.getHreflang());
+			assertNull(l.getMeta());
+		}
+	}
+
+	@Nested
+	class B_Version {
+
+		@Test
+		void roundTrip() {
+			var v = new JsonApiVersion("1.1").setMeta(map("k", "v"));
+			assertJsonRoundTrip(v, JsonApiVersion.class);
+			assertEquals("1.1", v.getVersion());
+		}
+
+		@Test
+		void emptyConstructor() {
+			var v = new JsonApiVersion();
+			assertNull(v.getVersion());
+			assertNull(v.getMeta());
+			assertNotNull(v.toString());
+		}
+	}
+
+	@Nested
+	class C_ResourceIdentifier {
+
+		@Test
+		void roundTrip() {
+			var rid = new JsonApiResourceIdentifier("people", "9").setMeta(map("k", "v"));
+			assertJsonRoundTrip(rid, JsonApiResourceIdentifier.class);
+		}
+
+		@Test
+		void settersAndDefaultConstructor() {
+			var rid = new JsonApiResourceIdentifier()
+				.setType("articles")
+				.setId("1")
+				.setMeta(map("m", 1));
+			assertEquals("articles", rid.getType());
+			assertEquals("1", rid.getId());
+			assertNotNull(rid.getMeta());
+			assertNotNull(rid.toString());
+		}
+	}
+
+	@Nested
+	class D_Relationship {
+
+		@Test
+		void singleIdentifierData_roundTrip() {
+			var rel = new JsonApiRelationship()
+				.setData(new JsonApiResourceIdentifier("people", "9"))
+				.setLinks(new LinkedHashMap<>(Map.of("self", "/articles/1/relationships/author")))
+				.setMeta(map("k", "v"));
+			assertJsonRoundTrip(rel, JsonApiRelationship.class);
+			assertNotNull(rel.getData());
+		}
+
+		@Test
+		void manyIdentifiersData_roundTrip() {
+			var rel = new JsonApiRelationship()
+				.setData(list(
+					new JsonApiResourceIdentifier("comments", "5"),
+					new JsonApiResourceIdentifier("comments", "12")
+				));
+			assertJsonRoundTrip(rel, JsonApiRelationship.class);
+		}
+
+		@Test
+		void linksAcceptStringOrLinkObject() {
+			var rel = new JsonApiRelationship()
+				.setLinks(new LinkedHashMap<>(Map.of(
+					"self", "/articles/1/relationships/author",
+					"related", new JsonApiLink("/articles/1/author").setTitle("Author")
+				)));
+			var j = SER.serialize(rel);
+			// One value should be a string, the other a JSON object.
+			assertTrue(j.contains("\"self\":\"/articles/1/relationships/author\""), () -> j);
+			assertTrue(j.contains("\"related\":{"), () -> j);
+			var back = PAR.parse(j, JsonApiRelationship.class);
+			assertInstanceOf(String.class, back.getLinks().get("self"));
+			assertInstanceOf(JsonApiLink.class, back.getLinks().get("related"));
+		}
+
+		@Test
+		void nullData_isPreserved() {
+			var rel = new JsonApiRelationship().setData((Object) null).setMeta(map("k", "v"));
+			assertNull(rel.getData());
+			assertJsonRoundTrip(rel, JsonApiRelationship.class);
+		}
+
+		@Test
+		void toString_doesNotThrow() {
+			assertNotNull(new JsonApiRelationship().toString());
+		}
+	}
+
+	@Nested
+	class E_ErrorAndSource {
+
+		@Test
+		void allFields_roundTrip() {
+			var src = new JsonApiErrorSource()
+				.setPointer("/data/attributes/title")
+				.setParameter("filter[title]")
+				.setHeader("If-Match");
+			var err = new JsonApiError()
+				.setId("err-1")
+				.setLinks(new LinkedHashMap<>(Map.of("about", "/errors/err-1")))
+				.setStatus("422")
+				.setCode("E422")
+				.setTitle("Invalid Attribute")
+				.setDetail("Title must contain at least three characters.")
+				.setSource(src)
+				.setMeta(map("k", "v"));
+			assertJsonRoundTrip(err, JsonApiError.class);
+		}
+
+		@Test
+		void source_emptyConstructorAndToString() {
+			var s = new JsonApiErrorSource();
+			assertNull(s.getPointer());
+			assertNull(s.getParameter());
+			assertNull(s.getHeader());
+			assertNotNull(s.toString());
+		}
+
+		@Test
+		void error_emptyConstructorAndToString() {
+			var e = new JsonApiError();
+			assertNull(e.getId());
+			assertNull(e.getLinks());
+			assertNull(e.getStatus());
+			assertNull(e.getCode());
+			assertNull(e.getTitle());
+			assertNull(e.getDetail());
+			assertNull(e.getSource());
+			assertNull(e.getMeta());
+			assertNotNull(e.toString());
+		}
+	}
+
+	@Nested
+	class F_Resource {
+
+		@Test
+		void allFields_roundTrip() {
+			var res = new JsonApiResource("articles", "1")
+				.setAttributes(JsonMap.of("title", "JSON:API paints my bikeshed!"))
+				.putAttribute("body", "lorem")
+				.putRelationship("author",
+					new JsonApiRelationship().setData(new JsonApiResourceIdentifier("people", "9"))
+				)
+				.setLinks(new LinkedHashMap<>(Map.of("self", "/articles/1")))
+				.setMeta(map("rev", "abc"));
+			assertJsonRoundTrip(res, JsonApiResource.class);
+		}
+
+		@Test
+		void putAttribute_setsBackingMapIfNull() {
+			var r = new JsonApiResource().putAttribute("k", "v");
+			assertEquals("v", r.getAttributes().get("k"));
+		}
+
+		@Test
+		void putRelationship_setsBackingMapIfNull() {
+			var r = new JsonApiResource().putRelationship("author",
+				new JsonApiRelationship().setData(new JsonApiResourceIdentifier("people", "9")));
+			assertNotNull(r.getRelationships().get("author"));
+		}
+
+		@Test
+		void typeIsPlainString_notDiscriminator() {
+			// JSON:API ‘type’ is just data. Round-tripping a custom value must not require a Java dictionary entry.
+			var r = new JsonApiResource("very-custom-resource-not-in-any-registry", "1");
+			var j = SER.serialize(r);
+			assertTrue(j.contains("\"type\":\"very-custom-resource-not-in-any-registry\""), () -> j);
+			var back = PAR.parse(j, JsonApiResource.class);
+			assertEquals("very-custom-resource-not-in-any-registry", back.getType());
+		}
+
+		@Test
+		void settersAndDefaultConstructor() {
+			var r = new JsonApiResource()
+				.setType("a")
+				.setId("1")
+				.setAttributes(map("k", "v"))
+				.setRelationships(map("r", new JsonApiRelationship()))
+				.setLinks(new LinkedHashMap<>(Map.of("self", "/a/1")))
+				.setMeta(map("m", 1));
+			assertEquals("a", r.getType());
+			assertEquals("1", r.getId());
+			assertNotNull(r.getAttributes());
+			assertNotNull(r.getRelationships());
+			assertNotNull(r.getLinks());
+			assertNotNull(r.getMeta());
+			assertNotNull(r.toString());
+		}
+	}
+
+	@Nested
+	class G_Document {
+
+		@Test
+		void singleResource_roundTrip() {
+			var doc = new JsonApiDocument()
+				.setData(new JsonApiResource("articles", "1"))
+				.setJsonapi(new JsonApiVersion("1.1"));
+			assertJsonRoundTrip(doc, JsonApiDocument.class);
+		}
+
+		@Test
+		void manyResources_roundTrip() {
+			var doc = new JsonApiDocument().setData(list(
+				new JsonApiResource("articles", "1"),
+				new JsonApiResource("articles", "2")
+			));
+			assertJsonRoundTrip(doc, JsonApiDocument.class);
+		}
+
+		@Test
+		void errorsDocument_roundTrip() {
+			var doc = new JsonApiDocument().addErrors(
+				new JsonApiError().setStatus("422").setTitle("Bad")
+			);
+			assertJsonRoundTrip(doc, JsonApiDocument.class);
+		}
+
+		@Test
+		void compoundDocumentExample_roundTrip() {
+			// Spec example: top-level data + included.
+			var author = new JsonApiResource("people", "9")
+				.setAttributes(JsonMap.of("firstName", "Dan", "lastName", "Gebhardt"))
+				.setLinks(new LinkedHashMap<>(Map.of("self", "/people/9")));
+			var comment1 = new JsonApiResource("comments", "5")
+				.setAttributes(JsonMap.of("body", "First!"));
+			var comment2 = new JsonApiResource("comments", "12")
+				.setAttributes(JsonMap.of("body", "I like XML better"));
+			var article = new JsonApiResource("articles", "1")
+				.setAttributes(JsonMap.of("title", "JSON:API paints my bikeshed!"))
+				.putRelationship("author",
+					new JsonApiRelationship()
+						.setData(new JsonApiResourceIdentifier("people", "9"))
+						.setLinks(new LinkedHashMap<>(Map.of(
+							"self", "/articles/1/relationships/author",
+							"related", "/articles/1/author"
+						)))
+				)
+				.putRelationship("comments",
+					new JsonApiRelationship().setData(list(
+						new JsonApiResourceIdentifier("comments", "5"),
+						new JsonApiResourceIdentifier("comments", "12")
+					))
+				)
+				.setLinks(new LinkedHashMap<>(Map.of("self", "/articles/1")));
+			var doc = new JsonApiDocument()
+				.setLinks(new LinkedHashMap<>(Map.of("self", "/articles")))
+				.setJsonapi(new JsonApiVersion("1.1"))
+				.setData(article)
+				.addIncluded(author, comment1, comment2)
+				.setMeta(map("count", 1));
+			assertJsonRoundTrip(doc, JsonApiDocument.class);
+		}
+
+		@Test
+		void setData_allOverloads() {
+			var doc1 = new JsonApiDocument().setData((Object) "x");
+			assertEquals("x", doc1.getData());
+			var doc2 = new JsonApiDocument().setData(new JsonApiResource("a", "1"));
+			assertInstanceOf(JsonApiResource.class, doc2.getData());
+			var doc3 = new JsonApiDocument().setData(list(new JsonApiResource("a", "1")));
+			assertInstanceOf(List.class, doc3.getData());
+		}
+
+		@Test
+		void setIncludedAndErrors_replacesBackingList() {
+			var doc = new JsonApiDocument()
+				.setIncluded(list(new JsonApiResource("a", "1")))
+				.setErrors(list(new JsonApiError().setTitle("x")));
+			assertEquals(1, doc.getIncluded().size());
+			assertEquals(1, doc.getErrors().size());
+		}
+
+		@Test
+		void validate_throwsWhenDataAndErrorsCoexist() {
+			var doc = new JsonApiDocument()
+				.setData(new JsonApiResource("a", "1"))
+				.addErrors(new JsonApiError());
+			assertThrows(IllegalStateException.class, doc::validate);
+		}
+
+		@Test
+		void validate_throwsWhenEmpty() {
+			var doc = new JsonApiDocument();
+			assertThrows(IllegalStateException.class, doc::validate);
+		}
+
+		@Test
+		void validate_okWithJustMeta() {
+			var doc = new JsonApiDocument().setMeta(map("k", "v"));
+			assertSame(doc, doc.validate());
+		}
+
+		@Test
+		void validate_okWithJustData() {
+			var doc = new JsonApiDocument().setData(new JsonApiResource("a", "1"));
+			assertSame(doc, doc.validate());
+		}
+
+		@Test
+		void validate_okWithJustErrors() {
+			var doc = new JsonApiDocument().addErrors(new JsonApiError().setTitle("x"));
+			assertSame(doc, doc.validate());
+		}
+
+		@Test
+		void addErrors_andAddIncluded_appendToBackingLists() {
+			var doc = new JsonApiDocument()
+				.addErrors(new JsonApiError().setTitle("a"))
+				.addErrors(new JsonApiError().setTitle("b"));
+			assertEquals(2, doc.getErrors().size());
+			doc.addIncluded(new JsonApiResource("a", "1")).addIncluded(new JsonApiResource("a", "2"));
+			assertEquals(2, doc.getIncluded().size());
+		}
+
+		@Test
+		void links_emptyConstructorTrip() {
+			var d = new JsonApiDocument();
+			assertNull(d.getLinks());
+			assertNull(d.getMeta());
+			assertNull(d.getJsonapi());
+			assertNull(d.getData());
+			assertNull(d.getErrors());
+			assertNull(d.getIncluded());
+			assertNotNull(d.toString());
+		}
+	}
+
+	@Nested
+	class H_LinkSwap_directInvocation {
+
+		@Test
+		void unswap_nullIsNull() {
+			var swap = new JsonApiLinkOrStringSwap();
+			assertNull(swap.unswap(null, null, null));
+		}
+
+		@Test
+		void swap_isIdentity() {
+			var swap = new JsonApiLinkOrStringSwap();
+			var input = new Object();
+			assertSame(input, swap.swap(null, input));
+		}
+
+		@Test
+		void unswap_nonMapPassesThrough() {
+			var swap = new JsonApiLinkOrStringSwap();
+			var input = "raw-string";
+			assertSame(input, swap.unswap(null, input, null));
+		}
+
+		@Test
+		void unswap_mapWithScalarEntry_passesValueThrough() {
+			// A nested map (object) that itself contains a scalar at the "x" key still parses to a JsonApiLink
+			// (the per-entry rule); a more direct scalar-passthrough is hit via the JsonApiDocument case below.
+			var json = "{\"links\":{\"self\":\"/x\"}}";
+			var rel = JsonParser.DEFAULT.parse(json, JsonApiRelationship.class);
+			assertEquals("/x", rel.getLinks().get("self"));
+		}
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/jsonpatch/JsonPatch_RoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonpatch/JsonPatch_RoundTrip_Test.java
new file mode 100644
index 0000000000..b6d999b59f
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonpatch/JsonPatch_RoundTrip_Test.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.jsonpatch;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.list;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.collections.*;
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@code juneau-bean-jsonpatch} wire beans.
+ */
+class JsonPatch_RoundTrip_Test {
+
+	private static final JsonSerializer SER =
+		JsonSerializer.create()
+			.addBeanTypes()
+			.addRootType()
+			.typePropertyName(JsonPatchOperation.class, "op")
+			.build();
+
+	private static final JsonParser PAR =
+		JsonParser.create()
+			.typePropertyName(JsonPatchOperation.class, "op")
+			.build();
+
+	private static void assertJsonRoundTrip(Object bean, Class<?> type) {
+		var j1 = SER.serialize(bean);
+		var copy = PAR.parse(j1, type);
+		var j2 = SER.serialize(copy);
+		assertEquals(j1, j2, () -> "Round-trip JSON mismatch for " + type.getName() + ": " + j1);
+	}
+
+	@Nested
+	class A_IndividualOps {
+
+		@Test
+		void addOp_roundTrip() {
+			var op = new AddOp("/a/b/c", JsonMap.of("k", "v"));
+			assertJsonRoundTrip(op, JsonPatchOperation.class);
+		}
+
+		@Test
+		void removeOp_roundTrip() {
+			var op = new RemoveOp("/a/b/c");
+			assertJsonRoundTrip(op, JsonPatchOperation.class);
+		}
+
+		@Test
+		void replaceOp_roundTrip() {
+			var op = new ReplaceOp("/a/b/c", 42);
+			assertJsonRoundTrip(op, JsonPatchOperation.class);
+		}
+
+		@Test
+		void moveOp_roundTrip() {
+			var op = new MoveOp("/a/b/c", "/a/b/d");
+			assertJsonRoundTrip(op, JsonPatchOperation.class);
+		}
+
+		@Test
+		void copyOp_roundTrip() {
+			var op = new CopyOp("/a/b/c", "/a/b/e");
+			assertJsonRoundTrip(op, JsonPatchOperation.class);
+		}
+
+		@Test
+		void testOp_roundTrip() {
+			var op = new TestOp("/a/b/c", "foo");
+			assertJsonRoundTrip(op, JsonPatchOperation.class);
+		}
+	}
+
+	@Nested
+	class B_OpsContainNullValuesAndDefaultConstructors {
+
+		@Test
+		void addOp_defaultsAreNull() {
+			var op = new AddOp();
+			assertNull(op.getPath());
+			assertNull(op.getValue());
+			op.setValue("v").setPath("/x");
+			assertEquals("/x", op.getPath());
+			assertEquals("v", op.getValue());
+		}
+
+		@Test
+		void removeOp_defaultsAreNull() {
+			var op = new RemoveOp();
+			assertNull(op.getPath());
+			op.setPath("/x");
+			assertEquals("/x", op.getPath());
+		}
+
+		@Test
+		void replaceOp_defaultsAreNull() {
+			var op = new ReplaceOp();
+			assertNull(op.getPath());
+			assertNull(op.getValue());
+			op.setValue(99).setPath("/x");
+			assertEquals("/x", op.getPath());
+			assertEquals(99, op.getValue());
+		}
+
+		@Test
+		void moveOp_defaultsAreNull() {
+			var op = new MoveOp();
+			assertNull(op.getPath());
+			assertNull(op.getFrom());
+			op.setFrom("/y").setPath("/x");
+			assertEquals("/x", op.getPath());
+			assertEquals("/y", op.getFrom());
+		}
+
+		@Test
+		void copyOp_defaultsAreNull() {
+			var op = new CopyOp();
+			assertNull(op.getPath());
+			assertNull(op.getFrom());
+			op.setFrom("/y").setPath("/x");
+			assertEquals("/x", op.getPath());
+			assertEquals("/y", op.getFrom());
+		}
+
+		@Test
+		void testOp_defaultsAreNull() {
+			var op = new TestOp();
+			assertNull(op.getPath());
+			assertNull(op.getValue());
+			op.setValue("v").setPath("/x");
+			assertEquals("/x", op.getPath());
+			assertEquals("v", op.getValue());
+		}
+	}
+
+	@Nested
+	class C_PolymorphicDispatch {
+
+		@Test
+		void allSixOps_roundTripViaPatchDocument() {
+			var patch = new JsonPatch()
+				.append(new AddOp("/a/b/c", "foo"))
+				.append(new RemoveOp("/a/b/c"))
+				.append(new ReplaceOp("/a/b/c", 42))
+				.append(new MoveOp("/a/b/c", "/a/b/d"))
+				.append(new CopyOp("/a/b/c", "/a/b/e"))
+				.append(new TestOp("/a/b/c", "foo"));
+			assertJsonRoundTrip(patch, JsonPatch.class);
+		}
+
+		@Test
+		void parserPicksRightSubclassForEachOp() {
+			var patch = new JsonPatch()
+				.append(new AddOp("/a", 1))
+				.append(new RemoveOp("/b"))
+				.append(new ReplaceOp("/c", "x"))
+				.append(new MoveOp("/d", "/e"))
+				.append(new CopyOp("/f", "/g"))
+				.append(new TestOp("/h", true));
+			var j = SER.serialize(patch);
+			var back = PAR.parse(j, JsonPatch.class);
+			assertEquals(6, back.size());
+			assertInstanceOf(AddOp.class, back.get(0));
+			assertInstanceOf(RemoveOp.class, back.get(1));
+			assertInstanceOf(ReplaceOp.class, back.get(2));
+			assertInstanceOf(MoveOp.class, back.get(3));
+			assertInstanceOf(CopyOp.class, back.get(4));
+			assertInstanceOf(TestOp.class, back.get(5));
+			assertEquals("/a", back.get(0).getPath());
+		}
+
+		@Test
+		void wireFormat_isJsonArrayOfOperationObjects() {
+			var patch = new JsonPatch().append(new AddOp("/x", 1));
+			var j = SER.serialize(patch);
+			assertTrue(j.startsWith("["), () -> "Top-level should be a JSON array: " + j);
+			assertTrue(j.contains("\"op\":\"add\""), () -> j);
+			assertTrue(j.contains("\"path\":\"/x\""), () -> j);
+			assertTrue(j.contains("\"value\":1"), () -> j);
+		}
+	}
+
+	@Nested
+	class D_PatchConstruction {
+
+		@Test
+		void varargsConstructor_andAppend() {
+			var p = new JsonPatch(new AddOp("/a", 1), new RemoveOp("/b"));
+			assertEquals(2, p.size());
+			p.append(new TestOp("/c", "v"));
+			assertEquals(3, p.size());
+		}
+
+		@Test
+		void emptyConstructor() {
+			var p = new JsonPatch();
+			assertTrue(p.isEmpty());
+		}
+
+		@Test
+		void roundTrips_aListReadFromTheSpec() {
+			// Mirrors the JSON Patch RFC 6902 §3 example.
+			var p = new JsonPatch().append(
+				new TestOp("/a/b/c", "foo"),
+				new RemoveOp("/a/b/c"),
+				new AddOp("/a/b/c", list("foo", "bar")),
+				new ReplaceOp("/a/b/c", 42),
+				new MoveOp("/a/b/d", "/a/b/c"),
+				new CopyOp("/a/b/e", "/a/b/d")
+			);
+			assertJsonRoundTrip(p, JsonPatch.class);
+		}
+
+		@Test
+		void operationToStringDoesNotThrow() {
+			assertNotNull(new AddOp("/x", 1).toString());
+			assertNotNull(new RemoveOp("/x").toString());
+		}
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator_Test.java
index 7b0929b4b7..b212602e3e 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator_Test.java
@@ -96,7 +96,7 @@ class JsonSchemaBeanGenerator_Test extends TestBase {
 	}
 
 	@Test void a08_toBeanConversion() {
-		var map = new JsonMap(Json5Map.ofText("{type:'integer',format:'int32','$comment':'c',deprecated:true}"));
+		var map = new JsonMap(Json5Map.ofString("{type:'integer',format:'int32','$comment':'c',deprecated:true}"));
 		var bean = JsonSchemaBeanGenerator.toBean(map);
 		assertEquals("integer", bean.getTypeAsJsonType().toString().toLowerCase(Locale.ROOT));
 		assertEquals("int32", bean.getFormat());
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/rfc7807/Problem_RoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/rfc7807/Problem_RoundTrip_Test.java
new file mode 100644
index 0000000000..0b86db6275
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/rfc7807/Problem_RoundTrip_Test.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.bean.rfc7807;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@link Problem} (RFC 7807 {@code application/problem+json}).
+ */
+class Problem_RoundTrip_Test {
+
+	private static final JsonSerializer SER = JsonSerializer.DEFAULT;
+	private static final JsonSerializer READABLE = JsonSerializer.DEFAULT_READABLE;
+	private static final JsonParser PAR = JsonParser.DEFAULT;
+
+	@Nested
+	class A_basicTests {
+
+		@Test
+		void a01_basicProperties() throws Exception {
+			var a = new Problem()
+				.setType(new URI("https://example.com/probs/out-of-credit"))
+				.setTitle("You do not have enough credit.")
+				.setStatus(403)
+				.setDetail("Your current balance is 30, but that costs 50.")
+				.setInstance(new URI("/account/12345/msgs/abc"));
+
+			assertEquals(new URI("https://example.com/probs/out-of-credit"), a.getType());
+			assertEquals("You do not have enough credit.", a.getTitle());
+			assertEquals(Integer.valueOf(403), a.getStatus());
+			assertEquals("Your current balance is 30, but that costs 50.", a.getDetail());
+			assertEquals(new URI("/account/12345/msgs/abc"), a.getInstance());
+		}
+
+		@Test
+		void a02_fluentSetters_returnThis() {
+			var a = new Problem();
+			assertSame(a, a.setType(null));
+			assertSame(a, a.setTitle(null));
+			assertSame(a, a.setStatus(null));
+			assertSame(a, a.setDetail(null));
+			assertSame(a, a.setInstance(null));
+			assertSame(a, a.set("k", "v"));
+		}
+
+		@Test
+		void a03_typeNullByDefault() {
+			var a = new Problem();
+			assertNull(a.getType());
+			assertNull(a.getTitle());
+			assertNull(a.getStatus());
+			assertNull(a.getDetail());
+			assertNull(a.getInstance());
+		}
+
+		@Test
+		void a04_typeOrDefault_returnsAboutBlank_whenUnset() {
+			var a = new Problem();
+			assertEquals(URI.create("about:blank"), a.getTypeOrDefault());
+			assertEquals(Problem.DEFAULT_TYPE, a.getTypeOrDefault());
+		}
+
+		@Test
+		void a05_typeOrDefault_returnsSetValue_whenPresent() throws Exception {
+			var a = new Problem().setType(new URI("https://example.com/x"));
+			assertEquals(new URI("https://example.com/x"), a.getTypeOrDefault());
+		}
+
+		@Test
+		void a06_fromStatus_populatesThreeFields() {
+			var a = Problem.fromStatus(404, "Not Found", "Resource missing");
+			assertNull(a.getType());
+			assertEquals("Not Found", a.getTitle());
+			assertEquals(Integer.valueOf(404), a.getStatus());
+			assertEquals("Resource missing", a.getDetail());
+			assertNull(a.getInstance());
+		}
+
+		@Test
+		void a07_fromStatus_handlesNullStrings() {
+			var a = Problem.fromStatus(500, null, null);
+			assertEquals(Integer.valueOf(500), a.getStatus());
+			assertNull(a.getTitle());
+			assertNull(a.getDetail());
+		}
+	}
+
+	@Nested
+	class B_serialization {
+
+		@Test
+		void b01_allFiveFields_roundTripReadable() throws Exception {
+			var a = new Problem()
+				.setType(new URI("https://example.com/probs/out-of-credit"))
+				.setTitle("You do not have enough credit.")
+				.setStatus(403)
+				.setDetail("Your current balance is 30, but that costs 50.")
+				.setInstance(new URI("/account/12345/msgs/abc"));
+
+			var j1 = READABLE.serialize(a);
+			var copy = PAR.parse(j1, Problem.class);
+			assertEquals(a.getType(), copy.getType());
+			assertEquals(a.getTitle(), copy.getTitle());
+			assertEquals(a.getStatus(), copy.getStatus());
+			assertEquals(a.getDetail(), copy.getDetail());
+			assertEquals(a.getInstance(), copy.getInstance());
+			var j2 = READABLE.serialize(copy);
+			assertEquals(j1, j2);
+		}
+
+		@Test
+		void b02_allFiveFields_roundTripCompact() throws Exception {
+			var a = new Problem()
+				.setType(new URI("https://example.com/x"))
+				.setTitle("t")
+				.setStatus(400)
+				.setDetail("d")
+				.setInstance(new URI("/i"));
+
+			var j1 = SER.serialize(a);
+			assertEquals(
+				"{\"detail\":\"d\",\"instance\":\"/i\",\"status\":400,\"title\":\"t\",\"type\":\"https://example.com/x\"}",
+				j1
+			);
+			var copy = PAR.parse(j1, Problem.class);
+			assertEquals(j1, SER.serialize(copy));
+		}
+
+		@Test
+		void b03_emptyProblem_serializesAsEmptyObject() {
+			var a = new Problem();
+			assertEquals("{}", SER.serialize(a));
+		}
+
+		@Test
+		void b04_typeAbsent_doesNotAppearInJson() {
+			var a = new Problem().setTitle("t").setStatus(500);
+			var json = SER.serialize(a);
+			assertFalse(json.contains("type"), () -> "JSON should not contain 'type': " + json);
+			assertFalse(json.contains("about:blank"), () -> "JSON should not contain 'about:blank': " + json);
+		}
+
+		@Test
+		void b05_typeOrDefault_doesNotMutateField() {
+			var a = new Problem().setTitle("t");
+			a.getTypeOrDefault();
+			assertNull(a.getType(), "getTypeOrDefault() must not eagerly populate the type field");
+			assertFalse(SER.serialize(a).contains("type"));
+		}
+
+		@Test
+		void b06_fromStatus_exactJson_404() {
+			var a = Problem.fromStatus(404, "Not Found", "Resource missing");
+			assertEquals(
+				"{\"detail\":\"Resource missing\",\"status\":404,\"title\":\"Not Found\"}",
+				SER.serialize(a)
+			);
+		}
+
+		@Test
+		void b07_fromStatus_exactJson_500() {
+			var a = Problem.fromStatus(500, "Internal Server Error", "boom");
+			assertEquals(
+				"{\"detail\":\"boom\",\"status\":500,\"title\":\"Internal Server Error\"}",
+				SER.serialize(a)
+			);
+		}
+
+		@Test
+		void b08_statusOmitted_whenNull() {
+			var a = new Problem().setTitle("t");
+			var json = SER.serialize(a);
+			assertEquals("{\"title\":\"t\"}", json);
+		}
+
+		@Test
+		void b09_contentType_constant() {
+			assertEquals("application/problem+json", ContentType.APPLICATION_PROBLEM_JSON.getValue());
+			assertEquals("application/problem+xml", ContentType.APPLICATION_PROBLEM_XML.getValue());
+		}
+
+		@Test
+		void b10_contentType_roundTripsWhenSetExplicitly() {
+			var ct = ContentType.of("application/problem+json");
+			assertEquals(ContentType.APPLICATION_PROBLEM_JSON.getValue(), ct.getValue());
+		}
+	}
+
+	@Nested
+	class C_extraProperties {
+
+		@Test
+		void c01_extensionsFlatten_intoTopLevelJson() throws Exception {
+			var a = new Problem()
+				.setType(new URI("https://example.com/x"))
+				.setTitle("t")
+				.set("balance", 30)
+				.set("accounts", new String[]{"/account/12345", "/account/67890"});
+
+			var json = SER.serialize(a);
+			assertTrue(json.contains("\"balance\":30"), () -> "Missing flat balance: " + json);
+			assertTrue(json.contains("\"accounts\":[\"/account/12345\",\"/account/67890\"]"),
+				() -> "Missing flat accounts: " + json);
+			assertFalse(json.contains("\"extra\""), () -> "Extension members must not nest under 'extra': " + json);
+			assertFalse(json.contains("\"extensions\""),
+				() -> "Extension members must not nest under 'extensions': " + json);
+		}
+
+		@Test
+		void c02_extensionsRoundTrip_backIntoExtraMap() throws Exception {
+			var a = new Problem()
+				.setType(new URI("https://example.com/x"))
+				.setTitle("You do not have enough credit.")
+				.setStatus(403)
+				.set("balance", 30)
+				.set("accountId", "abc");
+
+			var json = SER.serialize(a);
+			var copy = PAR.parse(json, Problem.class);
+
+			assertEquals(30, ((Number) copy.get("balance")).intValue());
+			assertEquals("abc", copy.get("accountId"));
+			assertTrue(copy.extraKeys().contains("balance"));
+			assertTrue(copy.extraKeys().contains("accountId"));
+			assertEquals(json, SER.serialize(copy));
+		}
+
+		@Test
+		void c03_extraKeys_emptyOnFreshBean() {
+			var a = new Problem();
+			assertTrue(a.extraKeys().isEmpty());
+			assertNull(a.get("anything"));
+		}
+
+		@Test
+		void c04_extensionsRoundTripFromJson_intoExtra() throws Exception {
+			var json = "{\"type\":\"https://example.com/x\",\"title\":\"t\",\"status\":403,"
+				+ "\"detail\":\"d\",\"instance\":\"/i\",\"balance\":30,\"answer\":42}";
+			var a = PAR.parse(json, Problem.class);
+
+			assertEquals(new URI("https://example.com/x"), a.getType());
+			assertEquals("t", a.getTitle());
+			assertEquals(Integer.valueOf(403), a.getStatus());
+			assertEquals("d", a.getDetail());
+			assertEquals(new URI("/i"), a.getInstance());
+			assertEquals(30, ((Number) a.get("balance")).intValue());
+			assertEquals(42, ((Number) a.get("answer")).intValue());
+		}
+
+		@Test
+		void c05_extensionsSetTwice_overwritesPreviousValue() {
+			var a = new Problem().set("k", "v1").set("k", "v2");
+			assertEquals("v2", a.get("k"));
+		}
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborCompliance_Test.java b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborCompliance_Test.java
index 22fc4a79ef..4cd4d15147 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborCompliance_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborCompliance_Test.java
@@ -90,7 +90,7 @@ class CborCompliance_Test extends TestBase {
 	void g17_simpleArray() throws Exception { enc(ints(1, 2, 3), "83 01 02 03"); }
 
 	@Test
-	void g18_emptyMap() throws Exception { enc(JsonMap.ofText("{}"), "A0"); }
+	void g18_emptyMap() throws Exception { enc(JsonMap.ofString("{}"), "A0"); }
 
 	@Test
 	void g19_false() throws Exception { enc(false, "F4"); }
diff --git a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborOutputStream_Test.java b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborOutputStream_Test.java
index 3666a90edc..602ce1b63a 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborOutputStream_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborOutputStream_Test.java
@@ -219,12 +219,12 @@ class CborOutputStream_Test extends TestBase {
 
 	@Test
 	void a29_emptyMap() throws Exception {
-		enc(JsonMap.ofText("{}"), "A0");
+		enc(JsonMap.ofString("{}"), "A0");
 	}
 
 	@Test
 	void a30_shortMap() throws Exception {
-		enc(JsonMap.ofText("{\"a\":1,\"b\":2}"), "A2 61 61 01 61 62 02");
+		enc(JsonMap.ofString("{\"a\":1,\"b\":2}"), "A2 61 61 01 61 62 02");
 	}
 
 	@Test
diff --git a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborRoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborRoundTrip_Test.java
index 9c1f4a737f..5414907e9f 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborRoundTrip_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborRoundTrip_Test.java
@@ -130,7 +130,7 @@ class CborRoundTrip_Test extends TestBase {
 	void e13_emptyCollectionsRoundTrip() throws Exception {
 		var emptyList = CborParser.DEFAULT.parse(CborSerializer.DEFAULT.serialize(list()), JsonList.class);
 		assertTrue(emptyList.isEmpty());
-		var emptyMap = CborParser.DEFAULT.parse(CborSerializer.DEFAULT.serialize(JsonMap.ofText("{}")), JsonMap.class);
+		var emptyMap = CborParser.DEFAULT.parse(CborSerializer.DEFAULT.serialize(JsonMap.ofString("{}")), JsonMap.class);
 		assertTrue(emptyMap.isEmpty());
 	}
 
diff --git a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborSerializer_Test.java b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborSerializer_Test.java
index a9f69d806b..d37bd9daf6 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborSerializer_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborSerializer_Test.java
@@ -55,7 +55,7 @@ class CborSerializer_Test extends TestBase {
 		test("a", "61 61");
 		test(ints(), "80");
 		test(ints(1, 2, 3), "83 01 02 03");
-		test(JsonMap.ofText("{}"), "A0");
+		test(JsonMap.ofString("{}"), "A0");
 	}
 
 	public static class Person {
@@ -190,7 +190,7 @@ class CborSerializer_Test extends TestBase {
 
 	@Test
 	void c13_emptyBean() throws Exception {
-		var bytes = CborSerializer.DEFAULT.serialize(JsonMap.ofText("{}"));
+		var bytes = CborSerializer.DEFAULT.serialize(JsonMap.ofString("{}"));
 		assertEquals("A0", toSpacedHex(bytes));
 		var parsed = CborParser.DEFAULT.parse(bytes, JsonMap.class);
 		assertTrue(parsed.isEmpty());
@@ -199,7 +199,7 @@ class CborSerializer_Test extends TestBase {
 	@Test
 	void c14_emptyCollections() throws Exception {
 		test(ints(), "80");
-		test(JsonMap.ofText("{}"), "A0");
+		test(JsonMap.ofString("{}"), "A0");
 	}
 
 	@Test
diff --git a/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java b/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java
index f3f639d1f6..79d4607089 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java
@@ -62,15 +62,21 @@ class MarshalledList_Test extends TestBase {
 		assertEquals("a", l.getString(0));
 	}
 
-	@Test void a05_parseViaOfText() throws Exception {
-		var l = MarshalledList.ofText("[1,2,3]", Json5Parser.DEFAULT);
+	@Test void a05_parseViaOfString() throws Exception {
+		var l = MarshalledList.ofString("[1,2,3]", Json5Parser.DEFAULT);
 		assertNotNull(l);
 		assertEquals(3, l.size());
 		assertEquals(2, l.getInt(1));
 	}
 
-	@Test void a06_parseViaOfTextNullInput() throws Exception {
-		assertNull(MarshalledList.ofText((CharSequence)null, Json5Parser.DEFAULT));
+	@Test void a06_parseViaOfStringNullInput() throws Exception {
+		assertNull(MarshalledList.ofString((CharSequence)null, Json5Parser.DEFAULT));
+	}
+
+	@Test void a06b_ofTextAliasStillWorks() throws Exception {
+		var l = MarshalledList.ofString("[1,2,3]", Json5Parser.DEFAULT);
+		assertNotNull(l);
+		assertEquals(3, l.size());
 	}
 
 	@Test void a07_getMapReturnsNeutralType() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java
index cb78504194..3036e0578a 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java
@@ -62,15 +62,21 @@ class MarshalledMap_Test extends TestBase {
 		assertEquals(1, m.getInt("a"));
 	}
 
-	@Test void a05_parseViaOfText() throws Exception {
-		var m = MarshalledMap.ofText("{a:1,b:'two'}", Json5Parser.DEFAULT);
+	@Test void a05_parseViaOfString() throws Exception {
+		var m = MarshalledMap.ofString("{a:1,b:'two'}", Json5Parser.DEFAULT);
 		assertNotNull(m);
 		assertEquals(1, m.getInt("a"));
 		assertEquals("two", m.getString("b"));
 	}
 
-	@Test void a06_parseViaOfTextNullInput() throws Exception {
-		assertNull(MarshalledMap.ofText((CharSequence)null, Json5Parser.DEFAULT));
+	@Test void a06_parseViaOfStringNullInput() throws Exception {
+		assertNull(MarshalledMap.ofString((CharSequence)null, Json5Parser.DEFAULT));
+	}
+
+	@Test void a06b_ofTextAliasStillWorks() throws Exception {
+		var m = MarshalledMap.ofString("{a:1}", Json5Parser.DEFAULT);
+		assertNotNull(m);
+		assertEquals(1, m.getInt("a"));
 	}
 
 	@Test void a07_getMapReturnsNeutralType() {
@@ -92,7 +98,7 @@ class MarshalledMap_Test extends TestBase {
 	}
 
 	@Test void a08b_parseProducesJson5MapInternally() throws Exception {
-		var m = MarshalledMap.ofText("{nested:{x:1}}", Json5Parser.DEFAULT);
+		var m = MarshalledMap.ofString("{nested:{x:1}}", Json5Parser.DEFAULT);
 		var nested = m.getMap("nested");
 		assertTrue(nested instanceof Json5Map, "Json5Parser produces Json5Map for nested objects");
 		assertFalse(nested instanceof JsonMap, "Strict-JSON JsonMap is not used by Json5Parser");
@@ -158,7 +164,7 @@ class MarshalledMap_Test extends TestBase {
 	}
 
 	@Test void a17_castToMapInterfaceReturnsNeutralFallback() throws Exception {
-		var m = MarshalledMap.ofText("{a:1,b:2}", Json5Parser.DEFAULT);
+		var m = MarshalledMap.ofString("{a:1,b:2}", Json5Parser.DEFAULT);
 		// Casting from a MarshalledMap to the Map interface should fall back to MarshalledMap.
 		Map<?,?> casted = m.cast(Map.class);
 		assertNotNull(casted);
diff --git a/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java b/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
index 09c9e7b640..e5535207fc 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
@@ -799,8 +799,8 @@ class OpenApiPartSerializer_Test extends TestBase {
 		var ps = tObject().allowEmptyValue().build();
 		assertEquals("f1=1,f2=2,f3=true", serialize(ps, new H1("1",2,true)));
 		assertEquals("", serialize(ps, new H1(null,null,null)));
-		assertEquals("f1=1,f2=2,f3=true", serialize(ps, Json5Map.ofText("{f1:'1',f2:2,f3:true}")));
-		assertEquals("f1=null,f2=null,f3=null", serialize(ps, Json5Map.ofText("{f1:null,f2:null,f3:null}")));
+		assertEquals("f1=1,f2=2,f3=true", serialize(ps, Json5Map.ofString("{f1:'1',f2:2,f3:true}")));
+		assertEquals("f1=null,f2=null,f3=null", serialize(ps, Json5Map.ofString("{f1:null,f2:null,f3:null}")));
 		assertEquals("null", serialize(ps, null));
 	}
 
@@ -808,8 +808,8 @@ class OpenApiPartSerializer_Test extends TestBase {
 		var ps = T_OBJECT_UON;
 		assertEquals("(f1='1',f2=2,f3=true)", serialize(ps, new H1("1",2,true)));
 		assertEquals("()", serialize(ps, new H1(null,null,null)));
-		assertEquals("(f1='1',f2=2,f3=true)", serialize(ps, Json5Map.ofText("{f1:'1',f2:2,f3:true}")));
-		assertEquals("(f1=null,f2=null,f3=null)", serialize(ps, Json5Map.ofText("{f1:null,f2:null,f3:null}")));
+		assertEquals("(f1='1',f2=2,f3=true)", serialize(ps, Json5Map.ofString("{f1:'1',f2:2,f3:true}")));
+		assertEquals("(f1=null,f2=null,f3=null)", serialize(ps, Json5Map.ofString("{f1:null,f2:null,f3:null}")));
 		assertEquals("null", serialize(ps, null));
 	}
 
@@ -817,30 +817,30 @@ class OpenApiPartSerializer_Test extends TestBase {
 		var ps = tArray(tObject().allowEmptyValue()).build();
 		assertEquals("f1=1\\,f2=2\\,f3=true,,null", serialize(ps, a(new H1("1",2,true),new H1(null,null,null),null)));
 		assertEquals("f1=1\\,f2=2\\,f3=true,,null", serialize(ps, l(new H1("1",2,true),new H1(null,null,null),null)));
-		assertEquals("f1=1\\,f2=2\\,f3=true,f1=null\\,f2=null\\,f3=null,null", serialize(ps, a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
-		assertEquals("f1=1\\,f2=2\\,f3=true,f1=null\\,f2=null\\,f3=null,null", serialize(ps, l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
-		assertEquals("f1=1\\,f2=2\\,f3=true,f1=1\\,f2=2\\,f3=true,null", serialize(ps, ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
-		assertEquals("f1=1\\,f2=2\\,f3=true,f1=1\\,f2=2\\,f3=true,null", serialize(ps, l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
+		assertEquals("f1=1\\,f2=2\\,f3=true,f1=null\\,f2=null\\,f3=null,null", serialize(ps, a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+		assertEquals("f1=1\\,f2=2\\,f3=true,f1=null\\,f2=null\\,f3=null,null", serialize(ps, l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+		assertEquals("f1=1\\,f2=2\\,f3=true,f1=1\\,f2=2\\,f3=true,null", serialize(ps, ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
+		assertEquals("f1=1\\,f2=2\\,f3=true,f1=1\\,f2=2\\,f3=true,null", serialize(ps, l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
 	}
 
 	@Test void h03_objectType_2d_pipes() throws Exception {
 		var ps = tArrayPipes(tObject().allowEmptyValue()).build();
 		assertEquals("f1=1,f2=2,f3=true||null", serialize(ps, a(new H1("1",2,true),new H1(null,null,null),null)));
 		assertEquals("f1=1,f2=2,f3=true||null", serialize(ps, l(new H1("1",2,true),new H1(null,null,null),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=null,f2=null,f3=null|null", serialize(ps, a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=null,f2=null,f3=null|null", serialize(ps, l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true|null", serialize(ps, ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true|null", serialize(ps, l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=null,f2=null,f3=null|null", serialize(ps, a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=null,f2=null,f3=null|null", serialize(ps, l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true|null", serialize(ps, ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true|null", serialize(ps, l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
 	}
 
 	@Test void h04_objectType_2d_uon() throws Exception {
 		var ps = tArrayUon(tObject()).build();
 		assertEquals("@((f1='1',f2=2,f3=true),(),null)", serialize(ps, a(new H1("1",2,true),new H1(null,null,null),null)));
 		assertEquals("@((f1='1',f2=2,f3=true),(),null)", serialize(ps, l(new H1("1",2,true),new H1(null,null,null),null)));
-		assertEquals("@((f1='1',f2=2,f3=true),(f1=null,f2=null,f3=null),null)", serialize(ps, a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
-		assertEquals("@((f1='1',f2=2,f3=true),(f1=null,f2=null,f3=null),null)", serialize(ps, l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
-		assertEquals("@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true),null)", serialize(ps, ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
-		assertEquals("@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true),null)", serialize(ps, l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
+		assertEquals("@((f1='1',f2=2,f3=true),(f1=null,f2=null,f3=null),null)", serialize(ps, a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+		assertEquals("@((f1='1',f2=2,f3=true),(f1=null,f2=null,f3=null),null)", serialize(ps, l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+		assertEquals("@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true),null)", serialize(ps, ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
+		assertEquals("@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true),null)", serialize(ps, l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
 	}
 
 	@Test void h03_objectType_3d() throws Exception {
@@ -848,12 +848,12 @@ class OpenApiPartSerializer_Test extends TestBase {
 		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=3\\\\\\,f3=false,\\,null,null", serialize(ps, a(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
 		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=3\\\\\\,f3=false,\\,null,null", serialize(ps, l(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
 		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=3\\\\\\,f3=false,\\,null,null", serialize(ps, l(l(new H1("1",2,true),new H1("x",3,false)),l(new H1(null,null,null),null),null)));
-		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, a(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, a(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
 	}
 
 	@Test void h03_objectType_3d_ssvAndPipes() throws Exception {
@@ -863,12 +863,12 @@ class OpenApiPartSerializer_Test extends TestBase {
 		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=3,f3=false |null null", serialize(ps, a(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
 		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=3,f3=false |null null", serialize(ps, l(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
 		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=3,f3=false |null null", serialize(ps, l(l(new H1("1",2,true),new H1("x",3,false)),l(new H1(null,null,null),null),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, a(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, l(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, l(l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, a(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, l(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, l(l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
 	}
 
 	@Test void h03_objectType_3d_uon() throws Exception {
@@ -876,12 +876,12 @@ class OpenApiPartSerializer_Test extends TestBase {
 		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=3,f3=false)),@((),null),null)", serialize(ps, a(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
 		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=3,f3=false)),@((),null),null)", serialize(ps, l(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
 		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=3,f3=false)),@((),null),null)", serialize(ps, l(l(new H1("1",2,true),new H1("x",3,false)),l(new H1(null,null,null),null),null)));
-		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, a(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, l(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, l(l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
-		assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, a(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, l(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, l(l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+		assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
 	}
 
 	public static class H2 {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/json/Json_Test.java b/juneau-utest/src/test/java/org/apache/juneau/json/Json_Test.java
index 41993c1000..89da55123f 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/json/Json_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/json/Json_Test.java
@@ -62,7 +62,7 @@ class Json_Test extends TestBase{
 		var r = "\\w[\\w\\-\\.]{3,}\\w";
 		m.put("x", r);
 		assertEquals("{\"x\":\"\\\\w[\\\\w\\\\-\\\\.]{3,}\\\\w\"}", s2.serialize(m));
-		assertEquals(r, JsonMap.ofText(s2.serialize(m)).getString("x"));
+		assertEquals(r, JsonMap.ofString(s2.serialize(m)).getString("x"));
 
 		// String = [foo\bar]
 		m.clear();
@@ -138,7 +138,7 @@ class Json_Test extends TestBase{
 	// Indentation
 	//====================================================================================================
 	@Test void a04_indentation() throws Exception {
-		var m = Json5Map.ofText("{J:{B:['c',{D:'e'},['f',{G:'h'},1,false]]},I:'j'}");
+		var m = Json5Map.ofString("{J:{B:['c',{D:'e'},['f',{G:'h'},1,false]]},I:'j'}");
 		var e = """
 			{
 				J: {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java b/juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java
index 21b32ae3b0..63d4cb0b75 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java
@@ -39,8 +39,8 @@ class Json5List_Test extends TestBase {
 		assertEquals("[1,'two',true]", l.toString());
 	}
 
-	@Test void a03_factoryOfText() throws Exception {
-		var l = Json5List.ofText("[1,'two',true]");
+	@Test void a03_factoryOfString() throws Exception {
+		var l = Json5List.ofString("[1,'two',true]");
 		assertNotNull(l);
 		assertEquals(3, l.size());
 		assertEquals(1, l.getInt(0));
@@ -48,8 +48,14 @@ class Json5List_Test extends TestBase {
 		assertTrue(l.getBoolean(2));
 	}
 
-	@Test void a04_factoryOfTextNullInput() throws Exception {
-		assertNull(Json5List.ofText((CharSequence)null));
+	@Test void a04_factoryOfStringNullInput() throws Exception {
+		assertNull(Json5List.ofString((CharSequence)null));
+	}
+
+	@Test void a04b_ofTextAliasStillWorks() throws Exception {
+		var l = Json5List.ofString("[1,2,3]");
+		assertNotNull(l);
+		assertEquals(3, l.size());
 	}
 
 	@Test void a05_factoryCreateAndAppend() {
@@ -139,7 +145,7 @@ class Json5List_Test extends TestBase {
 	@Test void a17_roundTripJson5() throws Exception {
 		var original = Json5List.of(1, "two", true);
 		var serialized = original.toString();
-		var parsed = Json5List.ofText(serialized);
+		var parsed = Json5List.ofString(serialized);
 		assertEquals(1, parsed.getInt(0));
 		assertEquals("two", parsed.getString(1));
 		assertTrue(parsed.getBoolean(2));
diff --git a/juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java b/juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java
index 2b07a3dc28..31972cf992 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java
@@ -31,7 +31,7 @@ import org.junit.jupiter.api.*;
  * These confirm that:
  * <ul>
  * 	<li>{@link Json5Map#toString()} produces JSON5 form.
- * 	<li>JSON5 default-parser constructors and {@code ofText} factories work.
+ * 	<li>JSON5 default-parser constructors and {@code ofString} factories work.
  * 	<li>Covariant overrides on the get/fluent surface return {@code Json5Map} / {@code Json5List}, not the neutral base.
  * 	<li>{@code writeTo(Writer)} uses {@link Json5Serializer#DEFAULT}.
  * </ul>
@@ -49,15 +49,21 @@ class Json5Map_Test extends TestBase {
 		assertEquals("{a:1,b:'two'}", m.toString());
 	}
 
-	@Test void a03_factoryOfText() throws Exception {
-		var m = Json5Map.ofText("{a:1,b:'two'}");
+	@Test void a03_factoryOfString() throws Exception {
+		var m = Json5Map.ofString("{a:1,b:'two'}");
 		assertNotNull(m);
 		assertEquals(1, m.getInt("a"));
 		assertEquals("two", m.getString("b"));
 	}
 
-	@Test void a04_factoryOfTextNullInput() throws Exception {
-		assertNull(Json5Map.ofText((CharSequence)null));
+	@Test void a04_factoryOfStringNullInput() throws Exception {
+		assertNull(Json5Map.ofString((CharSequence)null));
+	}
+
+	@Test void a04b_ofTextAliasStillWorks() throws Exception {
+		var m = Json5Map.ofString("{a:1}");
+		assertNotNull(m);
+		assertEquals(1, m.getInt("a"));
 	}
 
 	@Test void a05_factoryOfCreateAppend() {
@@ -180,7 +186,7 @@ class Json5Map_Test extends TestBase {
 	@Test void a21_roundTripJson5() throws Exception {
 		var original = Json5Map.of("a", 1, "b", "two", "c", true);
 		var serialized = original.toString();
-		var parsed = Json5Map.ofText(serialized);
+		var parsed = Json5Map.ofString(serialized);
 		assertEquals(1, parsed.getInt("a"));
 		assertEquals("two", parsed.getString("b"));
 		assertTrue(parsed.getBoolean("c"));
diff --git a/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackMediaType_Test.java b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackMediaType_Test.java
new file mode 100644
index 0000000000..955c208e89
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackMediaType_Test.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.msgpack;
+
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Media type tests for MessagePack.
+ *
+ * <p>
+ * Verifies that the serializer emits the RFC-standard <c>application/msgpack</c> Content-Type and
+ * that the parser accepts both the new <c>application/msgpack</c> value and the legacy
+ * <c>octal/msgpack</c> alias (backward compatibility).
+ */
+class MsgPackMediaType_Test extends TestBase {
+
+	@Test
+	void i01_producesCorrectMediaType() {
+		var ct = MsgPackSerializer.DEFAULT.getResponseContentType();
+		assertEquals("application", ct.getType());
+		assertEquals("msgpack", ct.getSubType());
+		assertEquals("application/msgpack", ct.toString());
+	}
+
+	@Test
+	void i02_consumesApplicationMsgpack() {
+		var types = MsgPackParser.DEFAULT.getMediaTypes().stream()
+			.map(mt -> mt.getType() + "/" + mt.getSubType())
+			.toList();
+		assertTrue(types.contains("application/msgpack"),
+			"Expected parser to consume application/msgpack: " + types);
+		assertTrue(MsgPackParser.DEFAULT.canHandle("application/msgpack"),
+			"Expected canHandle(application/msgpack) to be true");
+	}
+
+	@Test
+	void i03_consumesOctalMsgpackAlias() {
+		var types = MsgPackParser.DEFAULT.getMediaTypes().stream()
+			.map(mt -> mt.getType() + "/" + mt.getSubType())
+			.toList();
+		assertTrue(types.contains("octal/msgpack"),
+			"Expected parser to retain octal/msgpack as a backward-compat alias: " + types);
+		assertTrue(MsgPackParser.DEFAULT.canHandle("octal/msgpack"),
+			"Expected canHandle(octal/msgpack) to be true");
+	}
+
+	@Test
+	void i04_serializerAcceptsBothMediaTypes() {
+		var types = new ArrayList<String>();
+		MsgPackSerializer.DEFAULT.forEachAcceptMediaType(mt -> types.add(mt.getType() + "/" + mt.getSubType()));
+		assertTrue(types.contains("application/msgpack"),
+			"Expected serializer Accept list to contain application/msgpack: " + types);
+		assertTrue(types.contains("octal/msgpack"),
+			"Expected serializer Accept list to retain octal/msgpack alias: " + types);
+	}
+
+	@Test
+	void i05_primaryMediaTypeIsApplicationMsgpack() {
+		assertEquals("application/msgpack", MsgPackSerializer.DEFAULT.getPrimaryMediaType().toString());
+		assertEquals("application/msgpack", MsgPackParser.DEFAULT.getPrimaryMediaType().toString());
+	}
+
+	@Test
+	void i06_parserStillDecodesWireData() throws Exception {
+		var bytes = fromSpacedHex("A5 68 65 6C 6C 6F");
+		var s = MsgPackParser.DEFAULT.parse(new ByteArrayInputStream(bytes), String.class);
+		assertEquals("hello", s);
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackSerializerTest.java b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackSerializerTest.java
index a3e029a4dc..aa8c5423e2 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackSerializerTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackSerializerTest.java
@@ -206,10 +206,10 @@ class MsgPackSerializerTest extends TestBase {
 		//		* odd elements in objects are keys of a map
 		//		* the next element of a key is its associated value
 
-		test(Json5Map.ofText("{}"), "80");
-		test(Json5Map.ofText("{1:1}"), "81 A1 31 01");
-		test(Json5Map.ofText("{1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,a:1,b:1,c:1,d:1,e:1,f:1}"), "8F A1 31 01 A1 32 01 A1 33 01 A1 34 01 A1 35 01 A1 36 01 A1 37 01 A1 38 01 A1 39 01 A1 61 01 A1 62 01 A1 63 01 A1 64 01 A1 65 01 A1 66 01");
-		test(Json5Map.ofText("{1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,a:1,b:1,c:1,d:1,e:1,f:1,g:1}"), "DE 00 10 A1 31 01 A1 32 01 A1 33 01 A1 34 01 A1 35 01 A1 36 01 A1 37 01 A1 38 01 A1 39 01 A1 61 01 A1 62 01 A1 63 01 A1 64 01 A1 65 01 A1 66 01 A1 67 01");
+		test(Json5Map.ofString("{}"), "80");
+		test(Json5Map.ofString("{1:1}"), "81 A1 31 01");
+		test(Json5Map.ofString("{1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,a:1,b:1,c:1,d:1,e:1,f:1}"), "8F A1 31 01 A1 32 01 A1 33 01 A1 34 01 A1 35 01 A1 36 01 A1 37 01 A1 38 01 A1 39 01 A1 61 01 A1 62 01 A1 63 01 A1 64 01 A1 65 01 A1 66 01");
+		test(Json5Map.ofString("{1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,a:1,b:1,c:1,d:1,e:1,f:1,g:1}"), "DE 00 10 A1 31 01 A1 32 01 A1 33 01 A1 34 01 A1 35 01 A1 36 01 A1 37 01 A1 38 01 A1 39 01 A1 61 01 A1 62 01 A1 63 01 A1 64 01 A1 65 01 A1 66 01 A1 67 01");
 	}
 
 	public static class Person {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java b/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
index d1448ace77..0cc23fca7a 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
@@ -174,11 +174,11 @@ class ObjectRest_Test extends TestBase {
 		assertEquals("{}", s);
 
 		// Make sure doing a PUT against "" or "/" replaces the root object.
-		var m2 = Json5Map.ofText("{x:1}");
+		var m2 = Json5Map.ofString("{x:1}");
 		model.put("", m2);
 		s = model.get("").toString();
 		assertEquals("{x:1}", s);
-		m2 = Json5Map.ofText("{x:2}");
+		m2 = Json5Map.ofString("{x:2}");
 		model.put("/", m2);
 		s = model.get("").toString();
 		assertEquals("{x:2}", s);
@@ -325,8 +325,8 @@ class ObjectRest_Test extends TestBase {
 	//====================================================================================================
 	@Test void f01_getMethods() throws Exception {
 		var model = ObjectRest.create(new A());
-		var l = Json5List.ofText("[{a:'b'}]");
-		var m = Json5Map.ofText("{a:'b'}");
+		var l = Json5List.ofString("[{a:'b'}]");
+		var m = Json5Map.ofString("{a:'b'}");
 		var jm = new JsonMap(m);
 		var jl = new JsonList(l);
 
@@ -719,10 +719,10 @@ class ObjectRest_Test extends TestBase {
 			f3a = 3L;
 			f4a = true;
 			try {
-				f5 = Json5Map.ofText("{f5a:'a'}");
-				f6 = Json5List.ofText("[{f6a:'a'}]");
-				f7 = Json5Map.ofText("{f5a:'a'}");
-				f8 = Json5List.ofText("[{f6a:'a'}]");
+				f5 = Json5Map.ofString("{f5a:'a'}");
+				f6 = Json5List.ofString("[{f6a:'a'}]");
+				f7 = Json5Map.ofString("{f5a:'a'}");
+				f8 = Json5List.ofString("[{f6a:'a'}]");
 			} catch (ParseException e) {
 				throw new RuntimeException(e);
 			}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Logging_StreamClosed_Test.java b/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Logging_StreamClosed_Test.java
new file mode 100644
index 0000000000..00093ec7ec
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Logging_StreamClosed_Test.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.rest.client.classic;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.util.logging.*;
+
+import org.apache.http.*;
+import org.apache.http.entity.*;
+import org.apache.http.message.*;
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies the fix for a bug where calling {@code RestResponse.getContent().asString()}
+ * after the response is closed (e.g. inside or after try-with-resources) would throw
+ * a "Stream closed" {@link IOException} when {@link DetailLevel#FULL} request logging
+ * is enabled.
+ *
+ * <p>
+ * The root cause: {@code RestResponse.close()} previously consumed/closed the underlying
+ * Apache HTTP entity stream <i>before</i> running FULL logging. The FULL logging path
+ * then tried to read the same (now-closed) entity to render the body, leaving
+ * {@code ResponseContent.body} unset. A subsequent user call to
+ * {@code getContent().asString()} would re-attempt to read the closed stream and surface
+ * the exception to the caller.
+ *
+ * <p>
+ * {@link MockRestClient} alone is insufficient to reproduce the bug because it wraps the
+ * response in an {@code InputStreamEntity} backed by {@link ByteArrayInputStream}, whose
+ * {@link InputStream#close()} is a no-op — so even after close(), the stream can be
+ * re-read. The tests below inject a streaming entity whose underlying input stream
+ * faithfully throws "Stream closed" after close.
+ */
+class RestClient_Logging_StreamClosed_Test extends TestBase {
+
+	@Rest
+	public static class A extends BasicRestObject {
+		@RestGet
+		public String hello() {
+			return "hello world";
+		}
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Test helpers.
+	//------------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Streaming input stream that throws {@link IOException} on any read attempt after close.
+	 * Mimics real HTTP client behavior (the Apache HTTP client closes the network stream when
+	 * {@code EntityUtils.consume} runs, and subsequent reads fail).
+	 */
+	private static final class FailingAfterCloseStream extends InputStream {
+		private final ByteArrayInputStream delegate;
+		private boolean closed;
+
+		FailingAfterCloseStream(byte[] data) {
+			delegate = new ByteArrayInputStream(data);
+		}
+
+		@Override public int read() throws IOException {
+			if (closed) throw new IOException("Stream closed");
+			return delegate.read();
+		}
+
+		@Override public int read(byte[] b, int off, int len) throws IOException {
+			if (closed) throw new IOException("Stream closed");
+			return delegate.read(b, off, len);
+		}
+
+		@Override public void close() throws IOException {
+			closed = true;
+			delegate.close();
+		}
+	}
+
+	/**
+	 * Test client that lets us inject an arbitrary {@link HttpEntity} as the response body.
+	 */
+	public static class TestClient extends MockRestClient {
+		private HttpEntity responseEntity;
+
+		public TestClient(MockRestClient.Builder builder) {
+			super(builder);
+		}
+
+		public TestClient entity(HttpEntity entity) {
+			responseEntity = entity;
+			return this;
+		}
+
+		@Override
+		protected MockRestResponse createResponse(RestRequest request, HttpResponse httpResponse, Parser parser) throws RestCallException {
+			var r = new BasicHttpResponse(new ProtocolVersion("http", 1, 1), 200, "");
+			r.setEntity(responseEntity);
+			r.addHeader("Content-Type", "application/json");
+			return new MockRestResponse(this, request, r, parser);
+		}
+	}
+
+	private static InputStreamEntity failingEntity(String body) {
+		var data = body.getBytes(StandardCharsets.UTF_8);
+		return new InputStreamEntity(new FailingAfterCloseStream(data));
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Reproduction / regression tests for the FULL logging stream-closed bug.
+	//------------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * After fix: closing the response and then calling {@code asString()} should
+	 * return the body instead of throwing "Stream closed".
+	 */
+	@Test void a01_asStringAfterCloseWithFullLogging() throws Exception {
+		var c = MockConsole.create();
+		var l = MockLogger.create();
+
+		var client = MockRestClient.create(A.class).json()
+			.logRequests(DetailLevel.FULL, Level.SEVERE, null)
+			.logToConsole()
+			.logger(l)
+			.console(c)
+			.build(TestClient.class)
+			.entity(failingEntity("\"hello world\""));
+
+		var response = client.get("/hello").run();
+		response.close();
+		var body = response.getContent().asString();
+		assertEquals("\"hello world\"", body);
+	}
+
+	/**
+	 * After fix: try-with-resources + asString() works the same as before, without
+	 * stream-closed errors, even with the failing stream wrapper.
+	 */
+	@Test void a02_asStringInsideTryWithResources() throws Exception {
+		var c = MockConsole.create();
+		var l = MockLogger.create();
+
+		var client = MockRestClient.create(A.class).json()
+			.logRequests(DetailLevel.FULL, Level.SEVERE, null)
+			.logToConsole()
+			.logger(l)
+			.console(c)
+			.build(TestClient.class)
+			.entity(failingEntity("\"hello world\""));
+
+		try (var response = client.get("/hello").run()) {
+			var body = response.getContent().asString();
+			assertEquals("\"hello world\"", body);
+		}
+	}
+
+	/**
+	 * After fix: FULL logging output still contains the response body (the buffering
+	 * fix must not regress what FULL logging actually logs).
+	 */
+	@Test void b01_fullLoggingStillContainsResponseBody() throws Exception {
+		var c = MockConsole.create();
+		c.reset();
+		var l = MockLogger.create();
+
+		var client = MockRestClient.create(A.class).json()
+			.logRequests(DetailLevel.FULL, Level.SEVERE, null)
+			.logToConsole()
+			.logger(l)
+			.console(c)
+			.build(TestClient.class)
+			.entity(failingEntity("\"hello world\""));
+
+		client.get("/hello").complete();
+
+		var logged = c.toString();
+		assertTrue(logged.contains("---response content---"), () -> "Missing response-content marker in: " + logged);
+		assertTrue(logged.contains("\"hello world\""), () -> "Missing response body in log output: " + logged);
+	}
+
+	/**
+	 * After fix: when FULL logging is NOT enabled, the response body is NOT eagerly
+	 * cached — original behavior preserved.  Reading via {@code asString()} after
+	 * {@code close()} should fail on a non-repeatable closed entity, just like before
+	 * the fix.  This guards against accidental over-buffering for the SIMPLE/NONE
+	 * paths.
+	 */
+	@Test void c01_nonFullLoggingDoesNotBufferEntity() throws Exception {
+		var l = MockLogger.create();
+
+		var client = MockRestClient.create(A.class).json()
+			.logRequests(DetailLevel.NONE, Level.SEVERE, null)
+			.logger(l)
+			.build(TestClient.class)
+			.entity(failingEntity("\"hello world\""));
+
+		var response = client.get("/hello").run();
+		response.close();
+		assertThrows(RestCallException.class, () -> response.getContent().asString());
+	}
+
+	/**
+	 * After fix: SIMPLE logging continues to log just the status line (no body access).
+	 */
+	@Test void c02_simpleLoggingDoesNotTouchBody() throws Exception {
+		var c = MockConsole.create();
+		c.reset();
+		var l = MockLogger.create();
+
+		var client = MockRestClient.create(A.class).json()
+			.logRequests(DetailLevel.SIMPLE, Level.SEVERE, null)
+			.logToConsole()
+			.logger(l)
+			.console(c)
+			.build(TestClient.class)
+			.entity(failingEntity("\"hello world\""));
+
+		client.get("/hello").complete();
+
+		var logged = c.toString();
+		assertTrue(logged.contains("HTTP GET"), () -> "Expected SIMPLE log line, got: " + logged);
+		assertFalse(logged.contains("---response content---"),
+			() -> "SIMPLE log should not contain body markers: " + logged);
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Marshalls_Test.java b/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Marshalls_Test.java
index f2b961b48d..386e3a8041 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Marshalls_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Marshalls_Test.java
@@ -84,7 +84,7 @@ public class RestClient_Marshalls_Test extends TestBase {
 		x2.post("/a01",bean).header("X-Accept","text/xml").header("X-Content-Type","text/xml").run().assertStatus(200).getContent().as(Bean.class).check();
 		x3.post("/a01",bean).header("X-Accept","text/html").header("X-Content-Type","text/html").run().assertStatus(200).getContent().as(Bean.class).check();
 		x4.post("/a01",bean).header("X-Accept","text/plain").header("X-Content-Type","text/plain").run().assertStatus(200).getContent().as(Bean.class).check();
-		x5.post("/a01",bean).header("X-Accept","octal/msgpack").header("X-Content-Type","octal/msgpack").run().assertStatus(200).getContent().as(Bean.class).check();
+		x5.post("/a01",bean).header("X-Accept","application/msgpack").header("X-Content-Type","application/msgpack").run().assertStatus(200).getContent().as(Bean.class).check();
 		x6.post("/a01",bean).header("X-Accept","text/uon").header("X-Content-Type","text/uon").run().assertStatus(200).getContent().as(Bean.class).check();
 		x7.post("/a01",bean).header("X-Accept","application/x-www-form-urlencoded").header("X-Content-Type","application/x-www-form-urlencoded").run().assertStatus(200).getContent().as(Bean.class).check();
 		x8.post("/a01",bean).header("X-Accept","text/openapi").header("X-Content-Type","text/openapi").run().assertStatus(200).getContent().as(Bean.class).check();
@@ -101,7 +101,7 @@ public class RestClient_Marshalls_Test extends TestBase {
 		x.post("/a01",bean).header("X-Accept","text/xml").header("X-Content-Type","text/xml").xml().run().assertStatus(200).getContent().as(Bean.class).check();
 		x.post("/a01",bean).header("X-Accept","text/html").header("X-Content-Type","text/html").html().run().assertStatus(200).getContent().as(Bean.class).check();
 		x.post("/a01",bean).header("X-Accept","text/plain").header("X-Content-Type","text/plain").plainText().run().assertStatus(200).getContent().as(Bean.class).check();
-		x.post("/a01",bean).header("X-Accept","octal/msgpack").header("X-Content-Type","octal/msgpack").msgPack().run().assertStatus(200).getContent().as(Bean.class).check();
+		x.post("/a01",bean).header("X-Accept","application/msgpack").header("X-Content-Type","application/msgpack").msgPack().run().assertStatus(200).getContent().as(Bean.class).check();
 		x.post("/a01",bean).header("X-Accept","text/uon").header("X-Content-Type","text/uon").uon().run().assertStatus(200).getContent().as(Bean.class).check();
 		x.post("/a01",bean).header("X-Accept","application/x-www-form-urlencoded").header("X-Content-Type","application/x-www-form-urlencoded").urlEnc().run().assertStatus(200).getContent().as(Bean.class).check();
 		x.post("/a01",bean).header("X-Accept","text/openapi").header("X-Content-Type","text/openapi").openApi().run().assertStatus(200).getContent().as(Bean.class).check();
diff --git a/juneau-utest/src/test/java/org/apache/juneau/sse/Sse_Test.java b/juneau-utest/src/test/java/org/apache/juneau/sse/Sse_Test.java
new file mode 100644
index 0000000000..b630d5ccb0
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/sse/Sse_Test.java
@@ -0,0 +1,728 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.sse;
+
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshaller.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.serializer.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Comprehensive tests for the {@code org.apache.juneau.sse} package and the {@link Sse} marshaller helper.
+ *
+ * <p>
+ * Covers WHATWG spec compliance, round-trips, edge cases, iterator semantics, and a REST smoke test.
+ */
+@SuppressWarnings({
+	"java:S5961" // High assertion count acceptable in comprehensive test
+})
+class Sse_Test extends TestBase {
+
+	//------------------------------------------------------------------------------------------------------------------
+	// SseEvent bean.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void a01_emptyConstructor() {
+		var e = new SseEvent();
+		assertBean(e, "event,data,id,retry", "<null>,<null>,<null>,<null>");
+	}
+
+	@Test void a02_twoArgConstructor() {
+		var e = new SseEvent("progress", "step 1");
+		assertBean(e, "event,data,id,retry", "progress,step 1,<null>,<null>");
+	}
+
+	@Test void a03_setters() {
+		var e = new SseEvent()
+			.setEvent("update")
+			.setData("payload")
+			.setId("42")
+			.setRetry(5000L);
+		assertBean(e, "event,data,id,retry", "update,payload,42,5000");
+	}
+
+	@Test void a04_equalsAndHashCode() {
+		var e1 = new SseEvent("e", "d").setId("1").setRetry(100L);
+		var e2 = new SseEvent("e", "d").setId("1").setRetry(100L);
+		var e3 = new SseEvent("e", "different").setId("1").setRetry(100L);
+		assertEquals(e1, e1);
+		assertEquals(e1, e2);
+		assertEquals(e1.hashCode(), e2.hashCode());
+		assertNotEquals(e1, e3);
+		assertNotEquals(e1, "not an event");
+		assertNotEquals(e1, null);
+	}
+
+	@Test void a05_toString() {
+		var e = new SseEvent("e1", "d1").setId("id1").setRetry(50L);
+		var s = e.toString();
+		assertTrue(s.contains("e1"));
+		assertTrue(s.contains("d1"));
+		assertTrue(s.contains("id1"));
+		assertTrue(s.contains("50"));
+	}
+
+	@Test void a06_defaultEventConstant() {
+		assertEquals("message", SseEvent.DEFAULT_EVENT);
+	}
+
+	@Test void a07_equalsSelfReferenceShortCircuit() {
+		// Direct equals() invocation to exercise the this==o reflexive branch — JUnit assertEquals
+		// routes through Objects.equals() which short-circuits on identity before calling equals().
+		var e = new SseEvent("e", "d").setId("1").setRetry(100L);
+		assertTrue(e.equals(e));  // NOSONAR java:S5863 — intentional reflexive equals call for branch coverage
+	}
+
+	@Test void a08_equalsDifferentEvent() {
+		var e1 = new SseEvent("a", "d").setId("1").setRetry(100L);
+		var e2 = new SseEvent("b", "d").setId("1").setRetry(100L);
+		assertNotEquals(e1, e2);
+	}
+
+	@Test void a09_equalsDifferentId() {
+		var e1 = new SseEvent("e", "d").setId("1").setRetry(100L);
+		var e2 = new SseEvent("e", "d").setId("2").setRetry(100L);
+		assertNotEquals(e1, e2);
+	}
+
+	@Test void a10_equalsDifferentRetry() {
+		var e1 = new SseEvent("e", "d").setId("1").setRetry(100L);
+		var e2 = new SseEvent("e", "d").setId("1").setRetry(200L);
+		assertNotEquals(e1, e2);
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Serializer wire format.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void b01_singleEvent() throws Exception {
+		var e = new SseEvent("progress", "step 1").setId("1").setRetry(3000L);
+		var s = Sse.of(e);
+		assertEquals("event: progress\ndata: step 1\nid: 1\nretry: 3000\n\n", s);
+	}
+
+	@Test void b02_listOfEvents() throws Exception {
+		var events = List.of(
+			new SseEvent("progress", "one"),
+			new SseEvent("progress", "two")
+		);
+		var s = Sse.of(events);
+		assertEquals("event: progress\ndata: one\n\nevent: progress\ndata: two\n\n", s);
+	}
+
+	@Test void b03_arrayOfEvents() throws Exception {
+		var arr = new SseEvent[]{ new SseEvent("e", "a"), new SseEvent("e", "b") };
+		var s = Sse.of(arr);
+		assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+	}
+
+	@Test void b04_objectArrayOfEvents() throws Exception {
+		var arr = new Object[]{ new SseEvent("e", "a"), new SseEvent("e", "b") };
+		var s = Sse.of(arr);
+		assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+	}
+
+	@Test void b05_streamOfEvents() throws Exception {
+		var events = Stream.of(new SseEvent("e", "a"), new SseEvent("e", "b"));
+		var s = Sse.of(events);
+		assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+	}
+
+	@Test void b06_multiLineData() throws Exception {
+		var e = new SseEvent("e", "line one\nline two\nline three");
+		var s = Sse.of(e);
+		assertEquals("event: e\ndata: line one\ndata: line two\ndata: line three\n\n", s);
+	}
+
+	@Test void b07_eventNullOrEmptyOmitted() throws Exception {
+		var e1 = new SseEvent().setData("d");
+		assertEquals("data: d\n\n", Sse.of(e1));
+		var e2 = new SseEvent("", "d");
+		assertEquals("data: d\n\n", Sse.of(e2));
+	}
+
+	@Test void b08_nullDataOmitted() throws Exception {
+		var e = new SseEvent("e", null);
+		assertEquals("event: e\n\n", Sse.of(e));
+	}
+
+	@Test void b09_emptyDataEmitsEmptyDataLine() throws Exception {
+		var e = new SseEvent("e", "");
+		assertEquals("event: e\ndata: \n\n", Sse.of(e));
+	}
+
+	@Test void b10_idEmittedWhenSet() throws Exception {
+		var e = new SseEvent().setData("d").setId("123");
+		assertEquals("data: d\nid: 123\n\n", Sse.of(e));
+	}
+
+	@Test void b11_retryEmittedWhenSet() throws Exception {
+		var e = new SseEvent().setData("d").setRetry(2500L);
+		assertEquals("data: d\nretry: 2500\n\n", Sse.of(e));
+	}
+
+	@Test void b12_nullElementsInIterableSkipped() throws Exception {
+		var events = new ArrayList<SseEvent>();
+		events.add(new SseEvent("e", "a"));
+		events.add(null);
+		events.add(new SseEvent("e", "b"));
+		var s = Sse.of(events);
+		assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+	}
+
+	@Test void b13_emptyIterable() throws Exception {
+		assertEquals("", Sse.of(List.of()));
+	}
+
+	@Test void b14_emptyArray() throws Exception {
+		assertEquals("", Sse.of(new SseEvent[0]));
+	}
+
+	@Test void b15_nonEventThrows() {
+		assertThrows(SerializeException.class, () -> Sse.of("not an event"));
+	}
+
+	@Test void b16_iterableWithNonEventThrows() {
+		assertThrows(SerializeException.class, () -> Sse.of(List.of("not", "events")));
+	}
+
+	@Test void b17_nullInputReturnsEmpty() throws Exception {
+		assertEquals("", Sse.of(null));
+	}
+
+	@Test void b18_writeComment() throws Exception {
+		var sw = new StringWriter();
+		SseSerializer.writeComment(sw, "ping");
+		assertEquals(": ping\n\n", sw.toString());
+	}
+
+	@Test void b19_writeCommentNullTreatedAsEmpty() throws Exception {
+		var sw = new StringWriter();
+		SseSerializer.writeComment(sw, null);
+		assertEquals(": \n\n", sw.toString());
+	}
+
+	@Test void b20_writeCommentMultiline() throws Exception {
+		var sw = new StringWriter();
+		SseSerializer.writeComment(sw, "line one\nline two");
+		assertEquals(": line one\n: line two\n\n", sw.toString());
+	}
+
+	@Test void b21_serializeToWriter() throws Exception {
+		var sw = new StringWriter();
+		Sse.of(new SseEvent("e", "d"), sw);
+		assertEquals("event: e\ndata: d\n\n", sw.toString());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Parser / SseEventReader compliance.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void c01_canonicalExample() throws Exception {
+		var wire = "event: progress\ndata: step 1\nid: 42\nretry: 1000\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertBean(events.get(0), "event,data,id,retry", "progress,step 1,42,1000");
+	}
+
+	@Test void c02_multiLineData() throws Exception {
+		var wire = "data: line one\ndata: line two\ndata: line three\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("line one\nline two\nline three", events.get(0).getData());
+	}
+
+	@Test void c03_crLineTerminator() throws Exception {
+		var wire = "event: e\rdata: d\r\r";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertBean(events.get(0), "event,data", "e,d");
+	}
+
+	@Test void c04_lfLineTerminator() throws Exception {
+		var wire = "event: e\ndata: d\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertBean(events.get(0), "event,data", "e,d");
+	}
+
+	@Test void c05_crLfLineTerminator() throws Exception {
+		var wire = "event: e\r\ndata: d\r\n\r\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertBean(events.get(0), "event,data", "e,d");
+	}
+
+	@Test void c06_mixedLineTerminators() throws Exception {
+		var wire = "event: e\rdata: line1\ndata: line2\r\n\r\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("line1\nline2", events.get(0).getData());
+	}
+
+	@Test void c07_singleBomStripped() throws Exception {
+		var wire = "\uFEFFevent: e\ndata: d\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("e", events.get(0).getEvent());
+	}
+
+	@Test void c08_secondBomPreserved() throws Exception {
+		var wire = "\uFEFFdata: foo\uFEFF\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("foo\uFEFF", events.get(0).getData());
+	}
+
+	@Test void c09_idWithNullByteIgnored() throws Exception {
+		var wire = "id: ab\u0000cd\ndata: x\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertNull(events.get(0).getId());
+	}
+
+	@Test void c10_retryNonDigitIgnored() throws Exception {
+		var wire = "retry: abc\ndata: x\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertNull(events.get(0).getRetry());
+	}
+
+	@Test void c11_retryEmptyIgnored() throws Exception {
+		var wire = "retry: \ndata: x\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertNull(events.get(0).getRetry());
+	}
+
+	@Test void c12_retryMixedDigitsIgnored() throws Exception {
+		var wire = "retry: 12x4\ndata: x\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertNull(events.get(0).getRetry());
+	}
+
+	@Test void c13_commentLinesIgnored() throws Exception {
+		var wire = ": ping\n: heartbeat\nevent: e\ndata: d\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertBean(events.get(0), "event,data", "e,d");
+	}
+
+	@Test void c14_noColonLineSpec() throws Exception {
+		var wire = "data\ndata: real\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("\nreal", events.get(0).getData());
+	}
+
+	@Test void c15_colonSpaceHandling() throws Exception {
+		var wire = "data:  value\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals(" value", events.get(0).getData());
+	}
+
+	@Test void c16_noSpaceAfterColon() throws Exception {
+		var wire = "data:value\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("value", events.get(0).getData());
+	}
+
+	@Test void c17_unknownFieldIgnored() throws Exception {
+		var wire = "unknown: x\ndata: real\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("real", events.get(0).getData());
+	}
+
+	@Test void c18_multipleEvents() throws Exception {
+		var wire = "data: one\n\ndata: two\n\ndata: three\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(3, events.size());
+		assertEquals("one", events.get(0).getData());
+		assertEquals("two", events.get(1).getData());
+		assertEquals("three", events.get(2).getData());
+	}
+
+	@Test void c19_emptyEventValueDefaultsToMessage() throws Exception {
+		var wire = "data: d\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertNull(events.get(0).getEvent());
+	}
+
+	@Test void c20_emptyEventField() throws Exception {
+		var wire = "event: \ndata: d\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("", events.get(0).getEvent());
+	}
+
+	@Test void c21_emptyIdField() throws Exception {
+		var wire = "id: \ndata: d\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("", events.get(0).getId());
+	}
+
+	@Test void c22_inputWithoutTrailingBlankLine() throws Exception {
+		var wire = "event: e\ndata: d\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertBean(events.get(0), "event,data", "e,d");
+	}
+
+	@Test void c23_pureBlankLinesProduceNoEvents() throws Exception {
+		var wire = "\n\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertTrue(events.isEmpty());
+	}
+
+	@Test void c24_emptyInput() throws Exception {
+		List<SseEvent> events = Sse.to("", List.class, SseEvent.class);
+		assertTrue(events.isEmpty());
+	}
+
+	@Test void c25_parseAsSingleEvent() throws Exception {
+		var wire = "event: e\ndata: d\n\n";
+		var e = Sse.to(wire, SseEvent.class);
+		assertNotNull(e);
+		assertBean(e, "event,data", "e,d");
+	}
+
+	@Test void c26_parseAsSingleEventEmptyInput() throws Exception {
+		var e = Sse.to("", SseEvent.class);
+		assertNull(e);
+	}
+
+	@Test void c27_parseAsArray() throws Exception {
+		var wire = "data: one\n\ndata: two\n\n";
+		var events = Sse.to(wire, SseEvent[].class);
+		assertEquals(2, events.length);
+		assertEquals("one", events[0].getData());
+		assertEquals("two", events[1].getData());
+	}
+
+	@Test void c28_parseUnsupportedTargetTypeThrows() {
+		var wire = "data: x\n\n";
+		assertThrows(ParseException.class, () -> Sse.to(wire, Integer.class));
+	}
+
+	@Test void c29_emptyDataFieldEmitsEmptyString() throws Exception {
+		// "data:" with no value — exercises processField's fieldValue.isEmpty() branch.
+		var wire = "data:\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertEquals("", events.get(0).getData());
+	}
+
+	@Test void c30_retryLowAsciiNonDigitIgnored() throws Exception {
+		// '!' (0x21) is below '0' — exercises parseRetry's c < '0' branch.
+		var wire = "retry: !23\ndata: x\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertNull(events.get(0).getRetry());
+	}
+
+	@Test void c31_eventWithoutDataFieldDispatches() throws Exception {
+		// Event with metadata fields but no data — exercises dispatch's dataBuf.length() == 0 branch.
+		var wire = "event: e\nid: 1\n\n";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertBean(events.get(0), "event,data,id,retry", "e,<null>,1,<null>");
+	}
+
+	@Test void c32_inputEndsMidLineWithoutTerminator() throws Exception {
+		// Stream ends without a final \n — exercises readLine's sawAny=true at EOF branch.
+		var wire = "event: e\ndata: d";
+		List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(1, events.size());
+		assertBean(events.get(0), "event,data", "e,d");
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// SseEventReader iterator semantics.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void d01_iteratorDrainsOneAtATime() throws Exception {
+		var wire = "data: a\n\ndata: b\n\ndata: c\n\n";
+		try (var r = new SseEventReader(new StringReader(wire))) {
+			assertTrue(r.hasNext());
+			assertEquals("a", r.next().getData());
+			assertTrue(r.hasNext());
+			assertEquals("b", r.next().getData());
+			assertTrue(r.hasNext());
+			assertEquals("c", r.next().getData());
+			assertFalse(r.hasNext());
+		}
+	}
+
+	@Test void d02_nextWithoutHasNext() throws Exception {
+		var wire = "data: a\n\n";
+		try (var r = new SseEventReader(new StringReader(wire))) {
+			assertEquals("a", r.next().getData());
+		}
+	}
+
+	@Test void d03_hasNextIdempotent() throws Exception {
+		var wire = "data: a\n\n";
+		try (var r = new SseEventReader(new StringReader(wire))) {
+			assertTrue(r.hasNext());
+			assertTrue(r.hasNext());
+			assertTrue(r.hasNext());
+			assertEquals("a", r.next().getData());
+			assertFalse(r.hasNext());
+			assertFalse(r.hasNext());
+		}
+	}
+
+	@Test void d04_nextThrowsNoSuchElement() throws Exception {
+		try (var r = new SseEventReader(new StringReader(""))) {
+			assertThrows(NoSuchElementException.class, r::next);
+		}
+	}
+
+	@Test void d05_nullReaderRejected() {
+		assertThrows(IllegalArgumentException.class, () -> new SseEventReader(null));
+	}
+
+	@Test void d06_closeClosesUnderlyingReader() throws Exception {
+		var closed = new boolean[1];
+		var sr = new StringReader("data: a\n\n") {
+			@Override public void close() { closed[0] = true; super.close(); }
+		};
+		var r = new SseEventReader(sr);
+		r.close();
+		assertTrue(closed[0]);
+	}
+
+	@Test void d07_toListClosesUnderlyingReader() throws Exception {
+		var closed = new boolean[1];
+		var sr = new StringReader("data: a\n\ndata: b\n\n") {
+			@Override public void close() { closed[0] = true; super.close(); }
+		};
+		var r = new SseEventReader(sr);
+		var events = r.toList();
+		assertEquals(2, events.size());
+		assertTrue(closed[0]);
+	}
+
+	@Test void d08_ioExceptionWrapped() {
+		var failingReader = new Reader() {
+			@Override public int read(char[] cbuf, int off, int len) throws IOException { throw new IOException("boom"); }
+			@Override public void close() {}
+		};
+		var r = new SseEventReader(failingReader);
+		var ex = assertThrows(UncheckedIOException.class, r::hasNext);
+		assertEquals("boom", ex.getCause().getMessage());
+	}
+
+	@Test void d09_toListPropagatesIo() {
+		var failingReader = new Reader() {
+			@Override public int read(char[] cbuf, int off, int len) throws IOException { throw new IOException("boom"); }
+			@Override public void close() {}
+		};
+		var r = new SseEventReader(failingReader);
+		var ex = assertThrows(IOException.class, r::toList);
+		assertEquals("boom", ex.getMessage());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Round-trip via Sse marshaller.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void e01_roundTripList() throws Exception {
+		var events = List.of(
+			new SseEvent("progress", "step 1").setId("1"),
+			new SseEvent("progress", "step 2").setId("2").setRetry(1000L)
+		);
+		var wire = Sse.of(events);
+		List<SseEvent> parsed = Sse.to(wire, List.class, SseEvent.class);
+		assertEquals(2, parsed.size());
+		assertBean(parsed.get(0), "event,data,id,retry", "progress,step 1,1,<null>");
+		assertBean(parsed.get(1), "event,data,id,retry", "progress,step 2,2,1000");
+	}
+
+	@Test void e02_roundTripMultiLine() throws Exception {
+		var e = new SseEvent("e", "line a\nline b");
+		var wire = Sse.of(e);
+		var parsed = Sse.to(wire, SseEvent.class);
+		assertEquals("line a\nline b", parsed.getData());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Defaults / context.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void f01_serializerMediaType() {
+		assertEquals("text/event-stream", SseSerializer.DEFAULT.getResponseContentType().toString());
+	}
+
+	@Test void f02_serializerCopy() {
+		var copy = SseSerializer.DEFAULT.copy().build();
+		assertNotNull(copy);
+	}
+
+	@Test void f03_parserCopy() {
+		var copy = SseParser.DEFAULT.copy().build();
+		assertNotNull(copy);
+	}
+
+	@Test void f04_serializerGetSession() {
+		assertNotNull(SseSerializer.DEFAULT.getSession());
+	}
+
+	@Test void f05_parserGetSession() {
+		assertNotNull(SseParser.DEFAULT.getSession());
+	}
+
+	@Test void f06_marshallerInstanceMethods() throws Exception {
+		var m = Sse.DEFAULT;
+		var s = m.write(new SseEvent("e", "d"));
+		assertEquals("event: e\ndata: d\n\n", s);
+	}
+
+	@Test void f07_explicitConstructor() throws Exception {
+		var m = new Sse(SseSerializer.DEFAULT, SseParser.DEFAULT);
+		assertNotNull(m);
+		assertEquals("event: e\ndata: d\n\n", m.write(new SseEvent("e", "d")));
+	}
+
+	@Test void f08_builderHashKeyStable() {
+		var b1 = SseSerializer.create();
+		var b2 = SseSerializer.create();
+		assertEquals(b1.hashKey(), b2.hashKey());
+		var pb1 = SseParser.create();
+		var pb2 = SseParser.create();
+		assertEquals(pb1.hashKey(), pb2.hashKey());
+	}
+
+	@Test void f09_splitDataKeepsTrailingEmpties() {
+		var parts = SseSerializerSession.splitData("a\nb\n");
+		assertEquals(3, parts.size());
+		assertEquals("a", parts.get(0));
+		assertEquals("b", parts.get(1));
+		assertEquals("", parts.get(2));
+	}
+
+	@Test void f10_builderCopyConstructorParser() {
+		var b1 = SseParser.create();
+		var b2 = b1.copy();
+		assertNotNull(b2);
+		assertNotSame(b1, b2);
+	}
+
+	@Test void f11_builderCopyConstructorSerializer() {
+		var b1 = SseSerializer.create();
+		var b2 = b1.copy();
+		assertNotNull(b2);
+		assertNotSame(b1, b2);
+	}
+
+	@Test void f12_arrayWithNullElement() throws Exception {
+		var arr = new SseEvent[]{ new SseEvent("e", "a"), null, new SseEvent("e", "b") };
+		// SseEvent[] direct array path: writeEvent(null) returns early.
+		var s = Sse.of(arr);
+		assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+	}
+
+	@Test void f13_streamWithNonEventWrapsInRuntime() {
+		// Stream path: writeIfEventUnchecked wraps SerializeException as RuntimeException.
+		var s = Stream.of("not an event");
+		assertThrows(RuntimeException.class, () -> Sse.of(s));
+	}
+
+	@Test void f14_parseAsObjectClass() throws Exception {
+		// Target type Object.class hits the type.isObject() branch.
+		var wire = "data: x\n\n";
+		var out = Sse.to(wire, Object.class);
+		assertNotNull(out);
+		assertTrue(out instanceof List);
+	}
+
+	@Test void f15_parseAsRawList() throws Exception {
+		// Raw List.class without args hits the "List.class.isAssignableFrom" fallback.
+		var wire = "data: x\n\n";
+		var out = Sse.to(wire, (Class<?>) List.class);
+		assertNotNull(out);
+		assertTrue(out instanceof List);
+	}
+
+	@Test void f16_parseAsSseEventEmptyReturnsNull() throws Exception {
+		// Empty input + target SseEvent.class hits the events.isEmpty() branch.
+		var e = Sse.to("\n\n", SseEvent.class);
+		assertNull(e);
+	}
+
+	@Test void f17_parseNullInputReturnsNullViaPublicApi() throws Exception {
+		// Null input -> ParserPipe.getReader() returns null -> doParse hits the r==null early-return branch.
+		var result = Sse.to((Object) null, SseEvent.class);
+		assertNull(result);
+	}
+
+	@Test void f18_doParseWithNullClassMetaReturnsList() throws Exception {
+		// Public parse() path requires non-null type (parseInner calls type.isVoid()), so the
+		// type==null branch in SseParserSession.doParse() is only reachable via direct invocation.
+		// Same package -> protected doParse is accessible.
+		var session = SseParser.DEFAULT.getSession();
+		try (var pipe = session.createPipe("data: x\n\n")) {
+			Object result = session.doParse(pipe, null);
+			assertTrue(result instanceof List);
+			var events = (List<?>) result;
+			assertEquals(1, events.size());
+			assertEquals("x", ((SseEvent) events.get(0)).getData());
+		}
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// REST smoke test.
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Rest(serializers = SseSerializer.class)
+	public static class SseEndpoint {
+		@RestGet(path = "/stream")
+		public List<SseEvent> stream() {
+			return List.of(
+				new SseEvent("progress", "step 1").setId("1"),
+				new SseEvent("progress", "step 2").setId("2"),
+				new SseEvent("progress", "step 3").setId("3")
+			);
+		}
+	}
+
+	@Test void g01_restEndpointEmitsAllEventsInOrder() throws Exception {
+		var c = MockRestClient.buildLax(SseEndpoint.class);
+		var body = c.get("/stream").header("Accept", "text/event-stream").run().getContent().asString();
+		var expected =
+			"event: progress\ndata: step 1\nid: 1\n\n"
+			+ "event: progress\ndata: step 2\nid: 2\n\n"
+			+ "event: progress\ndata: step 3\nid: 3\n\n";
+		assertEquals(expected, body);
+	}
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/transforms/ReaderObjectSwap_ComboSerializeTest.java b/juneau-utest/src/test/java/org/apache/juneau/transforms/ReaderObjectSwap_ComboSerializeTest.java
index 09040836c3..05b703d2b9 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/transforms/ReaderObjectSwap_ComboSerializeTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/transforms/ReaderObjectSwap_ComboSerializeTest.java
@@ -132,8 +132,8 @@ public class ReaderObjectSwap_ComboSerializeTest extends ComboSerializeTest_Base
 			.urlEnc("foo-application/x-www-form-urlencoded")
 			.urlEncT("foo-application/x-www-form-urlencoded")
 			.urlEncR("foo-application/x-www-form-urlencoded")
-			.msgPack("666F6F2D6F6374616C2F6D73677061636B")
-			.msgPackT("666F6F2D6F6374616C2F6D73677061636B")
+			.msgPack("666F6F2D6170706C69636174696F6E2F6D73677061636B")
+			.msgPackT("666F6F2D6170706C69636174696F6E2F6D73677061636B")
 			.build(),
 		tester(5, "BeanWithSwappedField", ()->new BeanWithSwappedField("x"))
 			.json("{\"f\":x-json}")
diff --git a/juneau-utest/src/test/java/org/apache/juneau/transforms/SwapsAnnotation_ComboSerializeTest.java b/juneau-utest/src/test/java/org/apache/juneau/transforms/SwapsAnnotation_ComboSerializeTest.java
index e5e14755de..d8582c5360 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/transforms/SwapsAnnotation_ComboSerializeTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/transforms/SwapsAnnotation_ComboSerializeTest.java
@@ -428,7 +428,7 @@ class SwapsAnnotation_ComboSerializeTest extends ComboSerializeTest_Base {
 	@Swap(value=SwapHtml.class, mediaTypes={"text/html"})
 	@Swap(value=SwapUon.class, mediaTypes={"text/uon"})
 	@Swap(value=SwapUrlEncoding.class, mediaTypes={"application/x-www-form-urlencoded"})
-	@Swap(value=SwapMsgPack.class, mediaTypes={"octal/msgpack"})
+	@Swap(value=SwapMsgPack.class, mediaTypes={"application/msgpack"})
 	@Swap(value=SwapRdfXml.class, mediaTypes={"text/xml+rdf"})
 	public static class TestMediaTypeLiterals {}
 
diff --git a/juneau-utest/src/test/java/org/apache/juneau/uon/UonSerializer_Test.java b/juneau-utest/src/test/java/org/apache/juneau/uon/UonSerializer_Test.java
index 1dbff17c94..7a7c61ceab 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/uon/UonSerializer_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/uon/UonSerializer_Test.java
@@ -43,26 +43,26 @@ class UonSerializer_Test {
 		assertEquals("a", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{a:'a'}");
+		t = Json5Map.ofString("{a:'a'}");
 		assertEquals("(a=a)", s.serialize(t));
 		assertEquals("(a=a)", su.serialize(t));
 		assertEquals("(\n\ta=a\n)", sr.serialize(t));
 
 		// Simple map
 		// Top level
-		t = Json5Map.ofText("{a:'b',c:123,d:false,e:true,f:null}");
+		t = Json5Map.ofString("{a:'b',c:123,d:false,e:true,f:null}");
 		assertEquals("(a=b,c=123,d=false,e=true,f=null)", s.serialize(t));
 		assertEquals("(a=b,c=123,d=false,e=true,f=null)", su.serialize(t));
 		assertEquals("(\n\ta=b,\n\tc=123,\n\td=false,\n\te=true,\n\tf=null\n)", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{a:{a:'b',c:123,d:false,e:true,f:null}}");
+		t = Json5Map.ofString("{a:{a:'b',c:123,d:false,e:true,f:null}}");
 		assertEquals("(a=(a=b,c=123,d=false,e=true,f=null))", s.serialize(t));
 		assertEquals("(a=(a=b,c=123,d=false,e=true,f=null))", su.serialize(t));
 		assertEquals("(\n\ta=(\n\t\ta=b,\n\t\tc=123,\n\t\td=false,\n\t\te=true,\n\t\tf=null\n\t)\n)", sr.serialize(t));
 
 		// Simple map with primitives as literals
-		t = Json5Map.ofText("{a:'b',c:'123',d:'false',e:'true',f:'null'}");
+		t = Json5Map.ofString("{a:'b',c:'123',d:'false',e:'true',f:'null'}");
 		assertEquals("(a=b,c='123',d='false',e='true',f='null')", s.serialize(t));
 		assertEquals("(a=b,c='123',d='false',e='true',f='null')", su.serialize(t));
 		assertEquals("(\n\ta=b,\n\tc='123',\n\td='false',\n\te='true',\n\tf='null'\n)", sr.serialize(t));
@@ -76,13 +76,13 @@ class UonSerializer_Test {
 		assertEquals("null", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{null:null}");
+		t = Json5Map.ofString("{null:null}");
 		assertEquals("(null=null)", s.serialize(t));
 		assertEquals("(null=null)", su.serialize(t));
 		assertEquals("(\n\tnull=null\n)", sr.serialize(t));
 
 		// 3rd level
-		t = Json5Map.ofText("{null:{null:null}}");
+		t = Json5Map.ofString("{null:{null:null}}");
 		assertEquals("(null=(null=null))", s.serialize(t));
 		assertEquals("(null=(null=null))", su.serialize(t));
 		assertEquals("(\n\tnull=(\n\t\tnull=null\n\t)\n)", sr.serialize(t));
@@ -95,7 +95,7 @@ class UonSerializer_Test {
 		assertEquals("@()", sr.serialize(t));
 
 		// 2nd level in map
-		t = Json5Map.ofText("{x:[]}");
+		t = Json5Map.ofString("{x:[]}");
 		assertEquals("(x=@())", s.serialize(t));
 		assertEquals("(x=@())", su.serialize(t));
 		assertEquals("(\n\tx=@()\n)", sr.serialize(t));
@@ -114,7 +114,7 @@ class UonSerializer_Test {
 		assertEquals("@(\n\t''\n)", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{x:['']}");
+		t = Json5Map.ofString("{x:['']}");
 		assertEquals("(x=@(''))", s.serialize(t));
 		assertEquals("(x=@(''))", su.serialize(t));
 		assertEquals("(\n\tx=@(\n\t\t''\n\t)\n)", sr.serialize(t));
@@ -133,7 +133,7 @@ class UonSerializer_Test {
 		assertEquals("\u0000", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{'\u0000':'\u0000'}");
+		t = Json5Map.ofString("{'\u0000':'\u0000'}");
 		assertEquals("(%00=%00)", s.serialize(t));
 		assertEquals("(\u0000=\u0000)", su.serialize(t));
 		assertEquals("(\n\t\u0000=\u0000\n)", sr.serialize(t));
@@ -146,7 +146,7 @@ class UonSerializer_Test {
 		assertEquals("false", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{x:false}");
+		t = Json5Map.ofString("{x:false}");
 		assertEquals("(x=false)", s.serialize(t));
 		assertEquals("(x=false)", su.serialize(t));
 		assertEquals("(\n\tx=false\n)", sr.serialize(t));
@@ -159,7 +159,7 @@ class UonSerializer_Test {
 		assertEquals("123", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{x:123}");
+		t = Json5Map.ofString("{x:123}");
 		assertEquals("(x=123)", s.serialize(t));
 		assertEquals("(x=123)", su.serialize(t));
 		assertEquals("(\n\tx=123\n)", sr.serialize(t));
@@ -172,7 +172,7 @@ class UonSerializer_Test {
 		assertEquals("x;/?:@-_.!*~'", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{x:'x;/?:@-_.!*\\''}");
+		t = Json5Map.ofString("{x:'x;/?:@-_.!*\\''}");
 		assertEquals("(x=x;/?:@-_.!*~')", s.serialize(t));
 		assertEquals("(x=x;/?:@-_.!*~')", su.serialize(t));
 		assertEquals("(\n\tx=x;/?:@-_.!*~'\n)", sr.serialize(t));
@@ -185,7 +185,7 @@ class UonSerializer_Test {
 		assertEquals("x{}|\\^[]`<>#%\"&+", sr.serialize(t));
 
 		// 2nd level
-		t = Json5Map.ofText("{'x{}|\\\\^[]`<>#%\"&+':'x{}|\\\\^[]`<>#%\"&+'}");
+		t = Json5Map.ofString("{'x{}|\\\\^[]`<>#%\"&+':'x{}|\\\\^[]`<>#%\"&+'}");
 		assertEquals("(x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B=x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B)", s.serialize(t));
 		assertEquals("(x{}|\\^[]`<>#%\"&+=x{}|\\^[]`<>#%\"&+)", su.serialize(t));
 		assertEquals("(\n\tx{}|\\^[]`<>#%\"&+=x{}|\\^[]`<>#%\"&+\n)", sr.serialize(t));
@@ -199,14 +199,14 @@ class UonSerializer_Test {
 
 		// 2nd level
 		// Note behavior on serializeParams() is different since 2nd-level is top level.
-		t = Json5Map.ofText("{'x$,()~':'x$,()~'}");
+		t = Json5Map.ofString("{'x$,()~':'x$,()~'}");
 		assertEquals("('x$,()~~'='x$,()~~')", s.serialize(t));
 		assertEquals("('x$,()~~'='x$,()~~')", su.serialize(t));
 		assertEquals("(\n\t'x$,()~~'='x$,()~~'\n)", sr.serialize(t));
 
 		// 3rd level
 		// Note behavior on serializeParams().
... 1451 lines suppressed ...