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 1dd7d22a06 refactor: empty-return policy for MarshalledList/Map 
factories + SchemaUtils parse helpers
1dd7d22a06 is described below

commit 1dd7d22a06920a60693bb4e0f829f73687c06af9
Author: James Bognar <[email protected]>
AuthorDate: Wed May 20 10:19:31 2026 -0400

    refactor: empty-return policy for MarshalledList/Map factories + 
SchemaUtils parse helpers
    
    - juneau-marshall collections + json5: every ofString(...) / 
ofJsonOrCdl(...) / ofJson5OrCdl(...) returns a fresh empty instance instead of 
null on null/empty input; drops 4 java:S1168 // TODO suppressions; Javadoc 
updated to "never null"
    - juneau-marshall jsonschema: SchemaUtils.parseMap (Object and String[] 
overloads) and parseSet switch to empty returns; drops 3 java:S1168 // TODO 
suppressions; caller audit confirmed all sites safe
    - BeanMapLoader / MarshallingSession.parseToMap: latent NPE paths become 
well-defined no-ops without source changes; MarshallingSession Javadoc tightened
    - new tests: BeanMapLoader_Test, MarshallingSession_Test, SchemaUtils_Test 
plus regression coverage on the six collection *_Test classes
    - archive: FINISHED-48 / FINISHED-49 replace the live plan files
---
 .../java/org/apache/juneau/MarshallingSession.java |   4 +-
 .../org/apache/juneau/collections/JsonList.java    |  30 +--
 .../org/apache/juneau/collections/JsonMap.java     |  20 +-
 .../apache/juneau/collections/MarshalledList.java  |  15 +-
 .../apache/juneau/collections/MarshalledMap.java   |  15 +-
 .../java/org/apache/juneau/json5/Json5List.java    |  28 +--
 .../java/org/apache/juneau/json5/Json5Map.java     |  20 +-
 .../org/apache/juneau/jsonschema/SchemaUtils.java  |  35 ++-
 .../java/org/apache/juneau/BeanMapLoader_Test.java |  60 +++++
 .../test/java/org/apache/juneau/JsonList_Test.java |  60 +++++
 .../test/java/org/apache/juneau/JsonMap_Test.java  |  42 ++++
 .../org/apache/juneau/MarshallingSession_Test.java |  56 +++++
 .../juneau/collections/MarshalledList_Test.java    |  16 +-
 .../juneau/collections/MarshalledMap_Test.java     |  16 +-
 .../org/apache/juneau/json5/Json5List_Test.java    |  52 +++-
 .../org/apache/juneau/json5/Json5Map_Test.java     |  34 ++-
 .../apache/juneau/jsonschema/SchemaUtils_Test.java | 127 ++++++++++
 ...ISHED-48-empty-return-marshalled-collections.md | 111 +++++++++
 todo/FINISHED-49-schemautils-null-returns.md       |  53 ++++
 .../TODO-48-empty-return-marshalled-collections.md | 271 ---------------------
 todo/TODO-49-schemautils-null-returns.md           |  38 ---
 21 files changed, 693 insertions(+), 410 deletions(-)

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 88396d36c5..2c51d70e4e 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
@@ -1386,8 +1386,8 @@ public class MarshallingSession extends ContextSession 
implements ConverterSessi
         * values written as <c>{a:'b'}</c>) continue to parse — the 
strict-JSON {@link JsonMap} retargeting in v9.5
         * would otherwise break those call sites.
         *
