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 3044ef4a2c Improved CSV support
3044ef4a2c is described below

commit 3044ef4a2c1ac33ff5a40cf324bcf812a938ea24
Author: James Bognar <[email protected]>
AuthorDate: Sun Mar 1 08:43:59 2026 -0500

    Improved CSV support
---
 AGENTS.md                                          |  14 +-
 RELEASE-NOTES.txt                                  |   9 +
 .../src/main/java/org/apache/juneau/ClassMeta.java |   3 +-
 .../src/main/java/org/apache/juneau/Context.java   |   2 +-
 .../org/apache/juneau/csv/ByteArrayFormat.java     |  34 +++
 .../java/org/apache/juneau/csv/CsvCellParser.java  | 218 ++++++++++++++++++
 .../org/apache/juneau/csv/CsvCellSerializer.java   | 233 +++++++++++++++++++
 .../main/java/org/apache/juneau/csv/CsvParser.java | 122 ++++++++--
 .../org/apache/juneau/csv/CsvParserSession.java    | 172 +++++++++++++-
 .../main/java/org/apache/juneau/csv/CsvReader.java |  29 ++-
 .../java/org/apache/juneau/csv/CsvSerializer.java  | 122 ++++++++--
 .../apache/juneau/csv/CsvSerializerSession.java    | 251 +++++++++++++++++++--
 .../html/HtmlStrippedDocSerializerSession.java     |   3 +
 .../juneau/oapi/OpenApiSerializerSession.java      |   1 -
 .../org/apache/juneau/svl/ResolvingJsonMap.java    |   2 +-
 .../apache/juneau/xml/XmlSerializerSession.java    |   5 +
 .../java/org/apache/juneau/yaml/YamlParser.java    |   3 -
 .../org/apache/juneau/yaml/YamlParserSession.java  | 196 +++++++++-------
 .../org/apache/juneau/yaml/YamlSerializer.java     |   1 -
 .../java/org/apache/juneau/yaml/YamlWriter.java    |   2 +-
 .../juneau/a/rttests/RoundTripTest_Base.java       |  43 ++--
 .../java/org/apache/juneau/csv/CsvParser_Test.java |   2 +-
 .../test/java/org/apache/juneau/csv/Csv_Test.java  | 234 ++++++++++++++++++-
 23 files changed, 1502 insertions(+), 199 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 6299dfed45..06f2f46edf 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -18,13 +18,21 @@ This document outlines the rules, guidelines, and best 
practices that AI assista
 ### Task Interpretation Commands
 - **"make a plan"** or **"come up with a plan"** - When the user asks to make 
a plan for something, provide a summary of suggested changes only. **Do NOT 
make actual code changes**. The plan should outline what needs to be done, but 
implementation should wait for explicit user approval.
 - **"suppress warnings"** or **"suppress issues"** - When the user asks to 
suppress warnings or issues that appear to refer to SonarLint/SonarQube issues, 
add `@SuppressWarnings` annotations to the appropriate class or method level. 
Use the specific rule ID from the warning (e.g., `java:S100`, `java:S115`, 
`java:S116`). Apply class-level suppressions when multiple methods/fields are 
affected, or method-level suppressions for specific methods.
-- **SuppressWarnings Format**: When adding `@SuppressWarnings` annotations, 
use this format:
+- **SuppressWarnings Format**: When adding `@SuppressWarnings` annotations, 
always use multi-line format with curly braces and include a brief comment 
explaining why:
   ```java
   @SuppressWarnings({
-      "java:Sxxx" // Short reason why
+      "resource" // CsvReader owns ParserReader; caller must close CsvReader
   })
   ```
-  Use the multi-line format with curly braces and include a brief comment 
explaining why the suppression is needed.
+  Or for multiple suppressions:
+  ```java
+  @SuppressWarnings({
+      "rawtypes", // Raw types necessary for generic type handling
+      "unchecked", // Type erasure requires unchecked casts
+      "resource" // w is closed by try-with-resources; lambdas capture it
+  })
+  ```
+  Never use single-line `@SuppressWarnings("xxx")` without the multi-line 
format and comment.
 
 ### Script Shortcut Commands
 - **"start docs"** or **"start docusaurus"** - Runs 
`scripts/start-docusaurus.py`
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index d8060c3256..837d2d3916 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -13,6 +13,15 @@
 
 Release Notes - Juneau - Version 9.2.1 - YYYY-MM-DD
 
