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

commit 8f291189e595490c3b8d2c8e6bdd1b48b131599b
Author: James Bognar <[email protected]>
AuthorDate: Tue May 12 12:46:00 2026 -0400

    refactor: make BeanMeta/BeanPropertyMeta constructible from 
BeanConfigContext (TODO-5 Step 6)
    
    Co-authored-by: Cursor <[email protected]>
---
 .../src/main/java/org/apache/juneau/BeanMap.java   |  25 ++-
 .../src/main/java/org/apache/juneau/BeanMeta.java  | 215 ++++++++++++++++-----
 .../java/org/apache/juneau/BeanPropertyMeta.java   | 200 +++++++++++--------
 .../apache/juneau/commons/bean/BeanMeta_Test.java  | 174 +++++++++++++++++
 todo/TODO-5-bean-runtime-types-to-commons.md       |  40 +++-
 5 files changed, 517 insertions(+), 137 deletions(-)

diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
index f3efd25e1d..7da3909d2b 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
@@ -84,6 +84,23 @@ public class BeanMap<T> extends AbstractMap<String,Object> 
implements Delegate<T
                return MarshallingContext.DEFAULT_SESSION.toBeanMap(bean);
        }
 
+       /**
+        * Convenience method for wrapping a bean inside a {@link BeanMap} 
using a pre-built {@link BeanMeta}.
+        *
+        * <p>
+        * This is the bean-modeling entry point — it builds a {@link BeanMap} 
without going through a
+        * {@link MarshallingSession}, paired with a {@link BeanMeta} typically 
produced by
+        * {@link BeanMeta#of(Class, BeanConfigContext)}.  No marshalling 
session is attached.
+        *
+        * @param <T> The bean type.
+        * @param bean The bean being wrapped.
+        * @param meta The bean metadata.  Must not be <jk>null</jk>.
+        * @return A new {@link BeanMap} instance wrapping the bean.
+        */
+       public static <T> BeanMap<T> of(T bean, BeanMeta<T> meta) {
+               return new BeanMap<>(bean, meta);
+       }
+
        /** The wrapped object. */
        protected T bean;
 
