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 a34c40a1a3 TODO-291: Fix @ParentProperty injection through 
collections/maps
a34c40a1a3 is described below

commit a34c40a1a3832d62a455cfaf8fab1daae994c201
Author: James Bognar <[email protected]>
AuthorDate: Fri Jul 24 09:08:15 2026 -0400

    TODO-291: Fix @ParentProperty injection through collections/maps
    
    A collection/map element's @ParentProperty now receives the nearest 
enclosing
    bean (skipping intermediate containers at any depth; null at root) instead 
of
    the containing List/Map. Separates the parser's dual-purpose 'outer' param 
via a
    new ParserSession.parentBean() tracker, applied across all 9 streaming 
parsers
    plus HOCON/HJSON reconciliation. Makes the canonical 
AddressBook/List<Person>
    example round-trip and unifies behavior across all formats.
    
    Behavior change for 10.0.0 (see release notes). New 
ParentPropertyContainer_Test
    (72 methods); full reactor gate green (122,889 tests, 0 failures).
    
    Co-authored-by: Cursor <[email protected]>
---
 .../org/apache/juneau/marshall/ParentProperty.java |  24 ++-
 .../juneau/marshall/bson/BsonParserSession.java    |  51 ++---
 .../juneau/marshall/cbor/CborParserSession.java    |  43 ++--
 .../juneau/marshall/hjson/HjsonParserSession.java  |  50 ++++-
 .../juneau/marshall/hocon/HoconParserSession.java  |  50 ++++-
 .../juneau/marshall/html/HtmlParserSession.java    | 108 +++++-----
 .../juneau/marshall/json/JsonParserSession.java    |   6 +-
 .../marshall/msgpack/MsgPackParserSession.java     |  43 ++--
 .../juneau/marshall/parser/ParserSession.java      |  33 +++
 .../juneau/marshall/uon/UonParserSession.java      |   6 +-
 .../urlencoding/UrlEncodingParserSession.java      |   6 +-
 .../juneau/marshall/xml/XmlParserSession.java      |  22 +-
 .../juneau/marshall/yaml/YamlParserSession.java    |  13 +-
 .../juneau/ParentPropertyContainer_Test.java       | 225 +++++++++++++++++++++
 14 files changed, 544 insertions(+), 136 deletions(-)

diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ParentProperty.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ParentProperty.java
index 67a620f775..db253d8591 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ParentProperty.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/ParentProperty.java
@@ -52,7 +52,7 @@ import java.lang.annotation.*;
  *
  *     <jk>public class</jk> Person {
  *             <ja>@ParentProperty</ja>
- *             <jk>public</jk> AddressBook addressBook;  <jc>// Automatically 
set to containing AddressBook</jc>
+ *             <jk>public</jk> AddressBook addressBook;  <jc>// Automatically 
set to the enclosing AddressBook (the intervening List is skipped).</jc>
  *
  *             <jk>public</jk> String name;
  *             <jk>public</jk> <jk>char</jk> sex;
@@ -79,6 +79,17 @@ import java.lang.annotation.*;
  *     <li>This allows child objects to navigate back to their parent if needed
  * </ul>
  *
+ * <h5 class='section'>Parent resolution through collections and maps:</h5>
+ * <ul class='spaced-list'>
+ *     <li>A collection/map element's <ja>@ParentProperty</ja> is set to the 
nearest enclosing bean, skipping any
+ *             intervening collections, maps, or arrays.  In the example 
above, each <c>Person</c> lives inside a
+ *             <c>List&lt;Person&gt;</c>, yet its <c>addressBook</c> 
back-reference receives the enclosing <c>AddressBook</c>
+ *             bean, not the <c>List</c>.  The same holds through arbitrarily 
deep nesting
+ *             (e.g. <c>List&lt;List&lt;Person&gt;&gt;</c>, 
<c>Map&lt;String,List&lt;Person&gt;&gt;</c>).
+ *     <li>A bean at the document root (or directly inside a top-level 
collection) has no enclosing bean, so its
+ *             <ja>@ParentProperty</ja> is left <jk>null</jk>.
+ * </ul>
+ *
  * <h5 class='section'>Cyclic graphs and serialization:</h5>
  * <ul class='spaced-list'>
  *     <li>When a <ja>@ParentProperty</ja> back-reference is also a 
normally-visible bean property (e.g. a <jk>public</jk>
@@ -98,6 +109,17 @@ import java.lang.annotation.*;
  *             <jk>null</jk>, and parsing re-injects the parent via this 
annotation.
  * </ul>
  *
+ * <p>
+ * Putting it together, the <c>AddressBook</c>/<c>List&lt;Person&gt;</c> graph 
above round-trips cleanly when the
+ * back-reference is omitted on serialize and re-injected on parse:
+ * <p class='bjava'>
+ *     Serializer <jv>serializer</jv> = 
JsonSerializer.<jsm>create</jsm>().ignoreRecursions().build();
+ *     String <jv>json</jv> = 
<jv>serializer</jv>.serialize(<jv>addressBook</jv>);
+ *     AddressBook <jv>parsed</jv> = 
JsonParser.<jsf>DEFAULT</jsf>.parse(<jv>json</jv>, AddressBook.<jk>class</jk>);
+ *     Person <jv>person</jv> = <jv>parsed</jv>.people.get(0);
+ *     <jsm>assertTrue</jsm>(<jv>person</jv>.addressBook == <jv>parsed</jv>);  
<jc>// Parent re-injected through the List.</jc>
+ * </p>
+ *
  * <h5 class='section'>See Also:</h5><ul>
  *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/ParentPropertyAnnotation";>@ParentProperty
 Annotation</a>
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
index 74d3dfb668..0f0c589424 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
@@ -125,8 +125,8 @@ public class BsonParserSession extends 
InputStreamParserSession implements Recor
                                yield null;
                        }
                };
