Hi Petr,

since you already have an idea what the problem is and how to fix it, feel free to open a PR with Tests that verify the problem and fix. There are many FXML Tests already which should help to catch regressions as well. More information how to contibute are here: https://github.com/openjdk/jfx/blob/master/CONTRIBUTING.md
Please note the AI Policy: https://openjdk.org/legal/ai

If something does not work for you, feel free to ask here!

-- Marius

Am 12.05.2026 um 16:37 schrieb [email protected]:
Hello,

I would like to report a bug in FXMLLoader that affects any Node subclass
whose
constructor carries a @NamedArg annotation.


PROBLEM
=======

When a class has a @NamedArg-annotated constructor, JavaFXBuilderFactory
returns a
ProxyBuilder for it. Due to how ProxyBuilder intercepts containsKey() and
get(), the
FXML <properties> child element — which the specification explicitly
documents as a
standard read-only map property — fails to load with one of the following
exceptions
depending on the syntax used:

   Nested element (<properties><myKey>...</myKey></properties>):
     UnsupportedOperationException: Cannot determine type for property.

   Attribute (<properties myKey="$ref"/>):
     PropertyNotFoundException: Property "myKey" does not exist or is read-
only.

The same FXML works correctly when the class does NOT have a @NamedArg
constructor.
Both the @NamedArg value and the <properties> entry are valid in isolation;
the failure
only occurs when they are combined.


MINIMAL REPRODUCER
==================

A self-contained Maven project is attached (javafx_bug_report.tar.gz).

Requirements: JDK 21+, JavaFX 25 (fetched automatically by Maven), Maven 3.9
+

To reproduce:

     tar xzf javafx_bug_report.tar.gz
     cd javafx_bug_report
     mvn compile exec:exec

Expected output:

     Bug: ProxyBuilder breaks <properties> for @NamedArg classes
     =============================================================
     SimpleButton — no @NamedArg                   (expected: PASS)
     CustomButton — @NamedArg("label") constructor  (expected: FAIL)

     [ Nested   <properties><tag>...</tag></properties> ]
       FXML load threw LoadException: ...
         Caused by UnsupportedOperationException: Cannot determine type for
property.

     [ Attribute <properties tag="$ref"/> ]
       FXML load threw LoadException: ...
         Caused by PropertyNotFoundException: Property "tag" does not exist
or is read-only.

     =============================================================
     Nested element approach : FAILED  <-- BUG
     Attribute approach      : FAILED  <-- BUG
     =============================================================

The project loads two buttons from each FXML:
   SimpleButton — no @NamedArg constructor, passes for both syntaxes
   CustomButton — @NamedArg("label") constructor present, fails for both
syntaxes

Both the @NamedArg value (label="Test Label") and the <properties> entry are
valid in
isolation; the failure only occurs when they are combined.


ROOT CAUSE
==========

ProxyBuilder overrides containsKey() and get() via getTemporaryContainer(),
which
calls getReadOnlyProperty(). That method was designed to provide a temporary
ArrayListWrapper for read-only List properties (e.g. styleClass, children)
during the
pre-build phase. The problem is that it does so unconditionally, for every
property name:

     // ProxyBuilder.java
     private Object getReadOnlyProperty(String propName) {
         // return ArrayListWrapper now and convert it to proper type later
         return new ArrayListWrapper<>();   // <- no check on propName
whatsoever
     }

     @Override
     public boolean containsKey(Object key) {
         return (getTemporaryContainer(key.toString()) != null); // ALWAYS
true
     }

     @Override
     public Object get(Object key) {
         return getTemporaryContainer(key.toString()); // ALWAYS returns
ArrayListWrapper
     }

FXMLLoader's PropertyElement for <properties> calls
parent.getProperties().containsKey("properties") on the ProxyBuilder (which
is a Map),
gets true, then calls get("properties") and receives an ArrayListWrapper. It
then treats
<properties> as a read-only list element rather than the node's
ObservableMap. All
subsequent child property resolution fails against ArrayList instead of
ObservableMap.


RELATION TO EXISTING BUGS
==========================

This is closely related to JDK-8203870 ("ProxyBuilder cannot handle Read-
Only List
Properties properly", open since 2018-05-26), which covers the same
ArrayListWrapper
mechanism breaking read-only List properties. The root cause is identical;
this report
covers the read-only Map case (Node.getProperties()), which has not been
filed before.

Both issues were introduced by the fix for JDK-8134600 (2016), which added
getTemporaryContainer().


SUGGESTED FIX
=============

ProxyBuilder.getReadOnlyProperty(propName) should introspect the target type
and return
new ArrayListWrapper<>() only when the property's getter actually returns a
Collection.
For all other cases (including read-only Map properties like getProperties()
) it should
return null, signalling to FXMLLoader that the property is not a read-only
list container.

     private Object getReadOnlyProperty(String propName) {
         // Only treat as a read-only list container if the getter actually
returns a Collection
         Method getter = findGetter(type, propName);
         if (getter != null && Collection.class.isAssignableFrom(getter.
getReturnType())) {
             return new ArrayListWrapper<>();
         }
         return null;
     }

This fix would also resolve JDK-8203870 for the list case, since it limits
the
interception to properties that are genuinely read-only collections on the
target type.




The bug report was generated using AI, but the bug itself was found
manually.


Thank you for your time.




Regards Petr Štechmüller

Reply via email to