-        * @param value The JSON-formatted character sequence to parse.  Must 
not be <jk>null</jk>.
-        * @return The parsed {@link Json5Map}.
+        * @param value The JSON-formatted character sequence to parse.
+        * @return The parsed {@link Json5Map} (empty if {@code value} is 
<jk>null</jk>), never <jk>null</jk>.
         */
        @Override /* BeanSession */
        public final Map<?,?> parseToMap(CharSequence value) {
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 87ff0377d6..ed1068b68c 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
@@ -233,11 +233,11 @@ public class JsonList extends MarshalledList {
         * @param json
         *      The JSON text to parse.
         *      <br>Can be normal or simplified JSON.
-        * @return A new list or <jk>null</jk> if the string was null.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static JsonList ofString(CharSequence json) throws 
ParseException {
-               return json == null ? null : new JsonList(json);
+               return json == null ? new JsonList() : new JsonList(json);
        }
 
        /**
@@ -246,14 +246,11 @@ public class JsonList extends MarshalledList {
         * @param json
         *      The reader containing JSON text to parse.
         *      <br>Can contain normal or simplified JSON.
-        * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
-       @SuppressWarnings({
-               "java:S1168"     // TODO: null input = null output by design. 
Consider empty JsonList.
-       })
        public static JsonList ofString(Reader json) throws ParseException {
-               return json == null ? null : new JsonList(json);
+               return json == null ? new JsonList() : new JsonList(json);
        }
 
        /**
@@ -263,15 +260,12 @@ public class JsonList extends MarshalledList {
         * The type of string is auto-detected.
         *
         * @param s The string to parse.
-        * @return The parsed string.
+        * @return A new list (empty if the input was <jk>null</jk> or empty), 
never <jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
-       @SuppressWarnings({
-               "java:S1168"     // TODO: null for empty input. Consider empty 
JsonList.
-       })
        public static JsonList ofJsonOrCdl(String s) throws ParseException {
                if (Utils.e(s))  // NOAI
-                       return null;
+                       return new JsonList();
                if (! isProbablyJsonArray(s, true))
                        return new JsonList((Object[])splita(s.trim(), ','));
                return new JsonList(s);
@@ -280,17 +274,15 @@ public class JsonList extends MarshalledList {
        /**
         * Construct a list initialized with the specified string.
         *
-        * @param in
-        *      The input being parsed.
-        *      <br>Can be <jk>null</jk>.
+        * @param in The input being parsed.
         * @param p
         *      The parser to use to parse the input.
         *      <br>If <jk>null</jk>, uses {@link JsonParser}.
-        * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static JsonList ofString(CharSequence in, Parser p) throws 
ParseException {
-               return in == null ? null : new JsonList(in, p);
+               return in == null ? new JsonList() : new JsonList(in, p);
        }
 
        /**
@@ -302,14 +294,14 @@ public class JsonList extends MarshalledList {
         * @param p
         *      The parser to use to parse the input.
         *      <br>If <jk>null</jk>, uses {@link JsonParser}.
-        * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        @SuppressWarnings({
                "java:S1172" // Parameter reserved for future parser context 
support
        })
        public static JsonList ofString(Reader in, Parser p) throws 
ParseException {
-               return in == null ? null : new JsonList(in);
+               return in == null ? new JsonList() : 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 7496223153..3ba09578ca 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
@@ -232,11 +232,11 @@ public class JsonMap extends MarshalledMap {
         * @param json
         *      The JSON text to parse.
         *      <br>Can be normal or simplified JSON.
-        * @return A new map or <jk>null</jk> if the string was null.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static JsonMap ofString(CharSequence json) throws ParseException 
{
-               return json == null ? null : new JsonMap(json);
+               return json == null ? new JsonMap() : new JsonMap(json);
        }
 
        /**
@@ -245,27 +245,25 @@ public class JsonMap extends MarshalledMap {
         * @param json
         *      The reader containing JSON text to parse.
         *      <br>Can contain normal or simplified JSON.
-        * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static JsonMap ofString(Reader json) throws ParseException {
-               return json == null ? null : new JsonMap(json);
+               return json == null ? new JsonMap() : new JsonMap(json);
        }
 
        /**
         * Construct a map initialized with the specified string.
         *
-        * @param in
-        *      The input being parsed.
-        *      <br>Can be <jk>null</jk>.
+        * @param in The input being parsed.
         * @param p
         *      The parser to use to parse the input.
         *      <br>If <jk>null</jk>, uses {@link JsonParser}.
-        * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static JsonMap ofString(CharSequence in, Parser p) throws 
ParseException {
-               return in == null ? null : new JsonMap(in, p);
+               return in == null ? new JsonMap() : new JsonMap(in, p);
        }
 
        /**
@@ -277,11 +275,11 @@ public class JsonMap extends MarshalledMap {
         * @param p
         *      The parser to use to parse the input.
         *      <br>If <jk>null</jk>, uses {@link JsonParser}.
-        * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static JsonMap ofString(Reader in, Parser p) throws 
ParseException {
-               return in == null ? null : new JsonMap(in, p);
+               return in == null ? new JsonMap() : 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 2d28ea0364..da4a3b3fc8 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
@@ -188,32 +188,29 @@ public class MarshalledList extends LinkedList<Object> {
        /**
         * Construct a list initialized by parsing the specified string with 
the specified parser.
         *
-        * @param in
-        *      The input being parsed.
-        *      <br>Can be <jk>null</jk>.
+        * @param in The input being parsed.
         * @param p
         *      The parser to use to parse the input.
         *      <br>Must not be <jk>null</jk>.
-        * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static MarshalledList ofString(CharSequence in, Parser p) throws 
ParseException {
-               return in == null ? null : new MarshalledList(in, p);
+               return in == null ? new MarshalledList() : new 
MarshalledList(in, p);
        }
 
        /**
         * Construct a list initialized by parsing the specified reader with 
the specified parser.
         *
-        * @param in
-        *      The reader containing the input being parsed.
+        * @param in The reader containing the input being parsed.
         * @param p
         *      The parser to use to parse the input.
         *      <br>Must not be <jk>null</jk>.
-        * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static MarshalledList ofString(java.io.Reader in, Parser p) 
throws ParseException {
-               return in == null ? null : new MarshalledList(in, p);
+               return in == null ? new MarshalledList() : new 
MarshalledList(in, p);
        }
 
        transient MarshallingSession session = null;
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 cea0087ea5..d70e1a8895 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
@@ -181,32 +181,29 @@ public class MarshalledMap extends 
LinkedHashMap<String,Object> {
        /**
         * Construct a map initialized by parsing the specified string with the 
specified parser.
         *
-        * @param in
-        *      The input being parsed.
-        *      <br>Can be <jk>null</jk>.
+        * @param in The input being parsed.
         * @param p
         *      The parser to use to parse the input.
         *      <br>Must not be <jk>null</jk>.
-        * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static MarshalledMap ofString(CharSequence in, Parser p) throws 
ParseException {
-               return in == null ? null : new MarshalledMap(in, p);
+               return in == null ? new MarshalledMap() : new MarshalledMap(in, 
p);
        }
 
        /**
         * Construct a map initialized by parsing the specified reader with the 
specified parser.
         *
-        * @param in
-        *      The reader containing the input being parsed.
+        * @param in The reader containing the input being parsed.
         * @param p
         *      The parser to use to parse the input.
         *      <br>Must not be <jk>null</jk>.
-        * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static MarshalledMap ofString(java.io.Reader in, Parser p) 
throws ParseException {
-               return in == null ? null : new MarshalledMap(in, p);
+               return in == null ? new MarshalledMap() : new MarshalledMap(in, 
p);
        }
 
        /*
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 d886af6cbe..29e9711e47 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
@@ -197,25 +197,22 @@ public class Json5List extends MarshalledList {
         * Construct a list initialized with the specified JSON5 string.
         *
         * @param json5 The JSON5 text to parse.
-        * @return A new list or <jk>null</jk> if the string was null.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static Json5List ofString(CharSequence json5) throws 
ParseException {
-               return json5 == null ? null : new Json5List(json5);
+               return json5 == null ? new Json5List() : new Json5List(json5);
        }
 
        /**
         * Construct a list initialized with the specified reader containing 
JSON5.
         *
         * @param json5 The reader containing JSON5 text to parse.
-        * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
-       @SuppressWarnings({
-               "java:S1168"     // TODO: null input = null output by design. 
Consider empty Json5List.
-       })
        public static Json5List ofString(Reader json5) throws ParseException {
-               return json5 == null ? null : new Json5List(json5);
+               return json5 == null ? new Json5List() : new Json5List(json5);
        }
 
        /**
@@ -225,15 +222,12 @@ public class Json5List extends MarshalledList {
         * The type of string is auto-detected.
         *
         * @param s The string to parse.
-        * @return The parsed string.
+        * @return A new list (empty if the input was <jk>null</jk> or empty), 
never <jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
-       @SuppressWarnings({
-               "java:S1168"     // TODO: null for empty input. Consider empty 
Json5List.
-       })
        public static Json5List ofJson5OrCdl(String s) throws ParseException {
                if (Utils.e(s))  // NOAI
-                       return null;
+                       return new Json5List();
                if (! isProbablyJsonArray(s, true))
                        return new Json5List((Object[])splita(s.trim(), ','));
                return new Json5List(s);
@@ -242,13 +236,13 @@ public class Json5List extends MarshalledList {
        /**
         * Construct a list initialized with the specified string.
         *
-        * @param in The input being parsed. Can be <jk>null</jk>.
+        * @param in The input being parsed.
         * @param p The parser to use. If <jk>null</jk>, uses {@link 
Json5Parser}.
-        * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static Json5List ofString(CharSequence in, Parser p) throws 
ParseException {
-               return in == null ? null : new Json5List(in, p);
+               return in == null ? new Json5List() : new Json5List(in, p);
        }
 
        /**
@@ -256,14 +250,14 @@ public class Json5List extends MarshalledList {
         *
         * @param in The reader containing the input being parsed.
         * @param p The parser to use. If <jk>null</jk>, uses {@link 
Json5Parser}.
-        * @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new list (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        @SuppressWarnings({
                "java:S1172" // Parameter reserved for future parser context 
support
        })
        public static Json5List ofString(Reader in, Parser p) throws 
ParseException {
-               return in == null ? null : new Json5List(in);
+               return in == null ? new Json5List() : 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 3ba3320268..35162c4334 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
@@ -198,11 +198,11 @@ public class Json5Map extends MarshalledMap {
         *
         * @param json5
         *      The JSON5 text to parse.
-        * @return A new map or <jk>null</jk> if the string was null.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static Json5Map ofString(CharSequence json5) throws 
ParseException {
-               return json5 == null ? null : new Json5Map(json5);
+               return json5 == null ? new Json5Map() : new Json5Map(json5);
        }
 
        /**
@@ -210,27 +210,25 @@ public class Json5Map extends MarshalledMap {
         *
         * @param json5
         *      The reader containing JSON5 text to parse.
-        * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static Json5Map ofString(Reader json5) throws ParseException {
-               return json5 == null ? null : new Json5Map(json5);
+               return json5 == null ? new Json5Map() : new Json5Map(json5);
        }
 
        /**
         * Construct a map initialized with the specified string and parser.
         *
-        * @param in
-        *      The input being parsed.
-        *      <br>Can be <jk>null</jk>.
+        * @param in The input being parsed.
         * @param p
         *      The parser to use to parse the input.
         *      <br>If <jk>null</jk>, uses {@link Json5Parser}.
-        * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static Json5Map ofString(CharSequence in, Parser p) throws 
ParseException {
-               return in == null ? null : new Json5Map(in, p);
+               return in == null ? new Json5Map() : new Json5Map(in, p);
        }
 
        /**
@@ -241,11 +239,11 @@ public class Json5Map extends MarshalledMap {
         * @param p
         *      The parser to use to parse the input.
         *      <br>If <jk>null</jk>, uses {@link Json5Parser}.
-        * @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
+        * @return A new map (empty if the input was <jk>null</jk>), never 
<jk>null</jk>.
         * @throws ParseException Malformed input encountered.
         */
        public static Json5Map ofString(Reader in, Parser p) throws 
ParseException {
-               return in == null ? null : new Json5Map(in, p);
+               return in == null ? new Json5Map() : 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 9fc1bf6342..bdc3a78474 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
@@ -59,21 +59,20 @@ public class SchemaUtils {
         * Parses a generic object as JSON and converts it to an {@link 
JsonMap}.
         *
         * @param o The object to convert.
-        * @return The parsed contents.
+        * @return
+        *      The parsed contents (a new empty {@link JsonMap} if {@code o} 
is <jk>null</jk> or an empty string),
+        *      never <jk>null</jk>.
         * @throws ParseException Invalid JSON encountered.
         */
-       @SuppressWarnings({
-               "java:S1168"     // TODO: null = not configured. Consider empty 
JsonMap for config parsing.
-       })
        public static JsonMap parseMap(Object o) throws ParseException {
                if (o == null)
-                       return null;
+                       return new JsonMap();
                if (o instanceof String[] o2)
                        o = joinnl(o2);
                if (o instanceof String o2) {
                        var s = o2;
                        if (s.isEmpty())
-                               return null;
+                               return new JsonMap();
                        if ("IGNORE".equalsIgnoreCase(s))
                                return JsonMap.of("ignore", true);
                        if (! isProbablyJsonObject(s, true))
@@ -91,18 +90,17 @@ public class SchemaUtils {
         * Concatenates and parses a string array as a JSON object.
         *
         * @param ss The array to concatenate and parse.
-        * @return The parsed contents.
+        * @return
+        *      The parsed contents (a new empty {@link JsonMap} if the array 
is empty or joins to an empty string),
+        *      never <jk>null</jk>.
         * @throws ParseException Invalid JSON encountered.
         */
-       @SuppressWarnings({
-               "java:S1168"     // TODO: null = not configured. Consider empty 
JsonMap.
-       })
        public static JsonMap parseMap(String[] ss) throws ParseException {
                if (ss.length == 0)
-                       return null;
+                       return new JsonMap();
                String s = joinnl(ss);
                if (s.isEmpty())
-                       return null;
+                       return new JsonMap();
                if (! isProbablyJsonObject(s, true))
                        s = "{" + s + "}";
                return new JsonMap(Json5Map.ofString(s));
@@ -112,19 +110,18 @@ public class SchemaUtils {
         * Concatenates and parses a string array as JSON array or 
comma-delimited list.
         *
         * @param ss The array to concatenate and parse.
-        * @return The parsed contents.
+        * @return
+        *      The parsed contents (a new empty mutable {@link Set} if the 
array is empty or joins to an empty string),
+        *      never <jk>null</jk>.
         * @throws ParseException Invalid JSON encountered.
         */
-       @SuppressWarnings({
-               "java:S1168"     // TODO: null = not configured. Consider empty 
set.
-       })
        public static Set<String> parseSet(String[] ss) throws ParseException {
+               Set<String> set = set();
                if (ss.length == 0)
-                       return null;
+                       return set;
                String s = joinnl(ss);
                if (s.isEmpty())
-                       return null;
-               Set<String> set = set();
+                       return set;
                Json5List.ofJson5OrCdl(s).forEach(x -> set.add(x.toString()));
                return set;
        }
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/BeanMapLoader_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/BeanMapLoader_Test.java
new file mode 100644
index 0000000000..c232f9700e
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/BeanMapLoader_Test.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;
+
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.json5.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link BeanMapLoader}.
+ *
+ * <p>
+ * Confirms that the load helpers are no-ops when given null input — a 
contract that became achievable
+ * once {@link org.apache.juneau.collections.JsonMap#ofString(java.io.Reader)} 
and
+ * {@link org.apache.juneau.json5.Json5Map#ofString(CharSequence)} switched to 
returning empty
+ * instances instead of {@code null}.
+ */
+class BeanMapLoader_Test extends TestBase {
+
+       public static class A {
+               public String name;
+               public int age;
+       }
+
+       @Test void a01_loadStringPopulatesBean() throws Exception {
+               var m = MarshallingContext.DEFAULT.newBeanMap(A.class);
+               BeanMapLoader.load(m, "{name:'John',age:21}");
+               assertBean(m.getBean(), "name,age", "John,21");
+       }
+
+       @Test void a02_loadNullStringIsNoOp() throws Exception {
+               var m = MarshallingContext.DEFAULT.newBeanMap(A.class);
+               var result = BeanMapLoader.load(m, (String)null);
+               assertSame(m, result);
+               assertBean(m.getBean(), "name,age", "<null>,0");
+       }
+
+       @Test void a03_loadNullReaderIsNoOp() throws Exception {
+               var m = MarshallingContext.DEFAULT.newBeanMap(A.class);
+               var result = BeanMapLoader.load(m, null, Json5Parser.DEFAULT);
+               assertSame(m, result);
+               assertBean(m.getBean(), "name,age", "<null>,0");
+       }
+}
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 b34ff0836e..486bf87ffd 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
@@ -21,8 +21,11 @@ import static 
org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.io.*;
 import java.util.*;
 
+import org.apache.juneau.collections.*;
+import org.apache.juneau.json.*;
 import org.apache.juneau.json5.*;
 import org.junit.jupiter.api.*;
 
@@ -150,4 +153,61 @@ class JsonList_Test extends TestBase {
                // toString(WriterSerializer) — generalized
                assertString("['b','a']", 
l.toString(org.apache.juneau.json5.Json5Serializer.DEFAULT));
        }
+
+       
//====================================================================================================
+       // Empty-instead-of-null returns on null/empty input.
+       
//====================================================================================================
+       @Test void a08_factoryOfStringNullCharSequence() throws Exception {
+               var l = JsonList.ofString((CharSequence)null);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(JsonList.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a09_factoryOfStringNullReader() throws Exception {
+               var l = JsonList.ofString((Reader)null);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(JsonList.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a10_factoryOfStringNullCharSequenceWithParser() throws 
Exception {
+               var l = JsonList.ofString((CharSequence)null, 
JsonParser.DEFAULT);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(JsonList.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a11_factoryOfStringNullReaderWithParser() throws Exception {
+               var l = JsonList.ofString((Reader)null, JsonParser.DEFAULT);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(JsonList.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a12_factoryOfJsonOrCdlNullInput() throws Exception {
+               var l = JsonList.ofJsonOrCdl(null);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(JsonList.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a13_factoryOfJsonOrCdlEmptyInput() throws Exception {
+               var l = JsonList.ofJsonOrCdl("");
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(JsonList.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
 }
\ No newline at end of file
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 4a1f926c2f..7ba78abb0b 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
@@ -20,8 +20,11 @@ import static org.apache.juneau.TestUtils.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.io.*;
 import java.util.*;
 
+import org.apache.juneau.collections.*;
+import org.apache.juneau.json.*;
 import org.apache.juneau.json5.*;
 import org.apache.juneau.objecttools.*;
 import org.junit.jupiter.api.*;
@@ -380,4 +383,43 @@ class JsonMap_Test extends TestBase {
                // toString(WriterSerializer) — generalized
                assertString("{b:'2',a:'1'}", 
m.toString(org.apache.juneau.json5.Json5Serializer.DEFAULT));
        }
+
+       
//====================================================================================================
+       // Empty-instead-of-null returns on null/empty input.
+       
//====================================================================================================
+       @Test void a10_factoryOfStringNullCharSequence() throws Exception {
+               var m = JsonMap.ofString((CharSequence)null);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(JsonMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a11_factoryOfStringNullReader() throws Exception {
+               var m = JsonMap.ofString((Reader)null);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(JsonMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a12_factoryOfStringNullCharSequenceWithParser() throws 
Exception {
+               var m = JsonMap.ofString((CharSequence)null, 
JsonParser.DEFAULT);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(JsonMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a13_factoryOfStringNullReaderWithParser() throws Exception {
+               var m = JsonMap.ofString((Reader)null, JsonParser.DEFAULT);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(JsonMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/MarshallingSession_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/MarshallingSession_Test.java
new file mode 100644
index 0000000000..ecef6d6619
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/MarshallingSession_Test.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.json5.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for {@link MarshallingSession}.
+ *
+ * <p>
+ * Confirms that the session-level parse helpers tolerate null input now that
+ * {@link Json5Map#ofString(CharSequence)} returns an empty instance instead 
of {@code null}.
+ */
+class MarshallingSession_Test extends TestBase {
+
+       @Test void a01_parseToMapNullReturnsEmptySessionAttachedMap() {
+               var session = MarshallingContext.DEFAULT.getSession();
+               var m = session.parseToMap(null);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertTrue(m instanceof Json5Map);
+       }
+
+       @Test void a02_parseToMapEmptyParsesToEmptyMap() {
+               var session = MarshallingContext.DEFAULT.getSession();
+               var m = session.parseToMap("{}");
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertTrue(m instanceof Json5Map);
+       }
+
+       @Test void a03_parseToMapPopulatedInputParsesNormally() {
+               var session = MarshallingContext.DEFAULT.getSession();
+               var m = session.parseToMap("{a:1,b:'two'}");
+               assertEquals(2, m.size());
+               assertEquals(1, ((Json5Map)m).getInt("a"));
+               assertEquals("two", ((Json5Map)m).getString("b"));
+       }
+}
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 79d4607089..06009dd3d7 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
@@ -70,7 +70,21 @@ class MarshalledList_Test extends TestBase {
        }
 
        @Test void a06_parseViaOfStringNullInput() throws Exception {
-               assertNull(MarshalledList.ofString((CharSequence)null, 
Json5Parser.DEFAULT));
+               var l = MarshalledList.ofString((CharSequence)null, 
Json5Parser.DEFAULT);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(MarshalledList.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a06c_parseViaOfStringNullReader() throws Exception {
+               var l = MarshalledList.ofString((java.io.Reader)null, 
Json5Parser.DEFAULT);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(MarshalledList.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
        }
 
        @Test void a06b_ofTextAliasStillWorks() throws Exception {
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 3036e0578a..f3e47badde 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
@@ -70,7 +70,21 @@ class MarshalledMap_Test extends TestBase {
        }
 
        @Test void a06_parseViaOfStringNullInput() throws Exception {
-               assertNull(MarshalledMap.ofString((CharSequence)null, 
Json5Parser.DEFAULT));
+               var m = MarshalledMap.ofString((CharSequence)null, 
Json5Parser.DEFAULT);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(MarshalledMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a06c_parseViaOfStringNullReader() throws Exception {
+               var m = MarshalledMap.ofString((java.io.Reader)null, 
Json5Parser.DEFAULT);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(MarshalledMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
        }
 
        @Test void a06b_ofTextAliasStillWorks() throws Exception {
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 63d4cb0b75..254a738633 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
@@ -49,7 +49,57 @@ class Json5List_Test extends TestBase {
        }
 
        @Test void a04_factoryOfStringNullInput() throws Exception {
-               assertNull(Json5List.ofString((CharSequence)null));
+               var l = Json5List.ofString((CharSequence)null);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(Json5List.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a04c_factoryOfStringNullReader() throws Exception {
+               var l = Json5List.ofString((Reader)null);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(Json5List.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a04d_factoryOfStringNullCharSequenceWithParser() throws 
Exception {
+               var l = Json5List.ofString((CharSequence)null, 
Json5Parser.DEFAULT);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(Json5List.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a04e_factoryOfStringNullReaderWithParser() throws Exception {
+               var l = Json5List.ofString((Reader)null, Json5Parser.DEFAULT);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(Json5List.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a04f_factoryOfJson5OrCdlNullInput() throws Exception {
+               var l = Json5List.ofJson5OrCdl(null);
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(Json5List.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
+       }
+
+       @Test void a04g_factoryOfJson5OrCdlEmptyInput() throws Exception {
+               var l = Json5List.ofJson5OrCdl("");
+               assertNotNull(l);
+               assertTrue(l.isEmpty());
+               assertEquals(Json5List.class, l.getClass());
+               l.add("x");
+               assertEquals(1, l.size());
        }
 
        @Test void a04b_ofTextAliasStillWorks() throws Exception {
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 31972cf992..2f1eadc18c 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
@@ -57,7 +57,39 @@ class Json5Map_Test extends TestBase {
        }
 
        @Test void a04_factoryOfStringNullInput() throws Exception {
-               assertNull(Json5Map.ofString((CharSequence)null));
+               var m = Json5Map.ofString((CharSequence)null);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(Json5Map.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a04c_factoryOfStringNullReader() throws Exception {
+               var m = Json5Map.ofString((Reader)null);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(Json5Map.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a04d_factoryOfStringNullCharSequenceWithParser() throws 
Exception {
+               var m = Json5Map.ofString((CharSequence)null, 
Json5Parser.DEFAULT);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(Json5Map.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a04e_factoryOfStringNullReaderWithParser() throws Exception {
+               var m = Json5Map.ofString((Reader)null, Json5Parser.DEFAULT);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(Json5Map.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
        }
 
        @Test void a04b_ofTextAliasStillWorks() throws Exception {
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/SchemaUtils_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/SchemaUtils_Test.java
new file mode 100644
index 0000000000..8bf9604e4d
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/SchemaUtils_Test.java
@@ -0,0 +1,127 @@
+/*
+ * 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.jsonschema;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.collections.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Smoke tests for {@link SchemaUtils}.
+ *
+ * <p>
+ * Confirms the empty-instead-of-null contract on {@link 
SchemaUtils#parseMap(Object)},
+ * {@link SchemaUtils#parseMap(String[])}, and {@link 
SchemaUtils#parseSet(String[])}.
+ */
+class SchemaUtils_Test extends TestBase {
+
+       @Test void a01_parseMapObjectNullReturnsEmptyMutableJsonMap() throws 
Exception {
+               var m = SchemaUtils.parseMap((Object)null);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(JsonMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a02_parseMapObjectEmptyStringReturnsEmptyMutableJsonMap() 
throws Exception {
+               var m = SchemaUtils.parseMap("");
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(JsonMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a03_parseMapObjectIgnoreSentinelStillReturnsIgnoreMap() 
throws Exception {
+               var m = SchemaUtils.parseMap("IGNORE");
+               assertNotNull(m);
+               assertEquals(Boolean.TRUE, m.get("ignore"));
+       }
+
+       @Test void a04_parseMapObjectIgnoreSentinelMixedCase() throws Exception 
{
+               var m = SchemaUtils.parseMap("ignore");
+               assertNotNull(m);
+               assertEquals(Boolean.TRUE, m.get("ignore"));
+       }
+
+       @Test void a05_parseMapObjectPopulatedJson() throws Exception {
+               var m = SchemaUtils.parseMap("{a:1,b:'two'}");
+               assertEquals(2, m.size());
+               assertEquals(1, m.getInt("a"));
+               assertEquals("two", m.getString("b"));
+       }
+
+       @Test void a06_parseMapStringArrayEmptyReturnsEmptyMutableJsonMap() 
throws Exception {
+               var m = SchemaUtils.parseMap(new String[0]);
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(JsonMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void 
a07_parseMapStringArrayJoinedEmptyReturnsEmptyMutableJsonMap() throws Exception 
{
+               var m = SchemaUtils.parseMap(new String[]{""});
+               assertNotNull(m);
+               assertTrue(m.isEmpty());
+               assertEquals(JsonMap.class, m.getClass());
+               m.put("x", 1);
+               assertEquals(1, m.size());
+       }
+
+       @Test void a08_parseMapStringArrayPopulated() throws Exception {
+               var m = SchemaUtils.parseMap(new String[]{"{a:1, b:'two'}"});
+               assertEquals(2, m.size());
+               assertEquals(1, m.getInt("a"));
+               assertEquals("two", m.getString("b"));
+       }
+
+       @Test void b01_parseSetEmptyArrayReturnsEmptyMutableSet() throws 
Exception {
+               var s = SchemaUtils.parseSet(new String[0]);
+               assertNotNull(s);
+               assertTrue(s.isEmpty());
+               s.add("x");
+               assertEquals(1, s.size());
+       }
+
+       @Test void b02_parseSetJoinedEmptyReturnsEmptyMutableSet() throws 
Exception {
+               var s = SchemaUtils.parseSet(new String[]{""});
+               assertNotNull(s);
+               assertTrue(s.isEmpty());
+               s.add("x");
+               assertEquals(1, s.size());
+       }
+
+       @Test void b03_parseSetCdlPopulated() throws Exception {
+               var s = SchemaUtils.parseSet(new String[]{"a,b,c"});
+               assertEquals(3, s.size());
+               assertTrue(s.contains("a"));
+               assertTrue(s.contains("b"));
+               assertTrue(s.contains("c"));
+       }
+
+       @Test void b04_parseSetJsonArrayPopulated() throws Exception {
+               var s = SchemaUtils.parseSet(new String[]{"['a','b','c']"});
+               assertEquals(3, s.size());
+               assertTrue(s.contains("a"));
+               assertTrue(s.contains("b"));
+               assertTrue(s.contains("c"));
+       }
+}
diff --git a/todo/FINISHED-48-empty-return-marshalled-collections.md 
b/todo/FINISHED-48-empty-return-marshalled-collections.md
new file mode 100644
index 0000000000..10b7700d1c
--- /dev/null
+++ b/todo/FINISHED-48-empty-return-marshalled-collections.md
@@ -0,0 +1,111 @@
+# FINISHED-48: empty-return on marshalled-collection factories
+
+Archived from `TODO-48-empty-return-marshalled-collections.md` on 2026-05-20.
+
+## What shipped
+
+Every `ofString(...)` overload (plus `ofJsonOrCdl(...)` / `ofJson5OrCdl(...)`) 
on `MarshalledList`, `MarshalledMap`, `JsonList`, `JsonMap`, `Json5List`, 
`Json5Map` now returns a fresh empty instance of the precise subclass when 
input is null or empty, instead of `null`. The four flagged `// TODO 
java:S1168` markers and their `@SuppressWarnings` annotations are gone. Javadoc 
on every affected site now reads `@return A new list/map (empty if the input 
was null), never null.` `of(Collection [...]
+
+Two latent NPE call sites became well-defined no-ops without any source change 
in the caller — `BeanMapLoader.load(BeanMap, String)`, 
`BeanMapLoader.load(BeanMap, Reader, ReaderParser)`, and 
`MarshallingSession.parseToMap(CharSequence)` all relied on the factories 
returning non-null; once the factories never return null, `m.putAll(empty)` / 
`empty.session(this)` are no-ops by definition. 
`MarshallingSession.parseToMap`'s Javadoc was tightened to reflect the new 
tolerant contract.
+
+## Files delivered
+
+Source (7):
+
+- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java`
+- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java`
+- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java`
+- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java`
+- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java`
+- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java`
+- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java`
 (Javadoc only)
+
+Tests (8):
+
+- `juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java` 
(rewrote `a04_factoryOfStringNullInput`, added empty-input regression coverage)
+- `juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java` 
(same)
+- 
`juneau-utest/src/test/java/org/apache/juneau/collections/JsonList_Test.java` 
(added empty-input regression coverage)
+- `juneau-utest/src/test/java/org/apache/juneau/collections/JsonMap_Test.java` 
(same)
+- 
`juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java`
 (rewrote `a06_parseViaOfStringNullInput`, added empty-input coverage)
+- 
`juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java`
 (same)
+- `juneau-utest/src/test/java/org/apache/juneau/BeanMapLoader_Test.java` (new 
— 3 tests, including null-input no-op regression)
+- `juneau-utest/src/test/java/org/apache/juneau/MarshallingSession_Test.java` 
(new — 3 tests, including null-input → empty session-attached `Json5Map`)
+
+No release-notes entry (per locked-in decision — wire-compatible, 
internal-leaning contract).
+
+## Verification
+
+- All 9 focused-cluster classes green (`Json5List_Test`, `Json5Map_Test`, 
`JsonList_Test`, `JsonMap_Test`, `MarshalledList_Test`, `MarshalledMap_Test`, 
`BeanMap_Test`, `BeanMapLoader_Test`, `MarshallingSession_Test`).
+- Full `./scripts/test.py`: BUILD SUCCESS.
+- Coverage on touched files (no regression vs. pre-change):
+  - `JsonList.java` — 42% instr / 46% br.
+  - `JsonMap.java` — 34% instr / 18% br.
+  - `MarshalledList.java` — 78% instr / 62% br.
+  - `MarshalledMap.java` — 65% instr / 59% br.
+  - `Json5List.java` — 64% instr / 62% br.
+  - `Json5Map.java` — 77% instr / 68% br.
+- Remaining coverage gaps are pre-existing in unrelated methods (`ofArrays`, 
`ofCollections`, deep getter overloads).
+- `ReadLints` clean across all 15 files.
+
+## Decisions (locked in 2026-05-20)
+
+1. **Scope** = all symmetric `ofString(...)` / `ofJsonOrCdl(...)` / 
`ofJson5OrCdl(...)` factories on every `MarshalledList` / `MarshalledMap` 
subclass. Family-wide uniformity.
+2. **Return value** = fresh `new <Type>()` per call. NOT shared `EMPTY_LIST` / 
`EMPTY_MAP` singletons. Callers mutate the result.
+3. **`of(Collection<?>)` / `of(Map<?,?>)` copy factories** = leave as-is. They 
still return `null` on `null` input. SonarLint doesn't flag them and "copy" 
semantics legitimately differ from "parse".
+4. **Javadoc** = explicit one-line `@return A new list/map (empty if the input 
was null), never null.` on every affected site. Drop the `// TODO` comments and 
the `or null if...` clauses.
+5. **Sibling `SchemaUtils.parseMap` / `parseSet`** = spun out to `TODO-49` / 
`FINISHED-49` as a separate plan (different semantics — config-loading helpers 
rather than user-input parsers).
+6. **Release notes** = OMIT. No entry in `9.5.0.md`. Wire-compatible, 
internal-leaning contract.
+
+## Caller impact reference
+
+The audit at plan time classified callers into three buckets; the 
implementation confirmed each bucket's behavior:
+
+### Safe (no observable change)
+
+- `SchemaUtils.parseMap(Object)` L81, `SchemaUtils.parseMap(String[])` L108, 
`SchemaUtils.parseSet(String[])` L128 — all guarded against null/empty before 
calling the factory; null branch unreachable.
+- `Entry.asList(Parser)` L354, `Entry.asMap(Parser)` L428 — `s = toString()` 
is non-null; null branch unreachable.
+- `BasicSwaggerProviderSession.parseListOrCdl(...)` L1042 — guarded; null 
branch unreachable.
+- ~200 test-only call sites — pass literal non-null non-empty strings.
+
+### Affected — new behavior is the desired one
+
+| Site | Today | After |
+| --- | --- | --- |
+| `BeanMapLoader.load(BeanMap<T>, String input)` L53 | NPE if `input == null` 
| No-op |
+| `BeanMapLoader.load(BeanMap<T>, Reader r, ReaderParser p)` L72 | NPE if `r 
== null` | No-op |
+| `MarshallingSession.parseToMap(CharSequence value)` L1394 | NPE if `value == 
null` | Returns empty session-attached `Json5Map` |
+
+None of these had a runtime null-guard; the new behavior turns three latent 
NPEs into well-defined no-ops.
+
+### Tests rewritten to assert the new contract
+
+- `Json5List_Test#a04_factoryOfStringNullInput`
+- `Json5Map_Test#a04_factoryOfStringNullInput`
+- `MarshalledList_Test#a06_parseViaOfStringNullInput`
+- `MarshalledMap_Test#a06_parseViaOfStringNullInput`
+
+Each was updated to assert non-null + `isEmpty()` + mutable (`add`/`put` 
succeeds) + exact-subclass.
+
+## Cross-subclass consistency reference
+
+| File | Sites updated |
+| --- | --- |
+| `MarshalledList.java` | `ofString(CharSequence, Parser)`, `ofString(Reader, 
Parser)` |
+| `MarshalledMap.java` | `ofString(CharSequence, Parser)`, `ofString(Reader, 
Parser)` |
+| `JsonList.java` | `ofString(CharSequence)`, `ofString(Reader)` (flagged), 
`ofString(CharSequence, Parser)`, `ofString(Reader, Parser)`, 
`ofJsonOrCdl(String)` (flagged) |
+| `JsonMap.java` | `ofString(CharSequence)`, `ofString(Reader)`, 
`ofString(CharSequence, Parser)`, `ofString(Reader, Parser)` |
+| `Json5List.java` | `ofString(CharSequence)`, `ofString(Reader)` (flagged), 
`ofString(CharSequence, Parser)`, `ofString(Reader, Parser)`, 
`ofJson5OrCdl(String)` (flagged) |
+| `Json5Map.java` | `ofString(CharSequence)`, `ofString(Reader)`, 
`ofString(CharSequence, Parser)`, `ofString(Reader, Parser)` |
+
+Subclasses that needed no edits because they inherit the new behavior 
transparently or are instance wrappers without static factories: 
`ResolvingMarshalledMap`, `DelegateList`, `DelegateMap`, 
`MarshalledList.UnmodifiableMarshalledList`, 
`MarshalledMap.UnmodifiableMarshalledMap`, `JsonList.UnmodifiableJsonList`, 
`JsonMap.UnmodifiableJsonMap`, `Json5List.UnmodifiableJson5List`, 
`Json5Map.UnmodifiableJson5Map`. The anonymous `EMPTY_LIST` / `EMPTY_MAP` 
singletons on every class were unrelate [...]
+
+## Why fresh-instance, not the EMPTY_LIST / EMPTY_MAP singleton
+
+The codebase already exposes unmodifiable `EMPTY_LIST` / `EMPTY_MAP` 
singletons on every class, but every one is an unmodifiable anonymous subclass 
that throws on `add` / `put` / `remove`. The three known affected callers 
(`BeanMapLoader.load`, `MarshallingSession.parseToMap`, 
`SchemaUtils.parseMap`'s inner `new JsonMap(...)`) mutate the result; tests 
routinely do `Json5Map.ofString("{a:'b'}").put(...)`. Returning the shared 
singleton would silently swap `NullPointerException` for `Unsup [...]
+
+## References
+
+- SonarLint rule: `java:S1168` — "Empty arrays and collections should be 
returned instead of `null`."
+- Sibling archive for the `SchemaUtils` carve-out: 
`todo/FINISHED-49-schemautils-null-returns.md`.
+- Conventions: `AGENTS.md`, `.cursor/skills/code-conventions/SKILL.md` 
(Javadoc tags, `@SuppressWarnings` placement, fresh-instance vs singleton 
precedent).
+- Existing "empty rather than null" precedent in the codebase: the 
`EMPTY_LIST` / `EMPTY_MAP` singletons on every collection class, 
`JsonMap.getList(key, createIfNotExists)` family, and the `opt(...)` / `opte()` 
helpers in `org.apache.juneau.commons.utils.Utils`.
diff --git a/todo/FINISHED-49-schemautils-null-returns.md 
b/todo/FINISHED-49-schemautils-null-returns.md
new file mode 100644
index 0000000000..9b622e95be
--- /dev/null
+++ b/todo/FINISHED-49-schemautils-null-returns.md
@@ -0,0 +1,53 @@
+# FINISHED-49: SchemaUtils parse helpers — null vs empty returns
+
+Archived from `TODO-49-schemautils-null-returns.md` on 2026-05-20.
+
+## What shipped
+
+The three flagged helpers in 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java`
 — `parseMap(Object)`, `parseMap(String[])`, `parseSet(String[])` — now return 
`new JsonMap()` / `new LinkedHashSet<>()` on null or empty input instead of 
`null`. All three `@SuppressWarnings({"java:S1168"})` annotations and their `// 
TODO` comments are removed. Javadoc on every site rewritten to reflect the new 
contract. The `parseMap("IGNORE")` sentinel path still retur [...]
+
+The required caller audit covered every external call site across the repo and 
patched **zero** of them — all three callers (`SchemaAnnotation.merge` × 2, 
`SubItemsAnnotation.merge` × 1) feed the result through `appendFirst(nec, …)` 
where `nec = Utils::ne` for `Collection<?>` returns false for both null and 
empty, so the emitted schema is byte-identical to today's output. The 
`parseMap(...)` overloads have zero external callers (the `parseMap` references 
in `BasicSwaggerProviderSession`  [...]
+
+## Files delivered
+
+Source (1):
+
+- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java`
+
+Tests (1):
+
+- 
`juneau-utest/src/test/java/org/apache/juneau/jsonschema/SchemaUtils_Test.java` 
(new — 12 cases covering null / empty / `IGNORE` / populated paths on all three 
methods)
+
+No release-notes entry, no caller patches, no wider `java:S1168` sweep.
+
+## Verification
+
+- `SchemaUtils_Test` green (12/12).
+- Phase-2 focused cluster green: `SchemaUtils_Test`, 
`JsonSchemaGeneratorTest`, `JsonSchemaConfigAnnotationTest`, 
`SchemaAnnotation_Test`, `SubItemsAnnotation_Test`, 
`SchemaApplyAnnotation_Test`, `JsonSchemaBeanGenerator_Test`.
+- Full `./scripts/test.py`: BUILD SUCCESS.
+- Coverage on `SchemaUtils.java` improved versus pre-change because the new 
tests exercise the null/empty branches that were previously dead-coded — 75% 
instr / 73% br. Remaining gaps are pre-existing in unrelated methods.
+- `ReadLints` clean.
+
+## Decisions (locked in 2026-05-20)
+
+1. **Direction** = B — switch to empty returns. Dropped 
`@SuppressWarnings({"java:S1168"})` and `// TODO` markers.
+2. **Caller audit** = completed during implementation. 3 sites classified 
Safe, 0 Affected, 0 patches needed.
+3. **Escalation gate** = not triggered (audit surfaced no non-trivial caller 
changes).
+
+## Caller audit reference
+
+| Call site | Result | Classification |
+| --- | --- | --- |
+| `SchemaAnnotation.merge` (× 2 occurrences of `SchemaUtils.parseSet(...)`) | 
Feeds into `appendFirst(nec, ...)` with `nec = Utils::ne` for `Collection<?>` — 
empty and null both produce false, so the emitter behaves identically. | Safe |
+| `SubItemsAnnotation.merge` (× 1 occurrence of `SchemaUtils.parseSet(...)`) | 
Same `appendFirst(Utils::ne, ...)` pattern as above. | Safe |
+| `SchemaUtils.parseMap(Object)` / `parseMap(String[])` | Zero external 
callers anywhere in the repo. (The `parseMap` references in 
`BasicSwaggerProviderSession` are a private same-named method on that class.) | 
Safe by virtue of being unused externally. |
+
+## Why these were carved out from TODO-48
+
+TODO-48 sites are parsers — `Json5Map.ofString("")` is asking "what data did 
you give me?" and an empty document is a perfectly good answer. 
`SchemaUtils.parseMap(...)` is asking "did the user configure this annotation 
field at all?" — historically null was the documented "not configured" signal, 
distinct from "configured to empty". The audit confirmed that no caller 
currently differentiates the two states (every caller funnels the result 
through an emptiness-aware helper), so the semant [...]
+
+## References
+
+- SonarLint rule: `java:S1168` — "Empty arrays and collections should be 
returned instead of `null`."
+- Sibling archive: `todo/FINISHED-48-empty-return-marshalled-collections.md`.
+- Conventions: `AGENTS.md`, `.cursor/skills/code-conventions/SKILL.md`.
diff --git a/todo/TODO-48-empty-return-marshalled-collections.md 
b/todo/TODO-48-empty-return-marshalled-collections.md
deleted file mode 100644
index 7b1b406084..0000000000
--- a/todo/TODO-48-empty-return-marshalled-collections.md
+++ /dev/null
@@ -1,271 +0,0 @@
-# TODO-48: Replace `null`-on-empty-input with empty-instance return on 
marshalled-collection factories
-
-Source: created on 2026-05-20 from a SonarLint walkthrough of four `// TODO` 
`java:S1168` suppressions across `JsonList` / `Json5List` static factories. 
User direction: **return empty objects** instead of `null`, **consistent across 
every `MarshalledMap` / `MarshalledList` subclass**, without breaking existing 
callers.
-
-## Goal
-
-Drop the four `// TODO: ... Consider empty ...` suppressions on `JsonList` / 
`Json5List` static factory methods by changing them to return a fresh empty 
instance instead of `null` when the input is null / empty. Apply the same 
policy symmetrically across all `ofString(...)` overloads on every 
`MarshalledList` / `MarshalledMap` subclass — including the analogous spots on 
`JsonMap` / `Json5Map` / `MarshalledMap` / `MarshalledList` that do not carry 
the `// TODO` marker today but follow the [...]
-
-In short:
-
-- `ofString(CharSequence | Reader)` and `ofString(...,Parser)` factories: 
null/empty input now yields `new JsonList()` / `new JsonMap()` / `new 
Json5List()` / `new Json5Map()` / `new MarshalledList()` / `new 
MarshalledMap()`, never `null`.
-- `JsonList.ofJsonOrCdl(String)` and `Json5List.ofJson5OrCdl(String)`: 
empty/null input now yields `new JsonList()` / `new Json5List()`.
-- All four `java:S1168` suppressions and TODO comments on the flagged sites 
are removed.
-- `of(Collection)` / `of(Map)` factories keep their current null-in-null-out 
behavior — see Out of scope.
-
-## Why now
-
-- Four `// TODO` markers in the `juneau-marshall` module flag this exact 
change to SonarLint; the suppressions are intentionally temporary placeholders 
awaiting a decision.
-- Several internal callers already crash on the `null` return today (NPE on 
`m.putAll(null)` in `BeanMapLoader`, NPE on 
`Json5Map.ofString(value).session(this)` in `MarshallingSession#parseToMap`). 
Returning an empty instance turns these latent NPEs into no-ops without 
changing any successful path.
-- The "empty rather than null" idiom is already established in the codebase 
(`JsonList.EMPTY_LIST`, `JsonMap.EMPTY_MAP`, `MarshalledList.EMPTY_LIST`, 
`MarshalledMap.EMPTY_MAP`, `opte()` helper, the `getList(key, 
createIfNotExists)` / `getMap(key, createIfNotExists)` family). Aligning the 
parser-factory contract with that idiom removes a small but persistent 
inconsistency.
-- The change is locally bounded: all sites live under 
`org.apache.juneau.collections` and `org.apache.juneau.json5`; no public-API 
rename, no removed method, only a tightened return contract (non-null instead 
of `@Nullable`).
-
-## Current behavior (per-file inventory)
-
-All four flagged sites return `null` when their input is null (or empty, for 
the `OrCdl` variants). The Javadoc on every site explicitly says "or `null` if 
the input was `null`". The `@SuppressWarnings({"java:S1168"})` annotation + `// 
TODO` comment marks each one as awaiting this decision.
-
-### Sites flagged with `// TODO` (the four in the user's request)
-
-- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java`
-  - **L214-219** — `ofString(Reader json5)`: returns `null` when `json5 == 
null`. Javadoc: "A new list or `null` if the input was `null`."
-  - **L231-240** — `ofJson5OrCdl(String s)`: returns `null` when `Utils.e(s)` 
(empty *or* null). Javadoc: "The parsed string." (does not document null 
return.)
-- 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java`
-  - **L252-257** — `ofString(Reader json)`: returns `null` when `json == 
null`. Javadoc: "A new list or `null` if the input was `null`."
-  - **L269-278** — `ofJsonOrCdl(String s)`: returns `null` when `Utils.e(s)`. 
Javadoc: "The parsed string."
-
-### Symmetric sites that do **not** carry a `// TODO` marker today (in scope 
for consistency)
-
-These follow the identical null-in-null-out pattern; per the user's 
"consistent across every subclass" requirement they need to move with the 
flagged sites.
-
-- `Json5List.ofString(CharSequence json5)` — L203-205
-- `Json5List.ofString(CharSequence in, Parser p)` — L250-252
-- `Json5List.ofString(Reader in, Parser p)` — L265-267
-- `JsonList.ofString(CharSequence json)` — L239-241
-- `JsonList.ofString(CharSequence in, Parser p)` — L292-294
-- `JsonList.ofString(Reader in, Parser p)` — L311-313
-- `Json5Map.ofString(CharSequence json5)` — L204-206
-- `Json5Map.ofString(Reader json5)` — L216-218
-- `Json5Map.ofString(CharSequence in, Parser p)` — L232-234
-- `Json5Map.ofString(Reader in, Parser p)` — L247-249
-- `JsonMap.ofString(CharSequence json)` — L238-240
-- `JsonMap.ofString(Reader json)` — L251-253
-- `JsonMap.ofString(CharSequence in, Parser p)` — L267-269
-- `JsonMap.ofString(Reader in, Parser p)` — L283-285
-- `MarshalledList.ofString(CharSequence in, Parser p)` — L200-202
-- `MarshalledList.ofString(Reader in, Parser p)` — L215-217
-- `MarshalledMap.ofString(CharSequence in, Parser p)` — L193-195
-- `MarshalledMap.ofString(Reader in, Parser p)` — L208-210
-
-### Sites that intentionally stay null-in-null-out (Out of scope — see that 
section)
-
-- `JsonList.of(Collection<?>)`, `Json5List.of(Collection<?>)`, 
`MarshalledList.of(Collection<?>)`
-- `JsonMap.of(Map<?,?>)`, `Json5Map.of(Map<?,?>)`, `MarshalledMap.of(Map<?,?>)`
-
-## Proposed behavior (per-file changes)
-
-For every `ofString(...)` overload (and the two `OrCdl` factories): replace 
the `null` branch with `new <Type>()`, drop the `// TODO` comment, drop the 
`@SuppressWarnings({"java:S1168"})` annotation when it becomes empty, and 
rewrite the `@return` Javadoc to drop the "or null" language.
-
-Concrete shape (illustrative — exact code is the implementation TODO's 
responsibility):
-
-Before (`Json5List.ofString(Reader)`):
-
-```java
-@SuppressWarnings({
-    "java:S1168"     // null input = null output by design. Consider empty 
Json5List.
-})
-public static Json5List ofString(Reader json5) throws ParseException {
-    return json5 == null ? null : new Json5List(json5);
-}
-```
-
-After:
-
-```java
-public static Json5List ofString(Reader json5) throws ParseException {
-    return json5 == null ? new Json5List() : new Json5List(json5);
-}
-```
-
-Before (`JsonList.ofJsonOrCdl(String)`):
-
-```java
-@SuppressWarnings({
-    "java:S1168"     // null for empty input. Consider empty JsonList.
-})
-public static JsonList ofJsonOrCdl(String s) throws ParseException {
-    if (Utils.e(s))  // NOAI
-        return null;
-    if (! isProbablyJsonArray(s, true))
-        return new JsonList((Object[])splita(s.trim(), ','));
-    return new JsonList(s);
-}
-```
-
-After:
-
-```java
-public static JsonList ofJsonOrCdl(String s) throws ParseException {
-    if (Utils.e(s))  // NOAI
-        return new JsonList();
-    if (! isProbablyJsonArray(s, true))
-        return new JsonList((Object[])splita(s.trim(), ','));
-    return new JsonList(s);
-}
-```
-
-The same shape applies symmetrically to every site listed in **Current 
behavior**.
-
-### "Empty" means a fresh, mutable instance — not the shared `EMPTY_LIST` / 
`EMPTY_MAP` singleton
-
-The codebase already exposes `JsonList.EMPTY_LIST`, `JsonMap.EMPTY_MAP`, 
`Json5List.EMPTY_LIST`, `Json5Map.EMPTY_MAP`, `MarshalledList.EMPTY_LIST`, 
`MarshalledMap.EMPTY_MAP` — but every one of those is an **unmodifiable** 
anonymous subclass that throws on `add` / `put` / `remove`. Callers of the 
parser factories almost always mutate the result:
-
-- `BeanMapLoader.load(BeanMap, String)` does 
`m.putAll(Json5Map.ofString(input))`.
-- `MarshallingSession.parseToMap(CharSequence)` does 
`Json5Map.ofString(value).session(this)`, which internally writes the `session` 
field on the returned map.
-- `SchemaUtils.parseMap(...)` wraps via `new JsonMap(Json5Map.ofString(s))` — 
works either way, but the inner instance would be needlessly read-only.
-- Tests routinely do `Json5Map.ofString("{a:'b'}").put(...)`-style mutation.
-
-Returning the shared singleton would silently break every one of these the 
first time the null branch was hit. Fresh-instance allocation is the only safe 
choice; the allocation cost is negligible because the null branch is hit only 
on null/empty input, which by definition is not a hot path.
-
-## Caller impact assessment
-
-Project-wide audit of every public call site (`Grep` across `*.java` for 
`JsonList.ofString(`, `JsonMap.ofString(`, `Json5List.ofString(`, 
`Json5Map.ofString(`, `MarshalledList.ofString(`, `MarshalledMap.ofString(`, 
`JsonList.ofJsonOrCdl(`, `Json5List.ofJson5OrCdl(`):
-
-### Safe — no observable change
-
-| Site | Why safe |
-| --- | --- |
-| `SchemaUtils.parseMap(Object)` L81 → `new JsonMap(Json5Map.ofString(s))` | 
`s` is guaranteed non-empty by guards at L75-79. Null branch unreachable. |
-| `SchemaUtils.parseMap(String[])` L108 → same as above | Same guards at 
L101-105. Unreachable. |
-| `SchemaUtils.parseSet(String[])` L128 → 
`Json5List.ofJson5OrCdl(s).forEach(...)` | `s` is guaranteed non-empty at L125. 
Unreachable. After the change, the call becomes NPE-free even if a future 
caller removes the guard — strict improvement. |
-| `Entry.asList(Parser)` L354 → `opt(JsonList.ofString(s, parser))` | `s = 
toString()`, non-null. Null branch unreachable. `opt(emptyList)` → 
`Optional.of(emptyList)` would only matter if `s` could become null, which it 
cannot. |
-| `Entry.asMap(Parser)` L428 → `opt(JsonMap.ofString(s, parser))` | Same as 
above. |
-| `BasicSwaggerProviderSession.parseListOrCdl(...)` L1042 → 
`Json5List.ofJson5OrCdl(s)` | Guarded by `o == null` (L1036) and `s.isEmpty()` 
(L1039). Unreachable. |
-| ~200 test-only call sites (`MsgPackSerializerTest`, 
`OpenApiPartSerializer_Test`, `JsonMap_Test`, `UonSerializer_Test`, …) | All 
pass literal non-null non-empty strings. Null branch unreachable. |
-
-### Affected — semantic change, but the new behavior is the desired one
-
-| Site | Today | After |
-| --- | --- | --- |
-| `BeanMapLoader.load(BeanMap<T>, String input)` L53 → 
`m.putAll(Json5Map.ofString(input))` | NPE if `input == null` (`putAll(null)` 
is undefined) | No-op when `input == null` |
-| `BeanMapLoader.load(BeanMap<T>, Reader r, ReaderParser p)` L72 → 
`m.putAll(JsonMap.ofString(r, p))` | NPE if `r == null` | No-op when `r == 
null` |
-| `MarshallingSession.parseToMap(CharSequence value)` L1394 → 
`Json5Map.ofString(value).session(this)` | NPE if `value == null` (Javadoc says 
"Must not be null" but no runtime check) | Returns an empty session-attached 
`Json5Map` |
-
-None of these three has a runtime null-guard today, so the only callers that 
ever exercised the null path were ones that crashed. The new behavior turns 
three latent NPEs into well-defined no-ops. Worth flagging to James for 
sign-off, but no caller code needs changing.
-
-### Test-only — needs updating
-
-Four tests assert the current `null`-return contract directly:
-
-| Test | Assertion |
-| --- | --- |
-| `juneau-utest/.../json5/Json5List_Test.java#a04_factoryOfStringNullInput` 
L52 | `assertNull(Json5List.ofString((CharSequence)null));` |
-| `juneau-utest/.../json5/Json5Map_Test.java#a04_factoryOfStringNullInput` L60 
| `assertNull(Json5Map.ofString((CharSequence)null));` |
-| 
`juneau-utest/.../collections/MarshalledList_Test.java#a06_parseViaOfStringNullInput`
 L73 | `assertNull(MarshalledList.ofString((CharSequence)null, 
Json5Parser.DEFAULT));` |
-| 
`juneau-utest/.../collections/MarshalledMap_Test.java#a06_parseViaOfStringNullInput`
 L73 | `assertNull(MarshalledMap.ofString((CharSequence)null, 
Json5Parser.DEFAULT));` |
-
-Implementation must rewrite each to assert "non-null, empty, mutable":
-
-```java
-@Test void a04_factoryOfStringNullInput() throws Exception {
-    var l = Json5List.ofString((CharSequence)null);
-    assertNotNull(l);
-    assertTrue(l.isEmpty());
-    l.add("x");                    // confirm result is mutable, not the 
shared EMPTY_LIST singleton
-    assertEquals(1, l.size());
-}
-```
-
-No other test asserts the null contract on these factories (`Grep` for 
`assertNull\(.*\.ofString` returns exactly those four hits).
-
-### Three sibling `java:S1168` `// TODO` suppressions in `SchemaUtils.java` 
(NOT in this TODO's scope)
-
-For context only — `SchemaUtils.java` L65-67, L97-99, L118-120 carry the same 
SonarLint marker on `parseMap(Object)` / `parseMap(String[])` / 
`parseSet(String[])`. These return `null` to signal "no schema configured" and 
are read by downstream `null`-checking callers; flipping them to empty would 
change schema-resolution semantics in a non-obvious way. Recommend filing a 
separate TODO if the user wants those reconsidered.
-
-## Cross-subclass consistency checklist
-
-The change must land in every file below in one PR so the `MarshalledList` / 
`MarshalledMap` family stays uniform.
-
-| File | Sites to update |
-| --- | --- |
-| 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java`
 | `ofString(CharSequence, Parser)` L200-202; `ofString(Reader, Parser)` 
L215-217 |
-| 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java`
 | `ofString(CharSequence, Parser)` L193-195; `ofString(Reader, Parser)` 
L208-210 |
-| 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java`
 | `ofString(CharSequence)` L239-241; `ofString(Reader)` L252-257 
**(flagged)**; `ofString(CharSequence, Parser)` L292-294; `ofString(Reader, 
Parser)` L311-313; `ofJsonOrCdl(String)` L269-278 **(flagged)** |
-| 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java`
 | `ofString(CharSequence)` L238-240; `ofString(Reader)` L251-253; 
`ofString(CharSequence, Parser)` L267-269; `ofString(Reader, Parser)` L283-285 |
-| 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java`
 | `ofString(CharSequence)` L203-205; `ofString(Reader)` L214-219 
**(flagged)**; `ofString(CharSequence, Parser)` L250-252; `ofString(Reader, 
Parser)` L265-267; `ofJson5OrCdl(String)` L231-240 **(flagged)** |
-| 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java`
 | `ofString(CharSequence)` L204-206; `ofString(Reader)` L216-218; 
`ofString(CharSequence, Parser)` L232-234; `ofString(Reader, Parser)` L247-249 |
-
-Subclasses that do **not** need source edits:
-
-- `ResolvingMarshalledMap` (extends `MarshalledMap`) — defines no factory 
methods of its own; inherits the updated base behavior transparently.
-- `DelegateList` / `DelegateMap` (extend `JsonList` / `JsonMap`) — same.
-- `MarshalledList.UnmodifiableMarshalledList`, 
`MarshalledMap.UnmodifiableMarshalledMap`, `JsonList.UnmodifiableJsonList`, 
`JsonMap.UnmodifiableJsonMap`, `Json5List.UnmodifiableJson5List`, 
`Json5Map.UnmodifiableJson5Map` — instance wrappers, no static factories.
-- The `EMPTY_LIST` / `EMPTY_MAP` anonymous singletons on every class — 
unrelated to the factory contract.
-
-Javadoc updates required on every site:
-
-- Drop `"or null if the input was null"` from the `@return` text.
-- Reword to `"A new empty list/map if the input is null."` or `"A new 
list/map, never null."` to match the runtime contract.
-- Drop the `<br>Can be <jk>null</jk>.` annotation on the parameter where it 
currently appears — the parameter is still nullable, but the return contract 
change makes the "can be null" guidance redundant. Optional, but consistent.
-
-## Test plan
-
-New tests (or expand existing `*_Test.java` files alongside the four updated 
assertions):
-
-1. For every updated site, add a "null input ⇒ empty instance" assertion 
proving:
-   - Result is non-null.
-   - Result is empty (`isEmpty()` true).
-   - Result is mutable (try `add(...)` / `put(...)`; expect success, not 
`UnsupportedOperationException`).
-   - Result type is the precise subclass (`Json5List`, not just `JsonList` 
etc.).
-2. For each `ofJsonOrCdl` / `ofJson5OrCdl` factory, add an explicit 
"empty-string input ⇒ empty mutable instance" assertion (separate from the 
null-input case, because `Utils.e(s)` treats both as equivalent).
-3. Add a focused regression test in `BeanMapLoader_Test` (create the file if 
it does not exist — `Grep` should confirm) that calls 
`BeanMapLoader.load(beanMap, (String) null)` and asserts the bean map is left 
empty rather than throwing.
-4. Add a focused regression test in `MarshallingSession_Test` for 
`parseToMap((CharSequence) null)` returning an empty session-attached 
`Json5Map`.
-5. Run the existing `Json5List_Test`, `Json5Map_Test`, `JsonList_Test`, 
`JsonMap_Test`, `MarshalledList_Test`, `MarshalledMap_Test`, `BeanMap_Test`, 
`MarshalledConfig_Test`, `OpenApiPartSerializer_Test`, `UonSerializer_Test`, 
`MsgPackSerializerTest`, `ObjectSwap_Test`, `JsonSchemaBeanGenerator_Test` 
suites unchanged — all 200+ existing `Json*Map/List.ofString("...literal...")` 
call sites pass literal non-null non-empty strings and must continue to pass.
-6. Coverage gate: `./scripts/coverage.py 
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java
 --run` should not drop coverage on the modified methods; ideally each 
`null`/empty branch becomes a covered line.
-
-(Tests/coverage run during the implementation TODO; this plan TODO only 
enumerates them.)
-
-## Out of scope
-
-- **`of(Collection<?>)` / `of(Map<?,?>)` factories.** These are explicit 
"copy-from-existing-collection" entry points; a `null` argument here is most 
plausibly a programmer error caller-side, and changing them to return empty 
would hide that. SonarLint does not flag these methods either. Leave behavior 
unchanged.
-- **The three `// TODO java:S1168` suppressions in `SchemaUtils.java` (L65-67, 
L97-99, L118-120).** Each one returns `null` to signal "no schema configured" — 
distinct semantic from "parsed an empty document". Separate decision, separate 
TODO if the user wants it.
-- **`BeanMapLoader.load(...)` / `MarshallingSession.parseToMap(...)` defensive 
null guards.** Once the underlying factories never return `null`, these callers 
no longer need their own guards. Adding explicit guards would be redundant.
-- **Constructor-side changes (`new JsonList((CharSequence) null)` etc.).** The 
constructors already tolerate null input gracefully via the 
`MarshalledList(CharSequence, Parser)` / `MarshalledMap(CharSequence, Parser)` 
paths (`if (nn(in))` / `if (ne(in))` guards). No edits needed there.
-- **Release-notes entry.** This is a behavior-tightening of an undocumented 
"may return null" contract on internal-leaning static factories; not surprising 
enough to warrant a release-notes call-out. Confirm with James — if he wants 
it, the file is `juneau-docs/pages/release-notes/9.5.0.md` per `AGENTS.md` 
(under `### juneau-marshall`).
-- **Updating SonarLint suppression on the three sibling `java:S1168` markers 
in `SchemaUtils.java`.** Out of scope per above.
-
-## Open questions
-
-These should be settled before implementation starts.
-
-1. **Scope: only the four flagged sites, or every analogous site across the 
family?** The plan recommends "all symmetric `ofString(...)` + `OrCdl` 
factories on every subclass" because the user's "consistent across every 
`MarshalledMap` / `MarshalledList` subclass" wording implies family-wide 
uniformity. The alternative — fixing only the four `// TODO`-flagged sites — 
would leave six other overloads (one on `JsonMap.ofString(Reader)`, one on 
`Json5Map.ofString(Reader)`, plus all the `(Cha [...]
-
-2. **Fresh instance vs shared `EMPTY_LIST` / `EMPTY_MAP` singleton?** The plan 
recommends fresh `new <Type>()` per call because all three known affected 
callers (`BeanMapLoader`, `MarshallingSession#parseToMap`, plus 
`SchemaUtils.parseMap`'s inner copy) need a mutable result. Using the 
singletons would silently swap a `NullPointerException` for an 
`UnsupportedOperationException`. Confirm.
-
-3. **Should the `of(Collection<?>)` / `of(Map<?,?>)` "copy" factories move 
too?** The plan recommends no — null-in null-out is a reasonable contract for 
an explicit copy operation, and SonarLint does not flag them. Confirm the 
asymmetry is acceptable.
-
-4. **Should the base `MarshalledList.ofString(...)` / 
`MarshalledMap.ofString(...)` carry an explicit Javadoc note about the policy 
("never returns null"), or rely on the type contract alone?** Recommend 
explicit one-line `@return A new list/map (empty if the input was null), never 
null.` on every site for clarity; it's the only durable signal once the `// 
TODO` comment is gone.
-
-5. **Three sibling `// TODO java:S1168` suppressions in `SchemaUtils.java` 
(L65, L97, L118) — file as a separate TODO?** These are a related-but-distinct 
decision because the `null` there signals "no schema configured" rather than 
"parsed an empty value". Recommend a separate one-line TODO bullet rather than 
rolling into this work.
-
-6. **Release-notes mention?** Behavior is wire-compatible (no serialized 
payload changes), library-internal-feeling, and the previous "null return" 
contract was barely documented. Plan recommends omitting from `9.5.0.md`. 
Confirm.
-
-## References
-
-- Flagged sites:
-  - 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java`
 L214-219 (`ofString(Reader)`), L231-240 (`ofJson5OrCdl(String)`).
-  - 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java`
 L252-257 (`ofString(Reader)`), L269-278 (`ofJsonOrCdl(String)`).
-- Symmetric sites (full per-file index in **Cross-subclass consistency 
checklist** above):
-  - 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java`,
 `MarshalledMap.java`, `JsonList.java`, `JsonMap.java`.
-  - 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java`,
 `Json5Map.java`.
-- Sibling subclasses confirmed to require no edits: `ResolvingMarshalledMap`, 
`DelegateList`, `DelegateMap`, plus all `Unmodifiable*` private inner classes.
-- Existing "empty rather than null" precedent: `JsonList.EMPTY_LIST` (anon 
read-only), `JsonMap.EMPTY_MAP`, `Json5List.EMPTY_LIST`, `Json5Map.EMPTY_MAP`, 
`MarshalledList.EMPTY_LIST`, `MarshalledMap.EMPTY_MAP`; `JsonMap.getList(key, 
createIfNotExists)` family; the `opt(...)` / `opte()` helpers in 
`org.apache.juneau.commons.utils.Utils`.
-- Affected (latent-NPE-becomes-no-op) callers:
-  - 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java`
 L53, L72.
-  - 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java`
 L1394.
-- Tests asserting today's `null` contract (must be rewritten):
-  - `juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java` 
L51-53.
-  - `juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java` 
L59-61.
-  - 
`juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java`
 L72-74.
-  - 
`juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java`
 L72-74.
-- Conventions: `AGENTS.md` ("save a rule" / TODO workflow), 
`.cursor/skills/code-conventions/SKILL.md` (Javadoc tags, `@SuppressWarnings` 
placement, fresh-instance vs singleton precedent).
-- SonarLint rule reference: `java:S1168` — "Empty arrays and collections 
should be returned instead of `null`." Exactly the precedent this TODO codifies.
diff --git a/todo/TODO-49-schemautils-null-returns.md 
b/todo/TODO-49-schemautils-null-returns.md
deleted file mode 100644
index 591a4e4080..0000000000
--- a/todo/TODO-49-schemautils-null-returns.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# TODO-49 — `SchemaUtils` parse helpers: null vs empty returns
-
-## Goal
-
-Decide whether the three `parseMap` / `parseSet` helpers in 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java`
 should keep their current `null = not configured` semantics or switch to 
returning empty collections. If we change them, also drop the 
`@SuppressWarnings({"java:S1168"})` and the `// TODO` comment on each site.
-
-## Why now
-
-SonarLint flags all three as `java:S1168` ("Empty arrays and collections 
should be returned instead of null"). The suppressions carry a `// TODO` that 
the on-the-fly check then re-flags. Either we commit to the current contract 
(drop the `// TODO`, keep the suppression, update the rationale) or change the 
contract (return empty, drop the suppression).
-
-This was spun out of TODO-48 because the semantics here are different from the 
`MarshalledList` / `MarshalledMap` `ofString(...)` family:
-
-- TODO-48 sites parse user-supplied input — null/empty input genuinely means 
"empty result".
-- These sites are config-loading helpers — `null` is the documented "not 
configured" signal, distinct from "configured to empty".
-
-## Sites
-
-All in 
`juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java`:
-
-- Line 65 — `parseMap(Object o)`
-- Line 97 — `parseMap(String[] ss)`
-- Line 118 — `parseSet(String[] ss)`
-
-## Decisions needed
-
-1. Keep `null = not configured` (then drop the `// TODO` and reword the 
rationale), or switch to empty returns and update every caller that 
distinguishes `null` from `isEmpty()`?
-2. If switching: do callers in `JsonSchemaBeanGenerator` / annotation-driven 
schema introspection rely on `null` to skip section emission, or do they 
already `nn(...)` / `ne(...)` defensively?
-3. If keeping: is `java:S1168` justifiable here as a documented "tri-state" 
return, and should the rationale comment make that explicit?
-
-## Out of scope
-
-- TODO-48's `MarshalledList` / `MarshalledMap` `ofString(...)` family — that 
lives in `todo/TODO-48-empty-return-marshalled-collections.md`.
-- Any wider `java:S1168` sweep elsewhere in the codebase.
-
-## References
-
-- `todo/TODO-48-empty-return-marshalled-collections.md` — sibling plan, 
recommends the empty-return policy but explicitly excludes these `SchemaUtils` 
sites.
-- SonarSource `java:S1168` rule description.

Reply via email to