+** New Features - CSV Marshalling Parity with JSON
+
+    * Type discriminator: addBeanTypes().addRootType() adds _type column for 
polymorphic parsing.
+    * Byte arrays: byteArrayFormat(BASE64) or SEMICOLON_DELIMITED; primitive 
arrays as [1;2;3].
+    * Nested structures: allowNestedStructures(true) enables inline {key:val} 
and [val;val] in cells.
+    * Null marker: nullValue("<NULL>") (default) for unambiguous null 
serialization/parsing.
+    * New classes: CsvCellParser, CsvCellSerializer, ByteArrayFormat.
+    * New builder options: byteArrayFormat(), allowNestedStructures(), 
nullValue().
+
 ** Changes
 
     * Bump junit.version from a mix of 5.13.4 and 6.0.1 to 6.0.3.
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
index 95f25d1384..4f41cc762e 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ClassMeta.java
@@ -1440,7 +1440,8 @@ public class ClassMeta<T> extends ClassInfoTyped<T> {
        }
 
        @SuppressWarnings({
-               "unchecked" // Type erasure requires cast for 
List<ObjectSwap<T,?>>
+               "unchecked",   // Type erasure requires cast for 
List<ObjectSwap<T,?>>
+               "java:S3776"   // Cognitive complexity acceptable for swap 
resolution logic
        })
        private List<ObjectSwap<T,?>> findSwaps() {
                if (beanContext == null)
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
index a6a62400fa..585873086e 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Context.java
@@ -483,7 +483,7 @@ public abstract class Context {
                 * @param x The object to traverse (Class, ClassInfo, Method, 
or MethodInfo).
                 * @return The work list.
                 */
-               private AnnotationWorkList traverse(AnnotationWorkList work, 
Object x) {
+               private static AnnotationWorkList traverse(AnnotationWorkList 
work, Object x) {
                        var ap = AP;
                        CollectionUtils.traverse(x, y -> {
                                if (x instanceof Class<?> x2)
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/ByteArrayFormat.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/ByteArrayFormat.java
new file mode 100644
index 0000000000..66e513885f
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/ByteArrayFormat.java
@@ -0,0 +1,34 @@
+/*
+ * 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.csv;
+
+/**
+ * Format for serializing {@code byte[]} arrays in CSV cells.
+ *
+ * <ul>
+ *   <li>{@link #BASE64} — Base64 encoding (e.g. {@code SGVsbG8gV29ybGQ=}). 
Matches JSON default.
+ *   <li>{@link #SEMICOLON_DELIMITED} — Semicolon-delimited decimal byte 
values (e.g. {@code 72;101;108;108;111}).
+ * </ul>
+ */
+public enum ByteArrayFormat {
+
+       /** Base64 encoding. */
+       BASE64,
+
+       /** Semicolon-delimited decimal byte values. */
+       SEMICOLON_DELIMITED
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvCellParser.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvCellParser.java
new file mode 100644
index 0000000000..ae3f610fb1
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvCellParser.java
@@ -0,0 +1,218 @@
+/*
+ * 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.csv;
+
+import java.util.*;
+
+import org.apache.juneau.parser.*;
+
+/**
+ * Parses CSV cell inline notation: {@code {key:val;key2:val2}} and {@code 
[val1;val2;val3]}.
+ *
+ * <p>
+ * Grammar (CSV-specific, semicolon-separated):
+ * <ul>
+ *   <li>Object: {@code \{ (key ':' value (';' key ':' value)*)? \}}
+ *   <li>Array: {@code \[ (value (';' value)*)? \]}
+ *   <li>Values: object | array | quoted string | number | identifier | null
+ * </ul>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ *     <li class='note'>Reserved characters: {@code ; : \{ \} [ ] " \}
+ * </ul>
+ */
+public final class CsvCellParser {
+
+       private final String input;
+       private final String nullMarker;
+       private int pos;
+       private int length;
+
+       private CsvCellParser(String input, String nullMarker) {
+               this.input = input != null ? input.trim() : "";
+               this.nullMarker = nullMarker != null ? nullMarker : "null";
+               this.length = this.input.length();
+       }
+
+       /**
+        * Parses a cell value string into a Map, List, or scalar.
+        *
+        * @param cell The cell string (e.g. {@code {a:1;b:2}} or {@code 
[1;2;3]}).
+        * @param nullMarker The string that denotes null (e.g. {@code <NULL>} 
or {@code null}).
+        * @return Parsed object: Map, List, String, Number, Boolean, or null.
+        * @throws ParseException If the input is invalid.
+        */
+       public static Object parse(String cell, String nullMarker) throws 
ParseException {
+               if (cell == null || cell.isEmpty())
+                       return null;
+               var p = new CsvCellParser(cell, nullMarker);
+               try {
+                       return p.parseValue();
+               } catch (Exception e) {
+                       throw new ParseException(e, "Invalid CSV cell notation 
at position {0}: ''{1}''", p.pos, cell);
+               }
+       }
+
+       private void skipWhitespace() {
+               while (pos < length && 
Character.isWhitespace(input.charAt(pos)))
+                       pos++;
+       }
+
+       private boolean hasMore() {
+               skipWhitespace();
+               return pos < length;
+       }
+
+       private char peek() {
+               return pos < length ? input.charAt(pos) : '\0';
+       }
+
+       private char consume() {
+               return pos < length ? input.charAt(pos++) : '\0';
+       }
+
+       private Object parseValue() throws ParseException {
+               skipWhitespace();
+               if (pos >= length)
+                       return null;
+               var c = peek();
+               if (c == '{')
+                       return parseObject();
+               if (c == '[')
+                       return parseArray();
+               if (c == '"')
+                       return parseQuotedString();
+               return parseSimpleValue();
+       }
+
+       private Map<String, Object> parseObject() throws ParseException {
+               if (consume() != '{')
+                       throw new ParseException("Expected opening brace");
+               var m = new LinkedHashMap<String, Object>();
+               skipWhitespace();
+               if (peek() == '}') {
+                       consume();
+                       return m;
+               }
+               while (hasMore()) {
+                       var key = parseKey();
+                       skipWhitespace();
+                       if (consume() != ':')
+                               throw new ParseException("Expected ':' after 
key at position " + pos);
+                       var value = parseValue();
+                       m.put(key, value);
+                       skipWhitespace();
+                       var c = peek();
+                       if (c == '}') {
+                               consume();
+                               break;
+                       }
+                       if (c != ';')
+                               throw new ParseException("Expected ';' or '}' 
at position " + pos);
+                       consume(); // skip ';'
+               }
+               return m;
+       }
+
+       private String parseKey() throws ParseException {
+               skipWhitespace();
+               if (pos >= length)
+                       throw new ParseException("Unexpected end of input in 
key");
+               if (peek() == '"')
+                       return parseQuotedString();
+               return parseIdentifier();
+       }
+
+       private List<Object> parseArray() throws ParseException {
+               if (consume() != '[')
+                       throw new ParseException("Expected opening bracket");
+               var list = new ArrayList<>();
+               skipWhitespace();
+               if (peek() == ']') {
+                       consume();
+                       return list;
+               }
+               while (hasMore()) {
+                       list.add(parseValue());
+                       skipWhitespace();
+                       var c = peek();
+                       if (c == ']') {
+                               consume();
+                               break;
+                       }
+                       if (c != ';')
+                               throw new ParseException("Expected ';' or ']' 
at position " + pos);
+                       consume(); // skip ';'
+               }
+               return list;
+       }
+
+       private String parseQuotedString() throws ParseException {
+               if (consume() != '"')
+                       throw new ParseException("Expected quote");
+               var sb = new StringBuilder();
+               while (pos < length) {
+                       var c = consume();
+                       if (c == '"')
+                               return sb.toString();
+                       if (c == '\\' && pos < length)
+                               sb.append(consume()); // escaped char
+                       else
+                               sb.append(c);
+               }
+               throw new ParseException("Unterminated quoted string");
+       }
+
+       private String parseIdentifier() {
+               var start = pos;
+               while (pos < length) {
+                       var c = input.charAt(pos);
+                       if (c == ';' || c == ':' || c == '{' || c == '}' || c 
== '[' || c == ']' || c == '"' || Character.isWhitespace(c))
+                               break;
+                       pos++;
+               }
+               return input.substring(start, pos);
+       }
+
+       private Object parseSimpleValue() throws ParseException {
+               skipWhitespace();
+               if (pos >= length || peek() == ';' || peek() == '}' || peek() 
== ']')
+                       return "";
+               var id = parseIdentifier();
+               if (id.isEmpty())
+                       throw new ParseException("Expected value at position " 
+ pos);
+               if (id.equalsIgnoreCase(nullMarker))
+                       return null;
+               if (id.equals("true"))
+                       return Boolean.TRUE;
+               if (id.equals("false"))
+                       return Boolean.FALSE;
+               // Try number
+               try {
+                       if (id.contains(".")) {
+                               var d = Double.parseDouble(id);
+                               if (d == (long) d)
+                                       return Long.valueOf((long) d);
+                               return Double.valueOf(d);
+                       }
+                       return Long.valueOf(id);
+               } catch (@SuppressWarnings("unused") NumberFormatException e) {
+                       // Not a number, treat as string
+                       return id;
+               }
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvCellSerializer.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvCellSerializer.java
new file mode 100644
index 0000000000..9419d708fa
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvCellSerializer.java
@@ -0,0 +1,233 @@
+/*
+ * 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.csv;
+
+import static org.apache.juneau.commons.utils.StringUtils.*;
+
+import java.util.*;
+
+/**
+ * Serializes nested objects and arrays to CSV cell inline notation:
+ * {@code {key:val;key2:val2}} and {@code [val1;val2;val3]}.
+ *
+ * <p>
+ * Used when {@code allowNestedStructures()} is enabled on the serializer.
+ */
+public final class CsvCellSerializer {
+
+       private final ByteArrayFormat byteArrayFormat;
+       private final String nullMarker;
+
+       /**
+        * Creates a new serializer.
+        *
+        * @param byteArrayFormat Format for byte[] values.
+        * @param nullMarker String to use for null values.
+        */
+       public CsvCellSerializer(ByteArrayFormat byteArrayFormat, String 
nullMarker) {
+               this.byteArrayFormat = byteArrayFormat != null ? 
byteArrayFormat : ByteArrayFormat.BASE64;
+               this.nullMarker = nullMarker != null ? nullMarker : "null";
+       }
+
+       /**
+        * Serializes a value to inline notation.
+        *
+        * @param value The value to serialize.
+        * @param session The serializer session (for bean conversion, date 
formatting, etc.).
+        * @return The serialized string.
+        */
+       public String serialize(Object value, CsvSerializerSession session) {
+               return serializeValue(value, session);
+       }
+
+       private String serializeValue(Object value, CsvSerializerSession 
session) {
+               if (value == null)
+                       return nullMarker;
+               if (value instanceof Map<?, ?> m)
+                       return serializeMap(m, session);
+               if (value instanceof Collection<?> c)
+                       return serializeCollection(c, session);
+               if (value instanceof Object[] a)
+                       return serializeObjectArray(a, session);
+               if (value instanceof byte[] b)
+                       return byteArrayFormat == 
ByteArrayFormat.SEMICOLON_DELIMITED ? formatByteArraySemicolon(b) : 
base64Encode(b);
+               if (value instanceof int[] a) return formatIntArray(a);
+               if (value instanceof long[] a) return formatLongArray(a);
+               if (value instanceof double[] a) return formatDoubleArray(a);
+               if (value instanceof float[] a) return formatFloatArray(a);
+               if (value instanceof short[] a) return formatShortArray(a);
+               if (value instanceof boolean[] a) return formatBooleanArray(a);
+               if (value instanceof char[] a) return formatCharArray(a);
+               // Bean or simple: use session to convert/format
+               var prepared = session.prepareForInlineValue(value);
+               if (prepared instanceof Map) return serializeMap((Map<?, ?>) 
prepared, session);
+               if (prepared instanceof Collection) return 
serializeCollection((Collection<?>) prepared, session);
+               if (prepared instanceof Object[]) return 
serializeObjectArray((Object[]) prepared, session);
+               return escapeIfNeeded(prepared.toString());
+       }
+
+       private String serializeMap(Map<?, ?> m, CsvSerializerSession session) {
+               var sb = new StringBuilder();
+               sb.append('{');
+               var first = true;
+               for (var e : m.entrySet()) {
+                       if (!first) sb.append(';');
+                       first = false;
+                       var k = e.getKey();
+                       var v = e.getValue();
+                       sb.append(escapeIfNeeded(k != null ? k.toString() : 
nullMarker));
+                       sb.append(':');
+                       sb.append(serializeValue(v, session));
+               }
+               sb.append('}');
+               return sb.toString();
+       }
+
+       private String serializeCollection(Collection<?> c, 
CsvSerializerSession session) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               var first = true;
+               for (var v : c) {
+                       if (!first) sb.append(';');
+                       first = false;
+                       sb.append(serializeValue(v, session));
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private String serializeObjectArray(Object[] a, CsvSerializerSession 
session) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               for (var i = 0; i < a.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(serializeValue(a[i], session));
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private static String formatIntArray(int[] a) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               for (var i = 0; i < a.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(a[i]);
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private static String formatLongArray(long[] a) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               for (var i = 0; i < a.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(a[i]);
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private static String formatDoubleArray(double[] a) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               for (var i = 0; i < a.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(a[i]);
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private static String formatFloatArray(float[] a) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               for (var i = 0; i < a.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(a[i]);
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private static String formatShortArray(short[] a) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               for (var i = 0; i < a.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(a[i]);
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private static String formatBooleanArray(boolean[] a) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               for (var i = 0; i < a.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(a[i]);
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private static String formatCharArray(char[] a) {
+               var sb = new StringBuilder();
+               sb.append('[');
+               for (var i = 0; i < a.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append((int) a[i]);
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private static String formatByteArraySemicolon(byte[] b) {
+               var sb = new StringBuilder();
+               for (var i = 0; i < b.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(b[i] & 0xff);
+               }
+               return sb.toString();
+       }
+
+       private static String escapeIfNeeded(String s) {
+               if (s == null)
+                       return "";
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (c == ';' || c == ':' || c == '{' || c == '}' || c 
== '[' || c == ']' || c == '"' || c == '\\')
+                               return quoted(s);
+               }
+               return s;
+       }
+
+       private static String quoted(String s) {
+               var sb = new StringBuilder();
+               sb.append('"');
+               for (var i = 0; i < s.length(); i++) {
+                       var c = s.charAt(i);
+                       if (c == '"' || c == '\\')
+                               sb.append('\\');
+                       sb.append(c);
+               }
+               sb.append('"');
+               return sb.toString();
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParser.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParser.java
index 6d84768d0b..9cfcfccc04 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParser.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParser.java
@@ -36,32 +36,28 @@ import org.apache.juneau.parser.*;
  * Parses RFC 4180-compliant CSV into collections of beans, maps, or simple 
values. Each row becomes an
  * element; the header row defines column names.
  *
- * <h5 class='section'>Data Structures Incompatible with CSV (vs. JSON):</h5>
- * <p>
- * CSV is a flat, tabular format. Unlike {@link 
org.apache.juneau.json.JsonParser JSON}, which
- * parses nested structures and supports type discriminators, CSV cannot 
faithfully parse:
+ * <h5 class='section'>JSON Parity Features (opt-in):</h5>
+ * <ul>
+ *   <li><b>Type discriminator</b> — Parse {@code _type} column when present; 
use {@link #beanDictionary(Class[])
+ *       beanDictionary()} for polymorphic types.
+ *   <li><b>Byte arrays</b> — {@link #byteArrayFormat(ByteArrayFormat) 
byteArrayFormat(BASE64)} or
+ *       {@code SEMICOLON_DELIMITED}; primitive arrays from {@code [1;2;3]}.
+ *   <li><b>Nested structures</b> — {@link #allowNestedStructures(boolean) 
allowNestedStructures(true)}
+ *       parses inline {@code {key:val}} and {@code [val;val]} in cells.
+ *   <li><b>Null marker</b> — {@link #nullValue(String) 
nullValue("&lt;NULL&gt;")} (default); cells
+ *       matching this are parsed as null.
+ * </ul>
+ *
+ * <h5 class='section'>Data Structures Not Supported:</h5>
  * <ul>
- *   <li><b>Raw primitive arrays</b> ({@code byte[]}, {@code int[]}, etc.) — 
Cells are strings; no
- *       unambiguous encoding. <i>JSON</i> parses arrays and base64-encoded 
bytes.
- *   <li><b>Nested beans</b> — Cannot reconstruct bean- or map-valued 
properties from flat columns.
- *       <i>JSON</i> parses nested objects directly.
- *   <li><b>Collections/arrays within beans</b> — Cannot parse {@code 
List&lt;X&gt;},
- *       {@code Map&lt;K,V&gt;}, or array properties from a single cell. 
<i>JSON</i> parses arrays
- *       and nested objects.
- *   <li><b>Generic type parameters</b> — No type discriminator. <i>JSON</i> 
uses {@code @type} when
- *       configured with {@code addBeanTypes().addRootType()}.
- *   <li><b>Parent/inherited properties</b> — Bean hierarchy cannot be 
reconstructed.
- *       <i>JSON</i> reconstructs flattened properties into the correct bean 
hierarchy.
- *   <li><b>Optional wrappers</b> — Round-trip may differ from tree formats. 
<i>JSON</i> handles
- *       Optional consistently.
- *   <li><b>Interface/abstract types</b> — No discriminator to select 
implementation. <i>JSON</i>
- *       uses {@code @type} with {@code implClass} mappings.
+ *   <li><b>Parent/inherited properties</b> — Bean hierarchy cannot be 
reconstructed from flat columns.
+ *   <li><b>Optional wrappers</b> — Round-trip may differ from tree formats.
  * </ul>
  *
  * <h5 class='section'>Best Supported:</h5>
  * <p>
  * Parsing into {@link java.util.Collection} of flat beans or maps, or single 
beans/maps with simple
- * property types (primitives, strings, numbers, enums, dates).
+ * property types (primitives, strings, numbers, enums, dates, byte arrays, or 
nested structures when enabled).
  *
  * <h5 class='section'>Notes:</h5><ul>
  *     <li class='note'>This class is thread safe and reusable.
@@ -83,11 +79,17 @@ public class CsvParser extends ReaderParser implements 
CsvMetaProvider {
 
                private static final Cache<HashKey,CsvParser> CACHE = 
Cache.of(HashKey.class, CsvParser.class).build();
 
+               private ByteArrayFormat byteArrayFormat;
+               private boolean allowNestedStructures;
+               private String nullValue;
+
                /**
                 * Constructor, default settings.
                 */
                protected Builder() {
                        consumes("text/csv");
+                       byteArrayFormat = ByteArrayFormat.BASE64;
+                       nullValue = "<NULL>";
                }
 
                /**
@@ -98,6 +100,9 @@ public class CsvParser extends ReaderParser implements 
CsvMetaProvider {
                 */
                protected Builder(Builder copyFrom) {
                        super(assertArgNotNull(ARG_copyFrom, copyFrom));
+                       byteArrayFormat = copyFrom.byteArrayFormat;
+                       allowNestedStructures = copyFrom.allowNestedStructures;
+                       nullValue = copyFrom.nullValue;
                }
 
                /**
@@ -108,6 +113,31 @@ public class CsvParser extends ReaderParser implements 
CsvMetaProvider {
                 */
                protected Builder(CsvParser copyFrom) {
                        super(assertArgNotNull(ARG_copyFrom, copyFrom));
+                       byteArrayFormat = copyFrom.byteArrayFormat;
+                       allowNestedStructures = copyFrom.allowNestedStructures;
+                       nullValue = copyFrom.nullValue;
+               }
+
+               /**
+                * String that denotes null when parsing. Must match the 
serializer's nullValue.
+                *
+                * @param value The null marker string.
+                * @return This object.
+                */
+               public Builder nullValue(String value) {
+                       nullValue = value;
+                       return this;
+               }
+
+               /**
+                * Enables parsing of inline {@code {key:val}} and {@code 
[val;val]} notation in cells.
+                *
+                * @param value Whether to allow nested structures.
+                * @return This object.
+                */
+               public Builder allowNestedStructures(boolean value) {
+                       allowNestedStructures = value;
+                       return this;
                }
 
                @Override /* Overridden from Builder */
@@ -146,6 +176,20 @@ public class CsvParser extends ReaderParser implements 
CsvMetaProvider {
                        return this;
                }
 
+               /**
+                * Format for parsing {@code byte[]} arrays from CSV cells.
+                *
+                * <p>
+                * Must match the format used by the serializer. Default is 
{@link ByteArrayFormat#BASE64}.
+                *
+                * @param value The format to use.
+                * @return This object.
+                */
+               public Builder byteArrayFormat(ByteArrayFormat value) {
+                       byteArrayFormat = value;
+                       return this;
+               }
+
                @Override /* Overridden from Builder */
                public Builder beanClassVisibility(Visibility value) {
                        super.beanClassVisibility(value);
@@ -290,6 +334,11 @@ public class CsvParser extends ReaderParser implements 
CsvMetaProvider {
                        return this;
                }
 
+               @Override /* Overridden from Context.Builder */
+               public HashKey hashKey() {
+                       return HashKey.of(super.hashKey(), byteArrayFormat, 
allowNestedStructures, nullValue);
+               }
+
                @Override /* Overridden from Context.Builder */
                public CsvParser build() {
                        return cache(CACHE).build(CsvParser.class);
@@ -633,6 +682,9 @@ public class CsvParser extends ReaderParser implements 
CsvMetaProvider {
 
        private final Map<ClassMeta<?>,CsvClassMeta> csvClassMetas = new 
ConcurrentHashMap<>();
        private final Map<BeanPropertyMeta,CsvBeanPropertyMeta> 
csvBeanPropertyMetas = new ConcurrentHashMap<>();
+       private final ByteArrayFormat byteArrayFormat;
+       private final boolean allowNestedStructures;
+       private final String nullValue;
 
        /**
         * Constructor.
@@ -641,6 +693,36 @@ public class CsvParser extends ReaderParser implements 
CsvMetaProvider {
         */
        public CsvParser(Builder builder) {
                super(builder);
+               byteArrayFormat = builder.byteArrayFormat != null ? 
builder.byteArrayFormat : ByteArrayFormat.BASE64;
+               allowNestedStructures = builder.allowNestedStructures;
+               nullValue = builder.nullValue != null ? builder.nullValue : 
"<NULL>";
+       }
+
+       /**
+        * Returns the format for parsing {@code byte[]} arrays from CSV cells.
+        *
+        * @return The byte array format.
+        */
+       public ByteArrayFormat getByteArrayFormat() {
+               return byteArrayFormat;
+       }
+
+       /**
+        * Returns whether inline {@code {key:val}} and {@code [val;val]} 
notation parsing is enabled.
+        *
+        * @return Whether nested structures are allowed.
+        */
+       public boolean isAllowNestedStructures() {
+               return allowNestedStructures;
+       }
+
+       /**
+        * Returns the string that denotes null when parsing.
+        *
+        * @return The null marker.
+        */
+       public String getNullValue() {
+               return nullValue;
        }
 
        @Override /* Overridden from Context */
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParserSession.java
index 794a4296ad..3274be683c 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvParserSession.java
@@ -17,6 +17,8 @@
 package org.apache.juneau.csv;
 
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
 
 import java.io.*;
 import java.lang.reflect.*;
@@ -59,11 +61,19 @@ import org.apache.juneau.swap.*;
 })
 public class CsvParserSession extends ReaderParserSession {
 
+       private final ByteArrayFormat byteArrayFormat;
+       private final boolean allowNestedStructures;
+       private final String nullValue;
+
        /**
         * Builder class.
         */
        public static class Builder extends ReaderParserSession.Builder {
 
+               private ByteArrayFormat byteArrayFormat;
+               private boolean allowNestedStructures;
+               private String nullValue;
+
                /**
                 * Constructor
                 *
@@ -71,6 +81,9 @@ public class CsvParserSession extends ReaderParserSession {
                 */
                protected Builder(CsvParser ctx) {
                        super(ctx);
+                       byteArrayFormat = ctx.getByteArrayFormat();
+                       allowNestedStructures = ctx.isAllowNestedStructures();
+                       nullValue = ctx.getNullValue();
                }
 
                @Override /* Overridden from Builder */
@@ -192,6 +205,9 @@ public class CsvParserSession extends ReaderParserSession {
         */
        protected CsvParserSession(Builder builder) {
                super(builder);
+               byteArrayFormat = builder.byteArrayFormat;
+               allowNestedStructures = builder.allowNestedStructures;
+               nullValue = builder.nullValue != null ? builder.nullValue : 
"<NULL>";
        }
 
        @Override /* Overridden from ParserSession */
@@ -291,6 +307,10 @@ public class CsvParserSession extends ReaderParserSession {
 
        /**
         * Parses a single data row into the appropriate element object.
+        *
+        * <p>
+        * Matches JSON behavior: for polymorphic types (interface/abstract 
with dictionary), parse to map
+        * first then use {@link #cast(JsonMap, BeanPropertyMeta, ClassMeta)} 
when <code>_type</code> is present.
         */
        private Object parseRow(List<String> headers, List<String> row, 
ClassMeta<?> eType, Object outer) throws ParseException {
                if (eType == null || eType.isObject()) {
@@ -300,23 +320,53 @@ public class CsvParserSession extends ReaderParserSession 
{
                                m.put(headers.get(i), parseCellValue(val, 
object()));
                        }
                        return m;
-               } else if (eType.isBean()) {
+               }
+               // Polymorphic (registry + _type column): parse to map then 
cast - matches JSON
+               var typeColName = getBeanTypePropertyName(eType);
+               if (eType.getBeanRegistry() != null && 
headers.contains(typeColName)) {
+                       var m = new JsonMap(this);
+                       for (var i = 0; i < headers.size(); i++) {
+                               var val = i < row.size() ? row.get(i) : null;
+                               m.put(headers.get(i), parseCellValue(val, 
object()));
+                       }
+                       if (m.containsKey(typeColName))
+                               return cast(m, null, eType);
+                       throw new ParseException(this, "Polymorphic type 
''{0}'' requires _type column for resolution", eType);
+               }
+               if (eType.isBean()) {
                        return parseRowIntoBean(headers, row, eType, outer);
-               } else if (eType.isMap()) {
+               }
+               if (eType.isMap()) {
                        return parseRowIntoMap(headers, row, eType, outer);
-               } else {
-                       // Simple type: use the "value" column (first column) 
or the only column present
-                       var val = row.isEmpty() ? null : row.get(0);
-                       return parseCellValue(val, eType);
                }
+               // Simple type: use the "value" column (first column) or the 
only column present
+               var val = row.isEmpty() ? null : row.get(0);
+               return parseCellValue(val, eType);
        }
 
        /**
         * Parses a single data row into a bean of the specified type.
+        *
+        * <p>
+        * When a <code>_type</code> column (or configured type property) is 
present with a non-empty
+        * value, the type name is resolved to a concrete class for polymorphic 
bean creation.
         */
        private <T> T parseRowIntoBean(List<String> headers, List<String> row, 
ClassMeta<T> eType, Object outer) throws ParseException {
-               var m = newBeanMap(outer, eType.inner());
+               var typeColName = getBeanTypePropertyName(eType);
+               var typeColIdx = headers.indexOf(typeColName);
+               ClassMeta<?> beanType = eType;
+               if (typeColIdx >= 0 && typeColIdx < row.size()) {
+                       var typeVal = row.get(typeColIdx);
+                       if (nn(typeVal) && ! typeVal.trim().isEmpty()) {
+                               var resolved = getClassMeta(typeVal.trim(), 
null, eType);
+                               if (resolved != null && resolved.isBean())
+                                       beanType = resolved;
+                       }
+               }
+               var m = newBeanMap(outer, beanType.inner());
                for (var i = 0; i < headers.size(); i++) {
+                       if (i == typeColIdx)
+                               continue;
                        var header = headers.get(i);
                        var val = i < row.size() ? row.get(i) : null;
                        var pm = m.getPropertyMeta(header);
@@ -329,7 +379,7 @@ public class CsvParserSession extends ReaderParserSession {
                                onUnknownProperty(header, m, val);
                        }
                }
-               return m.getBean();
+               return (T) m.getBean();
        }
 
        /**
@@ -359,21 +409,119 @@ public class CsvParserSession extends 
ReaderParserSession {
         *
         * <p>
         * The unquoted literal {@code null} maps to Java {@code null}.
+        * Handles {@code byte[]} (BASE64 or semicolon-delimited) and primitive 
arrays {@code [1;2;3]}.
         * All other values are converted via {@link #convertToType(Object, 
ClassMeta)}.
         */
        private <T> T parseCellValue(String val, ClassMeta<T> eType) throws 
ParseException {
-               if (val == null || val.equals("null"))
+               if (val == null)
                        return null;
-               // Apply trimStrings at the cell level (before type conversion) 
so that the String
-               // value passed to convertToType() is already trimmed.
-               if (isTrimStrings() && val != null)
+               // Apply trimStrings at the cell level (before type conversion)
+               if (isTrimStrings())
                        val = val.trim();
+               if (nullValue != null && val.equals(nullValue))
+                       return null;
                if (val.isEmpty() && eType.isCharSequence())
                        return null;
                try {
+                       if (allowNestedStructures && !val.isEmpty()) {
+                               var trimmed = isTrimStrings() ? val.trim() : 
val;
+                               if (trimmed.startsWith("{") || 
trimmed.startsWith("[")) {
+                                       var parsed = 
CsvCellParser.parse(trimmed, nullValue);
+                                       if (parsed != null)
+                                               return convertToType(parsed, 
eType);
+                               }
+                       }
+                       var csvParsed = parseCsvCellValue(val, eType);
+                       if (csvParsed != null)
+                               return (T) csvParsed;
                        return convertToType(val, eType);
                } catch (InvalidDataConversionException e) {
                        throw new ParseException(e, "Could not convert CSV cell 
value ''{0}'' to type ''{1}''.", val, eType);
                }
        }
+
+       /**
+        * CSV-specific parsing for byte[] and primitive arrays. Returns null 
if not applicable.
+        */
+       private Object parseCsvCellValue(String val, ClassMeta<?> eType) throws 
ParseException {
+               if (val == null || val.isEmpty())
+                       return null;
+               if (eType.isByteArray()) {
+                       if (byteArrayFormat == 
ByteArrayFormat.SEMICOLON_DELIMITED) {
+                               var parts = val.split(";");
+                               var b = new byte[parts.length];
+                               for (var i = 0; i < parts.length; i++) {
+                                       b[i] = (byte) 
Integer.parseInt(parts[i].trim());
+                               }
+                               return b;
+                       }
+                       return base64Decode(val);
+               }
+               if (eType.isArray() && eType.getElementType().isPrimitive()) {
+                       if (!val.startsWith("[") || !val.endsWith("]"))
+                               return null;
+                       var inner = val.substring(1, val.length() - 1).trim();
+                       if (inner.isEmpty()) {
+                               return createEmptyPrimitiveArray(eType);
+                       }
+                       var parts = inner.split(";");
+                       var et = eType.getElementType();
+                       if (et.is(int.class)) {
+                               var a = new int[parts.length];
+                               for (var i = 0; i < parts.length; i++)
+                                       a[i] = 
Integer.parseInt(parts[i].trim());
+                               return a;
+                       }
+                       if (et.is(long.class)) {
+                               var a = new long[parts.length];
+                               for (var i = 0; i < parts.length; i++)
+                                       a[i] = Long.parseLong(parts[i].trim());
+                               return a;
+                       }
+                       if (et.is(double.class)) {
+                               var a = new double[parts.length];
+                               for (var i = 0; i < parts.length; i++)
+                                       a[i] = 
Double.parseDouble(parts[i].trim());
+                               return a;
+                       }
+                       if (et.is(float.class)) {
+                               var a = new float[parts.length];
+                               for (var i = 0; i < parts.length; i++)
+                                       a[i] = 
Float.parseFloat(parts[i].trim());
+                               return a;
+                       }
+                       if (et.is(short.class)) {
+                               var a = new short[parts.length];
+                               for (var i = 0; i < parts.length; i++)
+                                       a[i] = 
Short.parseShort(parts[i].trim());
+                               return a;
+                       }
+                       if (et.is(boolean.class)) {
+                               var a = new boolean[parts.length];
+                               for (var i = 0; i < parts.length; i++)
+                                       a[i] = 
Boolean.parseBoolean(parts[i].trim());
+                               return a;
+                       }
+                       if (et.is(char.class)) {
+                               var a = new char[parts.length];
+                               for (var i = 0; i < parts.length; i++)
+                                       a[i] = (char) 
Integer.parseInt(parts[i].trim());
+                               return a;
+                       }
+               }
+               return null;
+       }
+
+       private static Object createEmptyPrimitiveArray(ClassMeta<?> arrayType) 
throws ParseException {
+               var et = arrayType.getElementType();
+               if (et.is(int.class)) return new int[0];
+               if (et.is(long.class)) return new long[0];
+               if (et.is(double.class)) return new double[0];
+               if (et.is(float.class)) return new float[0];
+               if (et.is(short.class)) return new short[0];
+               if (et.is(boolean.class)) return new boolean[0];
+               if (et.is(char.class)) return new char[0];
+               if (et.is(byte.class)) return new byte[0];
+               return null;
+       }
 }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvReader.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvReader.java
index feb5f07780..5cb197b767 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvReader.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvReader.java
@@ -39,6 +39,9 @@ import org.apache.juneau.parser.*;
  *     <li class='note'>This class is not intended for external use.
  * </ul>
  */
+@SuppressWarnings({
+       "resource" // CsvReader owns ParserReader; caller must close CsvReader
+})
 public class CsvReader implements Closeable {
 
        private final ParserReader r;
@@ -90,7 +93,11 @@ public class CsvReader implements Closeable {
         * @throws IOException Thrown by underlying stream.
         * @throws ParseException If the CSV is malformed (e.g. an unclosed 
quoted field).
         */
-       @SuppressWarnings("java:S3776")
+       @SuppressWarnings({
+               "java:S1168", // null = end of input (distinct from empty row)
+               "java:S3776", // Cognitive complexity acceptable for CSV row 
parsing state machine
+               "java:S6541" // Brain method acceptable for CSV row parsing 
state machine
+       })
        public List<String> readRow() throws IOException, ParseException {
                if (eof)
                        return null;
@@ -121,7 +128,7 @@ public class CsvReader implements Closeable {
 
                        if (c == -1) {
                                eof = true;
-                               fields.add(finalize(field));
+                               fields.add(finishField(field));
                                return fields;
                        }
 
@@ -131,35 +138,35 @@ public class CsvReader implements Closeable {
                                int next = r.read();
                                if (next == -1) {
                                        eof = true;
-                                       fields.add(finalize(field));
+                                       fields.add(finishField(field));
                                        return fields;
                                } else if (next == delimiter) {
-                                       fields.add(finalize(field));
+                                       fields.add(finishField(field));
                                        field = new StringBuilder();
                                } else if (next == '\r') {
                                        int peek = r.read();
                                        if (peek != '\n' && peek != -1)
                                                r.unread();
-                                       fields.add(finalize(field));
+                                       fields.add(finishField(field));
                                        return fields;
                                } else if (next == '\n') {
-                                       fields.add(finalize(field));
+                                       fields.add(finishField(field));
                                        return fields;
                                } else {
                                        // Lenient: treat extra content after 
closing quote as unquoted
                                        field.append((char) next);
                                }
                        } else if (c == delimiter) {
-                               fields.add(finalize(field));
+                               fields.add(finishField(field));
                                field = new StringBuilder();
                        } else if (c == '\r') {
                                int next = r.read();
                                if (next != '\n' && next != -1)
                                        r.unread();
-                               fields.add(finalize(field));
+                               fields.add(finishField(field));
                                return fields;
                        } else if (c == '\n') {
-                               fields.add(finalize(field));
+                               fields.add(finishField(field));
                                return fields;
                        } else {
                                field.append((char) c);
@@ -188,7 +195,7 @@ public class CsvReader implements Closeable {
                                int next = r.read();
                                if (next == quoteChar) {
                                        // Doubled quote → literal quote
-                                       field.append((char) quoteChar);
+                                       field.append(quoteChar);
                                } else {
                                        // Closing quote; push back the 
following character
                                        if (next != -1)
@@ -201,7 +208,7 @@ public class CsvReader implements Closeable {
                }
        }
 
-       private String finalize(StringBuilder sb) {
+       private String finishField(StringBuilder sb) {
                var s = sb.toString();
                return trimStrings ? s.trim() : s;
        }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializer.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializer.java
index 3b9ff7639c..88f4061a6f 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializer.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializer.java
@@ -37,30 +37,27 @@ import org.apache.juneau.serializer.*;
  * becomes a row and bean properties become columns. The first row typically 
contains column headers derived
  * from bean property names.
  *
- * <h5 class='section'>Data Structures Incompatible with CSV (vs. JSON):</h5>
- * <p>
- * CSV is a flat, tabular format. Unlike {@link 
org.apache.juneau.json.JsonSerializer JSON}, which
- * supports nested objects, arrays, and type discriminators, CSV cannot 
faithfully represent:
+ * <h5 class='section'>JSON Parity Features (opt-in):</h5>
+ * <ul>
+ *   <li><b>Type discriminator</b> — {@link #addBeanTypes() 
addBeanTypes()}.{@link #addRootType() addRootType()}
+ *       adds a {@code _type} column for polymorphic parsing.
+ *   <li><b>Byte arrays</b> — {@link #byteArrayFormat(ByteArrayFormat) 
byteArrayFormat(BASE64)} (default) or
+ *       {@code SEMICOLON_DELIMITED} for {@code byte[]}; primitive arrays as 
{@code [1;2;3]}.
+ *   <li><b>Nested structures</b> — {@link #allowNestedStructures(boolean) 
allowNestedStructures(true)}
+ *       enables inline {@code {key:val}} and {@code [val;val]} in cells.
+ *   <li><b>Null marker</b> — {@link #nullValue(String) 
nullValue("&lt;NULL&gt;")} (default) for unambiguous null.
+ * </ul>
+ *
+ * <h5 class='section'>Data Structures Not Supported:</h5>
  * <ul>
- *   <li><b>Raw primitive arrays</b> ({@code byte[]}, {@code int[]}, etc.) — 
Serialize as
- *       {@code Object.toString()} (e.g. {@code [B@12345}). <i>JSON</i> 
supports arrays natively
- *       and typically uses base64 for {@code byte[]}.
- *   <li><b>Nested beans</b> — Flatten to a single row; structure is lost. 
<i>JSON</i> preserves
- *       nested objects naturally ({@code {"a":{"b":"c"}}}).
- *   <li><b>Collections/arrays within beans</b> — {@code List&lt;X&gt;}, 
{@code Map&lt;K,V&gt;}, and
- *       array properties serialize as {@code toString()}; they cannot be 
parsed back. <i>JSON</i>
- *       supports arrays and objects as first-class values.
- *   <li><b>Generic type preservation</b> — No type discriminator. <i>JSON</i> 
can add {@code @type}
- *       via {@code addBeanTypes().addRootType()}.
  *   <li><b>Parent/inherited properties</b> — Hierarchy flattens; may collide 
with child names.
- *       <i>JSON</i> serializes all properties in a single object without loss.
- *   <li><b>Optional wrappers</b> — May serialize as the inner value; 
round-trip differs from
- *       tree formats. <i>JSON</i> handles Optional consistently as value or 
null.
+ *   <li><b>Optional wrappers</b> — May serialize as the inner value; 
round-trip differs from tree formats.
  * </ul>
  *
  * <h5 class='section'>Best Supported:</h5>
  * <p>
- * Collections of flat beans or maps whose properties are primitives, strings, 
numbers, enums, or dates.
+ * Collections of flat beans or maps whose properties are primitives, strings, 
numbers, enums, dates,
+ * byte arrays, or (with {@code allowNestedStructures}) nested beans, maps, 
and lists.
  *
  * <h5 class='section'>Notes:</h5><ul>
  *     <li class='note'>This class is thread safe and reusable.
@@ -83,11 +80,17 @@ public class CsvSerializer extends WriterSerializer 
implements CsvMetaProvider {
 
                private static final Cache<HashKey,CsvSerializer> CACHE = 
Cache.of(HashKey.class, CsvSerializer.class).build();
 
+               private ByteArrayFormat byteArrayFormat;
+               private boolean allowNestedStructures;
+               private String nullValue;
+
                /**
                 * Constructor, default settings.
                 */
                protected Builder() {
                        produces("text/csv");
+                       byteArrayFormat = ByteArrayFormat.BASE64;
+                       nullValue = "<NULL>";
                }
 
                /**
@@ -98,6 +101,9 @@ public class CsvSerializer extends WriterSerializer 
implements CsvMetaProvider {
                 */
                protected Builder(Builder copyFrom) {
                        super(assertArgNotNull(ARG_copyFrom, copyFrom));
+                       byteArrayFormat = copyFrom.byteArrayFormat;
+                       allowNestedStructures = copyFrom.allowNestedStructures;
+                       nullValue = copyFrom.nullValue;
                }
 
                /**
@@ -108,6 +114,34 @@ public class CsvSerializer extends WriterSerializer 
implements CsvMetaProvider {
                 */
                protected Builder(CsvSerializer copyFrom) {
                        super(assertArgNotNull(ARG_copyFrom, copyFrom));
+                       byteArrayFormat = copyFrom.byteArrayFormat;
+                       allowNestedStructures = copyFrom.allowNestedStructures;
+                       nullValue = copyFrom.nullValue;
+               }
+
+               /**
+                * String to write for null values. Parser treats cells 
matching this as null.
+                *
+                * <p>
+                * Default is {@code <NULL>} to avoid confusion with the 
literal string {@code "null"}.
+                *
+                * @param value The null marker string.
+                * @return This object.
+                */
+               public Builder nullValue(String value) {
+                       nullValue = value;
+                       return this;
+               }
+
+               /**
+                * Enables inline {@code {key:val}} and {@code [val;val]} 
notation in cells for nested beans, maps, and lists.
+                *
+                * @param value Whether to allow nested structures.
+                * @return This object.
+                */
+               public Builder allowNestedStructures(boolean value) {
+                       allowNestedStructures = value;
+                       return this;
                }
 
                @Override /* Overridden from Builder */
@@ -140,6 +174,20 @@ public class CsvSerializer extends WriterSerializer 
implements CsvMetaProvider {
                        return this;
                }
 
+               /**
+                * Format for serializing {@code byte[]} arrays in CSV cells.
+                *
+                * <p>
+                * Default is {@link ByteArrayFormat#BASE64} (matches JSON).
+                *
+                * @param value The format to use.
+                * @return This object.
+                */
+               public Builder byteArrayFormat(ByteArrayFormat value) {
+                       byteArrayFormat = value;
+                       return this;
+               }
+
                @Override /* Overridden from Builder */
                public Builder annotations(Annotation...values) {
                        super.annotations(values);
@@ -308,6 +356,11 @@ public class CsvSerializer extends WriterSerializer 
implements CsvMetaProvider {
                        return this;
                }
 
+               @Override /* Overridden from Context.Builder */
+               public HashKey hashKey() {
+                       return HashKey.of(super.hashKey(), byteArrayFormat, 
allowNestedStructures, nullValue);
+               }
+
                @Override /* Overridden from Context.Builder */
                public CsvSerializer build() {
                        return cache(CACHE).build(CsvSerializer.class);
@@ -777,6 +830,9 @@ public class CsvSerializer extends WriterSerializer 
implements CsvMetaProvider {
 
        private final Map<ClassMeta<?>,CsvClassMeta> csvClassMetas = new 
ConcurrentHashMap<>();
        private final Map<BeanPropertyMeta,CsvBeanPropertyMeta> 
csvBeanPropertyMetas = new ConcurrentHashMap<>();
+       private final ByteArrayFormat byteArrayFormat;
+       private final boolean allowNestedStructures;
+       private final String nullValue;
 
        /**
         * Constructor.
@@ -785,6 +841,36 @@ public class CsvSerializer extends WriterSerializer 
implements CsvMetaProvider {
         */
        public CsvSerializer(Builder builder) {
                super(builder);
+               byteArrayFormat = builder.byteArrayFormat != null ? 
builder.byteArrayFormat : ByteArrayFormat.BASE64;
+               allowNestedStructures = builder.allowNestedStructures;
+               nullValue = builder.nullValue != null ? builder.nullValue : 
"<NULL>";
+       }
+
+       /**
+        * Returns the format for serializing {@code byte[]} arrays in CSV 
cells.
+        *
+        * @return The byte array format.
+        */
+       public ByteArrayFormat getByteArrayFormat() {
+               return byteArrayFormat;
+       }
+
+       /**
+        * Returns whether inline {@code {key:val}} and {@code [val;val]} 
notation is enabled.
+        *
+        * @return Whether nested structures are allowed.
+        */
+       public boolean isAllowNestedStructures() {
+               return allowNestedStructures;
+       }
+
+       /**
+        * Returns the string written for null values.
+        *
+        * @return The null marker.
+        */
+       public String getNullValue() {
+               return nullValue;
        }
 
        @Override /* Overridden from Context */
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
index a1d3238eb6..e99755255d 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
@@ -18,6 +18,7 @@ package org.apache.juneau.csv;
 
 import static org.apache.juneau.commons.utils.AssertionUtils.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.StringUtils.*;
 import static org.apache.juneau.commons.utils.ThrowableUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 
@@ -47,16 +48,33 @@ import org.apache.juneau.utils.*;
  * </ul>
  *
  */
+@SuppressWarnings({
+       "java:S110", // Inheritance depth acceptable for serializer session 
hierarchy
+       "java:S115", // Constants use UPPER_snakeCase convention (e.g., ARG_ctx)
+       "java:S3776", // Cognitive complexity acceptable for doSerialize, 
formatForCsvCell
+       "java:S6541", // Brain method acceptable for doSerialize, 
formatForCsvCell
+})
 public class CsvSerializerSession extends WriterSerializerSession {
 
        // Argument name constants for assertArgNotNull
        private static final String ARG_ctx = "ctx";
 
+       private final ByteArrayFormat byteArrayFormat;
+       private final boolean allowNestedStructures;
+       private final String nullValue;
+
        /**
         * Builder class.
         */
+       @SuppressWarnings({
+               "java:S110" // Inheritance depth acceptable for builder 
hierarchy
+       })
        public static class Builder extends WriterSerializerSession.Builder {
 
+               private ByteArrayFormat byteArrayFormat;
+               private boolean allowNestedStructures;
+               private String nullValue;
+
                /**
                 * Constructor
                 *
@@ -65,6 +83,9 @@ public class CsvSerializerSession extends 
WriterSerializerSession {
                 */
                protected Builder(CsvSerializer ctx) {
                        super(assertArgNotNull(ARG_ctx, ctx));
+                       byteArrayFormat = ctx.getByteArrayFormat();
+                       allowNestedStructures = ctx.isAllowNestedStructures();
+                       nullValue = ctx.getNullValue();
                }
 
                @Override /* Overridden from Builder */
@@ -199,6 +220,9 @@ public class CsvSerializerSession extends 
WriterSerializerSession {
         */
        protected CsvSerializerSession(Builder builder) {
                super(builder);
+               byteArrayFormat = builder.byteArrayFormat;
+               allowNestedStructures = builder.allowNestedStructures;
+               nullValue = builder.nullValue != null ? builder.nullValue : 
"<NULL>";
        }
 
        /**
@@ -235,15 +259,22 @@ public class CsvSerializerSession extends 
WriterSerializerSession {
        @SuppressWarnings({
                "rawtypes", // Raw types necessary for generic type handling
                "unchecked", // Type erasure requires unchecked casts
+               "resource", // w is closed by try-with-resources; lambdas 
capture it
        })
        @Override /* Overridden from SerializerSession */
        protected void doSerialize(SerializerPipe pipe, Object o) throws 
IOException, SerializeException {
-
-               try (var w = getCsvWriter(pipe)) {
-                       var cm = getClassMetaForObject(o);
-                       Collection<?> l = null;
-                       if (cm.isArray()) {
-                               l = l((Object[])o);
+               var cm = push2("root", o, getClassMetaForObject(o));
+               if (cm == null)
+                       return;
+               try {
+                       try (var w = getCsvWriter(pipe)) {
+                               Collection<?> l = null;
+                               if (cm.isArray()) {
+                               // Primitive arrays and byte[] serialize as 
single row (value = [1;2;3] or base64)
+                               if 
(o.getClass().getComponentType().isPrimitive())
+                                       l = Collections.singletonList(o);
+                               else
+                                       l = l((Object[])o);
                        } else if (cm.isCollection()) {
                                l = (Collection<?>)o;
                        } else if (cm.isStreamable()) {
@@ -262,78 +293,148 @@ public class CsvSerializerSession extends 
WriterSerializerSession {
                                var firstRaw = firstOpt.get();
                                var firstEntry = applySwap(firstRaw, 
getClassMetaForObject(firstRaw));
                                var entryType = 
getClassMetaForObject(firstEntry);
+                               // If swapped type is not a bean (e.g. 
interface proxy), use raw type for strategy
+                               if (! entryType.isBean() && firstRaw != null) {
+                                       var rawType = 
getClassMetaForObject(firstRaw);
+                                       if (rawType.isBean())
+                                               entryType = rawType;
+                               }
+
+                               // Expected type for each element (for type 
discriminator)
+                               var eType = cm.isArray() || cm.isCollection() 
|| cm.isStreamable()
+                                       ? cm.getElementType()
+                                       : getExpectedRootType(firstRaw);
+
+                               // CSV is flat; when addBeanTypes or 
addRootType is set, add _type column to all rows
+                               var addTypeColumn = isAddBeanTypes() || 
isAddRootType();
+                               var typeColName = addTypeColumn ? 
getBeanTypePropertyName(entryType) : null;
 
                                // Determine the best representation strategy.
                                // Use the BeanMeta from the entry type for 
bean serialization.
                                var bm = entryType.isBean() ? 
entryType.getBeanMeta() : null;
 
                                if (bm != null) {
-                                       // Bean or DynaBean path: header row = 
property names
+                                       // Bean or DynaBean path: header row = 
property names + optional _type
                                        var addComma = Flag.create();
                                        
bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).forEach(x
 -> {
                                                addComma.ifSet(() -> 
w.w(',')).set();
                                                w.writeEntry(x.getName());
                                        });
+                                       // Always append _type when 
addBeanTypes or addRootType to support polymorphic parsing
+                                       if (addTypeColumn && ne(typeColName)) {
+                                               w.w(',');
+                                               w.writeEntry(typeColName);
+                                       }
                                        w.append('\n');
                                        var readableProps = 
bm.getProperties().values().stream().filter(BeanPropertyMeta::canRead).toList();
                                        l.forEach(x -> {
                                                var addComma2 = Flag.create();
                                                if (x == null) {
-                                                       // Null entry: write 
null for each column
+                                                       // Null entry: write 
null marker for each column
                                                        readableProps.forEach(y 
-> {
                                                                
addComma2.ifSet(() -> w.w(',')).set();
-                                                               
w.writeEntry(null);
+                                                               
w.writeEntry(nullValue);
                                                        });
+                                                       if (addTypeColumn && 
ne(typeColName)) {
+                                                               w.w(',');
+                                                               
w.writeEntry("");
+                                                       }
                                                } else {
                                                        // Apply swap before 
extracting bean properties (e.g. surrogate swaps)
                                                        var swapped = 
applySwap(x, getClassMetaForObject(x));
-                                                       BeanMap<?> bean = 
toBeanMap(swapped);
+                                                       var aType = 
getClassMetaForObject(swapped);
+                                                       // When swap yields 
non-bean (e.g. String), use raw object for property extraction
+                                                       var objForBean = 
aType.isBean() ? swapped : x;
+                                                       BeanMap<?> bean = 
toBeanMap(objForBean);
                                                        readableProps.forEach(y 
-> {
                                                                
addComma2.ifSet(() -> w.w(',')).set();
                                                                var value = 
y.get(bean, y.getName());
                                                                value = 
formatIfDateOrDuration(value);
+                                                               value = 
formatForCsvCell(value);
                                                                // Use 
toString() to respect trimStrings setting on String values
                                                                if (value 
instanceof String s) value = toString(s);
-                                                               
w.writeEntry(value);
+                                                               
w.writeEntry(value != null ? value : nullValue);
                                                        });
+                                                       if (addTypeColumn && 
ne(typeColName)) {
+                                                               var typeName = 
getBeanTypeName(this, eType, aType, null);
+                                                               w.w(',');
+                                                               
w.writeEntry(typeName != null ? typeName : "");
+                                                       }
                                                }
                                                w.w('\n');
                                        });
                                } else if (entryType.isMap()) {
-                                       // Map path: header row = map keys from 
the first entry
+                                       // Map path: header row = map keys from 
the first entry + optional _type
                                        var addComma = Flag.create();
                                        var first = (Map) firstEntry;
                                        first.keySet().forEach(x -> {
                                                addComma.ifSet(() -> 
w.w(',')).set();
                                                // Apply trimStrings to map 
keys as well
-                                               w.writeEntry(x instanceof 
String s ? toString(s) : x);
+                                               Object keyVal;
+                                               if (x == null)
+                                                       keyVal = nullValue;
+                                               else if (x instanceof String s)
+                                                       keyVal = toString(s);
+                                               else
+                                                       keyVal = x;
+                                               w.writeEntry(keyVal);
                                        });
+                                       if (addTypeColumn && ne(typeColName)) {
+                                               w.w(',');
+                                               w.writeEntry(typeColName);
+                                       }
                                        w.append('\n');
                                        l.forEach(x -> {
                                                var addComma2 = Flag.create();
                                                var swapped = applySwap(x, 
getClassMetaForObject(x));
+                                               var aType = 
getClassMetaForObject(swapped);
                                                var map = (Map) swapped;
                                                map.values().forEach(y -> {
                                                        addComma2.ifSet(() -> 
w.w(',')).set();
                                                        var value = 
applySwap(y, getClassMetaForObject(y));
+                                                       value = 
formatForCsvCell(value);
                                                        // Apply trimStrings to 
map values
                                                        if (value instanceof 
String s) value = toString(s);
-                                                       w.writeEntry(value);
+                                                       w.writeEntry(value != 
null ? value : nullValue);
                                                });
+                                               if (addTypeColumn && 
ne(typeColName)) {
+                                                       var typeName = 
getBeanTypeName(this, eType, aType, null);
+                                                       w.w(',');
+                                                       w.writeEntry(typeName 
!= null ? typeName : "");
+                                               }
                                                w.w('\n');
                                        });
                                } else {
-                                       // Simple value path: single "value" 
column
+                                       // Simple value path: single "value" 
column + optional _type
                                        w.writeEntry("value");
+                                       if (addTypeColumn && ne(typeColName)) {
+                                               w.w(',');
+                                               w.writeEntry(typeColName);
+                                       }
                                        w.append('\n');
                                        l.forEach(x -> {
                                                var value = applySwap(x, 
getClassMetaForObject(x));
+                                               value = formatForCsvCell(value);
                                                // Use toString() to respect 
trimStrings setting
-                                               w.writeEntry(value == null ? 
null : toString(value));
+                                               w.writeEntry(value != null ? 
toString(value) : nullValue);
+                                               if (addTypeColumn && 
ne(typeColName)) {
+                                                       if (x != null) {
+                                                               var aType = 
getClassMetaForObject(x);
+                                                               var typeName = 
getBeanTypeName(this, eType, aType, null);
+                                                               w.w(',');
+                                                               
w.writeEntry(typeName != null ? typeName : "");
+                                                       } else {
+                                                               w.w(',');
+                                                               
w.writeEntry("");
+                                                       }
+                                               }
                                                w.w('\n');
                                        });
                                }
                        }
+                       }
+               } finally {
+                       pop();
                }
        }
 
@@ -349,6 +450,124 @@ public class CsvSerializerSession extends 
WriterSerializerSession {
                return value;
        }
 
+       /**
+        * Formats a value for a CSV cell, handling byte[], primitive arrays, 
and nested structures.
+        */
+       private Object formatForCsvCell(Object value) {
+               if (value == null)
+                       return null;
+               if (allowNestedStructures) {
+                       var type = getClassMetaForObject(value);
+                       if (value instanceof Map || value instanceof Collection 
|| value instanceof Object[]
+                                       || (type.isBean() && !(value instanceof 
Map)))
+                               return new CsvCellSerializer(byteArrayFormat, 
nullValue).serialize(value, this);
+               }
+               if (value instanceof byte[] b) {
+                       return byteArrayFormat == 
ByteArrayFormat.SEMICOLON_DELIMITED
+                               ? formatByteArraySemicolon(b)
+                               : base64Encode(b);
+               }
+               if (value instanceof int[] a) {
+                       var sb = new StringBuilder();
+                       sb.append('[');
+                       for (var i = 0; i < a.length; i++) {
+                               if (i > 0) sb.append(';');
+                               sb.append(a[i]);
+                       }
+                       sb.append(']');
+                       return sb.toString();
+               }
+               if (value instanceof long[] a) {
+                       var sb = new StringBuilder();
+                       sb.append('[');
+                       for (var i = 0; i < a.length; i++) {
+                               if (i > 0) sb.append(';');
+                               sb.append(a[i]);
+                       }
+                       sb.append(']');
+                       return sb.toString();
+               }
+               if (value instanceof double[] a) {
+                       var sb = new StringBuilder();
+                       sb.append('[');
+                       for (var i = 0; i < a.length; i++) {
+                               if (i > 0) sb.append(';');
+                               sb.append(a[i]);
+                       }
+                       sb.append(']');
+                       return sb.toString();
+               }
+               if (value instanceof float[] a) {
+                       var sb = new StringBuilder();
+                       sb.append('[');
+                       for (var i = 0; i < a.length; i++) {
+                               if (i > 0) sb.append(';');
+                               sb.append(a[i]);
+                       }
+                       sb.append(']');
+                       return sb.toString();
+               }
+               if (value instanceof short[] a) {
+                       var sb = new StringBuilder();
+                       sb.append('[');
+                       for (var i = 0; i < a.length; i++) {
+                               if (i > 0) sb.append(';');
+                               sb.append(a[i]);
+                       }
+                       sb.append(']');
+                       return sb.toString();
+               }
+               if (value instanceof boolean[] a) {
+                       var sb = new StringBuilder();
+                       sb.append('[');
+                       for (var i = 0; i < a.length; i++) {
+                               if (i > 0) sb.append(';');
+                               sb.append(a[i]);
+                       }
+                       sb.append(']');
+                       return sb.toString();
+               }
+               if (value instanceof char[] a) {
+                       var sb = new StringBuilder();
+                       sb.append('[');
+                       for (var i = 0; i < a.length; i++) {
+                               if (i > 0) sb.append(';');
+                               sb.append((int) a[i]);
+                       }
+                       sb.append(']');
+                       return sb.toString();
+               }
+               return value;
+       }
+
+       private static String formatByteArraySemicolon(byte[] b) {
+               var sb = new StringBuilder();
+               for (var i = 0; i < b.length; i++) {
+                       if (i > 0) sb.append(';');
+                       sb.append(b[i] & 0xff);
+               }
+               return sb.toString();
+       }
+
+       /**
+        * Prepares a value for inline cell serialization (bean→Map, date 
format, etc.).
+        * Used by {@link CsvCellSerializer}.
+        */
+       Object prepareForInlineValue(Object value) {
+               if (value == null)
+                       return null;
+               var swapped = applySwap(value, getClassMetaForObject(value));
+               var type = getClassMetaForObject(swapped);
+               if (type.isBean() && !(swapped instanceof Map))
+                       return toBeanMap(swapped);
+               if (swapped instanceof Calendar || swapped instanceof Date || 
swapped instanceof Temporal
+                               || swapped instanceof 
javax.xml.datatype.XMLGregorianCalendar)
+                       return Iso8601Utils.format(swapped, type, 
getTimeZone());
+               if (swapped instanceof java.time.Duration)
+                       return swapped.toString();
+               return swapped;
+       }
+
        CsvWriter getCsvWriter(SerializerPipe out) {
                var output = out.getRawOutput();
                if (output instanceof CsvWriter output2)
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializerSession.java
index 0d4658e692..7c67570bba 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlStrippedDocSerializerSession.java
@@ -189,6 +189,9 @@ public class HtmlStrippedDocSerializerSession extends 
HtmlSerializerSession {
                super(builder);
        }
 
+       @SuppressWarnings({
+               "resource" // w is closed by try-with-resources; analyzer false 
positive on super.doSerialize path
+       })
        @Override /* Overridden from SerializerSession */
        protected void doSerialize(SerializerPipe out, Object o) throws 
IOException, SerializeException {
                try (var w = getHtmlWriter(out)) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
index e8968985a4..8e81d7759f 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiSerializerSession.java
@@ -25,7 +25,6 @@ import static org.apache.juneau.httppart.HttpPartFormat.*;
 import java.io.*;
 import java.lang.reflect.*;
 import java.nio.charset.*;
-import java.time.temporal.*;
 import java.util.*;
 import java.util.function.*;
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/svl/ResolvingJsonMap.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/svl/ResolvingJsonMap.java
index d54eef6aaa..321895d305 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/svl/ResolvingJsonMap.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/svl/ResolvingJsonMap.java
@@ -57,7 +57,7 @@ public class ResolvingJsonMap extends JsonMap {
 
        @Override /* Overridden from Object */
        public boolean equals(Object o) {
-               return this == o || (o instanceof ResolvingJsonMap other && 
super.equals(o));
+               return this == o || (o instanceof ResolvingJsonMap && 
super.equals(o));
        }
 
        @Override /* Overridden from Object */
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
index 0152a2cbf1..aefab5e43a 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlSerializerSession.java
@@ -50,6 +50,11 @@ import org.apache.juneau.xml.annotation.*;
 
  * </ul>
  */
+@SuppressWarnings({
+       "rawtypes",  // Raw Map/Collection necessary for serializer dispatch 
over heterogeneous types
+       "unchecked", // Type erasure requires unchecked casts in serializer 
dispatch
+       "resource"  // Writer/Closeable resources managed by 
try-with-resources; analyzer FP in lambdas
+})
 public class XmlSerializerSession extends WriterSerializerSession {
 
        // Argument name constants for assertArgNotNull
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
index 89ba21c316..1c9a8119b8 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParser.java
@@ -17,15 +17,12 @@
 package org.apache.juneau.yaml;
 
 import static org.apache.juneau.commons.utils.AssertionUtils.*;
-import static org.apache.juneau.commons.utils.Utils.*;
 
 import java.lang.annotation.*;
 import java.nio.charset.*;
 import java.util.*;
-import java.util.concurrent.*;
 
 import org.apache.juneau.*;
-import org.apache.juneau.collections.*;
 import org.apache.juneau.commons.collections.*;
 import org.apache.juneau.commons.function.*;
 import org.apache.juneau.commons.reflect.*;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java
index 10633d0133..b083a0ba9e 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlParserSession.java
@@ -29,7 +29,6 @@ import java.util.function.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.collections.*;
-import org.apache.juneau.commons.lang.*;
 import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.httppart.*;
@@ -46,6 +45,10 @@ import org.apache.juneau.utils.Iso8601Utils;
  */
 @SuppressWarnings({
        "java:S125",  // State-machine and parse-path comments are 
documentation, not commented-out code
+       "java:S135",  // Multiple break/continue acceptable for YAML parsing 
state machines
+       "java:S2589", // State checks in error path - analyzer FP on state 
machine flow
+       "java:S2677", // r.read() return value intentionally ignored when 
consuming/skipping chars
+       "java:S3626", // Redundant jump acceptable for state machine clarity
        "unchecked",
        "rawtypes",
        "resource"
@@ -181,6 +184,7 @@ public class YamlParserSession extends ReaderParserSession {
                return new Builder(ctx);
        }
 
+       @SuppressWarnings("unused") // Stored for API consistency with 
Builder.create(YamlParser)
        private final YamlParser ctx;
 
        /**
@@ -231,7 +235,8 @@ public class YamlParserSession extends ReaderParserSession {
        }
 
        @SuppressWarnings({
-               "java:S3776" // Cognitive complexity acceptable for parser 
dispatch
+               "java:S3776", // Cognitive complexity acceptable for parser 
dispatch
+               "java:S6541" // Brain method acceptable for parser dispatch
        })
        private <T> T parseAnything(ClassMeta<?> eType, ParserReader r, Object 
outer, BeanPropertyMeta pMeta) throws IOException, ParseException, 
ExecutableException {
 
@@ -335,7 +340,7 @@ public class YamlParserSession extends ReaderParserSession {
                        o = convertToType(s, sType, eType, outer, pMeta);
                } else if (c == '~') {
                        r.read(); // consume '~'
-                       o = null;
+                       // o remains null (initialized at line 255)
                } else {
                        String s = parsePlainScalar(r, 0);
                        o = handlePlainScalar(s, r, sType, eType, builder, 
outer, pMeta, contentColumn > 0 ? contentColumn - 1 : 0);
@@ -350,9 +355,13 @@ public class YamlParserSession extends ReaderParserSession 
{
                return (T)o;
        }
 
-       @SuppressWarnings("rawtypes")
+       @SuppressWarnings({
+               "java:S107", // 8 parameters required for YAML scalar handling 
dispatch
+               "java:S3776", // Cognitive complexity acceptable for scalar 
type dispatch
+               "java:S6541" // Brain method acceptable for scalar handling
+       })
        private <T> Object handleQuotedScalar(String s, ParserReader r, 
ClassMeta<?> sType, ClassMeta<?> eType, BuilderSwap<T,Object> builder, Object 
outer, BeanPropertyMeta pMeta, int keyIndent) throws IOException, 
ParseException, ExecutableException {
-               if (looksLikeMappingKey(s, r)) {
+               if (looksLikeMappingKey(r)) {
                        r.read(); // consume ':'
                        int cp = r.peek();
                        if (cp == ' ')
@@ -401,7 +410,11 @@ public class YamlParserSession extends ReaderParserSession 
{
                return convertToType(s, sType, eType, outer, pMeta);
        }
 
-       @SuppressWarnings("rawtypes")
+       @SuppressWarnings({
+               "java:S107", // 8 parameters required for YAML scalar handling 
dispatch
+               "java:S3776", // Cognitive complexity acceptable for scalar 
type dispatch
+               "java:S6541" // Brain method acceptable for scalar handling
+       })
        private <T> Object handlePlainScalar(String s, ParserReader r, 
ClassMeta<?> sType, ClassMeta<?> eType, BuilderSwap<T,Object> builder, Object 
outer, BeanPropertyMeta pMeta, int keyIndent) throws IOException, 
ParseException, ExecutableException {
                if (s.isEmpty())
                        return null;
@@ -415,7 +428,7 @@ public class YamlParserSession extends ReaderParserSession {
                        return convertToType(s, sType, eType, outer, pMeta);
                }
 
-               if (looksLikeMappingKey(s, r)) {
+               if (looksLikeMappingKey(r)) {
                        r.read(); // consume ':'
                        int cp = r.peek();
                        if (cp == ' ')
@@ -463,6 +476,9 @@ public class YamlParserSession extends ReaderParserSession {
                return convertToType(s, sType, eType, outer, pMeta);
        }
 
+       @SuppressWarnings({
+               "java:S1172" // eType, pMeta kept for API consistency with 
callers
+       })
        private Object convertToType(String s, ClassMeta<?> sType, ClassMeta<?> 
eType, Object outer, BeanPropertyMeta pMeta) throws ParseException {
                if (sType.isObject()) {
                        return resolveScalarType(trim(s));
@@ -483,12 +499,8 @@ public class YamlParserSession extends ReaderParserSession 
{
                }
        }
 
-       private boolean looksLikeMappingKey(String s, ParserReader r) throws 
IOException {
-               int c = r.peek();
-               if (c != ':')
-                       return false;
-               // Peek past ':' to make sure it's followed by space, newline, 
or EOF (not part of value)
-               return true;
+       private static boolean looksLikeMappingKey(ParserReader r) throws 
IOException {
+               return r.peek() == ':';
        }
 
        private <K,V> void parseBlockMappingRemainder(ParserReader r, Map<K,V> 
m, ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta, int 
parentIndent) throws IOException, ParseException, ExecutableException {
@@ -518,7 +530,7 @@ public class YamlParserSession extends ReaderParserSession {
                setCurrentProperty(null);
        }
 
-       private boolean isNullBlockValue(ParserReader r, int blockIndent) 
throws IOException {
+       private static boolean isNullBlockValue(ParserReader r, int 
blockIndent) throws IOException {
                int c = r.peek();
                if (c == -1)
                        return true;
@@ -540,7 +552,7 @@ public class YamlParserSession extends ReaderParserSession {
                return nextColumn <= blockIndent;
        }
 
-       private int peekSecondChar(ParserReader r) throws IOException {
+       private static int peekSecondChar(ParserReader r) throws IOException {
                r.read();
                int c2 = r.peek();
                r.unread();
@@ -554,7 +566,8 @@ public class YamlParserSession extends ReaderParserSession {
                "java:S1168",
                "java:S135",
                "java:S2583",
-               "java:S3776"
+               "java:S3776",
+               "java:S6541" // Brain method acceptable for flow mapping state 
machine
        })
        private <K,V> Map<K,V> parseFlowMapping(ParserReader r, Map<K,V> m, 
ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta) throws 
IOException, ParseException, ExecutableException {
 
@@ -596,16 +609,14 @@ public class YamlParserSession extends 
ReaderParserSession {
                                else if (isWhitespace(c))
                                        continue;
                        } else if (state == S4) {
-                               if (isWhitespace(c)) {
+                               if (isWhitespace(c))
                                        continue;
-                               } else {
-                                       r.unread();
-                                       K key = convertAttrToType(m, currKey, 
keyType);
-                                       V value = parseAnything(valueType, r, 
m, pMeta);
-                                       setName(valueType, value, key);
-                                       m.put(key, value);
-                                       state = S5;
-                               }
+                               r.unread();
+                               K key = convertAttrToType(m, currKey, keyType);
+                               V value = parseAnything(valueType, r, m, pMeta);
+                               setName(valueType, value, key);
+                               m.put(key, value);
+                               state = S5;
                        } else if (state == S5) {
                                if (c == ',') {
                                        state = S6;
@@ -653,7 +664,7 @@ public class YamlParserSession extends ReaderParserSession {
                return parsePlainFlowKey(r);
        }
 
-       private String parsePlainFlowKey(ParserReader r) throws IOException {
+       private static String parsePlainFlowKey(ParserReader r) throws 
IOException {
                var sb = new StringBuilder();
                int c;
                while ((c = r.read()) != -1) {
@@ -742,6 +753,9 @@ public class YamlParserSession extends ReaderParserSession {
        // ==========================================
        // Block mapping
        // ==========================================
+       @SuppressWarnings({
+               "java:S3776" // Cognitive complexity acceptable for block 
mapping parsing
+       })
        private <K,V> Map<K,V> parseBlockMapping(ParserReader r, Map<K,V> m, 
ClassMeta<K> keyType, ClassMeta<V> valueType, BeanPropertyMeta pMeta, int 
parentIndent) throws IOException, ParseException, ExecutableException {
 
                if (keyType == null)
@@ -793,7 +807,7 @@ public class YamlParserSession extends ReaderParserSession {
                return m;
        }
 
-       private int skipBlanksAndCountIndent(ParserReader r) throws IOException 
{
+       private static int skipBlanksAndCountIndent(ParserReader r) throws 
IOException {
                int indent = 0;
                int c;
                while ((c = r.read()) != -1) {
@@ -811,7 +825,7 @@ public class YamlParserSession extends ReaderParserSession {
                return indent;
        }
 
-       private void unreadSpaces(ParserReader r, int count) throws IOException 
{
+       private static void unreadSpaces(ParserReader r, int count) throws 
IOException {
                for (int i = 0; i < count; i++)
                        r.unread();
        }
@@ -861,7 +875,8 @@ public class YamlParserSession extends ReaderParserSession {
        @SuppressWarnings({
                "java:S1168",
                "java:S2583",
-               "java:S3776"
+               "java:S3776",
+               "java:S6541" // Brain method acceptable for bean map flow state 
machine
        })
        private <T> BeanMap<T> parseIntoBeanMapFlow(ParserReader r, BeanMap<T> 
m) throws IOException, ParseException, ExecutableException {
 
@@ -902,31 +917,29 @@ public class YamlParserSession extends 
ReaderParserSession {
                                        else if (isWhitespace(c))
                                                continue;
                                } else if (state == S4) {
-                                       if (isWhitespace(c)) {
+                                       if (isWhitespace(c))
                                                continue;
-                                       } else {
-                                               if (! 
currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
-                                                       var pm = 
m.getPropertyMeta(currAttr);
-                                                       setCurrentProperty(pm);
-                                                       if (pm == null) {
-                                                               
onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), 
m.getBean(false), null));
-                                                               unmark();
-                                                       } else {
-                                                               unmark();
-                                                               var cm = 
pm.getClassMeta();
-                                                               Object value = 
parseAnything(cm, r.unread(), m.getBean(false), pm);
-                                                               setName(cm, 
value, currAttr);
-                                                               try {
-                                                                       
pm.set(m, currAttr, value);
-                                                               } catch 
(BeanRuntimeException e) {
-                                                                       
onBeanSetterException(pm, e);
-                                                                       throw e;
-                                                               }
+                                       if (! 
currAttr.equals(getBeanTypePropertyName(m.getClassMeta()))) {
+                                               var pm = 
m.getPropertyMeta(currAttr);
+                                               setCurrentProperty(pm);
+                                               if (pm == null) {
+                                                       
onUnknownProperty(currAttr, m, parseAnything(object(), r.unread(), 
m.getBean(false), null));
+                                                       unmark();
+                                               } else {
+                                                       unmark();
+                                                       var cm = 
pm.getClassMeta();
+                                                       Object value = 
parseAnything(cm, r.unread(), m.getBean(false), pm);
+                                                       setName(cm, value, 
currAttr);
+                                                       try {
+                                                               pm.set(m, 
currAttr, value);
+                                                       } catch 
(BeanRuntimeException e) {
+                                                               
onBeanSetterException(pm, e);
+                                                               throw e;
                                                        }
-                                                       
setCurrentProperty(null);
                                                }
-                                               state = S5;
+                                               setCurrentProperty(null);
                                        }
+                                       state = S5;
                                } else if (state == S5) {
                                        if (c == ',')
                                                state = S2;
@@ -1034,6 +1047,9 @@ public class YamlParserSession extends 
ReaderParserSession {
        // ==========================================
        // Block sequence: - value\n- value\n...
        // ==========================================
+       @SuppressWarnings({
+               "java:S3776" // Cognitive complexity acceptable for block 
sequence parsing
+       })
        private <E> Collection<E> parseBlockSequence(ParserReader r, 
Collection<E> l, ClassMeta<?> type, BeanPropertyMeta pMeta, int parentIndent) 
throws IOException, ParseException, ExecutableException {
 
                int blockIndent = -1;
@@ -1173,7 +1189,10 @@ public class YamlParserSession extends 
ReaderParserSession {
        // ==========================================
        // Plain scalar (unquoted)
        // ==========================================
-       private String parsePlainScalar(ParserReader r, int indent) throws 
IOException {
+       @SuppressWarnings({
+               "java:S3776" // Cognitive complexity acceptable for plain 
scalar parsing
+       })
+       private static String parsePlainScalar(ParserReader r, int indent) 
throws IOException {
                var sb = new StringBuilder();
                int c;
                while ((c = r.read()) != -1) {
@@ -1196,10 +1215,7 @@ public class YamlParserSession extends 
ReaderParserSession {
                                        break;
                                }
                                sb.append((char)c);
-                       } else if (c == ',' || c == '}' || c == ']') {
-                               r.unread();
-                               break;
-                       } else if (c == '#' && sb.isEmpty()) {
+                       } else if ((c == ',' || c == '}' || c == ']') || (c == 
'#' && sb.isEmpty())) {
                                r.unread();
                                break;
                        } else {
@@ -1218,7 +1234,8 @@ public class YamlParserSession extends 
ReaderParserSession {
        // Block scalar: | or >
        // ==========================================
        @SuppressWarnings({
-               "java:S3776"
+               "java:S3776", // Cognitive complexity acceptable for block 
scalar parsing
+               "java:S6541" // Brain method acceptable for block scalar state 
machine
        })
        private String parseBlockScalar(ParserReader r, char indicator) throws 
IOException, ParseException {
                r.read(); // consume '|' or '>'
@@ -1354,7 +1371,7 @@ public class YamlParserSession extends 
ReaderParserSession {
        // Helper methods
        // ==========================================
 
-       private void skipWhitespaceAndComments(ParserReader r) throws 
IOException {
+       private static void skipWhitespaceAndComments(ParserReader r) throws 
IOException {
                int c;
                while ((c = r.read()) != -1) {
                        if (c == '#') {
@@ -1370,7 +1387,7 @@ public class YamlParserSession extends 
ReaderParserSession {
                }
        }
 
-       private void skipDocumentMarker(ParserReader r) throws IOException {
+       private static void skipDocumentMarker(ParserReader r) throws 
IOException {
                int c = r.peek();
                if (c == '-') {
                        r.read();
@@ -1405,7 +1422,7 @@ public class YamlParserSession extends 
ReaderParserSession {
                }
        }
 
-       private void skipToEndOfLine(ParserReader r) throws IOException {
+       private static void skipToEndOfLine(ParserReader r) throws IOException {
                int c;
                while ((c = r.read()) != -1) {
                        if (c == '\n' || c == '\r')
@@ -1413,11 +1430,14 @@ public class YamlParserSession extends 
ReaderParserSession {
                }
        }
 
-       private boolean isWhitespace(int c) {
+       private static boolean isWhitespace(int c) {
                return c == ' ' || c == '\t' || c == '\n' || c == '\r';
        }
 
-       private Object resolveScalarType(String s) {
+       @SuppressWarnings({
+               "java:S3776" // Cognitive complexity acceptable for scalar type 
resolution
+       })
+       private static Object resolveScalarType(String s) {
                if (s == null || "null".equals(s) || "~".equals(s) || 
s.isEmpty())
                        return null;
                if ("true".equals(s) || "True".equals(s) || "TRUE".equals(s))
@@ -1425,32 +1445,38 @@ public class YamlParserSession extends 
ReaderParserSession {
                if ("false".equals(s) || "False".equals(s) || "FALSE".equals(s))
                        return Boolean.FALSE;
 
-               // Try parsing as number
-               if (!s.isEmpty()) {
-                       char first = s.charAt(0);
-                       if (first == '-' || first == '+' || (first >= '0' && 
first <= '9') || first == '.') {
-                               try {
-                                       if (s.contains(".") || s.contains("e") 
|| s.contains("E")) {
-                                               Double d = 
Double.parseDouble(s);
-                                               if (!d.isInfinite() && 
!d.isNaN())
-                                                       return d;
-                                       } else {
-                                               try {
-                                                       return 
Integer.parseInt(s);
-                                               } catch 
(@SuppressWarnings("unused") NumberFormatException e2) {
-                                                       try {
-                                                               return 
Long.parseLong(s);
-                                                       } catch 
(@SuppressWarnings("unused") NumberFormatException e3) {
-                                                               // Fall through 
to string
-                                                       }
-                                               }
-                                       }
-                               } catch (@SuppressWarnings("unused") 
NumberFormatException e) {
-                                       // Fall through to string
-                               }
+               Object num = tryParseNumber(s);
+               if (num != null)
+                       return num;
+               return s;
+       }
+
+       private static Object tryParseNumber(String s) {
+               if (s.isEmpty())
+                       return null;
+               char first = s.charAt(0);
+               if (first != '-' && first != '+' && (first < '0' || first > 
'9') && first != '.')
+                       return null;
+               try {
+                       if (s.contains(".") || s.contains("e") || 
s.contains("E")) {
+                               Double d = Double.parseDouble(s);
+                               return (!d.isInfinite() && !d.isNaN()) ? d : 
null;
                        }
+                       return tryParseIntegerOrLong(s);
+               } catch (@SuppressWarnings("unused") NumberFormatException e) {
+                       return null;
                }
+       }
 
-               return s;
+       private static Object tryParseIntegerOrLong(String s) {
+               try {
+                       return Integer.valueOf(s);
+               } catch (@SuppressWarnings("unused") NumberFormatException e) {
+                       try {
+                               return Long.valueOf(s);
+                       } catch (@SuppressWarnings("unused") 
NumberFormatException e2) {
+                               return null;
+                       }
+               }
        }
 }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
index b041d1c68e..86acd77c0b 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlSerializer.java
@@ -22,7 +22,6 @@ import static org.apache.juneau.commons.utils.Utils.*;
 import java.lang.annotation.*;
 import java.nio.charset.*;
 import java.util.*;
-import java.util.concurrent.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.commons.collections.*;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java
index b2e94a11c7..fd07a79bf2 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/yaml/YamlWriter.java
@@ -192,7 +192,7 @@ public class YamlWriter extends SerializerWriter {
        @SuppressWarnings({
                "java:S3776" // Cognitive complexity acceptable for YAML 
quoting checks
        })
-       private boolean needsQuoting(String s) {
+       private static boolean needsQuoting(String s) {
                if (s.isEmpty())
                        return true;
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
index 87356de200..0d78f46277 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/RoundTripTest_Base.java
@@ -131,14 +131,17 @@ public abstract class RoundTripTest_Base extends TestBase 
{
         * Returns true if the object can be serialized to CSV without error.
         *
         * <p>
-        * CSV can serialize any non-null input, but restricts to non-null, 
non-array inputs
-        * to avoid serialization errors on raw byte arrays and similar types.
+        * CSV supports byte[], primitive arrays ([1;2;3]), nested structures 
(when enabled),
+        * and type discriminators. Only 2D+ primitive arrays are excluded.
         */
        protected static boolean isCsvSerializableInput(Object o) {
                if (o == null) return false;
                var cls = o.getClass();
-               // Skip raw primitive arrays (byte[], char[], int[][], etc.) - 
they serialize as toString()
-               if (cls.isArray() && cls.getComponentType().isPrimitive()) 
return false;
+               // Skip 2D+ primitive arrays (int[][], etc.) - not supported
+               if (cls.isArray()) {
+                       var ct = cls.getComponentType();
+                       if (ct.isArray() && 
ct.getComponentType().isPrimitive()) return false;
+               }
                return true;
        }
 
@@ -146,17 +149,13 @@ public abstract class RoundTripTest_Base extends TestBase 
{
         * Returns true if the object can be faithfully round-tripped through 
CSV.
         *
         * <p>
-        * CSV is tabular and only round-trips cleanly when:
-        * <ul>
-        *   <li>The object is a non-empty {@link Collection} of flat beans or 
Maps.
-        *   <li>Primitive arrays, 2D arrays, scalar lists, and enum arrays are 
excluded
-        *       because CSV cannot unambiguously represent them during parsing.
-        * </ul>
+        * CSV round-trips when the object is a non-empty {@link Collection} of 
flat beans or Maps
+        * whose properties are primitives, strings, numbers, dates, byte 
arrays, or primitive arrays.
+        * Nested structures require {@code allowNestedStructures(true)}.
         */
        protected static boolean isCsvRoundTripCompatible(Object o) {
                if (o == null)
                        return false;
-               // Only Collections are supported; reject raw arrays of any kind
                if (!(o instanceof Collection<?> col))
                        return false;
                if (col.isEmpty())
@@ -170,18 +169,19 @@ public abstract class RoundTripTest_Base extends TestBase 
{
        private static boolean isCsvCompatibleElement(Object elem) {
                if (elem == null) return false;
                var cls = elem.getClass();
-               // Reject scalars, arrays, enums, Optional, and any type that 
isn't a bean or Map
-               if (cls.isPrimitive() || cls.isArray()) return false;
+               if (cls.isPrimitive()) return false;
                if (elem instanceof Number || elem instanceof Boolean || elem 
instanceof Character) return false;
                if (elem instanceof CharSequence || cls.isEnum()) return false;
                if (elem instanceof java.util.Optional || elem instanceof 
Collection) return false;
-               // Accept Maps only if all values are also simple types
+               // 1D primitive arrays and byte[] are supported
+               if (cls.isArray()) {
+                       var ct = cls.getComponentType();
+                       return !ct.isArray(); // 1D arrays only
+               }
                if (elem instanceof Map m) {
                        return m.values().stream().allMatch(v -> v == null || 
isCsvSimpleType(v.getClass()));
                }
-               // Reject JDK types that are not beans
                if (cls.getName().startsWith("java.") || 
cls.getName().startsWith("javax.")) return false;
-               // For POJO beans: only accept if all public fields have simple 
(flat) types
                for (var field : cls.getFields()) {
                        if (!isCsvSimpleType(field.getType())) return false;
                }
@@ -190,7 +190,7 @@ public abstract class RoundTripTest_Base extends TestBase {
 
        private static boolean isCsvSimpleType(Class<?> t) {
                if (t == null) return true;
-               return t.isPrimitive()
+               if (t.isPrimitive()
                        || t == String.class
                        || t == Boolean.class
                        || t == Character.class
@@ -198,6 +198,13 @@ public abstract class RoundTripTest_Base extends TestBase {
                        || t.isEnum()
                        || java.time.temporal.Temporal.class.isAssignableFrom(t)
                        || t == java.util.Date.class
-                       || t == java.util.Calendar.class;
+                       || t == java.util.Calendar.class)
+                       return true;
+               // byte[] and primitive arrays [1;2;3]
+               if (t.isArray()) {
+                       var ct = t.getComponentType();
+                       return ct.isPrimitive() || ct == Byte.class;
+               }
+               return false;
        }
 }
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/csv/CsvParser_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/csv/CsvParser_Test.java
index b41ebc320b..af3ee4f310 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/csv/CsvParser_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/csv/CsvParser_Test.java
@@ -120,7 +120,7 @@ class CsvParser_Test extends TestBase {
        
//====================================================================================================
 
        @Test void d01_parseNullValues() throws Exception {
-               var csv = "b,c\nnull,1\nb2,null\n";
+               var csv = "b,c\n<NULL>,1\nb2,<NULL>\n";
                var r = parseList(csv, B.class);
                assertEquals(2, r.size());
                assertNull(r.get(0).b);
diff --git a/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
index b50007f450..f681a75bd8 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
@@ -145,11 +145,11 @@ class Csv_Test extends TestBase {
                var s = CsvSerializer.create().swaps(DateSwap.class).build();
                var r = s.serialize(l);
 
-               // Should have users and null values
+               // Should have users and null values (null marker defaults to 
<NULL>)
                assertTrue(r.contains("user1"));
                assertTrue(r.contains("user2"));
                assertTrue(r.contains("user3"));
-               assertTrue(r.contains("null"));
+               assertTrue(r.contains("<NULL>"));
                assertTrue(r.contains("1970-01-01") || 
r.contains("1969-12-31"), "Should have formatted date but was: " + r);
        }
 
@@ -320,15 +320,14 @@ class Csv_Test extends TestBase {
                l.add(new G(null, "null"));
 
                var r = CsvSerializer.DEFAULT.serialize(l);
-               // Java null → unquoted "null"; the String "null" → quoted 
"\"null\""
-               // Both serialize as: null,"null"
-               assertTrue(r.contains("null,\"null\"") || 
r.contains("null,null"),
-                       "Unexpected output: " + r);
+               // Java null → null marker (<NULL> by default); the String 
"null" → quoted "\"null\""
+               assertTrue(r.contains("<NULL>") && r.contains("null"), 
"Unexpected output: " + r);
        }
 
        public static class G {
                public String a;
                public String b;
+               public G() {}
                public G(String a, String b) { this.a = a; this.b = b; }
        }
 
@@ -348,4 +347,227 @@ class Csv_Test extends TestBase {
                var r = CsvSerializer.DEFAULT.serialize(new F("hello", 42));
                assertEquals("b,c\nhello,42\n", r);
        }
+
+       
//====================================================================================================
+       // Test type discriminator - addBeanTypes adds _type column
+       
//====================================================================================================
+       
//====================================================================================================
+       // Test type discriminator - parser skips _type column when target is 
concrete
+       
//====================================================================================================
+       @Test void j01_typeDiscriminator_skipsTypeColumn() throws Exception {
+               // CSV with _type column - parser skips it for concrete Circle 
type
+               var csv = "name,radius,_type\nc1,10,Circle\nc2,20,Circle\n";
+               var p = CsvParser.create().build();
+
+               @SuppressWarnings("unchecked")
+               var parsed = (List<Circle>) p.parse(csv, List.class, 
Circle.class);
+
+               assertNotNull(parsed);
+               assertEquals(2, parsed.size());
+               assertEquals("c1", parsed.get(0).name);
+               assertEquals(10, parsed.get(0).radius);
+               assertEquals("c2", parsed.get(1).name);
+               assertEquals(20, parsed.get(1).radius);
+       }
+
+       
//====================================================================================================
+       // Test type discriminator - single bean with _type column
+       
//====================================================================================================
+       @Test void j02_typeDiscriminator_singleBean() throws Exception {
+               var csv = "name,radius,_type\nc1,10,Circle\n";
+               var p = CsvParser.create().build();
+
+               var parsed = p.parse(csv, Circle.class);
+               assertNotNull(parsed);
+               assertEquals("c1", parsed.name);
+               assertEquals(10, parsed.radius);
+       }
+
+       @org.apache.juneau.annotation.Bean(dictionary = {Circle.class, 
Rectangle.class})
+       public interface Shape {
+               String getName();
+       }
+
+       @org.apache.juneau.annotation.Bean(typeName = "Circle")
+       public static class Circle implements Shape {
+               public String name;
+               public int radius;
+
+               public Circle() {}
+               public Circle(String name, int radius) {
+                       this.name = name;
+                       this.radius = radius;
+               }
+               @Override
+               public String getName() { return name; }
+       }
+
+       @org.apache.juneau.annotation.Bean(typeName = "Rectangle")
+       public static class Rectangle implements Shape {
+               public String name;
+               public int width;
+               public int height;
+
+               public Rectangle() {}
+               public Rectangle(String name, int width, int height) {
+                       this.name = name;
+                       this.width = width;
+                       this.height = height;
+               }
+               @Override
+               public String getName() { return name; }
+       }
+
+       
//====================================================================================================
+       // Test byte[] BASE64 round-trip
+       
//====================================================================================================
+       @Test void k01_byteArray_base64() throws Exception {
+               var bytes = "Hello 
World".getBytes(java.nio.charset.StandardCharsets.UTF_8);
+               var l = new LinkedList<>();
+               l.add(new I("row1", bytes));
+               l.add(new I("row2", new byte[]{1, 2, 3}));
+
+               var s = 
CsvSerializer.create().byteArrayFormat(ByteArrayFormat.BASE64).build();
+               var p = 
CsvParser.create().byteArrayFormat(ByteArrayFormat.BASE64).build();
+
+               var csv = s.serialize(l);
+               assertTrue(csv.contains("SGVsbG8gV29ybGQ=") || 
csv.contains("data"), "Should have base64: " + csv);
+
+               @SuppressWarnings("unchecked")
+               var parsed = (List<I>) p.parse(csv, List.class, I.class);
+               assertNotNull(parsed);
+               assertEquals(2, parsed.size());
+               assertArrayEquals(bytes, parsed.get(0).data);
+               assertArrayEquals(new byte[]{1, 2, 3}, parsed.get(1).data);
+       }
+
+       
//====================================================================================================
+       // Test byte[] SEMICOLON_DELIMITED round-trip
+       
//====================================================================================================
+       @Test void k02_byteArray_semicolonDelimited() throws Exception {
+               var bytes = new byte[]{72, 101, 108, 108, 111};
+               var l = new LinkedList<>();
+               l.add(new I("row1", bytes));
+
+               var s = 
CsvSerializer.create().byteArrayFormat(ByteArrayFormat.SEMICOLON_DELIMITED).build();
+               var p = 
CsvParser.create().byteArrayFormat(ByteArrayFormat.SEMICOLON_DELIMITED).build();
+
+               var csv = s.serialize(l);
+               assertTrue(csv.contains("72;101;108;108;111"), "Should have 
semicolon format: " + csv);
+
+               @SuppressWarnings("unchecked")
+               var parsed = (List<I>) p.parse(csv, List.class, I.class);
+               assertNotNull(parsed);
+               assertArrayEquals(bytes, parsed.get(0).data);
+       }
+
+       public static class I {
+               public String name;
+               public byte[] data;
+
+               public I() {}
+               public I(String name, byte[] data) {
+                       this.name = name;
+                       this.data = data;
+               }
+       }
+
+       
//====================================================================================================
+       // Test int[] and double[] via [1;2;3] format
+       
//====================================================================================================
+       @Test void k03_primitiveArrays() throws Exception {
+               var l = new LinkedList<>();
+               l.add(new H("row1", new int[]{1, 2, 3}, new double[]{1.5, 2.5, 
3.5}));
+               l.add(new H("row2", new int[]{}, new double[]{}));
+
+               var s = CsvSerializer.DEFAULT;
+               var p = CsvParser.DEFAULT;
+
+               var csv = s.serialize(l);
+               assertTrue(csv.contains("[1;2;3]"), "Should have int array 
format: " + csv);
+               assertTrue(csv.contains("[1.5;2.5;3.5]"), "Should have double 
array format: " + csv);
+
+               @SuppressWarnings("unchecked")
+               var parsed = (List<H>) p.parse(csv, List.class, H.class);
+               assertNotNull(parsed);
+               assertEquals(2, parsed.size());
+               assertArrayEquals(new int[]{1, 2, 3}, parsed.get(0).ints);
+               assertArrayEquals(new double[]{1.5, 2.5, 3.5}, 
parsed.get(0).doubles);
+               assertArrayEquals(new int[0], parsed.get(1).ints);
+               assertArrayEquals(new double[0], parsed.get(1).doubles);
+       }
+
+       public static class H {
+               public String name;
+               public int[] ints;
+               public double[] doubles;
+
+               public H() {}
+               public H(String name, int[] ints, double[] doubles) {
+                       this.name = name;
+                       this.ints = ints;
+                       this.doubles = doubles;
+               }
+       }
+
+       
//====================================================================================================
+       // Test nested structures - allowNestedStructures with {key:val} and 
[val;val]
+       
//====================================================================================================
+       @Test void l01_nestedStructures() throws Exception {
+               var l = new LinkedList<>();
+               l.add(new J("row1", List.of("a", "b", "c"), Map.of("x", 1, "y", 
2)));
+               l.add(new J("row2", List.of(), Map.of()));
+
+               var s = 
CsvSerializer.create().allowNestedStructures(true).build();
+               var p = CsvParser.create().allowNestedStructures(true).build();
+
+               var csv = s.serialize(l);
+               assertTrue(csv.contains("[a;b;c]") || csv.contains("tags"), 
"Should have array notation: " + csv);
+               assertTrue(csv.contains("x:1") || csv.contains("y:2") || 
csv.contains("meta"), "Should have object notation: " + csv);
+
+               @SuppressWarnings("unchecked")
+               var parsed = (List<J>) p.parse(csv, List.class, J.class);
+               assertNotNull(parsed);
+               assertEquals(2, parsed.size());
+               assertEquals(List.of("a", "b", "c"), parsed.get(0).tags);
+               assertEquals(2, parsed.get(0).meta.size());
+               assertEquals(1, ((Number) 
parsed.get(0).meta.get("x")).intValue());
+               assertEquals(2, ((Number) 
parsed.get(0).meta.get("y")).intValue());
+               assertEquals(List.of(), parsed.get(1).tags);
+               assertTrue(parsed.get(1).meta.isEmpty());
+       }
+
+       public static class J {
+               public String name;
+               public List<String> tags;
+               public Map<String, Object> meta;
+
+               public J() {}
+               public J(String name, List<String> tags, Map<String, Object> 
meta) {
+                       this.name = name;
+                       this.tags = tags;
+                       this.meta = meta;
+               }
+       }
+
+       
//====================================================================================================
+       // Test nullValue() - custom null marker
+       
//====================================================================================================
+       @Test void m01_nullValue() throws Exception {
+               var l = new LinkedList<>();
+               l.add(new G(null, "x"));
+
+               var s = CsvSerializer.create().nullValue("<NULL>").build();
+               var p = CsvParser.create().nullValue("<NULL>").build();
+
+               var csv = s.serialize(l);
+               assertTrue(csv.contains("<NULL>"), "Should have null marker: " 
+ csv);
+
+               @SuppressWarnings("unchecked")
+               var parsed = (List<G>) p.parse(csv, List.class, G.class);
+               assertNotNull(parsed);
+               assertEquals(1, parsed.size());
+               assertNull(parsed.get(0).a);
+               assertEquals("x", parsed.get(0).b);
+       }
 }
\ No newline at end of file

Reply via email to