@@ -147,9 +164,9 @@ public class BeanMap<T> extends AbstractMap<String,Object> 
implements Delegate<T
        public void add(String property, Object value) {
                var p = getPropertyMeta(property);
                if (p == null) {
-                       if 
(meta.getMarshallingContext().isIgnoreUnknownBeanProperties())
+                       if (meta.getConfig().isIgnoreUnknownBeanProperties())
                                return;
-                       throw bex(meta.getClassMeta(), "Bean property ''{0}'' 
not found.", property);
+                       throw bex(meta.getClassInfo(), "Bean property ''{0}'' 
not found.", property);
                }
                p.add(this, property, value);
        }
@@ -672,12 +689,12 @@ public class BeanMap<T> extends 
AbstractMap<String,Object> implements Delegate<T
        public Object put(String property, Object value) {
                var p = getPropertyMeta(property);
                if (p == null) {
-                       if 
(meta.getMarshallingContext().isIgnoreUnknownBeanProperties() || 
property.equals(typePropertyName))
+                       if (meta.getConfig().isIgnoreUnknownBeanProperties() || 
property.equals(typePropertyName))
                                return meta.onWriteProperty(bean, property, 
null);
 
                        p = getPropertyMeta("*");
                        if (p == null)
-                               throw bex(meta.getClassMeta(), "Bean property 
''{0}'' not found.", property);
+                               throw bex(meta.getClassInfo(), "Bean property 
''{0}'' not found.", property);
                }
                return p.set(this, property, value);
        }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
index 6a741cbe70..eb0cc118cd 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
@@ -19,6 +19,7 @@ package org.apache.juneau;
 import static org.apache.juneau.BeanMeta.MethodType.*;
 import static org.apache.juneau.commons.reflect.AnnotationTraversal.*;
 import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
+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.*;
@@ -358,13 +359,14 @@ public class BeanMeta<T> {
        }
 
        private BeanConstructor beanConstructor;                                
   // The constructor for this bean.
-       private final MarshallingContext marshallingContext;                    
                 // The bean context that created this metadata object.
+       private final MarshallingContext marshallingContext;                    
   // The bean context that created this metadata object.  Null when 
constructed via {@link #of(Class, BeanConfigContext)}.
+       private final BeanConfigContext config;                                 
   // Bean-modeling settings facade — always non-null.  Sources: 
marshallingContext.getBeanConfigContext() (marshalling-side) or the explicit 
BeanConfigContext (commons-side).
        private final MarshalledFilter beanFilter;                              
         // Optional bean filter associated with the target class.
        private final NullableSupplier<InvocationHandler> 
beanProxyInvocationHandler;  // The invocation handler for this bean (if it's 
an interface).
        private final Supplier<BeanRegistry> beanRegistry;                      
   // The bean registry for this bean.
        private final Supplier<List<ClassInfo>> classHierarchy;                 
   // List of all classes traversed in the class hierarchy.
-       private final ClassMeta<T> classMeta;                                   
   // The target class type that this meta object describes.
-       private final ClassInfo classInfo;                                      
   // Pure-reflection view of the bean class (Step 2 of TODO-5 — decouples bean 
modeling from ClassMeta).
+       private final ClassMeta<T> classMeta;                                   
   // The target class type that this meta object describes.  Null when 
constructed via {@link #of(Class, BeanConfigContext)}.
+       private final ClassInfo classInfo;                                      
   // Pure-reflection view of the bean class (Step 2 of TODO-5 — decouples bean 
modeling from ClassMeta).  Always non-null.
        private final Supplier<String> dictionaryName;                          
   // The @Marshalled(typeName) annotation defined on this bean class.
        private final BeanPropertyMeta dynaProperty;                            
   // "extras" property.
        @SuppressWarnings("rawtypes")
@@ -382,6 +384,43 @@ public class BeanMeta<T> {
        private final String typePropertyName;                                  
   // "_type" property actual name.
        private final Map<BeanPropertyMeta,BeanRegistry> 
propertyBeanRegistries;   // Per-property BeanRegistry side-map (Step 5 of 
TODO-5 — keeps BeanRegistry off BeanPropertyMeta itself).
 
+       /**
+        * Creates a {@link BeanMeta} for the specified class using the 
supplied {@link BeanConfigContext}.
+        *
+        * <p>
+        * This is the bean-modeling entry point — it constructs a {@link 
BeanMeta} purely from a {@link Class}
+        * and a {@link BeanConfigContext}, without touching any 
marshalling-side type infrastructure
+        * ({@link ClassMeta}, {@link MarshallingContext}, {@link 
BeanRegistry}, {@link org.apache.juneau.swap.ObjectSwap}).
+        * The returned {@link BeanMeta} carries enough information to do raw 
getter/setter invocation and property
+        * iteration; marshalling-aware reads ({@link #getClassMeta()}, 
type-resolution on each
+        * {@link BeanPropertyMeta#getClassMeta()}, the per-property {@link 
BeanRegistry}) remain <jk>null</jk>.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      BeanMeta&lt;Person&gt; <jv>bm</jv> = 
BeanMeta.<jsm>of</jsm>(Person.<jk>class</jk>, 
BeanConfigContext.<jsf>DEFAULT</jsf>);
+        *      BeanPropertyMeta <jv>name</jv> = 
<jv>bm</jv>.getProperties().get(<js>"name"</js>);
+        * </p>
+        *
+        * @param <T> The bean class type.
+        * @param beanClass The class to build a {@link BeanMeta} for.  Must 
not be <jk>null</jk>.
+        * @param config The bean-modeling configuration to apply.  Must not be 
<jk>null</jk>.
+        * @return A new {@link BeanMeta} for the specified class.
+        */
+       public static <T> BeanMeta<T> of(Class<T> beanClass, BeanConfigContext 
config) {
+               return new BeanMeta<>(beanClass, config);
+       }
+
+       /**
+        * Same as {@link #of(Class, BeanConfigContext)} using {@link 
BeanConfigContext#DEFAULT}.
+        *
+        * @param <T> The bean class type.
+        * @param beanClass The class to build a {@link BeanMeta} for.  Must 
not be <jk>null</jk>.
+        * @return A new {@link BeanMeta} for the specified class.
+        */
+       public static <T> BeanMeta<T> of(Class<T> beanClass) {
+               return of(beanClass, BeanConfigContext.DEFAULT);
+       }
+
        /**
         * Constructor.
         *
@@ -405,23 +444,45 @@ public class BeanMeta<T> {
         * @param pNames Explicit list of property names and order. If 
<jk>null</jk>, properties are determined automatically.
         * @param implClass Optional implementation class constructor to use if 
one cannot be found. Can be <jk>null</jk>.
         */
+       protected BeanMeta(ClassMeta<T> cm, MarshalledFilter bf, String[] 
pNames, ClassInfo implClass) {
+               this(cm, cm, cm.getMarshallingContext().getBeanConfigContext(), 
cm.getMarshallingContext(), bf, pNames, implClass);
+       }
+
+       /**
+        * Bean-modeling constructor — builds a {@link BeanMeta} without a 
{@link MarshallingContext}.
+        *
+        * <p>
+        * See {@link #of(Class, BeanConfigContext)} for the public entry 
point.  The {@link #getClassMeta()},
+        * {@link #getMarshallingContext() marshallingContext}, and 
per-property {@link BeanRegistry} fields are
+        * left <jk>null</jk>; the per-property {@link 
BeanPropertyMeta#getClassMeta() rawTypeMeta}/{@code typeMeta}
+        * fields are also <jk>null</jk> (no type-resolution is performed in 
this path).
+        *
+        * @param beanClass The bean class.  Must not be <jk>null</jk>.
+        * @param config The bean-modeling configuration.  Must not be 
<jk>null</jk>.
+        */
+       protected BeanMeta(Class<T> beanClass, BeanConfigContext config) {
+               this(null, info(assertArgNotNull("beanClass", beanClass)), 
assertArgNotNull("config", config), null, null, null, null);
+       }
+
        @SuppressWarnings({
-               "java:S3776" // Cognitive complexity acceptable for bean 
metadata initialization
+               "java:S3776", // Cognitive complexity acceptable for bean 
metadata initialization
+               "java:S107"   // 7 parameters needed to support both 
construction paths
        })
-       protected BeanMeta(ClassMeta<T> cm, MarshalledFilter bf, String[] 
pNames, ClassInfo implClass) {
+       private BeanMeta(ClassMeta<T> cm, ClassInfo ci0, BeanConfigContext 
config, MarshallingContext mc, MarshalledFilter bf, String[] pNames, ClassInfo 
implClass) {
                classMeta = cm;
-               classInfo = cm;
-               marshallingContext = cm.getMarshallingContext();
+               classInfo = ci0;
+               this.config = config;
+               marshallingContext = mc;
                beanFilter = bf;
                implClassConstructor = opt(implClass).map(x -> 
x.getPublicConstructor(x2 -> x2.hasNumParameters(0)).orElse(null)).orElse(null);
-               fluentSetters = marshallingContext.isFindFluentSetters() || 
(nn(bf) && bf.isFluentSetters());
+               fluentSetters = config.isFindFluentSetters() || (nn(bf) && 
bf.isFluentSetters());
                stopClass = opt(bf).map(x -> 
x.getStopClass()).orElse(info(Object.class));
                beanRegistry = memoize(this::findBeanRegistry);
                classHierarchy = memoize(this::findClassHierarchy);
                beanConstructor = findBeanConstructor();
 
                // Local variables for initialization
-               var ap = marshallingContext.getAnnotationProvider();
+               var ap = config.getAnnotationProvider();
                var c = classInfo.inner();
                var ci = classInfo;
                String notABeanReasonTemp = null;
@@ -432,14 +493,14 @@ public class BeanMeta<T> {
                var dynaPropertyValue = Value.<BeanPropertyMeta>empty();
                var propertyBeanRegistriesTemp = 
CollectionUtils.<BeanPropertyMeta,BeanRegistry>map();  // Per-property 
BeanRegistry side-map (TODO-5 Step 5).
                var unsortedPropertiesTemp = false;
-               var ba = ap.find(Marshalled.class, cm);
-               var btList = 
ap.find(org.apache.juneau.commons.bean.BeanType.class, cm);
-               var propertyNamer = opt(bf).map(x -> 
x.getPropertyNamer()).orElse(marshallingContext.getPropertyNamer());
+               var ba = ap.find(Marshalled.class, classInfo);
+               var btList = 
ap.find(org.apache.juneau.commons.bean.BeanType.class, classInfo);
+               var propertyNamer = opt(bf).map(x -> 
x.getPropertyNamer()).orElse(config.getPropertyNamer());
 
-               this.typePropertyName = ba.stream().map(x -> 
x.inner().typePropertyName()).filter(Utils::ne).findFirst().orElseGet(marshallingContext::getBeanTypePropertyName);
+               this.typePropertyName = ba.stream().map(x -> 
x.inner().typePropertyName()).filter(Utils::ne).findFirst().orElseGet(config::getBeanTypePropertyName);
 
                // Check if constructor is required but not found (records are 
exempt since they use canonical constructors)
-               if (! beanConstructor.constructor().isPresent() && bf == null 
&& marshallingContext.isBeansRequireDefaultConstructor() && ! ci.isRecord())
+               if (! beanConstructor.constructor().isPresent() && bf == null 
&& config.isBeansRequireDefaultConstructor() && ! ci.isRecord())
                        notABeanReasonTemp = "Class does not have the required 
no-arg constructor";
 
                var bfo = opt(bf);
@@ -452,7 +513,7 @@ public class BeanMeta<T> {
                        // ensure that ordering first.
                        fixedBeanProps.forEach(x -> normalProps.put(x, 
BeanPropertyMeta.builder(this, x)));
 
-                       if (marshallingContext.isUseJavaBeanIntrospector()) {
+                       if (config.isUseJavaBeanIntrospector()) {
                                var c2 = bfo.map(x -> 
x.getInterfaceClass()).filter(Objects::nonNull).orElse(classInfo);
                                BeanInfo bi = null;
                                if (! c2.isInterface())
@@ -553,10 +614,10 @@ public class BeanMeta<T> {
                        }
 
                        // Make sure at least one property was found (records 
with no components are exempt).
-                       if (bf == null && 
marshallingContext.isBeansRequireSomeProperties() && normalProps.isEmpty() && ! 
ci.isRecord())
+                       if (bf == null && config.isBeansRequireSomeProperties() 
&& normalProps.isEmpty() && ! ci.isRecord())
                                notABeanReasonTemp = "No properties detected on 
bean class";
 
-                       unsortedPropertiesTemp = 
marshallingContext.isUnsortedProperties() || bfo.map(x -> 
x.isUnsortedProperties()).orElse(false) || !fixedBeanProps.isEmpty();
+                       unsortedPropertiesTemp = config.isUnsortedProperties() 
|| bfo.map(x -> x.isUnsortedProperties()).orElse(false) || 
!fixedBeanProps.isEmpty();
 
                        propertiesValue.set(unsortedPropertiesTemp ? map() : 
sortedMap());
 
@@ -567,8 +628,10 @@ public class BeanMeta<T> {
                                propertiesValue.get().put(k, pMeta);
                                // Build the property-level BeanRegistry 
side-map entry from the builder's accumulated
                                // dictionary classes.  Parents to the 
bean-level registry so @MarshalledProp(dictionary={})
-                               // entries chain on top of bean and global 
dictionaries.
-                               propertyBeanRegistriesTemp.put(pMeta, new 
BeanRegistry(marshallingContext, beanRegistry.get(), v.dictionaryClasses));
+                               // entries chain on top of bean and global 
dictionaries.  Skipped on the commons-side path
+                               // (no marshallingContext means no BeanRegistry 
construction).
+                               if (nn(marshallingContext) && 
nn(v.dictionaryClasses))
+                                       propertyBeanRegistriesTemp.put(pMeta, 
new BeanRegistry(marshallingContext, beanRegistry.get(), v.dictionaryClasses));
                        });
 
                        // If a beanFilter is defined, look for inclusion and 
exclusion lists.
@@ -619,11 +682,12 @@ public class BeanMeta<T> {
                typeProperty = BeanPropertyMeta.builder(this, 
typePropertyName).canRead().canWrite().rawMetaType(String.class).build();
                // Map the synthetic "_type" property to the bean-level 
BeanRegistry so consumers calling
                // typeProperty.getBeanRegistry() (currently none in-tree, but 
a public API path) get the same
-               // registry the property previously carried as a field.
-               propertyBeanRegistriesTemp.put(typeProperty, 
beanRegistry.get());
+               // registry the property previously carried as a field.  
Skipped on the commons-side path.
+               if (nn(marshallingContext))
+                       propertyBeanRegistriesTemp.put(typeProperty, 
beanRegistry.get());
                propertyBeanRegistries = u(propertyBeanRegistriesTemp);
                dictionaryName = memoize(this::findDictionaryName);
-               beanProxyInvocationHandler = memoize(() -> 
marshallingContext.isUseInterfaceProxies() && classInfo.isInterface() ? new 
BeanProxyInvocationHandler<>(this) : null);
+               beanProxyInvocationHandler = memoize(() -> 
config.isUseInterfaceProxies() && classInfo.isInterface() ? new 
BeanProxyInvocationHandler<>(this) : null);
                var factoryClassTemp = btList.stream().map(x -> 
x.inner().factory()).filter(x -> x != 
org.apache.juneau.commons.function.BeanFactory.Void.class).findFirst().orElse(null);
                factoryClass = factoryClassTemp;
        }
@@ -636,6 +700,8 @@ public class BeanMeta<T> {
                        if (p.field == null)
                                
findInnerBeanField(p.name).ifPresent(p::setInnerField);
 
+                       // When constructed via the commons-side path, 
marshallingContext is null and validate() runs in
+                       // raw-reflection mode (no 
ClassMeta/ObjectSwap/BeanRegistry resolution).
                        if (p.validate(marshallingContext, typeVarImpls, 
readOnlyProps, writeOnlyProps)) {
 
                                installSwapAwareTransforms(p);
@@ -795,10 +861,39 @@ public class BeanMeta<T> {
        /**
         * Returns the {@link ClassMeta} of this bean.
         *
-        * @return The {@link ClassMeta} of this bean.
+        * <p>
+        * Returns <jk>null</jk> when this {@link BeanMeta} was constructed via 
the commons-side path
+        * ({@link #of(Class, BeanConfigContext)}) — in that case {@link 
#getClassInfo()} carries the
+        * pure-reflection view of the bean class.
+        *
+        * @return The {@link ClassMeta} of this bean, or <jk>null</jk> for 
bean-modeling-only construction.
         */
        public ClassMeta<T> getClassMeta() { return classMeta; }
 
+       /**
+        * Returns the {@link ClassInfo} of this bean.
+        *
+        * <p>
+        * Pure-reflection view of the bean class.  Always non-null, regardless 
of construction path.  Use this in
+        * preference to {@link #getClassMeta()} for any bean-modeling read 
that does not depend on marshalling-aware
+        * type metadata; the commons-side construction path leaves {@link 
#getClassMeta()} <jk>null</jk>.
+        *
+        * @return The class info for this bean.  Never <jk>null</jk>.
+        */
+       public ClassInfo getClassInfo() { return classInfo; }
+
+       /**
+        * Returns the bean-modeling configuration snapshot used to build this 
{@link BeanMeta}.
+        *
+        * <p>
+        * For marshalling-side construction, this is sourced from {@link 
MarshallingContext#getBeanConfigContext()}.
+        * For commons-side construction (via {@link #of(Class, 
BeanConfigContext)}), this is the {@link BeanConfigContext}
+        * passed to the factory.  Always non-null.
+        *
+        * @return The bean-modeling configuration.  Never <jk>null</jk>.
+        */
+       public BeanConfigContext getConfig() { return config; }
+
        /**
         * Returns the dictionary name for this bean as defined through the 
{@link Bean#typeName() @Marshalled(typeName)} annotation.
         *
@@ -922,7 +1017,12 @@ public class BeanMeta<T> {
        /**
         * Returns the bean context that created this metadata object.
         *
-        * @return The bean context.
+        * <p>
+        * Returns <jk>null</jk> when this {@link BeanMeta} was constructed via 
the commons-side path
+        * ({@link #of(Class, BeanConfigContext)}).  Use {@link #getConfig()} 
for the always-non-null bean-modeling
+        * configuration facade.
+        *
+        * @return The bean context, or <jk>null</jk> for bean-modeling-only 
construction.
         */
        protected MarshallingContext getMarshallingContext() { return 
marshallingContext; }
 
@@ -1086,7 +1186,7 @@ public class BeanMeta<T> {
                "unchecked" // Unchecked casts required for factory class and 
BeanStore result
        })
        private org.apache.juneau.commons.function.BeanFactory 
resolveFactory(Class<? extends org.apache.juneau.commons.function.BeanFactory> 
fc) {
-               var bs = marshallingContext.getBeanStore();
+               var bs = config.getBeanStore();
                if (bs != null) {
                        var opt = bs.getBean(fc);
                        if (opt.isPresent())
@@ -1135,8 +1235,8 @@ public class BeanMeta<T> {
                "java:S3776" // Cognitive complexity acceptable for constructor 
finding logic
        })
        private BeanConstructor findBeanConstructor() {
-               var ap = marshallingContext.getAnnotationProvider();
-               var vis = marshallingContext.getBeanConstructorVisibility();
+               var ap = config.getAnnotationProvider();
+               var vis = config.getBeanConstructorVisibility();
                var ci = classInfo;
 
                var l = ci.getPublicConstructors().stream().filter(x -> 
ap.has(BeanCtor.class, x)).toList();
@@ -1219,9 +1319,9 @@ public class BeanMeta<T> {
         * @return A collection of all bean fields found in the class hierarchy.
         */
        private Collection<FieldInfo> findBeanFields() {
-               var v = marshallingContext.getBeanFieldVisibility();
-               var noIgnoreTransients = ! 
marshallingContext.isIgnoreTransientFields();
-               var ap = marshallingContext.getAnnotationProvider();
+               var v = config.getBeanFieldVisibility();
+               var noIgnoreTransients = ! config.isIgnoreTransientFields();
+               var ap = config.getAnnotationProvider();
                var isRecord = classInfo.isRecord();
                var recordComponentNames = isRecord
                        ? 
classInfo.getRecordComponents().stream().map(java.lang.reflect.RecordComponent::getName).collect(java.util.stream.Collectors.toSet())
@@ -1288,10 +1388,10 @@ public class BeanMeta<T> {
        })
        private List<BeanMethod> findBeanMethods() {
                var l = new LinkedList<BeanMethod>();
-               var ap = marshallingContext.getAnnotationProvider();
+               var ap = config.getAnnotationProvider();
                var ci = classInfo;
-               var v = marshallingContext.getBeanMethodVisibility();
-               var pn = opt(beanFilter).map(x -> 
x.getPropertyNamer()).orElse(marshallingContext.getPropertyNamer());
+               var v = config.getBeanMethodVisibility();
+               var pn = opt(beanFilter).map(x -> 
x.getPropertyNamer()).orElse(config.getPropertyNamer());
                var suppressedFromBeanIgnoredFields = 
findSuppressedPropertyNamesFromIgnoredFields(pn);
 
                classHierarchy.get().stream().forEach(c2 -> {
@@ -1422,11 +1522,17 @@ public class BeanMeta<T> {
         *      no dictionary classes are found.
         */
        private BeanRegistry findBeanRegistry() {
+               // BeanRegistry is a marshalling-side concern (polymorphic 
dispatch via @Marshalled(typeName)/dictionary).
+               // On the commons-side construction path there is no 
MarshallingContext to drive it — return null and let
+               // callers route through getPropertyBeanRegistry(...) (which is 
also empty on this path).
+               if (marshallingContext == null)
+                       return null;
+
                // Bean dictionary on bean filter.
                var beanDictionaryClasses = opt(beanFilter).map(x -> new 
ArrayList<>(x.getBeanDictionary())).orElse(new ArrayList<>());
 
                // Bean dictionary from @Marshalled(typeName) annotation.
-               var ba = 
marshallingContext.getAnnotationProvider().find(Marshalled.class, classInfo);
+               var ba = config.getAnnotationProvider().find(Marshalled.class, 
classInfo);
                ba.stream().map(x -> 
x.inner().typeName()).filter(Utils::ne).findFirst().ifPresent(x -> 
beanDictionaryClasses.add(classInfo));
 
                return new BeanRegistry(marshallingContext, null, 
beanDictionaryClasses);
@@ -1539,22 +1645,27 @@ public class BeanMeta<T> {
                                return s;
                }
 
-               var n = classInfo
-                       .getParentsAndInterfaces()
-                       .stream()
-                       .skip(1)
-                       .map(marshallingContext::getClassMeta)
-                       .map(ClassMeta::getBeanRegistry)
-                       .filter(Objects::nonNull)
-                       .map(x -> x.getTypeName(rawClass))
-                       .filter(Objects::nonNull)
-                       .findFirst()
-                       .orElse(null);
-
-               if (n != null)
-                       return n;
+               // Parent-class BeanRegistry lookup is a marshalling-side 
concern: it walks parents and interfaces to
+               // see if any of THEIR ClassMeta-backed BeanRegistries declares 
a typeName for our raw class.  On the
+               // commons-side path (no marshallingContext) we skip this 
entirely.
+               if (nn(marshallingContext)) {
+                       var n = classInfo
+                               .getParentsAndInterfaces()
+                               .stream()
+                               .skip(1)
+                               .map(marshallingContext::getClassMeta)
+                               .map(ClassMeta::getBeanRegistry)
+                               .filter(Objects::nonNull)
+                               .map(x -> x.getTypeName(rawClass))
+                               .filter(Objects::nonNull)
+                               .findFirst()
+                               .orElse(null);
+
+                       if (n != null)
+                               return n;
+               }
 
-               return 
marshallingContext.getAnnotationProvider().find(Marshalled.class, classInfo)
+               return config.getAnnotationProvider().find(Marshalled.class, 
classInfo)
                        .stream()
                        .map(AnnotationInfo::inner)
                        .filter(x -> ! x.typeName().isEmpty())
@@ -1600,7 +1711,7 @@ public class BeanMeta<T> {
        })
        private Set<String> 
findSuppressedPropertyNamesFromIgnoredFields(PropertyNamer propertyNamer) {
                var s = new HashSet<String>();
-               var ap = marshallingContext.getAnnotationProvider();
+               var ap = config.getAnnotationProvider();
                for (var c2 : classHierarchy.get()) {
                        for (var x : c2.getDeclaredFields()) {
                                if (! x.isNotStatic() || ! 
ap.has(BeanIgnore.class, x))
@@ -1656,8 +1767,8 @@ public class BeanMeta<T> {
         *      in the class hierarchy.
         */
        private Optional<FieldInfo> findInnerBeanField(String name) {
-               var noIgnoreTransients = ! 
marshallingContext.isIgnoreTransientFields();
-               var ap = marshallingContext.getAnnotationProvider();
+               var noIgnoreTransients = ! config.isIgnoreTransientFields();
+               var ap = config.getAnnotationProvider();
 
                // @formatter:off
                return classHierarchy.get().stream()
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
index 357993e386..22cf098fd6 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanPropertyMeta.java
@@ -85,14 +85,15 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
         */
        public static class Builder {
                BeanMeta<?> beanMeta;  // Package-private for BeanMeta access
-               MarshallingContext bc;  // Package-private for BeanMeta access
+               MarshallingContext bc;  // Package-private for BeanMeta access. 
 Null when the owning BeanMeta was built via the commons-side path.
+               BeanConfigContext config;  // Package-private for BeanMeta 
access.  Always non-null — sourced from the owning BeanMeta.
                String name;  // Package-private for BeanMeta access
                FieldInfo field;  // Package-private for BeanMeta access
                FieldInfo innerField;  // Package-private for BeanMeta access
                MethodInfo getter;  // Package-private for BeanMeta access
                MethodInfo setter;  // Package-private for BeanMeta access
                MethodInfo extraKeys;  // Package-private for BeanMeta access
-               ClassMeta<?> rawTypeMeta;  // Package-private for BeanMeta 
access (used to install swap-aware transforms)
+               ClassMeta<?> rawTypeMeta;  // Package-private for BeanMeta 
access (used to install swap-aware transforms).  Null on commons-side path (no 
type resolution).
                ObjectSwap swap;  // Package-private for BeanMeta access (used 
to install swap-aware transforms)
                BiFunction<MarshallingSession,Object,Object> readTransform;  // 
Package-private; defaults to identity if null.
                BiFunction<MarshallingSession,Object,Object> writeTransform; // 
Package-private; defaults to identity if null.
@@ -113,6 +114,7 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                Builder(BeanMeta<?> beanMeta, String name) {
                        this.beanMeta = beanMeta;
                        this.bc = beanMeta.getMarshallingContext();
+                       this.config = beanMeta.getConfig();
                        this.name = name;
                }
 
@@ -205,11 +207,19 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                 * {@link MarshallingContext}, allowing callers from the 
bean-modeling layer to seed the type without
                 * holding a {@link ClassMeta} reference.
                 *
+                * <p>
+                * When the owning {@link BeanMeta} was built via the 
commons-side path
+                * ({@link BeanMeta#of(Class, BeanConfigContext)}), no 
marshalling context is available, so
+                * {@code rawTypeMeta}/{@code typeMeta} are left <jk>null</jk> 
and the property runs in raw-reflection mode.
+                *
                 * @param value The raw metadata type for this bean property.
                 * @return This object.
                 */
                public Builder rawMetaType(Class<?> value) {
-                       return 
rawMetaType(bc.getClassMeta(assertArgNotNull(ARG_value, value)));
+                       assertArgNotNull(ARG_value, value);
+                       if (bc == null)
+                               return this;
+                       return rawMetaType(bc.getClassMeta(value));
                }
 
                private static ObjectSwap 
marshalledPropSwap(AnnotationInfo<MarshalledProp> ai) {
@@ -342,8 +352,15 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                 * side-map keyed by the built {@link BeanPropertyMeta}.  The 
bean-modeling layer itself no longer carries a
                 * {@link BeanRegistry} reference.
                 *
-                * @param bc The bean context.
-                * @param typeVarImpls Type variable implementations.
+                * <p>
+                * When {@code bc} is <jk>null</jk> (commons-side path), this 
method runs in raw-reflection mode:
+                * annotation reads are routed through {@link 
BeanConfigContext#getAnnotationProvider()} but no
+                * {@link ClassMeta} resolution or {@link ObjectSwap} discovery 
is performed; the property's
+                * {@code rawTypeMeta}/{@code typeMeta} stay <jk>null</jk> and 
the resulting {@link BeanPropertyMeta}
+                * exposes raw getter/setter invocation only.
+                *
+                * @param bc The bean context.  May be <jk>null</jk> for the 
commons-side path.
+                * @param typeVarImpls Type variable implementations.  Ignored 
when {@code bc} is <jk>null</jk>.
                 * @param bpro Bean properties read-only set.
                 * @param bpwo Bean properties write-only set.
                 * @return <jk>true</jk> if this property is valid, 
<jk>false</jk> otherwise.
@@ -357,12 +374,13 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                public boolean validate(MarshallingContext bc, TypeVariables 
typeVarImpls, Set<String> bpro, Set<String> bpwo) throws Exception {
 
                        var bdClasses = list();
-                       var ap = bc.getAnnotationProvider();
+                       var ap = nn(bc) ? bc.getAnnotationProvider() : 
config.getAnnotationProvider();
 
                        if (field == null && getter == null && setter == null)
                                return false;
 
-                       if (field == null && setter == null && 
bc.isBeansRequireSettersForGetters() && ! isConstructorArg)
+                       // Settings reads route through BeanConfigContext so 
the commons-side path works without a MarshallingContext.
+                       if (field == null && setter == null && 
config.isBeansRequireSettersForGetters() && ! isConstructorArg)
                                return false;
 
                        canRead |= (nn(field) || nn(getter));
@@ -375,7 +393,7 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                        if (nn(innerField)) {
                                var lbp = ap.find(BeanProp.class, ifi);
                                var lmp = ap.find(MarshalledProp.class, ifi);
-                               if (nn(field) || ne(lbp)) {
+                               if (nn(bc) && (nn(field) || ne(lbp))) {
                                        // Only use field type if it's a bean 
property or has @BeanProp annotation.
                                        // Otherwise, we want to infer the type 
from the getter or setter.
                                        rawTypeMeta = 
bc.resolveClassMeta(opt(last(lbp)).orElse(null), innerField.getFieldType(), 
typeVarImpls);
@@ -403,9 +421,11 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                        if (nn(getter)) {
                                var lbp = ap.find(BeanProp.class, gi);
                                var lmp = ap.find(MarshalledProp.class, gi);
-                               if (rawTypeMeta == null)
+                               if (nn(bc) && rawTypeMeta == null)
                                        rawTypeMeta = 
bc.resolveClassMeta(opt(last(lbp)).orElse(null), getter.getReturnType(), 
typeVarImpls);
-                               isUri |= (rawTypeMeta.isUri() || 
ap.has(Uri.class, gi));
+                               if (nn(rawTypeMeta))
+                                       isUri |= rawTypeMeta.isUri();
+                               isUri |= ap.has(Uri.class, gi);
                                lbp.forEach(x -> {
                                        var beanp = x.inner();
                                        if (ne(beanp.ro()))
@@ -427,9 +447,11 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                        if (nn(setter)) {
                                var lbp = ap.find(BeanProp.class, si);
                                var lmp = ap.find(MarshalledProp.class, si);
-                               if (rawTypeMeta == null)
+                               if (nn(bc) && rawTypeMeta == null)
                                        rawTypeMeta = 
bc.resolveClassMeta(opt(last(lbp)).orElse(null), 
setter.getParameterTypes().get(0), typeVarImpls);
-                               isUri |= (rawTypeMeta.isUri() || 
ap.has(Uri.class, si));
+                               if (nn(rawTypeMeta))
+                                       isUri |= rawTypeMeta.isUri();
+                               isUri |= ap.has(Uri.class, si);
                                lbp.forEach(x -> {
                                        var beanp = x.inner();
                                        if (ne(beanp.ro()))
@@ -448,72 +470,75 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                                ap.find(Swap.class, si).stream().forEach(x -> 
swap = swapSwap(x));
                        }
 
-                       if (rawTypeMeta == null)
+                       // On the commons-side path (bc == null), rawTypeMeta 
stays null and validate() accepts the property
+                       // in raw-reflection mode.  The marshalling-side path 
still requires a resolvable type.
+                       if (nn(bc) && rawTypeMeta == null)
                                return false;
 
                        dictionaryClasses = 
bdClasses.stream().map(ReflectionUtils::info).toList();
 
                        isDyna = "*".equals(name);
 
-                       // Do some annotation validation.
-                       var ci = rawTypeMeta;
-                       if (nn(getter)) {
-                               var pt = getter.getParameterTypes();
-                               if (isDyna) {
-                                       if (ci.isAssignableTo(Map.class) && 
e(pt)) {
-                                               isDynaGetterMap = true;
-                                       } else if (pt.size() == 1 && 
pt.get(0).is(String.class)) {
-                                               // OK.
+                       // Do some annotation validation.  Type-aware 
validation requires rawTypeMeta — on the commons-side
+                       // path we skip it entirely.
+                       if (nn(rawTypeMeta)) {
+                               var ci = rawTypeMeta;
+                               if (nn(getter)) {
+                                       var pt = getter.getParameterTypes();
+                                       if (isDyna) {
+                                               if 
(ci.isAssignableTo(Map.class) && e(pt)) {
+                                                       isDynaGetterMap = true;
+                                               } else if (pt.size() == 1 && 
pt.get(0).is(String.class)) {
+                                                       // OK.
+                                               } else {
+                                                       return false;
+                                               }
                                        } else {
-                                               return false;
+                                               if (! 
ci.isAssignableTo(getter.getReturnType()))
+                                                       return false;
                                        }
-                               } else {
-                                       if (! 
ci.isAssignableTo(getter.getReturnType()))
-                                               return false;
                                }
-                       }
-                       if (nn(setter)) {
-                               var pt = setter.getParameterTypes();
-                               if (isDyna) {
-                                       if (pt.size() == 2 && 
pt.get(0).is(String.class)) {
-                                               // OK.
+                               if (nn(setter)) {
+                                       var pt = setter.getParameterTypes();
+                                       if (isDyna) {
+                                               if (pt.size() == 2 && 
pt.get(0).is(String.class)) {
+                                                       // OK.
+                                               } else {
+                                                       return false;
+                                               }
                                        } else {
-                                               return false;
+                                               if (pt.size() != 1 || ! 
ci.isAssignableTo(pt.get(0).inner()))
+                                                       return false;
                                        }
-                               } else {
-                                       if (pt.size() != 1 || ! 
ci.isAssignableTo(pt.get(0).inner()))
-                                               return false;
                                }
-                       }
-                       if (nn(field)) {
-                               if (isDyna) {
-                                       if (! 
field.getFieldType().isAssignableTo(Map.class))
-                                               return false;
-                               } else {
-                                       if (! 
ci.isAssignableTo(field.getFieldType()))
-                                               return false;
+                               if (nn(field)) {
+                                       if (isDyna) {
+                                               if (! 
field.getFieldType().isAssignableTo(Map.class))
+                                                       return false;
+                                       } else {
+                                               if (! 
ci.isAssignableTo(field.getFieldType()))
+                                                       return false;
+                                       }
                                }
-                       }
 
-                       if (isDyna) {
-                               rawTypeMeta = rawTypeMeta.getValueType();
-                               if (rawTypeMeta == null)
-                                       rawTypeMeta = bc.object();
-                       }
-                       if (rawTypeMeta == null)
-                               return false;
+                               if (isDyna) {
+                                       rawTypeMeta = 
rawTypeMeta.getValueType();
+                                       if (rawTypeMeta == null && nn(bc))
+                                               rawTypeMeta = bc.object();
+                               }
 
-                       if (typeMeta == null) {
-                               if (nn(swap)) {
-                                       typeMeta = 
bc.getClassMeta(swap.getSwapClass());
-                               } else if (rawTypeMeta == null) {
-                                       typeMeta = bc.object();
-                               } else {
-                                       typeMeta = rawTypeMeta;
+                               if (typeMeta == null) {
+                                       if (nn(swap) && nn(bc)) {
+                                               typeMeta = 
bc.getClassMeta(swap.getSwapClass());
+                                       } else if (rawTypeMeta == null && 
nn(bc)) {
+                                               typeMeta = bc.object();
+                                       } else {
+                                               typeMeta = rawTypeMeta;
+                                       }
                                }
+                               if (typeMeta == null)
+                                       typeMeta = rawTypeMeta;
                        }
-                       if (typeMeta == null)
-                               typeMeta = rawTypeMeta;
 
                        if (bpro.contains(name) || bpro.contains("*"))
                                readOnly = true;
@@ -536,9 +561,10 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                return new Builder(beanMeta, name);
        }
 
-       private final AnnotationProvider ap;                             // 
Annotation provider for finding annotations on this property.
+       private final AnnotationProvider ap;                             // 
Annotation provider for finding annotations on this property.  Sourced from bc 
(marshalling-side) or beanMeta.getConfig() (commons-side).
        private final Supplier<List<AnnotationInfo<?>>> annotations;     // 
Memoized list of all annotations on this property.
-       private final MarshallingContext bc;                                    
// The context that created this meta.
+       private final MarshallingContext bc;                             // The 
context that created this meta.  Null when the owning BeanMeta was built via 
the commons-side path.
+       private final BeanConfigContext config;                          // 
Bean-modeling settings facade — always non-null.  Mirrors the BeanMeta's config.
        private final BeanMeta<?> beanMeta;                              // The 
bean that this property belongs to.
        private final boolean canRead;                                   // 
True if this property can be read.
        private final boolean canWrite;                                  // 
True if this property can be written.
@@ -571,6 +597,7 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
        protected BeanPropertyMeta(Builder b) {
                annotations = memoize(this::findAnnotations);
                bc = b.bc;
+               config = b.config;
                beanMeta = b.beanMeta;
                canRead = b.canRead;
                canWrite = b.canWrite;
@@ -594,7 +621,7 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                readTransform = b.readTransform != null ? b.readTransform : 
(session, o) -> o;
                writeTransform = b.writeTransform != null ? b.writeTransform : 
(session, o) -> o;
 
-               ap = bc.getAnnotationProvider();
+               ap = nn(bc) ? bc.getAnnotationProvider() : 
b.config.getAnnotationProvider();
                hashCode = h(beanMeta, name);
        }
 
@@ -1005,12 +1032,12 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                return invokeGetter(bean, pName);
 
        } catch (Exception e) {
-               if (bc.isIgnoreInvocationExceptionsOnGetters()) {
-                       if (rawTypeMeta.isPrimitive())
+               if (nn(bc) && bc.isIgnoreInvocationExceptionsOnGetters()) {
+                       if (nn(rawTypeMeta) && rawTypeMeta.isPrimitive())
                                return rawTypeMeta.getPrimitiveDefault();
                        return null;
                }
-               throw bex(e, beanMeta.getClassMeta(), "Exception occurred while 
getting property ''{0}''", name);
+               throw bex(e, beanMeta.getClassInfo(), "Exception occurred while 
getting property ''{0}''", name);
        }
        }
 
@@ -1098,13 +1125,23 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                                throw bex("Non-existent bean instance on 
bean.");
                        }
 
+                       // Raw-reflection path: when the owning BeanMeta was 
built via the commons-side path, rawTypeMeta and
+                       // bc are null and there is no marshalling-aware type 
conversion to perform.  Just invoke the setter
+                       // (or write the field) directly.
+                       if (rawTypeMeta == null) {
+                               var bean = m.getBean(true);
+                               var old = (nn(getter) || nn(field)) ? get(m, 
pName) : null;
+                               invokeSetter(bean, pName, value1);
+                               return old;
+                       }
+
                        var isMap = rawTypeMeta.isMap();
                        var isCollection = rawTypeMeta.isCollection();
 
                        if ((! isDyna) && field == null && setter == null && ! 
(isMap || isCollection)) {
-                               if ((value1 == null && 
bc.isIgnoreUnknownNullBeanProperties()) || bc.isIgnoreMissingSetters())
+                               if ((value1 == null && nn(bc) && 
bc.isIgnoreUnknownNullBeanProperties()) || config.isIgnoreMissingSetters())
                                        return null;
-                               throw bex(beanMeta.getClassMeta(), "Setter or 
public field not defined on property ''{0}''", name);
+                               throw bex(beanMeta.getClassInfo(), "Setter or 
public field not defined on property ''{0}''", name);
                        }
 
                        var bean = m.getBean(true);  // Don't use getBean() 
because it triggers array creation!
@@ -1344,12 +1381,12 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
        return swapAndFilterProperty(session, o);
 
        } catch (Exception e) {
-               if (bc.isIgnoreInvocationExceptionsOnGetters()) {
-                       if (rawTypeMeta.isPrimitive())
+               if (nn(bc) && bc.isIgnoreInvocationExceptionsOnGetters()) {
+                       if (nn(rawTypeMeta) && rawTypeMeta.isPrimitive())
                                return rawTypeMeta.getPrimitiveDefault();
                        return null;
                }
-                       throw bex(e, beanMeta.getClassMeta(), "Exception 
occurred while getting property ''{0}''", name);
+                       throw bex(e, beanMeta.getClassInfo(), "Exception 
occurred while getting property ''{0}''", name);
                }
        }
 
@@ -1388,14 +1425,14 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                        } else if (nn(field))
                                m = (Map)field.get(bean);
                        else
-                               throw bex(beanMeta.getClassMeta(), 
MSG_getterOrFieldNotDefined, name);
+                               throw bex(beanMeta.getClassInfo(), 
MSG_getterOrFieldNotDefined, name);
                        return (m == null ? null : m.get(pName));
                }
                if (nn(getter))
                        return getter.invoke(bean);
                if (nn(field))
                        return field.get(bean);
-               throw bex(beanMeta.getClassMeta(), MSG_getterOrFieldNotDefined, 
name);
+               throw bex(beanMeta.getClassInfo(), MSG_getterOrFieldNotDefined, 
name);
        }
 
        private Object invokeSetter(Object bean, String pName, Object val) 
throws IllegalArgumentException {
@@ -1408,8 +1445,8 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                        else if (nn(getter))
                                m = (Map<String,Object>)getter.invoke(bean);
                        else
-                               throw bex(beanMeta.getClassMeta(), "Cannot set 
property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter is 
defined on this property, and the existing property value is null",
-                                       name, getClassMeta().getName(), 
cn(val));
+                               throw bex(beanMeta.getClassInfo(), "Cannot set 
property ''{0}'' of type ''{1}'' to object of type ''{2}'' because no setter is 
defined on this property, and the existing property value is null",
+                                       name, classNameForError(), cn(val));
                        return (m == null ? null : m.put(pName, val));
                }
                if (nn(setter))
@@ -1418,8 +1455,15 @@ public class BeanPropertyMeta implements 
Comparable<BeanPropertyMeta> {
                        field.set(bean, val);
                        return null;
                }
-               throw bex(beanMeta.getClassMeta(), "Cannot set property ''{0}'' 
of type ''{1}'' to object of type ''{2}'' because no setter is defined on this 
property, and the existing property value is null", name,
-                       getClassMeta().getName(), cn(val));
+               throw bex(beanMeta.getClassInfo(), "Cannot set property ''{0}'' 
of type ''{1}'' to object of type ''{2}'' because no setter is defined on this 
property, and the existing property value is null", name,
+                       classNameForError(), cn(val));
+       }
+
+       private String classNameForError() {
+               var cm = getClassMeta();
+               if (nn(cm))
+                       return cm.getName();
+               return beanMeta.getClassInfo().getName();
        }
 
        /**
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanMeta_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanMeta_Test.java
new file mode 100644
index 0000000000..f20e24ecd1
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanMeta_Test.java
@@ -0,0 +1,174 @@
+/*
+ * 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.commons.bean;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests the commons-side construction path of {@link BeanMeta} via
+ * {@link BeanMeta#of(Class, BeanConfigContext)}.
+ *
+ * <p>
+ * Verifies that property discovery and raw getter/setter invocation work 
without a
+ * {@code MarshallingContext}.  All paths that touch the marshalling-aware 
fields
+ * ({@code classMeta}, {@code marshallingContext}, per-property {@code 
rawTypeMeta}) must
+ * gracefully fall through to bean-modeling-only behavior.
+ */
+class BeanMeta_Test extends TestBase {
+
+       
//====================================================================================================
+       // Test POJO
+       
//====================================================================================================
+
+       public static class A_Pojo {
+               private String x;
+               private int y;
+               public String getX() { return x; }
+               public void setX(String value) { x = value; }
+               public int getY() { return y; }
+               public void setY(int value) { y = value; }
+       }
+
+       
//====================================================================================================
+       // Construction
+       
//====================================================================================================
+
+       @Test
+       void a01_of_class_buildsBeanMeta() {
+               var bm = BeanMeta.of(A_Pojo.class);
+               assertNotNull(bm);
+               assertNotNull(bm.getClassInfo());
+               assertNotNull(bm.getConfig());
+       }
+
+       @Test
+       void a02_of_classWithExplicitConfig_buildsBeanMeta() {
+               var bm = BeanMeta.of(A_Pojo.class, BeanConfigContext.DEFAULT);
+               assertNotNull(bm);
+               assertSame(BeanConfigContext.DEFAULT, bm.getConfig());
+       }
+
+       @Test
+       void a03_commonsConstructed_marshallingContextIsNull() {
+               var bm = BeanMeta.of(A_Pojo.class);
+               // The public protected accessor returns null on the 
commons-side path.
+               assertNull(bm.getClassMeta());
+       }
+
+       
//====================================================================================================
+       // Property discovery
+       
//====================================================================================================
+
+       @Test
+       void b01_properties_discovered() {
+               var bm = BeanMeta.of(A_Pojo.class);
+               var props = bm.getProperties();
+               assertNotNull(props);
+               assertTrue(props.containsKey("x"));
+               assertTrue(props.containsKey("y"));
+               assertEquals(2, props.size());
+       }
+
+       @Test
+       void b02_propertyMeta_hasGetterAndSetter() {
+               var bm = BeanMeta.of(A_Pojo.class);
+               var px = bm.getPropertyMeta("x");
+               assertNotNull(px);
+               assertNotNull(px.getGetter());
+               assertNotNull(px.getSetter());
+               // rawTypeMeta is left null on the commons-side path — type 
resolution is a marshalling concern.
+               assertNull(px.getClassMeta());
+       }
+
+       
//====================================================================================================
+       // Raw getter/setter invocation via BeanPropertyMeta / BeanMap
+       
//====================================================================================================
+
+       @Test
+       void c01_rawGet_returnsPropertyValue() {
+               var bm = BeanMeta.of(A_Pojo.class);
+               var p = new A_Pojo();
+               p.setX("hello");
+               p.setY(42);
+
+               var map = BeanMap.of(p, bm);
+               var px = bm.getPropertyMeta("x");
+               var py = bm.getPropertyMeta("y");
+
+               assertEquals("hello", px.get(map, "x"));
+               assertEquals(42, py.get(map, "y"));
+       }
+
+       @Test
+       void c02_rawSet_updatesPropertyValue() {
+               var bm = BeanMeta.of(A_Pojo.class);
+               var p = new A_Pojo();
+               var map = BeanMap.of(p, bm);
+
+               bm.getPropertyMeta("x").set(map, "x", "world");
+               bm.getPropertyMeta("y").set(map, "y", 99);
+
+               assertEquals("world", p.getX());
+               assertEquals(99, p.getY());
+       }
+
+       @Test
+       void c03_beanMap_get_put_roundTrip() {
+               var bm = BeanMeta.of(A_Pojo.class);
+               var p = new A_Pojo();
+               var map = BeanMap.of(p, bm);
+
+               map.put("x", "round-trip");
+               map.put("y", 7);
+
+               assertEquals("round-trip", map.get("x"));
+               assertEquals(7, map.get("y"));
+               assertEquals("round-trip", p.getX());
+               assertEquals(7, p.getY());
+       }
+
+       @Test
+       void c04_getRaw_returnsRawPropertyValue() {
+               var bm = BeanMeta.of(A_Pojo.class);
+               var p = new A_Pojo();
+               p.setX("raw");
+
+               var map = BeanMap.of(p, bm);
+               var px = bm.getPropertyMeta("x");
+               assertEquals("raw", px.getRaw(map, "x"));
+       }
+
+       
//====================================================================================================
+       // Custom BeanConfigContext (e.g. fluent setters, visibility tweaks)
+       
//====================================================================================================
+
+       public static class D_FluentPojo {
+               private String name;
+               public String name() { return name; }
+               public D_FluentPojo name(String value) { name = value; return 
this; }
+       }
+
+       @Test
+       void d01_findFluentSetters_discoversFluentProperties() {
+               var cfg = 
BeanConfigContext.create().findFluentSetters(true).build();
+               var bm = BeanMeta.of(D_FluentPojo.class, cfg);
+               assertTrue(bm.getProperties().containsKey("name"));
+       }
+}
diff --git a/todo/TODO-5-bean-runtime-types-to-commons.md 
b/todo/TODO-5-bean-runtime-types-to-commons.md
index f6cdd47a87..51f0589d83 100644
--- a/todo/TODO-5-bean-runtime-types-to-commons.md
+++ b/todo/TODO-5-bean-runtime-types-to-commons.md
@@ -4,7 +4,41 @@ This is the remaining work from **Phase 5 of the bean-layer 
split**. Phase 5a (t
 
 ---
 
-## Status (as of Phase 5e checkpoint)
+## Status (as of Phase 5f checkpoint)
+
+**Step 6 complete.** `BeanMeta` and `BeanPropertyMeta` can now be constructed 
without a `MarshallingContext`. Public surface added:
+- `BeanMeta.of(Class<T>, BeanConfigContext)` and `BeanMeta.of(Class<T>)` 
static factories.
+- `protected BeanMeta(Class<T>, BeanConfigContext)` constructor.
+- `BeanMeta.getConfig()` and `BeanMeta.getClassInfo()` — always non-null 
accessors.
+- `BeanMap.of(T, BeanMeta<T>)` static factory for pairing a bean with a 
commons-built `BeanMeta` (no session).
+
+Internal restructuring:
+- Existing `BeanMeta(ClassMeta<T>, MarshalledFilter, String[], ClassInfo)` 
constructor preserved (still used by 
`ClassMeta.findBeanMeta()`/`BeanMetaFiltered`). Both constructors now delegate 
to a private all-args constructor that builds property metadata once.
+- New `BeanMeta.config` field (non-null `BeanConfigContext`) is the source of 
truth for ALL settings reads — visibility, namers, ignores, 
`findFluentSetters`, `isUnsortedProperties`, 
`isBeansRequireDefaultConstructor`/`SomeProperties`, `getBeanTypePropertyName`, 
`getAnnotationProvider`, `getBeanStore`, `isUseJavaBeanIntrospector`, 
`isUseInterfaceProxies`, `isIgnoreTransientFields`. The marshalling-side path 
sources it from `marshallingContext.getBeanConfigContext()` (wired in Step 1); 
th [...]
+- `BeanMeta.classMeta` and `BeanMeta.marshallingContext` are now 
documented-nullable. They stay non-null only on the marshalling-side 
construction path; commons-side construction leaves them null. `classInfo` and 
`config` are always non-null.
+- `BeanMeta.findBeanRegistry()` returns `null` when `marshallingContext == 
null` (BeanRegistry is a marshalling-side concern). 
`BeanMeta.findDictionaryName()`'s parents/interfaces 
`marshallingContext::getClassMeta` stream is gated behind a non-null check. The 
synthetic `_type` property is built unconditionally (rawMetaType is a no-op 
when bc is null) but the side-map entry pairing it with the bean-level registry 
is skipped on the commons-side path.
+
+`BeanPropertyMeta` changes:
+- `BeanPropertyMeta.Builder` carries both `MarshallingContext bc` (nullable on 
commons-side) and `BeanConfigContext config` (always non-null, sourced from the 
owning `BeanMeta`).
+- `BeanPropertyMeta.Builder.rawMetaType(Class<?>)` no-ops when `bc == null` — 
leaves `rawTypeMeta` null and the property runs in raw-reflection mode.
+- `BeanPropertyMeta.Builder.validate(...)`: annotation reads route through 
`bc.getAnnotationProvider()` when bc is non-null, falling back to 
`config.getAnnotationProvider()`. All `bc.resolveClassMeta(...)` / 
`bc.getClassMeta(...)` / `bc.object()` calls are guarded by `nn(bc)`. 
Type-aware validation (isAssignableTo checks for getter/setter/field type 
compatibility, isDyna value-type resolution) is wrapped in `if 
(nn(rawTypeMeta))`. The `if (rawTypeMeta == null) return false` bailout was r 
[...]
+- `BeanPropertyMeta` instance carries the same `bc` (nullable) plus a non-null 
`config` field mirrored from the builder. The `ap` field is sourced from 
`bc.getAnnotationProvider()` when bc is non-null, else 
`config.getAnnotationProvider()`.
+- `BeanPropertyMeta.getRaw(...)`: catch block guards 
`bc.isIgnoreInvocationExceptionsOnGetters()` and `rawTypeMeta.isPrimitive()` 
with null checks.
+- `BeanPropertyMeta.getInner(...)`: same catch-block guards. Error message now 
uses `beanMeta.getClassInfo()` instead of `beanMeta.getClassMeta()` so it works 
on both construction paths.
+- `BeanPropertyMeta.set(...)`: new early-return raw-reflection path when 
`rawTypeMeta == null` — calls `invokeSetter(bean, pName, value1)` directly 
after capturing the old value via `get(...)`. Skips all the 
`isMap/isCollection/setPropertyValue` machinery that depends on type metadata.
+- `BeanPropertyMeta.set(...)` settings reads route through 
`config.isIgnoreMissingSetters()` and guard 
`bc.isIgnoreUnknownNullBeanProperties()` with non-null check.
+- `BeanPropertyMeta.invokeGetter`/`invokeSetter` error messages: 
`beanMeta.getClassMeta()` → `beanMeta.getClassInfo()`. New private 
`classNameForError()` helper handles the type-name format (uses 
`getClassMeta().getName()` when non-null, else falls back to 
`classInfo.getName()`).
+
+`BeanMap` changes:
+- `BeanMap.add(...)` and `BeanMap.put(...)`: 
`meta.getMarshallingContext().isIgnoreUnknownBeanProperties()` → 
`meta.getConfig().isIgnoreUnknownBeanProperties()`. Error-message argument 
switched from `meta.getClassMeta()` to `meta.getClassInfo()` for compatibility 
with commons-built `BeanMeta`.
+- New `BeanMap.of(T bean, BeanMeta<T> meta)` static factory — the commons-side 
counterpart to `BeanMap.of(T bean)` (which still routes through 
`MarshallingContext.DEFAULT_SESSION.toBeanMap(bean)`).
+
+Test coverage: new `BeanMeta_Test` at 
`juneau-utest/src/test/java/org/apache/juneau/commons/bean/BeanMeta_Test.java` 
covers 10 scenarios — `BeanMeta.of(Class)` / `BeanMeta.of(Class, 
BeanConfigContext.DEFAULT)` factories, property discovery, raw getter/setter 
invocation via `BeanPropertyMeta.get/set/getRaw`, `BeanMap.get/put` 
round-trips, and a custom `BeanConfigContext` with `findFluentSetters` enabled. 
Verifies `getClassMeta() == null` on commons-built `BeanMeta` and 
`getClassInfo() !=  [...]
+
+Known limitations of the commons-side path (acceptable for Step 6, scoped for 
later cleanup):
+- `BeanPropertyMeta.set` short-circuits to raw setter invocation when 
`rawTypeMeta == null` — no type conversion, no collection/map setter-fallback, 
no `swap.unswap` path. Bean-modeling-only callers should pass values already of 
the property type.
+- `BeanPropertyMeta.add(BeanMap, String, Object)` / `add(BeanMap, String, 
String, Object)` (Collection/array/Map helpers) still read `rawTypeMeta` 
unconditionally — calling them on a commons-built `BeanPropertyMeta` will NPE. 
Same applies to `applyChildPropertiesFilter`, `setArray`, and the second half 
of `setPropertyValue`. None of these paths are reached by the test, and none 
are needed for the bean-modeling minimum.
+- `MarshalledProp` swap detection in `validate(...)` still runs even when bc 
is null (it doesn't require a `MarshallingContext`), but no 
`installSwapAwareTransforms` is invoked since the marshalling-side install 
helper short-circuits when `rawTypeMeta == null`.
 
 **Step 5 complete.** `BeanPropertyMeta` no longer carries a `BeanRegistry` 
field. Picked **Option B** (side-map keyed by `BeanPropertyMeta`) — there are 
only 3 marshalling-side call sites that read the per-property registry 
(`ParserSession.getClassMeta(...)`, `SerializerSession.getBeanTypeName(...)`, 
`XmlParserSession.parseIntoMap-mixed-content`), and they all use the existing 
`pMeta.getBeanRegistry()` public accessor. The accessor stays as a 
backwards-compat delegate that routes to the  [...]
 
@@ -17,13 +51,13 @@ This is the remaining work from **Phase 5 of the bean-layer 
split**. Phase 5a (t
 - [x] **Step 3** — Removed swap-aware `get`/`set` from `BeanPropertyMeta`. 
Picked option (a) (pluggable callbacks). Added 
`BiFunction<MarshallingSession,Object,Object>` `readTransform` / 
`writeTransform` fields with identity defaults; exposed corresponding 
`Builder.readTransform(...)` / `Builder.writeTransform(...)` setters. The 
bean-modeling `get`/`set` paths inside `BeanPropertyMeta` no longer call 
`ObjectSwap.swap` / `ObjectSwap.unswap` directly — instead they invoke the 
installed tra [...]
 - [x] **Step 4** — Removed `MarshallingSession` from the `BeanMap` constructor 
signature (Option c — transitional setter). `BeanMap` now exposes 
`setMarshallingSession(MarshallingSession)` that the marshalling layer wires in 
immediately after construction. The `session` field defaults to null on direct 
construction; only `BeanMap.getBean()` (for read-only beans with constructor 
args), `BeanPropertyMeta.add`/`set`, and child-properties-filter operations 
need it set, and they all go throug [...]
 - [x] **Step 5** — Removed `BeanRegistry` field from `BeanPropertyMeta`. 
Picked **Option B** — side-map keyed by `BeanPropertyMeta` lives on `BeanMeta` 
(`Map<BeanPropertyMeta,BeanRegistry> propertyBeanRegistries`). Per-property 
registries are constructed by `BeanMeta` after `v.build()` from the builder's 
package-private `dictionaryClasses` field (populated during 
`Builder.validate(...)`). `Builder.beanRegistry(...)` public setter, the 
`beanRegistry` builder field, and the `parentBeanRegi [...]
-- [ ] **Step 6** — `BeanMeta` becomes constructible by both `ClassMeta` and 
direct `commons.bean` callers via `BeanMeta.of(MyClass.class, 
BeanConfigContext.DEFAULT)`. `ClassMeta` becomes a *consumer* of `BeanMeta` 
rather than its creator.
+- [x] **Step 6** — `BeanMeta.of(Class<T>, BeanConfigContext)` factory + 
`protected BeanMeta(Class<T>, BeanConfigContext)` constructor wired up. 
`BeanMeta` now carries a non-null `BeanConfigContext config` facade for all 
settings reads; the `marshallingContext` and `classMeta` fields are 
documented-nullable and stay null on the commons-side path. 
`BeanPropertyMeta.Builder.bc` and `BeanPropertyMeta.bc` similarly nullable; new 
mirrored `config` field on both. `Builder.validate(...)` accepts [...]
 - [ ] **Step 7** — Re-check whether `ExtendedBeanMeta` and per-format 
extensions (`XmlBeanMeta`, `RdfBeanMeta`, `HtmlBeanMeta`) need to follow 
`BeanMeta` to `commons.bean`. Default expectation: they stay in 
`juneau-marshall`.
 - [ ] **Step 8** — `git mv` the eight runtime types into 
`juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/`. 
Verify `juneau-commons` still compiles standalone (`cd 
juneau-core/juneau-commons && mvn clean compile`).
 - [ ] **Step 9** — Reference sweep: 80–120 unique files (mostly inside 
`juneau-marshall`). Update imports, Javadoc `{@link …}` references, 
package-info docs.
 - [ ] **Step 10** — Update `juneau-docs` release notes / migration guide 
(`docs/pages/release-notes/9.5.0.md`, `## Package Moves` section) with the 
bean-runtime relocations.
 
-The "incomplete-but-documented over broken-build" rule from Phase 5a still 
applies. When picking up the next slice of this work, **Step 6 is the 
recommended next checkpoint** — Step 5 dropped the `BeanRegistry` field from 
`BeanPropertyMeta` (now on a `BeanMeta` side-map) and lifted two of the 
surviving `this.classMeta` references in `BeanMeta.findDictionaryName(...)` to 
`classInfo.inner()`. The remaining marshalling coupling on `BeanMeta` itself: 
(a) the `marshallingContext` field is rea [...]
+The "incomplete-but-documented over broken-build" rule from Phase 5a still 
applies. When picking up the next slice of this work, **Step 7 is the 
recommended next checkpoint** — Step 6 wired the commons-side construction path 
through `BeanMeta.of(Class<T>, BeanConfigContext)`, made 
`marshallingContext`/`classMeta` nullable on `BeanMeta`, and made 
`bc`/`rawTypeMeta` nullable on `BeanPropertyMeta`. The marshalling-side 
construction path remains behaviorally identical: `BeanMetaFiltered` and [...]
 
 ---
 

Reply via email to