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.