This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 6737f3fcc6 Document @ParentProperty collection/map enclosing-bean
behavior change (TODO-291)
6737f3fcc6 is described below
commit 6737f3fcc65b247c4d18d05692a1319811b81947
Author: James Bognar <[email protected]>
AuthorDate: Fri Jul 24 09:08:17 2026 -0400
Document @ParentProperty collection/map enclosing-bean behavior change
(TODO-291)
- 10.0.0 release note: breaking-change entry + migration guidance
- 03.03.08.ParentPropertyAnnotation.md: new "Enclosing Bean Through
Collections
and Maps" section + round-trip example
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/10.0.0.md | 1 +
pages/topics/03.03.08.ParentPropertyAnnotation.md | 34 +++++++++++++++++++++++
2 files changed, 35 insertions(+)
diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index 54b46b20f6..d30205b9a2 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -780,6 +780,7 @@ _TBD — to be filled in as development continues._
- **`Schema.exclusiveMaximum()` / `Schema.exclusiveMinimum()` boolean
annotation members removed (TODO-238).** These `@Deprecated(forRemoval)` legacy
Swagger 2.0/OpenAPI 3.0/JSON Schema Draft 04 boolean forms on
`org.apache.juneau.commons.Schema` are gone. **Migration:** use the existing
short-form boolean aliases `emax()` / `emin()` instead (unchanged); for JSON
Schema Draft 2020-12 numeric semantics, `exclusiveMaximumValue()` /
`exclusiveMinimumValue()` (also unchanged) remain availabl [...]
- **`HttpPartSchema.T_OBJECT_CSV` / `T_OBJECT_PIPES` / `T_OBJECT_SSV` /
`T_OBJECT_TSV` constants removed, along with their sole-purpose builder methods
`tObjectCsv()` / `tObjectPipes()` / `tObjectSsv()` / `tObjectTsv()`
(TODO-238).** A `collectionFormat` on an `object`-type part is not a valid
OpenAPI 2.0 concept (`collectionFormat` is defined only for `array` types), and
these presets had zero usage in the framework. The array-type equivalents
(`T_ARRAY_CSV`, `T_ARRAY_PIPES`, `T_ARRAY_S [...]
- **`XmlSerializer.DEFAULT_XS_NAMESPACE` removed (TODO-238).** This unused
`protected` namespace constant (`xs` → `http://www.w3.org/2001/XMLSchema`) had
no consumers anywhere in the framework. `DEFAULT_JUNEAU_NAMESPACE` is
unaffected. **Migration:** none expected for normal usage; any external
subclass that referenced the constant should declare its own namespace via
`Namespace.of("xs", "http://www.w3.org/2001/XMLSchema")`.
+- **`@ParentProperty` through collections/maps now injects the enclosing bean
(behavioral change) (TODO-291).** Previously, when a bean annotated with
`@ParentProperty` was nested inside a `List`, `Set`, array, or `Map` — rather
than being a direct bean property — the parser injected the
immediately-containing container (e.g. the `JsonList`/`JsonMap`) into the
`@ParentProperty` member. As of 10.0.0, the parser instead injects the nearest
enclosing *bean*, skipping all intermediate contai [...]
_Other entries TBD — to be filled in before release. See also the major
version bump note above._
diff --git a/pages/topics/03.03.08.ParentPropertyAnnotation.md
b/pages/topics/03.03.08.ParentPropertyAnnotation.md
index 92ffb3a61b..170a72dca3 100644
--- a/pages/topics/03.03.08.ParentPropertyAnnotation.md
+++ b/pages/topics/03.03.08.ParentPropertyAnnotation.md
@@ -65,6 +65,12 @@ public class Person {
- This allows child objects to navigate back to their parent if needed
- Useful for bidirectional relationships where child objects need access to
their parent context
+## Enclosing Bean Through Collections and Maps
+
+In the example above, each `Person` is not a direct property of `AddressBook`
— it's an element of the `List<Person> people` field. When resolving a
`@ParentProperty` member, the parser skips over any intervening `List`, `Set`,
array, and `Map` containers, regardless of nesting depth, and injects the
**nearest enclosing bean** instead. So `Person.addressBook` is set to the
`AddressBook`, not to the `List` that directly contains it.
+
+A bean that has no enclosing bean — for example, one parsed at the document
root, or one that's an element of a top-level collection passed directly to the
parser — has nothing to inject, so its `@ParentProperty` member is simply left
`null`.
+
## Cyclic Graphs and Serialization
When a `@ParentProperty` back-reference is also a normally-visible bean
property (e.g. a `public` field or getter, as in the field example above), it
forms a parent/child **cycle**. Unlike Jackson's `@JsonBackReference`, Juneau
does **not** auto-omit the `@ParentProperty` member on the write (serialize)
side — the annotation is a parse-time convenience only. This is intentional.
@@ -74,3 +80,31 @@ The serialization behavior of such a cyclic graph therefore
depends on the seria
- **Default configuration** (`detectRecursions=false`,
`ignoreRecursions=false`): the traversal is silently truncated at <a
href="/site/apidocs/org/apache/juneau/marshall/MarshallingTraverseContext.Builder.html#maxDepth(int)"
target="_blank">maxDepth</a> (default `100`), producing finite but
semantically-incomplete output. No exception is thrown — `maxDepth` is a size
guard, not a cycle detector.
- <a
href="/site/apidocs/org/apache/juneau/marshall/MarshallingTraverseContext.Builder.html#detectRecursions()"
target="_blank">detectRecursions()</a>: fail-fast on the cycle with a
`SerializeException` ("Recursion occurred, stack=...").
- <a
href="/site/apidocs/org/apache/juneau/marshall/MarshallingTraverseContext.Builder.html#ignoreRecursions()"
target="_blank">ignoreRecursions()</a>: omit the repeated node so the output
round-trips cleanly — parsing re-injects the parent via `@ParentProperty`.
+
+Using the `AddressBook`/`Person` classes from the field example above,
`ignoreRecursions()` round-trips cleanly end to end:
+
+```java
+AddressBook book = new AddressBook();
+book.people = new ArrayList<>();
+
+Person person = new Person();
+person.name = "John Smith";
+person.sex = 'M';
+book.people.add(person);
+
+// Clone an existing serializer and enable ignoring of recursions.
+WriterSerializer serializer = Json5Serializer
+ .DEFAULT_READABLE
+ .copy()
+ .ignoreRecursions()
+ .build();
+
+// Serialize to JSON. The Person.addressBook back-reference is omitted to
break the cycle.
+String json = serializer.write(book);
+
+// Parse it back. The parser re-injects the enclosing AddressBook into each
Person.
+AddressBook parsedBook = JsonParser.DEFAULT.read(json, AddressBook.class);
+Person parsedPerson = parsedBook.people.get(0);
+
+assert parsedPerson.addressBook == parsedBook; // Restored, even though
person is nested inside a List
+```