-               if (nn(outer) && nn(o))
-                       setParent(targetType, o, outer);
+               if (nn(parentBean()) && nn(o))
+                       setParent(targetType, o, parentBean());
                return o;
        }
 
@@ -179,29 +179,34 @@ public class BsonParserSession extends 
InputStreamParserSession implements Recor
                                result = raw;
                } else if (!eType.isOptional() && (nn(builder) || 
sType.canCreateNewBean(outer))) {
                        var beanMap = builder == null ? newBeanMap(outer, 
sType.inner()) : toBeanMap(builder.create(this, eType));
-                       while (!is.isDocumentEnd()) {
-                               var et = is.readElementType();
-                               var name = is.readElementName();
-                               var key = trimKey(name);
-                               var bpm = beanMap.getPropertyMeta(key);
-                               Object value;
-                               if 
(name.equals(getBeanTypePropertyName(eType))) {
-                                       value = readTypedValue(is, et, 
string(), null, null);
-                                       if (nn(value))
-                                               beanMap = 
applyTypeProperty(beanMap, value.toString(), eType);
-                               } else if (bpm != null) {
-                                       var bcm = (ClassMeta<?>) 
bpm.getBeanInfo();
-                                       value = readTypedValue(is, et, bcm, 
beanMap.getBean(false), bpm);
-                                       setName(bcm, value, key);
-                                       try {
-                                               bpm.set(beanMap, key, value);
-                                       } catch (BeanRuntimeException e) {
-                                               onBeanSetterException(nn(pMeta) 
? pMeta : bpm, e);
-                                               throw e;
+                       var pb = swapParentBean(beanMap.getBean(false));
+                       try {
+                               while (!is.isDocumentEnd()) {
+                                       var et = is.readElementType();
+                                       var name = is.readElementName();
+                                       var key = trimKey(name);
+                                       var bpm = beanMap.getPropertyMeta(key);
+                                       Object value;
+                                       if 
(name.equals(getBeanTypePropertyName(eType))) {
+                                               value = readTypedValue(is, et, 
string(), null, null);
+                                               if (nn(value))
+                                                       beanMap = 
applyTypeProperty(beanMap, value.toString(), eType);
+                                       } else if (bpm != null) {
+                                               var bcm = (ClassMeta<?>) 
bpm.getBeanInfo();
+                                               value = readTypedValue(is, et, 
bcm, beanMap.getBean(false), bpm);
+                                               setName(bcm, value, key);
+                                               try {
+                                                       bpm.set(beanMap, key, 
value);
+                                               } catch (BeanRuntimeException 
e) {
+                                                       
onBeanSetterException(nn(pMeta) ? pMeta : bpm, e);
+                                                       throw e;
+                                               }
+                                       } else {
+                                               onUnknownProperty(key, beanMap, 
readTypedValue(is, et, object(), null, null));
                                        }
-                               } else {
-                                       onUnknownProperty(key, beanMap, 
readTypedValue(is, et, object(), null, null));
                                }
+                       } finally {
+                               swapParentBean(pb);
                        }
                        is.readDocumentTerminator();
                        result = builder == null ? beanMap.getBean() : 
builder.build(this, beanMap.getBean(), eType);
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
index 251e26a07a..182687d618 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
@@ -221,25 +221,30 @@ public class CborParserSession extends 
InputStreamParserSession implements Token
                        } else if (nn(builder) || 
sType.canCreateNewBean(outer)) {
                                if (dt == MAP) {
                                        BeanMap m = builder == null ? 
newBeanMap(outer, sType.inner()) : toBeanMap(builder.create(this, eType));
-                                       for (var i = 0; 
shouldContinueContainer(is, len, i); i++) {
-                                               String pName = 
readAnything(string(), is, m.getBean(false), null);
-                                               var bpm = 
m.getPropertyMeta(pName);
-                                               if (bpm == null) {
-                                                       if 
(pName.equals(getBeanTypePropertyName(eType)))
-                                                               
readAnything(string(), is, null, null);
-                                                       else
-                                                               
onUnknownProperty(pName, m, readAnything(string(), is, null, null));
-                                               } else {
-                                                       var cm = (ClassMeta<?>) 
bpm.getBeanInfo();
-                                                       Object value = 
readAnything(cm, is, m.getBean(false), bpm);
-                                                       setName(cm, value, 
pName);
-                                                       try {
-                                                               bpm.set(m, 
pName, value);
-                                                       } catch 
(BeanRuntimeException e) {
-                                                               
onBeanSetterException(pMeta, e);
-                                                               throw e;
+                                       var pb = 
swapParentBean(m.getBean(false));
+                                       try {
+                                               for (var i = 0; 
shouldContinueContainer(is, len, i); i++) {
+                                                       String pName = 
readAnything(string(), is, m.getBean(false), null);
+                                                       var bpm = 
m.getPropertyMeta(pName);
+                                                       if (bpm == null) {
+                                                               if 
(pName.equals(getBeanTypePropertyName(eType)))
+                                                                       
readAnything(string(), is, null, null);
+                                                               else
+                                                                       
onUnknownProperty(pName, m, readAnything(string(), is, null, null));
+                                                       } else {
+                                                               var cm = 
(ClassMeta<?>) bpm.getBeanInfo();
+                                                               Object value = 
readAnything(cm, is, m.getBean(false), bpm);
+                                                               setName(cm, 
value, pName);
+                                                               try {
+                                                                       
bpm.set(m, pName, value);
+                                                               } catch 
(BeanRuntimeException e) {
+                                                                       
onBeanSetterException(pMeta, e);
+                                                                       throw e;
+                                                               }
                                                        }
                                                }
+                                       } finally {
+                                               swapParentBean(pb);
                                        }
                                        o = builder == null ? m.getBean() : 
builder.build(this, m.getBean(), eType);
                                } else {
@@ -324,8 +329,8 @@ public class CborParserSession extends 
InputStreamParserSession implements Token
                if (nn(swap) && nn(o))
                        o = unswap(swap, o, eType);
 
-               if (nn(outer))
-                       setParent(eType, o, outer);
+               if (nn(parentBean()))
+                       setParent(eType, o, parentBean());
 
                return (T)o;
        }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
index 7c2188a7d0..a4ebb89eef 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hjson/HjsonParserSession.java
@@ -327,16 +327,54 @@ public class HjsonParserSession extends 
ReaderParserSession implements RecordRea
                        var cm = (ClassMeta<?>) pm.getBeanInfo();
                        if (cm.getNameProperty() != null)
                                setName(cm, val, key);
-                       if (cm.getParentProperty() != null)
-                               setParent(cm, val, bean);
-                       if (cm.isMap() && val instanceof Map<?,?> val2 && 
!cm.getValueType().isObject() && cm.getValueType().getNameProperty() != null) {
-                               var valueType = cm.getValueType();
-                               for (Map.Entry<?,?> e : 
((Map<?,?>)val2).entrySet())
-                                       setName(valueType, e.getValue(), 
e.getKey());
+                       injectParentAnnotations(cm, val, entry.getValue(), 
bean);
+               }
+       }
+
+       /*
+        * Recursively injects @ParentProperty (and @NameProperty on map values 
/ nested beans) into val.
+        * Intermediate collections and maps are transparent: a collection/map 
element's parent is
+        * parentBean (the nearest enclosing bean), skipping all intermediate 
containers.  This keeps HJSON
+        * consistent with the streaming parsers under TODO-291 (Option A).
+        */
+       private void injectParentAnnotations(ClassMeta<?> cm, Object val, 
Object node, Object parentBean) throws ExecutableException {
+               if (val == null || cm == null)
+                       return;
+               if (cm.getParentProperty() != null)
+                       setParent(cm, val, parentBean);
+               if (cm.isCollectionOrArray()) {
+                       var et = cm.getElementType();
+                       if (et == null || et.isObject())
+                               return;
+                       var nodeList = node instanceof List<?> nl ? nl : null;
+                       var i = 0;
+                       for (var element : toIterable(val)) {
+                               injectParentAnnotations(et, element, nodeList 
!= null && i < nodeList.size() ? nodeList.get(i) : null, parentBean);
+                               i++;
                        }
+               } else if (cm.isMap() && val instanceof Map<?,?> valMap) {
+                       var vt = cm.getValueType();
+                       if (vt == null || vt.isObject())
+                               return;
+                       var nodeMap = node instanceof Map<?,?> nm ? nm : null;
+                       for (Map.Entry<?,?> e : valMap.entrySet()) {
+                               if (vt.getNameProperty() != null)
+                                       setName(vt, e.getValue(), e.getKey());
+                               injectParentAnnotations(vt, e.getValue(), 
nodeMap != null ? nodeMap.get(e.getKey()) : null, parentBean);
+                       }
+               } else if (cm.isBean() && !(val instanceof Map) && node 
instanceof MarshalledMap nodeMap) {
+                       injectAnnotations(nodeMap, val);
                }
        }
 
+       private static Iterable<?> toIterable(Object val) {
+               if (val instanceof Collection<?> c)
+                       return c;
+               if (val instanceof Object[] a)
+                       return Arrays.asList(a);
+               return List.of();
+       }
+
        private static Object getBeanValueSafely(BeanMap<?> bm, String key) {
                try {
                        return bm.get(key);
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
index 5263d2d23c..6df44d9bff 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/hocon/HoconParserSession.java
@@ -512,16 +512,54 @@ public class HoconParserSession extends 
ReaderParserSession implements RecordRea
                        var cm = (ClassMeta<?>) pm.getBeanInfo();
                        if (cm.getNameProperty() != null)
                                setName(cm, val, key);
-                       if (cm.getParentProperty() != null)
-                               setParent(cm, val, bean);
-                       if (cm.isMap() && val instanceof Map<?,?> val2 && 
!cm.getValueType().isObject() && cm.getValueType().getNameProperty() != null) {
-                               var valueType = cm.getValueType();
-                               for (Map.Entry<?,?> e : ((Map<?,?>) 
val2).entrySet())
-                                       setName(valueType, e.getValue(), 
e.getKey());
+                       injectParentAnnotations(cm, val, entry.getValue(), 
bean);
+               }
+       }
+
+       /*
+        * Recursively injects @ParentProperty (and @NameProperty on map values 
/ nested beans) into val.
+        * Intermediate collections and maps are transparent: a collection/map 
element's parent is
+        * parentBean (the nearest enclosing bean), skipping all intermediate 
containers.  This keeps HOCON
+        * consistent with the streaming parsers under TODO-291 (Option A).
+        */
+       private void injectParentAnnotations(ClassMeta<?> cm, Object val, 
Object node, Object parentBean) throws ExecutableException {
+               if (val == null || cm == null)
+                       return;
+               if (cm.getParentProperty() != null)
+                       setParent(cm, val, parentBean);
+               if (cm.isCollectionOrArray()) {
+                       var et = cm.getElementType();
+                       if (et == null || et.isObject())
+                               return;
+                       var nodeList = node instanceof List<?> nl ? nl : null;
+                       var i = 0;
+                       for (var element : toIterable(val)) {
+                               injectParentAnnotations(et, element, nodeList 
!= null && i < nodeList.size() ? nodeList.get(i) : null, parentBean);
+                               i++;
                        }
+               } else if (cm.isMap() && val instanceof Map<?,?> valMap) {
+                       var vt = cm.getValueType();
+                       if (vt == null || vt.isObject())
+                               return;
+                       var nodeMap = node instanceof Map<?,?> nm ? nm : null;
+                       for (Map.Entry<?,?> e : valMap.entrySet()) {
+                               if (vt.getNameProperty() != null)
+                                       setName(vt, e.getValue(), e.getKey());
+                               injectParentAnnotations(vt, e.getValue(), 
nodeMap != null ? nodeMap.get(e.getKey()) : null, parentBean);
+                       }
+               } else if (cm.isBean() && !(val instanceof Map) && node 
instanceof MarshalledMap nodeMap) {
+                       injectAnnotations(nodeMap, val);
                }
        }
 
+       private static Iterable<?> toIterable(Object val) {
+               if (val instanceof Collection<?> c)
+                       return c;
+               if (val instanceof Object[] a)
+                       return Arrays.asList(a);
+               return List.of();
+       }
+
        private static Object getBeanValueSafely(BeanMap<?> bm, String key) {
                try {
                        return bm.get(key);
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/html/HtmlParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/html/HtmlParserSession.java
index 1e72774208..1201d82e69 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/html/HtmlParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/html/HtmlParserSession.java
@@ -394,8 +394,8 @@ public class HtmlParserSession extends XmlParserSession {
                if (nn(swap) && nn(o))
                        o = unswap(swap, o, eType);
 
-               if (nn(outer))
-                       setParent(eType, o, outer);
+               if (nn(parentBean()))
+                       setParent(eType, o, parentBean());
 
                skipWs(r);
                return (T)o;
@@ -410,39 +410,44 @@ public class HtmlParserSession extends XmlParserSession {
                "java:S3776" // Cognitive complexity acceptable for HTML bean 
parsing
        })
        private <T> BeanMap<T> readIntoBean(XmlReader r, BeanMap<T> m) throws 
IOException, ParseException, ExecutableException, XMLStreamException {
-               while (true) {
-                       HtmlTag tag = nextTag(r, TR, X_TABLE);
-                       if (tag == X_TABLE)
-                               break;
-                       tag = nextTag(r, TD, TH);
-                       // Skip over the column headers.
-                       if (tag == TH) {
-                               skipTag(r);
-                               r.nextTag();
-                               skipTag(r);
-                       } else {
-                               String key = getElementText(r);
-                               nextTag(r, TD);
-                               var pMeta = m.getPropertyMeta(key);
-                               if (pMeta == null) {
-                                       onUnknownProperty(key, m, 
readAnything(object(), r, null, false, null));
+               var pb = swapParentBean(m.getBean(false));
+               try {
+                       while (true) {
+                               HtmlTag tag = nextTag(r, TR, X_TABLE);
+                               if (tag == X_TABLE)
+                                       break;
+                               tag = nextTag(r, TD, TH);
+                               // Skip over the column headers.
+                               if (tag == TH) {
+                                       skipTag(r);
+                                       r.nextTag();
+                                       skipTag(r);
                                } else {
-                                       var cm = (ClassMeta<?>) 
pMeta.getBeanInfo();
-                                       Object value = readAnything(cm, r, 
m.getBean(false), false, pMeta);
-                                       setName(cm, value, key);
-                                       try {
-                                               pMeta.set(m, key, value);
-                                       } catch (BeanRuntimeException e) {
-                                               onBeanSetterException(pMeta, e);
-                                               throw e;
+                                       String key = getElementText(r);
+                                       nextTag(r, TD);
+                                       var pMeta = m.getPropertyMeta(key);
+                                       if (pMeta == null) {
+                                               onUnknownProperty(key, m, 
readAnything(object(), r, null, false, null));
+                                       } else {
+                                               var cm = (ClassMeta<?>) 
pMeta.getBeanInfo();
+                                               Object value = readAnything(cm, 
r, m.getBean(false), false, pMeta);
+                                               setName(cm, value, key);
+                                               try {
+                                                       pMeta.set(m, key, 
value);
+                                               } catch (BeanRuntimeException 
e) {
+                                                       
onBeanSetterException(pMeta, e);
+                                                       throw e;
+                                               }
                                        }
                                }
+                               HtmlTag t = nextTag(r, X_TD, X_TR);
+                               if (t == X_TD)
+                                       nextTag(r, X_TR);
                        }
-                       HtmlTag t = nextTag(r, X_TD, X_TR);
-                       if (t == X_TD)
-                               nextTag(r, X_TR);
+                       return m;
+               } finally {
+                       swapParentBean(pb);
                }
-               return m;
        }
 
        /*
@@ -550,25 +555,30 @@ public class HtmlParserSession extends XmlParserSession {
                                        : newBeanMap(l, elementType.inner())
                                ;
                                // @formatter:on
-                               for (var key : keys) {
-                                       tag = nextTag(r, X_TD, TD, NULL);
-                                       if (tag == X_TD)
-                                               tag = nextTag(r, TD, NULL);
-                                       if (tag == NULL) {
-                                               m = null;
-                                               nextTag(r, X_NULL);
-                                               break;
-                                       }
-                                       BeanMapEntry e = m.getProperty(key);
-                                       if (e == null) {
-                                               readAnything(object(), r, l, 
false, null);
-                                       } else {
-                                               BeanPropertyMeta bpm = 
e.getMeta();
-                                               var cm = (ClassMeta<?>) 
bpm.getBeanInfo();
-                                               Object value = readAnything(cm, 
r, m.getBean(false), false, bpm);
-                                               setName(cm, value, key);
-                                               bpm.set(m, key, value);
+                               var pb = swapParentBean(m.getBean(false));
+                               try {
+                                       for (var key : keys) {
+                                               tag = nextTag(r, X_TD, TD, 
NULL);
+                                               if (tag == X_TD)
+                                                       tag = nextTag(r, TD, 
NULL);
+                                               if (tag == NULL) {
+                                                       m = null;
+                                                       nextTag(r, X_NULL);
+                                                       break;
+                                               }
+                                               BeanMapEntry e = 
m.getProperty(key);
+                                               if (e == null) {
+                                                       readAnything(object(), 
r, l, false, null);
+                                               } else {
+                                                       BeanPropertyMeta bpm = 
e.getMeta();
+                                                       var cm = (ClassMeta<?>) 
bpm.getBeanInfo();
+                                                       Object value = 
readAnything(cm, r, m.getBean(false), false, bpm);
+                                                       setName(cm, value, key);
+                                                       bpm.set(m, key, value);
+                                               }
                                        }
+                               } finally {
+                                       swapParentBean(pb);
                                }
                                E element;
                                if (m == null) {
@@ -578,6 +588,8 @@ public class HtmlParserSession extends XmlParserSession {
                                } else {
                                        element = (E)m.getBean();
                                }
+                               if (nn(parentBean()) && nn(element))
+                                       setParent(elementType, element, 
parentBean());
                                l.add(element);
                        } else {
                                String c = 
getAttributes(r).get(getBeanTypePropertyName(type.getElementType()));
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json/JsonParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json/JsonParserSession.java
index 948104ef38..480fb7c1ff 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json/JsonParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/json/JsonParserSession.java
@@ -276,8 +276,8 @@ public class JsonParserSession extends ReaderParserSession 
implements TokenReada
                if (nn(swap) && nn(o))
                        o = unswap(swap, o, eType);
 
-               if (nn(outer))
-                       setParent(eType, o, outer);
+               if (nn(parentBean()))
+                       setParent(eType, o, parentBean());
 
                return (T)o;
        }
@@ -328,6 +328,7 @@ public class JsonParserSession extends ReaderParserSession 
implements TokenReada
                var state = S1;
                var currAttr = "";
                int c = 0;
+               var pb = swapParentBean(m.getBean(false));
                mark();
                try {
                        while (c != -1) {
@@ -402,6 +403,7 @@ public class JsonParserSession extends ReaderParserSession 
implements TokenReada
                                throw new ParseException(this, "Could not find 
'}' marking end of JSON object.");
                } finally {
                        unmark();
+                       swapParentBean(pb);
                }
 
                return null; // Unreachable.
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
index 5e3940c2f5..835a6506a8 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
@@ -253,25 +253,30 @@ public class MsgPackParserSession extends 
InputStreamParserSession implements To
                        } else if (nn(builder) || 
sType.canCreateNewBean(outer)) {
                                if (dt == MAP) {
                                        BeanMap m = builder == null ? 
newBeanMap(outer, sType.inner()) : toBeanMap(builder.create(this, eType));
-                                       for (var i = 0; i < length; i++) {
-                                               String pName = 
readAnything(string(), is, m.getBean(false), null);
-                                               var bpm = 
m.getPropertyMeta(pName);
-                                               if (bpm == null) {
-                                                       if 
(pName.equals(getBeanTypePropertyName(eType)))
-                                                               
readAnything(string(), is, null, null);
-                                                       else
-                                                               
onUnknownProperty(pName, m, readAnything(string(), is, null, null));
-                                               } else {
-                                                       var cm = (ClassMeta<?>) 
bpm.getBeanInfo();
-                                                       Object value = 
readAnything(cm, is, m.getBean(false), bpm);
-                                                       setName(cm, value, 
pName);
-                                                       try {
-                                                               bpm.set(m, 
pName, value);
-                                                       } catch 
(BeanRuntimeException e) {
-                                                               
onBeanSetterException(pMeta, e);
-                                                               throw e;
+                                       var pb = 
swapParentBean(m.getBean(false));
+                                       try {
+                                               for (var i = 0; i < length; 
i++) {
+                                                       String pName = 
readAnything(string(), is, m.getBean(false), null);
+                                                       var bpm = 
m.getPropertyMeta(pName);
+                                                       if (bpm == null) {
+                                                               if 
(pName.equals(getBeanTypePropertyName(eType)))
+                                                                       
readAnything(string(), is, null, null);
+                                                               else
+                                                                       
onUnknownProperty(pName, m, readAnything(string(), is, null, null));
+                                                       } else {
+                                                               var cm = 
(ClassMeta<?>) bpm.getBeanInfo();
+                                                               Object value = 
readAnything(cm, is, m.getBean(false), bpm);
+                                                               setName(cm, 
value, pName);
+                                                               try {
+                                                                       
bpm.set(m, pName, value);
+                                                               } catch 
(BeanRuntimeException e) {
+                                                                       
onBeanSetterException(pMeta, e);
+                                                                       throw e;
+                                                               }
                                                        }
                                                }
+                                       } finally {
+                                               swapParentBean(pb);
                                        }
                                        o = builder == null ? m.getBean() : 
builder.build(this, m.getBean(), eType);
                                } else {
@@ -353,8 +358,8 @@ public class MsgPackParserSession extends 
InputStreamParserSession implements To
                if (nn(swap) && nn(o))
                        o = unswap(swap, o, eType);
 
-               if (nn(outer))
-                       setParent(eType, o, outer);
+               if (nn(parentBean()))
+                       setParent(eType, o, parentBean());
 
                return (T)o;
        }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
index b9808aca03..cde9b0ddf8 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserSession.java
@@ -279,6 +279,37 @@ public class ParserSession extends MarshallingSession {
                        m.set(o, parent);
        }
 
+       /**
+        * Returns the nearest enclosing bean used as the parent for {@link 
ParentProperty @ParentProperty} injection.
+        *
+        * <p>
+        * Unlike {@link #getOuter()} (which also serves as the instantiation 
context for non-static member/inner classes),
+        * this value tracks only the nearest enclosing <i>bean</i> as the 
object graph is parsed, deliberately skipping any
+        * intermediate collections/maps/arrays.  It is {@code null} at the 
document root (no enclosing bean), which means a
+        * bean parsed at the root or directly inside a top-level collection 
has no parent injected.
+        *
+        * @return The nearest enclosing bean, or {@code null} if there is none.
+        */
+       protected final Object parentBean() {
+               return parentBean;
+       }
+
+       /**
+        * Sets the nearest-enclosing-bean tracker used for {@link 
ParentProperty @ParentProperty} injection.
+        *
+        * <p>
+        * Callers should invoke this when they begin populating a bean's 
properties (passing that bean), then pass the
+        * returned previous value back to this method once done, restoring the 
tracker stack-style.
+        *
+        * @param value The new nearest enclosing bean (typically the bean 
currently being populated).
+        * @return The previous value, to be passed back to this method to 
restore the tracker.
+        */
+       protected final Object swapParentBean(Object value) {
+               var old = parentBean;
+               parentBean = value;
+               return old;
+       }
+
        private final HttpPartSchema schema;
        private final Method javaMethod;
        private final Object outer;
@@ -289,6 +320,7 @@ public class ParserSession extends MarshallingSession {
        private final Deque<StringBuilder> sbStack;
        private BeanPropertyMeta currentProperty;
        private ClassMeta<?> currentClass;
+       private Object parentBean;
        private Position mark = new Position(-1);
        private ParserPipe pipe;
 
@@ -303,6 +335,7 @@ public class ParserSession extends MarshallingSession {
                ctx = builder.ctx;
                javaMethod = builder.javaMethod;
                outer = builder.outer;
+               parentBean = builder.outer;
                schema = builder.schema;
                trimStrings = builder.trimStrings;
                nulls = builder.nulls == null ? Nulls.NOT_SET : builder.nulls;
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/uon/UonParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/uon/UonParserSession.java
index 918b8e9678..40db9ea1d9 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/uon/UonParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/uon/UonParserSession.java
@@ -422,8 +422,8 @@ public class UonParserSession extends ReaderParserSession 
implements HttpPartPar
                if (nn(swap) && nn(o))
                        o = unswap(swap, o, eType);
 
-               if (nn(outer))
-                       setParent(eType, o, outer);
+               if (nn(parentBean()))
+                       setParent(eType, o, parentBean());
 
                return (T)o;
        }
@@ -465,6 +465,7 @@ public class UonParserSession extends ReaderParserSession 
implements HttpPartPar
 
                var state = S1;
                var currAttr = "";
+               var pb = swapParentBean(m.getBean(false));
                mark();
                try {
                        while (true) {
@@ -548,6 +549,7 @@ public class UonParserSession extends ReaderParserSession 
implements HttpPartPar
                        }
                } finally {
                        unmark();
+                       swapParentBean(pb);
                }
        }
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/urlencoding/UrlEncodingParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/urlencoding/UrlEncodingParserSession.java
index e3098161f9..88ac48c94e 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/urlencoding/UrlEncodingParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/urlencoding/UrlEncodingParserSession.java
@@ -224,8 +224,8 @@ public class UrlEncodingParserSession extends 
UonParserSession {
                if (nn(swap) && nn(o))
                        o = unswap(swap, o, eType);
 
-               if (nn(outer))
-                       setParent(eType, o, outer);
+               if (nn(parentBean()))
+                       setParent(eType, o, parentBean());
 
                return (T)o;
        }
@@ -254,6 +254,7 @@ public class UrlEncodingParserSession extends 
UonParserSession {
 
                var state = S1;
                var currAttr = "";
+               var pb = swapParentBean(m.getBean(false));
                mark();
                try {
                        while (c != -1) {
@@ -365,6 +366,7 @@ public class UrlEncodingParserSession extends 
UonParserSession {
                                throw new ParseException(this, "Could not find 
end of object.");
                } finally {
                        unmark();
+                       swapParentBean(pb);
                }
 
                return null; // Unreachable.
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlParserSession.java
index fd726d4d33..3f23256afb 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/xml/XmlParserSession.java
@@ -969,13 +969,23 @@ public class XmlParserSession extends ReaderParserSession 
implements RecordReada
                                var m = nn(builder) ? 
toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.inner());
                                var bpm = 
getXmlBeanMeta(m.getMeta()).getPropertyMeta(fieldName);
                                var cm = (ClassMeta<?>) 
m.getMeta().getBeanInfo();
-                               Object value = readAnything(cm, currAttr, r, 
m.getBean(false), false, null);
-                               setName(cm, value, currAttr);
-                               bpm.set(m, currAttr, value);
+                               var pb = swapParentBean(m.getBean(false));
+                               try {
+                                       Object value = readAnything(cm, 
currAttr, r, m.getBean(false), false, null);
+                                       setName(cm, value, currAttr);
+                                       bpm.set(m, currAttr, value);
+                               } finally {
+                                       swapParentBean(pb);
+                               }
                                o = nn(builder) ? builder.build(this, 
m.getBean(), eType) : m.getBean();
                        } else {
                                var m = nn(builder) ? 
toBeanMap(builder.create(this, eType)) : newBeanMap(outer, sType.inner());
-                               m = readIntoBean(r, m, isNil);
+                               var pb = swapParentBean(m.getBean(false));
+                               try {
+                                       m = readIntoBean(r, m, isNil);
+                               } finally {
+                                       swapParentBean(pb);
+                               }
                                o = nn(builder) ? builder.build(this, 
m.getBean(), eType) : m.getBean();
                        }
                } else if (sType.isMap()) {
@@ -1026,8 +1036,8 @@ public class XmlParserSession extends ReaderParserSession 
implements RecordReada
                if (nn(swap) && nn(o))
                        o = unswap(swap, o, eType);
 
-               if (nn(outer))
-                       setParent(eType, o, outer);
+               if (nn(parentBean()))
+                       setParent(eType, o, parentBean());
 
                return (T)o;
        }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlParserSession.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlParserSession.java
index 047dd9e49a..330d188dd3 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlParserSession.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlParserSession.java
@@ -312,8 +312,8 @@ public class YamlParserSession extends ReaderParserSession 
implements RecordRead
                if (nn(swap) && nn(o))
                        o = unswap(swap, o, eType);
 
-               if (nn(outer))
-                       setParent(eType, o, outer);
+               if (nn(parentBean()))
+                       setParent(eType, o, parentBean());
 
                return (T)o;
        }
@@ -488,6 +488,8 @@ public class YamlParserSession extends ReaderParserSession 
implements RecordRead
        }
 
        private <T> void readBeanProperty(ParserReader r, BeanMap<T> m, String 
currAttr) throws IOException, ParseException, ExecutableException {
+               var pb = swapParentBean(m.getBean(false));
+               try {
                var pm = m.getPropertyMeta(currAttr);
                setCurrentProperty(pm);
                if (pm == null) {
@@ -503,6 +505,9 @@ public class YamlParserSession extends ReaderParserSession 
implements RecordRead
                                throw e;
                        }
                }
+               } finally {
+                       swapParentBean(pb);
+               }
                setCurrentProperty(null);
        }
 
@@ -868,6 +873,7 @@ public class YamlParserSession extends ReaderParserSession 
implements RecordRead
                var state = S1;
                var currAttr = "";
                int c = 0;
+               var pb = swapParentBean(m.getBean(false));
                mark();
                try {
                        while (c != -1) {
@@ -941,6 +947,7 @@ public class YamlParserSession extends ReaderParserSession 
implements RecordRead
                                throw new ParseException(this, "Could not find 
'}' marking end of YAML flow mapping.");
                } finally {
                        unmark();
+                       swapParentBean(pb);
                }
 
                return null; // Unreachable.
@@ -953,6 +960,7 @@ public class YamlParserSession extends ReaderParserSession 
implements RecordRead
 
                int blockIndent = -1;
 
+               var pb = swapParentBean(m.getBean(false));
                mark();
                try {
                        while (true) {
@@ -1018,6 +1026,7 @@ public class YamlParserSession extends 
ReaderParserSession implements RecordRead
                        }
                } finally {
                        unmark();
+                       swapParentBean(pb);
                }
 
                return m;
diff --git 
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
new file mode 100644
index 0000000000..623c64be16
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParentPropertyContainer_Test.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.marshall.*;
+import org.apache.juneau.marshall.bson.*;
+import org.apache.juneau.marshall.cbor.*;
+import org.apache.juneau.marshall.hjson.*;
+import org.apache.juneau.marshall.hocon.*;
+import org.apache.juneau.marshall.html.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.msgpack.*;
+import org.apache.juneau.marshall.parser.*;
+import org.apache.juneau.marshall.serializer.*;
+import org.apache.juneau.marshall.uon.*;
+import org.apache.juneau.marshall.urlencoding.*;
+import org.apache.juneau.marshall.xml.*;
+import org.apache.juneau.marshall.yaml.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Regression tests for TODO-291 / FINISHED-291.
+ *
+ * <p>
+ * Verifies that a bean annotated with {@link ParentProperty} that is nested 
inside a collection or
+ * map (or nested containers) receives its <b>nearest enclosing bean</b> as 
its parent when parsed,
+ * skipping all intermediate containers.  This is the canonical {@code 
AddressBook}/{@code List<Person>}
+ * case that previously failed to round-trip because the parser injected the 
containing {@code List}
+ * (not the grandparent bean) as the parent.
+ */
+class ParentPropertyContainer_Test extends TestBase {
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Test beans.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       public static class AddressBook {
+               public List<Person> people = new ArrayList<>();
+               public List<List<Person>> groups = new ArrayList<>();
+               public Map<String,Person> byName = new LinkedHashMap<>();
+               public Map<String,List<Person>> byCity = new LinkedHashMap<>();
+               public Set<Person> members = new LinkedHashSet<>();
+       }
+
+       public static class Person {
+               public String name;
+
+               @ParentProperty
+               public AddressBook addressBook;
+       }
+
+       static AddressBook newAddressBook() {
+               var ab = new AddressBook();
+               ab.people.add(person("p1"));
+               ab.people.add(person("p2"));
+               ab.groups.add(new ArrayList<>(List.of(person("g1"), 
person("g2"))));
+               ab.byName.put("k1", person("m1"));
+               ab.byCity.put("NYC", new ArrayList<>(List.of(person("c1"))));
+               ab.members.add(person("s1"));
+               return ab;
+       }
+
+       static Person person(String name) {
+               var p = new Person();
+               p.name = name;
+               return p;
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Format matrix.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       record Fmt(String name, Serializer s, Parser p) {
+               @Override public String toString() { return name; }
+       }
+
+       static Stream<Fmt> formats() {
+               return Stream.of(
+                       new Fmt("Json", JsonSerializer.DEFAULT, 
JsonParser.DEFAULT),
+                       new Fmt("Xml", XmlSerializer.DEFAULT, 
XmlParser.DEFAULT),
+                       new Fmt("Html", HtmlSerializer.DEFAULT, 
HtmlParser.DEFAULT),
+                       new Fmt("Uon", UonSerializer.DEFAULT, 
UonParser.DEFAULT),
+                       new Fmt("UrlEncoding", UrlEncodingSerializer.DEFAULT, 
UrlEncodingParser.DEFAULT),
+                       new Fmt("Yaml", YamlSerializer.DEFAULT, 
YamlParser.DEFAULT),
+                       new Fmt("MsgPack", MsgPackSerializer.DEFAULT, 
MsgPackParser.DEFAULT),
+                       new Fmt("Cbor", CborSerializer.DEFAULT, 
CborParser.DEFAULT),
+                       new Fmt("Bson", BsonSerializer.DEFAULT, 
BsonParser.DEFAULT),
+                       new Fmt("Hocon", HoconSerializer.DEFAULT, 
HoconParser.DEFAULT),
+                       new Fmt("Hjson", HjsonSerializer.DEFAULT, 
HjsonParser.DEFAULT)
+               );
+       }
+
+       static Object write(Serializer s, Object o) throws Exception {
+               if (s instanceof WriterSerializer ws)
+                       return ws.write(o);
+               return ((OutputStreamSerializer)s).write(o);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Tests.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest
+       @MethodSource("formats")
+       void a01_listElementParentIsEnclosingBean(Fmt f) throws Exception {
+               var ab = newAddressBook();
+               var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+               assertEquals(2, ab2.people.size(), f.name);
+               for (var p : ab2.people)
+                       assertSame(ab2, p.addressBook, () -> f.name + ": 
List<Person> element parent should be the enclosing AddressBook");
+       }
+
+       @ParameterizedTest
+       @MethodSource("formats")
+       void a02_nestedListElementParentSkipsAllContainers(Fmt f) throws 
Exception {
+               var ab = newAddressBook();
+               var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+               assertEquals(1, ab2.groups.size(), f.name);
+               var group = ab2.groups.get(0);
+               assertEquals(2, group.size(), f.name);
+               for (var p : group)
+                       assertSame(ab2, p.addressBook, () -> f.name + ": 
List<List<Person>> element parent should skip both lists");
+       }
+
+       @ParameterizedTest
+       @MethodSource("formats")
+       void a03_mapValueParentIsEnclosingBean(Fmt f) throws Exception {
+               var ab = newAddressBook();
+               var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+               assertFalse(ab2.byName.isEmpty(), f.name);
+               for (var p : ab2.byName.values())
+                       assertSame(ab2, p.addressBook, () -> f.name + ": 
Map<String,Person> value parent should be the enclosing AddressBook");
+       }
+
+       @ParameterizedTest
+       @MethodSource("formats")
+       void a04_mapOfListElementParentSkipsContainers(Fmt f) throws Exception {
+               var ab = newAddressBook();
+               var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+               assertFalse(ab2.byCity.isEmpty(), f.name);
+               for (var l : ab2.byCity.values())
+                       for (var p : l)
+                               assertSame(ab2, p.addressBook, () -> f.name + 
": Map<String,List<Person>> element parent should skip map and list");
+       }
+
+       @ParameterizedTest
+       @MethodSource("formats")
+       void a05_setElementParentIsEnclosingBean(Fmt f) throws Exception {
+               var ab = newAddressBook();
+               var ab2 = f.p.read(write(f.s, ab), AddressBook.class);
+
+               assertFalse(ab2.members.isEmpty(), f.name);
+               for (var p : ab2.members)
+                       assertSame(ab2, p.addressBook, () -> f.name + ": 
Set<Person> element parent should be the enclosing AddressBook");
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Top-level collection - parent should be null (no enclosing bean), 
not throw.
+       //
+       // Note: HOCON is excluded from the root-level-array cases.  HOCON is a 
config-file format whose document root
+       // must be an object (braceless key/value pairs), so a top-level array 
is a parser limitation unrelated to
+       // @ParentProperty.  HOCON's element-parent and null-parent semantics 
are still exercised by a01-a05.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       static Stream<Fmt> readerFormatsNoHocon() {
+               return formats().filter(f -> f.s instanceof WriterSerializer && 
f.p instanceof ReaderParser && ! "Hocon".equals(f.name));
+       }
+
+       static Stream<Fmt> formatsNoHocon() {
+               return formats().filter(f -> ! "Hocon".equals(f.name));
+       }
+
+       @ParameterizedTest
+       @MethodSource("readerFormatsNoHocon")
+       void b01_topLevelListElementHasNullParent(Fmt f) throws Exception {
+               var l = new ArrayList<Person>(List.of(person("x"), 
person("y")));
+               var out = write(f.s, l);
+               @SuppressWarnings("unchecked")
+               List<Person> l2 = (List<Person>) f.p.read((String) out, 
List.class, Person.class);
+
+               assertEquals(2, l2.size(), f.name);
+               for (var p : l2)
+                       assertNull(p.addressBook, () -> f.name + ": top-level 
list element should have a null parent (no enclosing bean)");
+       }
+
+       @ParameterizedTest
+       @MethodSource("formatsNoHocon")
+       void b02_topLevelListBinaryHasNullParent(Fmt f) throws Exception {
+               // Binary/stream formats: round-trip via byte[] / InputStream.
+               var l = new ArrayList<Person>(List.of(person("x")));
+               Object out = write(f.s, l);
+               @SuppressWarnings("unchecked")
+               List<Person> l2 = out instanceof byte[] b
+                       ? (List<Person>) f.p.read(new ByteArrayInputStream(b), 
List.class, Person.class)
+                       : (List<Person>) f.p.read((String) out, List.class, 
Person.class);
+
+               assertEquals(1, l2.size(), f.name);
+               assertNull(l2.get(0).addressBook, () -> f.name + ": top-level 
list element should have a null parent");
+       }
+}


Reply via email to