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 174dbdf3af Moving schema annotations into juneau-common
174dbdf3af is described below

commit 174dbdf3afd0ff89712bb9ba80fab433d4ee7b14
Author: James Bognar <[email protected]>
AuthorDate: Mon Apr 13 20:49:59 2026 -0400

    Moving schema annotations into juneau-common
---
 .../juneau/commons/reflect/AnnotationProvider.java |  20 +-
 .../java/org/apache/juneau/annotation/Schema.java  | 102 ---------
 .../apache/juneau/annotation/SchemaAnnotation.java | 105 +---------
 .../org/apache/juneau/annotation/SchemaApply.java  |  99 +++++++++
 .../juneau/annotation/SchemaApplyAnnotation.java   | 231 +++++++++++++++++++++
 .../juneau/jsonschema/JsonSchemaGeneratorTest.java |  16 +-
 .../annotation/SchemaAnnotation_Test.java          |  37 +---
 .../annotation/SchemaApplyAnnotation_Test.java     | 131 ++++++++++++
 todo/decouple-rest-common-from-marshall.md         |  32 +--
 todo/xapply-annotation-split.md                    | 230 ++++++++++++++++++++
 10 files changed, 740 insertions(+), 263 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AnnotationProvider.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AnnotationProvider.java
index 8d869155e9..5a10f5c726 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AnnotationProvider.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AnnotationProvider.java
@@ -345,18 +345,22 @@ public class AnnotationProvider {
                                try {
                                        var ci = ClassInfo.of(a.getClass());
 
+                                       // XApply pattern: if annotation has a 
value() returning a single Annotation,
+                                       // unwrap and store the inner 
annotation instead of the wrapper.
+                                       var annotationToStore = 
unwrapXApply(ci, a);
+
                                        ci.getPublicMethod(x -> 
x.hasName("onClass")).ifPresent(mi -> {
                                                if (! 
mi.getReturnType().is(Class[].class))
                                                        throw bex("Invalid 
annotation @{0} used in runtime annotations.  Annotation must define an 
onClass() method that returns a Class array.", cns(a));
                                                for (var c : 
(Class<?>[])mi.accessible().invoke(a))
-                                                       
runtimeAnnotations.append(c.getName(), a);
+                                                       
runtimeAnnotations.append(c.getName(), annotationToStore);
                                        });
 
                                        ci.getPublicMethod(x -> 
x.hasName("on")).ifPresent(mi -> {
                                                if (! 
mi.getReturnType().is(String[].class))
                                                        throw bex("Invalid 
annotation @{0} used in runtime annotations.  Annotation must define an on() 
method that returns a String array.", cns(a));
                                                for (var s : 
(String[])mi.accessible().invoke(a))
-                                                       
runtimeAnnotations.append(s, a);
+                                                       
runtimeAnnotations.append(s, annotationToStore);
                                        });
 
                                } catch (BeanRuntimeException e) {
@@ -368,6 +372,18 @@ public class AnnotationProvider {
                        return this;
                }
 
+               private static Annotation unwrapXApply(ClassInfo ci, Annotation 
a) {
+                       var valueMethod = ci.getPublicMethod(x -> 
x.hasName("value"));
+                       if (valueMethod.isEmpty())
+                               return a;
+                       var mi = valueMethod.get();
+                       var rt = mi.getReturnType().inner();
+                       if (! Annotation.class.isAssignableFrom(rt) || 
rt.isArray())
+                               return a;
+                       var inner = (Annotation) mi.accessible().invoke(a);
+                       return inner != null ? inner : a;
+               }
+
                /**
                 * Builds a new {@link AnnotationProvider} instance with the 
configured settings.
                 *
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
index db8539b76b..5151df5dee 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/Schema.java
@@ -96,7 +96,6 @@ import org.apache.juneau.oapi.*;
 @Target({ PARAMETER, METHOD, TYPE, FIELD })
 @Retention(RUNTIME)
 @Repeatable(SchemaAnnotation.Array.class)
-@ContextApply(SchemaAnnotation.Apply.class)
 @SuppressWarnings({
        "java:S100" // Annotation methods use underscore suffix or $ prefix to 
match JSON Schema keywords (e.g., default_, enum_, $ref)
 })
@@ -1344,107 +1343,6 @@ public @interface Schema {
         */
        String multipleOf() default "";
 
-       /**
-        * Dynamically apply this annotation to the specified 
classes/methods/fields.
-        *
-        * <p>
-        * Used in conjunction with {@link 
org.apache.juneau.BeanContext.Builder#applyAnnotations(Class...)} to 
dynamically apply an annotation to an existing class/method/field.
-        * It is ignored when the annotation is applied directly to 
classes/methods/fields.
-        *
-        * <h5 class='section'>Valid patterns:</h5>
-        * <ul class='spaced-list'>
-        *  <li>Classes:
-        *              <ul>
-        *                      <li>Fully qualified:
-        *                              <ul>
-        *                                      <li><js>"com.foo.MyClass"</js>
-        *                              </ul>
-        *                      <li>Fully qualified inner class:
-        *                              <ul>
-        *                                      
<li><js>"com.foo.MyClass$Inner1$Inner2"</js>
-        *                              </ul>
-        *                      <li>Simple:
-        *                              <ul>
-        *                                      <li><js>"MyClass"</js>
-        *                              </ul>
-        *                      <li>Simple inner:
-        *                              <ul>
-        *                                      
<li><js>"MyClass$Inner1$Inner2"</js>
-        *                                      <li><js>"Inner1$Inner2"</js>
-        *                                      <li><js>"Inner2"</js>
-        *                              </ul>
-        *              </ul>
-        *      <li>Methods:
-        *              <ul>
-        *                      <li>Fully qualified with args:
-        *                              <ul>
-        *                                      
<li><js>"com.foo.MyClass.myMethod(String,int)"</js>
-        *                                      
<li><js>"com.foo.MyClass.myMethod(java.lang.String,int)"</js>
-        *                                      
<li><js>"com.foo.MyClass.myMethod()"</js>
-        *                              </ul>
-        *                      <li>Fully qualified:
-        *                              <ul>
-        *                                      
<li><js>"com.foo.MyClass.myMethod"</js>
-        *                              </ul>
-        *                      <li>Simple with args:
-        *                              <ul>
-        *                                      
<li><js>"MyClass.myMethod(String,int)"</js>
-        *                                      
<li><js>"MyClass.myMethod(java.lang.String,int)"</js>
-        *                                      
<li><js>"MyClass.myMethod()"</js>
-        *                              </ul>
-        *                      <li>Simple:
-        *                              <ul>
-        *                                      <li><js>"MyClass.myMethod"</js>
-        *                              </ul>
-        *                      <li>Simple inner class:
-        *                              <ul>
-        *                                      
<li><js>"MyClass$Inner1$Inner2.myMethod"</js>
-        *                                      
<li><js>"Inner1$Inner2.myMethod"</js>
-        *                                      <li><js>"Inner2.myMethod"</js>
-        *                              </ul>
-        *              </ul>
-        *      <li>Fields:
-        *              <ul>
-        *                      <li>Fully qualified:
-        *                              <ul>
-        *                                      
<li><js>"com.foo.MyClass.myField"</js>
-        *                              </ul>
-        *                      <li>Simple:
-        *                              <ul>
-        *                                      <li><js>"MyClass.myField"</js>
-        *                              </ul>
-        *                      <li>Simple inner class:
-        *                              <ul>
-        *                                      
<li><js>"MyClass$Inner1$Inner2.myField"</js>
-        *                                      
<li><js>"Inner1$Inner2.myField"</js>
-        *                                      <li><js>"Inner2.myField"</js>
-        *                              </ul>
-        *              </ul>
-        *      <li>A comma-delimited list of anything on this list.
-        * </ul>
-        *
-        * <h5 class='section'>See Also:</h5><ul>
-        *      <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations";>Dynamically
 Applied Annotations</a>
-        * </ul>
-        *
-        * @return The annotation value.
-        */
-       String[] on() default {};
-
-       /**
-        * Dynamically apply this annotation to the specified classes.
-        *
-        * <p>
-        * Identical to {@link #on()} except allows you to specify class 
objects instead of a strings.
-        *
-        * <h5 class='section'>See Also:</h5><ul>
-        *      <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations";>Dynamically
 Applied Annotations</a>
-        * </ul>
-        *
-        * @return The annotation value.
-        */
-       Class<?>[] onClass() default {};
-
        /**
         * Synonym for {@link #pattern()}.
         *
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
index 8118764641..9f28d7caee 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaAnnotation.java
@@ -22,17 +22,13 @@ import static 
org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.jsonschema.SchemaUtils.*;
 
 import java.lang.annotation.*;
-import java.lang.reflect.*;
 import java.util.*;
 import java.util.function.*;
 
-import org.apache.juneau.*;
 import org.apache.juneau.collections.*;
 import org.apache.juneau.commons.annotation.*;
-import org.apache.juneau.commons.reflect.*;
 import org.apache.juneau.commons.utils.*;
 import org.apache.juneau.parser.*;
-import org.apache.juneau.svl.*;
 
 /**
  * Utility classes and methods for the {@link Schema @Schema} annotation.
@@ -97,29 +93,6 @@ public class SchemaAnnotation {
         */
        private SchemaAnnotation() {}
 
-       /**
-        * Applies targeted {@link Schema} annotations to a {@link 
org.apache.juneau.Context.Builder}.
-        */
-       public static class Apply extends 
AnnotationApplier<Schema,Context.Builder> {
-
-               /**
-                * Constructor.
-                *
-                * @param vr The resolver for resolving values in annotations.
-                */
-               public Apply(VarResolverSession vr) {
-                       super(Schema.class, Context.Builder.class, vr);
-               }
-
-               @Override
-               public void apply(AnnotationInfo<Schema> ai, Context.Builder b) 
{
-                       Schema a = ai.inner();
-                       if (isEmptyArray(a.on()) && isEmptyArray(a.onClass()))
-                               return;
-                       b.annotations(a);
-               }
-       }
-
        /**
         * A collection of {@link Schema @Schema annotations}.
         */
@@ -147,7 +120,7 @@ public class SchemaAnnotation {
        @SuppressWarnings({
                "java:S116" // Field names intentionally match JSON property 
names
        })
-       public static class Builder extends AppliedAnnotationObject.BuilderTMF {
+       public static class Builder extends AnnotationObject.Builder {
 
                private boolean aev;
                private boolean allowEmptyValue;
@@ -1088,67 +1061,13 @@ public class SchemaAnnotation {
                        return this;
                }
 
-               @Override /* Overridden from AppliedAnnotationObject.Builder */
-               public Builder on(String...value) {
-                       super.on(value);
-                       return this;
-               }
-
-               @Override /* Overridden from AppliedAnnotationObject.BuilderT */
-               public Builder on(Class<?>...value) {
-                       super.on(value);
-                       return this;
-               }
-
-               @Override /* Overridden from 
AppliedOnClassAnnotationObject.Builder */
-               public Builder onClass(Class<?>...value) {
-                       super.onClass(value);
-                       return this;
-               }
-
-               @Override /* Overridden from AppliedAnnotationObject.BuilderM */
-               public Builder on(Method...value) {
-                       super.on(value);
-                       return this;
-               }
-
-               @Override /* Overridden from AppliedAnnotationObject.BuilderMF 
*/
-               public Builder on(Field...value) {
-                       super.on(value);
-                       return this;
-               }
-
-               @Override /* Overridden from AppliedAnnotationObject.BuilderT */
-               public Builder on(ClassInfo...value) {
-                       super.on(value);
-                       return this;
-               }
-
-               @Override /* Overridden from AppliedAnnotationObject.BuilderT */
-               public Builder onClass(ClassInfo...value) {
-                       super.onClass(value);
-                       return this;
-               }
-
-               @Override /* Overridden from AppliedAnnotationObject.BuilderTMF 
*/
-               public Builder on(FieldInfo...value) {
-                       super.on(value);
-                       return this;
-               }
-
-               @Override /* Overridden from AppliedAnnotationObject.BuilderTMF 
*/
-               public Builder on(MethodInfo...value) {
-                       super.on(value);
-                       return this;
-               }
-
        }
 
        @SuppressWarnings({
                "java:S116",  // Field names intentionally match JSON property 
names
                "java:S2160"  // equals() inherited from AnnotationObject 
compares all annotation interface methods; subclass fields are accessed via 
those methods
        })
-       private static class Object extends AppliedOnClassAnnotationObject 
implements Schema {
+       private static class Object extends AnnotationObject implements Schema {
 
                private final String[] description;
                private final boolean aev;
@@ -1787,26 +1706,6 @@ public class SchemaAnnotation {
                return new Builder();
        }
 
-       /**
-        * Instantiates a new builder for this class.
-        *
-        * @param on The targets this annotation applies to.
-        * @return A new builder object.
-        */
-       public static Builder create(Class<?>...on) {
-               return create().on(on);
-       }
-
-       /**
-        * Instantiates a new builder for this class.
-        *
-        * @param on The targets this annotation applies to.
-        * @return A new builder object.
-        */
-       public static Builder create(String...on) {
-               return create().on(on);
-       }
-
        /**
         * Returns <jk>true</jk> if the specified annotation contains all 
default values.
         *
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApply.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApply.java
new file mode 100644
index 0000000000..446fb4b91d
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApply.java
@@ -0,0 +1,99 @@
+/*
+ * 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.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+/**
+ * Dynamically applies a {@link Schema @Schema} annotation to specified 
classes, methods, or fields.
+ *
+ * <p>
+ * This annotation separates the <b>targeting</b> concern ({@link 
#on()}/{@link #onClass()}) from the
+ * <b>content</b> concern ({@link #value()}), enabling {@link Schema @Schema} 
to be a pure data annotation
+ * without marshall-specific application machinery.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ *     <ja>@SchemaApply</ja>(on=<js>"com.example.Foo"</js>, 
value=<ja>@Schema</ja>(format=<js>"date-time"</js>))
+ *     <jk>public class</jk> MyConfig {}
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations";>Dynamically
 Applied Annotations</a>
+ * </ul>
+ */
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+@Repeatable(SchemaApply.Array.class)
+@ContextApply(SchemaApplyAnnotation.Applier.class)
+public @interface SchemaApply {
+
+       /**
+        * The {@link Schema @Schema} annotation to apply.
+        *
+        * @return The annotation value.
+        */
+       Schema value();
+
+       /**
+        * Dynamically apply this annotation to the specified 
classes/methods/fields.
+        *
+        * <p>
+        * Identifies the targets this annotation applies to using 
fully-qualified names.
+        *
+        * <h5 class='section'>See Also:</h5><ul>
+        *      <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations";>Dynamically
 Applied Annotations</a>
+        * </ul>
+        *
+        * @return The annotation value.
+        */
+       String[] on() default {};
+
+       /**
+        * Dynamically apply this annotation to the specified classes.
+        *
+        * <p>
+        * Identical to {@link #on()} except allows you to specify class 
objects instead of strings.
+        *
+        * <h5 class='section'>See Also:</h5><ul>
+        *      <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/DynamicallyAppliedAnnotations";>Dynamically
 Applied Annotations</a>
+        * </ul>
+        *
+        * @return The annotation value.
+        */
+       Class<?>[] onClass() default {};
+
+       /**
+        * A collection of {@link SchemaApply @SchemaApply annotations}.
+        */
+       @Documented
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       public @interface Array {
+
+               /**
+                * The child annotations.
+                *
+                * @return The annotation value.
+                */
+               SchemaApply[] value();
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApplyAnnotation.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApplyAnnotation.java
new file mode 100644
index 0000000000..7394e8340b
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/annotation/SchemaApplyAnnotation.java
@@ -0,0 +1,231 @@
+/*
+ * 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.annotation;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+
+import java.lang.annotation.*;
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.annotation.*;
+import org.apache.juneau.commons.reflect.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Utility classes and methods for the {@link SchemaApply @SchemaApply} 
annotation.
+ *
+ */
+public class SchemaApplyAnnotation {
+
+       /**
+        * Prevents instantiation.
+        */
+       private SchemaApplyAnnotation() {}
+
+       /**
+        * Applies targeted {@link SchemaApply} annotations to a {@link 
org.apache.juneau.Context.Builder}.
+        *
+        * <p>
+        * Passes the {@link SchemaApply @SchemaApply} annotation through to 
the builder's annotation list.
+        * The {@link 
org.apache.juneau.commons.reflect.AnnotationProvider.Builder#addRuntimeAnnotations(java.util.List)}
+        * method handles unwrapping the nested {@link Schema @Schema} from 
{@link SchemaApply#value()} and registering it
+        * under the targets specified by {@link SchemaApply#on()} and {@link 
SchemaApply#onClass()}.
+        */
+       public static class Applier extends 
AnnotationApplier<SchemaApply,Context.Builder> {
+
+               /**
+                * Constructor.
+                *
+                * @param vr The resolver for resolving values in annotations.
+                */
+               public Applier(VarResolverSession vr) {
+                       super(SchemaApply.class, Context.Builder.class, vr);
+               }
+
+               @Override
+               public void apply(AnnotationInfo<SchemaApply> ai, 
Context.Builder b) {
+                       SchemaApply a = ai.inner();
+                       if (isEmptyArray(a.on()) && isEmptyArray(a.onClass()))
+                               return;
+                       b.annotations(a);
+               }
+       }
+
+       /**
+        * Builder class.
+        *
+        * <h5 class='section'>See Also:</h5><ul>
+        *      <li class='jm'>{@link 
org.apache.juneau.BeanContext.Builder#annotations(Annotation...)}
+        * </ul>
+        */
+       public static class Builder extends AppliedAnnotationObject.BuilderTMF {
+
+               Schema value = SchemaAnnotation.DEFAULT;
+
+               /**
+                * Constructor.
+                */
+               protected Builder() {
+                       super(SchemaApply.class);
+               }
+
+               /**
+                * Sets the {@link SchemaApply#value()} property on this 
annotation.
+                *
+                * @param value The new value for this property.
+                * @return This object.
+                */
+               public Builder value(Schema value) {
+                       this.value = value;
+                       return this;
+               }
+
+               @Override /* Overridden from AppliedAnnotationObject.Builder */
+               public Builder on(String...value) {
+                       super.on(value);
+                       return this;
+               }
+
+               @Override /* Overridden from AppliedAnnotationObject.BuilderT */
+               public Builder on(Class<?>...value) {
+                       super.on(value);
+                       return this;
+               }
+
+               @Override /* Overridden from 
AppliedOnClassAnnotationObject.Builder */
+               public Builder onClass(Class<?>...value) {
+                       super.onClass(value);
+                       return this;
+               }
+
+               @Override /* Overridden from AppliedAnnotationObject.BuilderM */
+               public Builder on(Method...value) {
+                       super.on(value);
+                       return this;
+               }
+
+               @Override /* Overridden from AppliedAnnotationObject.BuilderMF 
*/
+               public Builder on(Field...value) {
+                       super.on(value);
+                       return this;
+               }
+
+               @Override /* Overridden from AppliedAnnotationObject.BuilderT */
+               public Builder on(ClassInfo...value) {
+                       super.on(value);
+                       return this;
+               }
+
+               @Override /* Overridden from AppliedAnnotationObject.BuilderT */
+               public Builder onClass(ClassInfo...value) {
+                       super.onClass(value);
+                       return this;
+               }
+
+               @Override /* Overridden from AppliedAnnotationObject.BuilderTMF 
*/
+               public Builder on(FieldInfo...value) {
+                       super.on(value);
+                       return this;
+               }
+
+               @Override /* Overridden from AppliedAnnotationObject.BuilderTMF 
*/
+               public Builder on(MethodInfo...value) {
+                       super.on(value);
+                       return this;
+               }
+
+               /**
+                * Instantiates a new {@link SchemaApply @SchemaApply} object 
initialized with this builder.
+                *
+                * @return A new {@link SchemaApply} object.
+                */
+               public SchemaApply build() {
+                       return new Object(this);
+               }
+       }
+
+       @SuppressWarnings({
+               "java:S2160" // equals() inherited from AnnotationObject 
compares all annotation interface methods; subclass fields are accessed via 
those methods
+       })
+       private static class Object extends AppliedOnClassAnnotationObject 
implements SchemaApply {
+
+               private final Schema value;
+
+               Object(SchemaApplyAnnotation.Builder b) {
+                       super(b);
+                       value = b.value;
+               }
+
+               @Override /* Overridden from SchemaApply */
+               public Schema value() {
+                       return value;
+               }
+
+               @Override /* Overridden from SchemaApply */
+               public String[] on() {
+                       return super.on();
+               }
+
+               @Override /* Overridden from SchemaApply */
+               public Class<?>[] onClass() {
+                       return super.onClass();
+               }
+       }
+
+       /** Default value */
+       public static final SchemaApply DEFAULT = create().build();
+
+       /**
+        * Instantiates a new builder for this class.
+        *
+        * @return A new builder object.
+        */
+       public static Builder create() {
+               return new Builder();
+       }
+
+       /**
+        * Instantiates a new builder for this class.
+        *
+        * @param on The targets this annotation applies to.
+        * @return A new builder object.
+        */
+       public static Builder create(Class<?>...on) {
+               return create().on(on);
+       }
+
+       /**
+        * Instantiates a new builder for this class.
+        *
+        * @param on The targets this annotation applies to.
+        * @return A new builder object.
+        */
+       public static Builder create(String...on) {
+               return create().on(on);
+       }
+
+       /**
+        * Returns <jk>true</jk> if the specified annotation contains all 
default values.
+        *
+        * @param a The annotation to check.
+        * @return <jk>true</jk> if the specified annotation contains all 
default values.
+        */
+       public static boolean empty(SchemaApply a) {
+               return a == null || DEFAULT.equals(a);
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
 
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
index 44f7174a0f..c9383722a1 100755
--- 
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/JsonSchemaGeneratorTest.java
@@ -1168,9 +1168,9 @@ class JsonSchemaGeneratorTest extends TestBase {
                
assertJson("{description:'baz',format:'bar',type:'foo',properties:{f1:{type:'integer',format:'int32'}}}",
 s.getSchema(A1a.class));
        }
 
-       @Schema(on="Dummy1",type="foo",format="bar",description="baz")
-       @Schema(on="A1a",type="foo",format="bar",description="baz")
-       @Schema(on="Dummy2",type="foo",format="bar",description="baz")
+       
@SchemaApply(on="Dummy1",value=@Schema(type="foo",format="bar",description="baz"))
+       
@SchemaApply(on="A1a",value=@Schema(type="foo",format="bar",description="baz"))
+       
@SchemaApply(on="Dummy2",value=@Schema(type="foo",format="bar",description="baz"))
        private static class A1aConfig {}
 
        public static class A1a {
@@ -1192,7 +1192,7 @@ class JsonSchemaGeneratorTest extends TestBase {
                
assertJson("{type:'object',properties:{f1:{description:'baz',format:'bar',type:'foo'}}}",
 s.getSchema(A2a.class));
        }
 
-       @Schema(on="A2a.f1",type="foo",format="bar",description="baz")
+       
@SchemaApply(on="A2a.f1",value=@Schema(type="foo",format="bar",description="baz"))
        private static class A2aConfig {}
 
        public static class A2a {
@@ -1214,7 +1214,7 @@ class JsonSchemaGeneratorTest extends TestBase {
                
assertJson("{type:'object',properties:{f1:{description:'baz',format:'bar',type:'foo'}}}",
 s.getSchema(A3a.class));
        }
 
-       @Schema(on="A3a.getF1",type="foo",format="bar",description="baz")
+       
@SchemaApply(on="A3a.getF1",value=@Schema(type="foo",format="bar",description="baz"))
        private static class A3aConfig {}
 
        public static class A3a {
@@ -1238,7 +1238,7 @@ class JsonSchemaGeneratorTest extends TestBase {
                
assertJson("{type:'object',properties:{f1:{description:'baz',format:'bar',type:'foo'}}}",
 s.getSchema(A4a.class));
        }
 
-       @Schema(on="A4a.setF1",type="foo",format="bar",description="baz")
+       
@SchemaApply(on="A4a.setF1",value=@Schema(type="foo",format="bar",description="baz"))
        private static class A4aConfig {}
 
        public static class A4a {
@@ -1272,7 +1272,7 @@ class JsonSchemaGeneratorTest extends TestBase {
                
assertJson("{type:'array',items:{type:'array',items:{description:'baz',format:'bar',type:'foo'}}}",
 s.getSchema(SimpleBean[][].class));
        }
 
-       @Schema(on="SwapWithAnnotation2", 
type="foo",format="bar",description="baz")
+       @SchemaApply(on="SwapWithAnnotation2", 
value=@Schema(type="foo",format="bar",description="baz"))
        private static class SwapWithAnnotation2Config {}
 
        public static class SwapWithAnnotation2 extends 
ObjectSwap<SimpleBean,Integer> {}
@@ -1281,7 +1281,7 @@ class JsonSchemaGeneratorTest extends TestBase {
        // @JsonSchema on ObjectSwap
        
//====================================================================================================
 
-       @Schema(onClass=B.class,$ref="ref")
+       @SchemaApply(onClass=B.class, value=@Schema($ref="ref"))
        static class BConfig {}
 
        static class B {}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
index 2f7f62702f..2bd1fb55ac 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaAnnotation_Test.java
@@ -30,10 +30,6 @@ import org.junit.jupiter.api.*;
 })
 class SchemaAnnotation_Test extends TestBase {
 
-       private static final String CNAME = 
SchemaAnnotation_Test.class.getName();
-
-       private static class X1 {}
-
        
//------------------------------------------------------------------------------------------------------------------
        // Basic tests
        
//------------------------------------------------------------------------------------------------------------------
@@ -78,8 +74,6 @@ class SchemaAnnotation_Test extends TestBase {
                .minProperties(12)
                .mo("s")
                .multipleOf("t")
-               .on("u")
-               .onClass(X1.class)
                .p("v")
                .pattern("w")
                .properties("x")
@@ -135,8 +129,6 @@ class SchemaAnnotation_Test extends TestBase {
                .minProperties(12)
                .mo("s")
                .multipleOf("t")
-               .on("u")
-               .onClass(X1.class)
                .p("v")
                .pattern("w")
                .properties("x")
@@ -154,8 +146,8 @@ class SchemaAnnotation_Test extends TestBase {
 
        @Test void a01_basic() {
                assertBean(a1,
-                       
"$ref,default_,enum_,additionalProperties,aev,allOf,allowEmptyValue,cf,collectionFormat,d,description,df,discriminator,e,emax,emin,exclusiveMaximum,exclusiveMinimum,externalDocs{description,url},f,format,ignore,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items,max,maxItems,maxLength,maxi,maximum,maxl
 [...]
-                       
"c,[a],[b],[d],false,[e],false,f,g,[h],[i],[j],k,[l],true,true,true,true,{[],},m,n,true,{,[],[],,,[],[],[],false,false,false,false,,,{,[],[],,,[],[],[],false,false,false,false,,,[],,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},o,2,4,6,1,p,3,5,q,8,10,12,7,r,9,11,s,t,[u],[X1],v,w,[x],true,true,true,true,false,false,z,aa,bb,true,true,[cc]");
+                       
"$ref,default_,enum_,additionalProperties,aev,allOf,allowEmptyValue,cf,collectionFormat,d,description,df,discriminator,e,emax,emin,exclusiveMaximum,exclusiveMinimum,externalDocs{description,url},f,format,ignore,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items{$ref,default_,enum_,cf,collectionFormat,description,df,e,emax,emin,exclusiveMaximum,exclusiveMinimum,f,format,items,max,maxItems,maxLength,maxi,maximum,maxl
 [...]
+                       
"c,[a],[b],[d],false,[e],false,f,g,[h],[i],[j],k,[l],true,true,true,true,{[],},m,n,true,{,[],[],,,[],[],[],false,false,false,false,,,{,[],[],,,[],[],[],false,false,false,false,,,[],,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},,-1,-1,-1,,-1,,-1,-1,-1,,-1,,,,,,,false,false},o,2,4,6,1,p,3,5,q,8,10,12,7,r,9,11,s,t,v,w,[x],true,true,true,true,false,false,z,aa,bb,true,true,[cc]");
        }
 
        @Test void a02_testEquivalency() {
@@ -178,27 +170,6 @@ class SchemaAnnotation_Test extends TestBase {
        // Other methods.
        
//------------------------------------------------------------------------------------------------------------------
 
-       public static class C1 {
-               public int f1;
-               public void m1() {}
-       }
-       public static class C2 {
-               public int f2;
-               public void m2() {}
-       }
-
-       @Test void c01_otherMethods() throws Exception {
-               var c1 = SchemaAnnotation.create(C1.class).on(C2.class).build();
-               var c2 = SchemaAnnotation.create("a").on("b").build();
-               var c3 = 
SchemaAnnotation.create().on(C1.class.getField("f1")).on(C2.class.getField("f2")).build();
-               var c4 = 
SchemaAnnotation.create().on(C1.class.getMethod("m1")).on(C2.class.getMethod("m2")).build();
-
-               assertBean(c1, "on", "["+CNAME+"$C1,"+CNAME+"$C2]");
-               assertBean(c2, "on", "[a,b]");
-               assertBean(c3, "on", "["+CNAME+"$C1.f1,"+CNAME+"$C2.f2]");
-               assertBean(c4, "on", "["+CNAME+"$C1.m1(),"+CNAME+"$C2.m2()]");
-       }
-
        
//------------------------------------------------------------------------------------------------------------------
        // Comparison with declared annotations.
        
//------------------------------------------------------------------------------------------------------------------
@@ -243,8 +214,6 @@ class SchemaAnnotation_Test extends TestBase {
                minProperties=12,
                mo="s",
                multipleOf="t",
-               on="u",
-               onClass=X1.class,
                p="v",
                pattern="w",
                properties="x",
@@ -302,8 +271,6 @@ class SchemaAnnotation_Test extends TestBase {
                minProperties=12,
                mo="s",
                multipleOf="t",
-               on="u",
-               onClass=X1.class,
                p="v",
                pattern="w",
                properties="x",
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaApplyAnnotation_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaApplyAnnotation_Test.java
new file mode 100644
index 0000000000..a710d6a0a4
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/jsonschema/annotation/SchemaApplyAnnotation_Test.java
@@ -0,0 +1,131 @@
+/*
+ * 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.jsonschema.annotation;
+
+import static org.apache.juneau.TestUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.junit.jupiter.api.*;
+
+@SuppressWarnings({
+       "java:S1186" // Empty test method intentional for framework testing
+})
+class SchemaApplyAnnotation_Test extends TestBase {
+
+       private static final String CNAME = 
SchemaApplyAnnotation_Test.class.getName();
+
+       private static class X1 {}
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Basic tests
+       
//------------------------------------------------------------------------------------------------------------------
+
+       SchemaApply a1 = SchemaApplyAnnotation.create()
+               .on("u")
+               .onClass(X1.class)
+               .value(SchemaAnnotation.create().format("date-time").build())
+               .build();
+
+       SchemaApply a2 = SchemaApplyAnnotation.create()
+               .on("u")
+               .onClass(X1.class)
+               .value(SchemaAnnotation.create().format("date-time").build())
+               .build();
+
+       @Test void a01_basic() {
+               assertBean(a1, "on,onClass,value{format}", 
"[u],[X1],{date-time}");
+       }
+
+       @Test void a02_testEquivalency() {
+               assertEquals(a2, a1);
+               assertNotEqualsAny(a1.hashCode(), 0, -1);
+               assertEquals(a1.hashCode(), a2.hashCode());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // PropertyStore equivalency.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void b01_testEquivalencyInPropertyStores() {
+               var bc1 = BeanContext.create().annotations(a1).build();
+               var bc2 = BeanContext.create().annotations(a2).build();
+               assertSame(bc1, bc2);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Targeting methods
+       
//------------------------------------------------------------------------------------------------------------------
+
+       public static class C1 {
+               public int f1;
+               public void m1() {}
+       }
+       public static class C2 {
+               public int f2;
+               public void m2() {}
+       }
+
+       @Test void c01_targetingMethods() throws Exception {
+               var c1 = 
SchemaApplyAnnotation.create(C1.class).on(C2.class).build();
+               var c2 = SchemaApplyAnnotation.create("a").on("b").build();
+               var c3 = 
SchemaApplyAnnotation.create().on(C1.class.getField("f1")).on(C2.class.getField("f2")).build();
+               var c4 = 
SchemaApplyAnnotation.create().on(C1.class.getMethod("m1")).on(C2.class.getMethod("m2")).build();
+
+               assertBean(c1, "on", "["+CNAME+"$C1,"+CNAME+"$C2]");
+               assertBean(c2, "on", "[a,b]");
+               assertBean(c3, "on", "["+CNAME+"$C1.f1,"+CNAME+"$C2.f2]");
+               assertBean(c4, "on", "["+CNAME+"$C1.m1(),"+CNAME+"$C2.m2()]");
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Comparison with declared annotations.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @SchemaApply(on="u", onClass=X1.class, 
value=@Schema(format="date-time"))
+       public static class D1 {}
+       SchemaApply d1 = D1.class.getAnnotationsByType(SchemaApply.class)[0];
+
+       @SchemaApply(on="u", onClass=X1.class, 
value=@Schema(format="date-time"))
+       public static class D2 {}
+       SchemaApply d2 = D2.class.getAnnotationsByType(SchemaApply.class)[0];
+
+       @Test void d01_comparisonWithDeclarativeAnnotations() {
+               assertEqualsAll(a1, d1, d2);
+               assertNotEqualsAny(a1.hashCode(), 0, -1);
+               assertEqualsAll(a1.hashCode(), d1.hashCode(), d2.hashCode());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Dynamic application via BeanContext
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void e01_dynamicApplication() {
+               var schema = 
SchemaAnnotation.create().type("string").format("date-time").build();
+               var apply = 
SchemaApplyAnnotation.create("com.example.Foo").value(schema).build();
+
+               assertBean(apply, "on,value{type,format}", 
"[com.example.Foo],{string,date-time}");
+       }
+
+       @Test void e02_emptyCheck() {
+               assertTrue(SchemaApplyAnnotation.empty(null));
+               
assertTrue(SchemaApplyAnnotation.empty(SchemaApplyAnnotation.DEFAULT));
+               assertFalse(SchemaApplyAnnotation.empty(a1));
+       }
+}
diff --git a/todo/decouple-rest-common-from-marshall.md 
b/todo/decouple-rest-common-from-marshall.md
index 2ea57815fe..3af7ed1347 100644
--- a/todo/decouple-rest-common-from-marshall.md
+++ b/todo/decouple-rest-common-from-marshall.md
@@ -33,21 +33,26 @@ juneau-commons -> juneau-rest-common -> juneau-marshall -> 
juneau-rest-client
 ## Phase 2 — Move `@Schema` and related annotations to `juneau-commons`
 
 **Difficulty**: Hard  
-**Impact**: 83 files (7 HTTP annotations + Swagger model classes)
+**Impact**: Large (HTTP annotations, Swagger models)
+
+**Prerequisite**: The global `XApply` annotation split must be completed 
first. See [`xapply-annotation-split.md`](xapply-annotation-split.md) for the 
full plan covering the removal of `on`/`onClass` from all 30 context-appliable 
annotations into companion `@XApply` annotations.
 
 `@Schema` is the #1 blocker. Every HTTP annotation references it via `Schema 
schema() default @Schema`. `InvalidAnnotationException` is also used.
 
-### Classes to move
-- `org.apache.juneau.annotation.Schema` → 
`org.apache.juneau.commons.annotation.Schema`
-- `org.apache.juneau.annotation.Items` → 
`org.apache.juneau.commons.annotation.Items`
-- `org.apache.juneau.annotation.SubItems` → 
`org.apache.juneau.commons.annotation.SubItems`
-- `org.apache.juneau.annotation.ExternalDocs` → 
`org.apache.juneau.commons.annotation.ExternalDocs`
-- `InvalidAnnotationException`
+### Classes to move (commons)
+
+- `org.apache.juneau.annotation.Schema` -> 
`org.apache.juneau.commons.annotation.Schema` (no 
`on`/`onClass`/`@ContextApply` after XApply split)
+- `SchemaAnnotation` -> commons **minus** apply-only / marshall-only inner 
types
+- `org.apache.juneau.annotation.Items` -> 
`org.apache.juneau.commons.annotation.Items`
+- `org.apache.juneau.annotation.SubItems` -> 
`org.apache.juneau.commons.annotation.SubItems`
+- `org.apache.juneau.annotation.ExternalDocs` -> 
`org.apache.juneau.commons.annotation.ExternalDocs`
+- `InvalidAnnotationException` (and any other schema-adjacent types that must 
compile without marshall)
 
 ### Considerations
-- `SchemaAnnotation` (empty(), DEFAULT, builder utilities) must also move or 
be split
-- `@Schema` references `JsonSchemaSerializer` in javadoc — javadoc link would 
break
-- `HttpPartSchema` reads `@Schema` attributes — must check if `HttpPartSchema` 
can also move or if reflective access suffices
+
+- **`@Schema` Javadoc** references marshall-only types (e.g. 
`JsonSchemaSerializer`) -- replace with neutral text or doclinks that do not 
create a compile dependency from commons to marshall.
+- **`HttpPartSchema`** reads `@Schema` -- after the move, references 
**`org.apache.juneau.commons.annotation.Schema`** (Phase 3 may move 
`HttpPartSchema` itself to commons).
+- **Repeatability**: `@Schema` keeps its own `@Repeatable(Schema.Array.class)` 
for inline use; `@SchemaApply` has its own `@Repeatable` container.
 
 ---
 
@@ -70,7 +75,7 @@ juneau-commons -> juneau-rest-common -> juneau-marshall -> 
juneau-rest-client
 - `HttpPartMarshalling` (references serializer/parser types)
 
 ### Considerations
-- `HttpPartSchema.Builder` currently has `apply(HttpPartMarshalling)` which 
reads `serializer()`/`parser()` — this creates a dependency on marshall. Could 
be split: schema model in commons, marshalling-aware builder extension in 
marshall.
+- `HttpPartSchema.Builder` currently has `apply(HttpPartMarshalling)` which 
reads `serializer()`/`parser()` -- this creates a dependency on marshall. Could 
be split: schema model in commons, marshalling-aware builder extension in 
marshall.
 
 ---
 
@@ -79,7 +84,7 @@ juneau-commons -> juneau-rest-common -> juneau-marshall -> 
juneau-rest-client
 **Difficulty**: Medium  
 **Impact**: 3 files (SerializedHeader, SerializedPart, SerializedEntity) + 3 
factory helpers
 
-These classes have the heaviest marshall dependencies — they use `httppart`, 
`oapi`, `serializer`, and `urlencoding` packages. They bridge the gap between 
REST-common HTTP types and marshall serialization.
+These classes have the heaviest marshall dependencies -- they use `httppart`, 
`oapi`, `serializer`, and `urlencoding` packages. They bridge the gap between 
REST-common HTTP types and marshall serialization.
 
 ### Options
 1. **Move to marshall**: These classes logically belong in the serialization 
layer
@@ -114,7 +119,7 @@ Move `SerializedHeader`, `SerializedPart`, 
`SerializedEntity` and their factory
 | Priority | Package | Files | Target module | Status |
 |----------|---------|-------|----------------|--------|
 | 1 | `MediaType`, `MediaRanges`, `StringRanges`, etc. | many | juneau-commons 
(`org.apache.juneau.commons.http`) | Done |
-| 2 | `@Schema`, `@Items`, `@SubItems` | 83 | juneau-commons | Pending |
+| 2 | `@Schema`, `@Items`, etc. + `@XApply` in marshall | 83+ | commons + 
marshall (`@SchemaApply`, etc.) | Pending |
 | 3 | `HttpPartSchema`, `HttpPartType` | 12 | juneau-commons | Pending |
 | 4 | `Serialized*` bridge classes | 6 | juneau-marshall or bridge | Pending |
 | 5 | `VarResolverSession`, `BeanCreator` | 5 | juneau-commons or optional | 
Pending |
@@ -124,4 +129,5 @@ Move `SerializedHeader`, `SerializedPart`, 
`SerializedEntity` and their factory
 - **Split-package**: HTTP media types moved out of `org.apache.juneau` into 
`org.apache.juneau.commons.http`
 - **Binary compatibility**: All moves require recompilation of downstream 
modules
 - **Schema complexity**: `@Schema` has ~50 attributes and deep integration 
with serializers
+- **Breaking `XApply` migration**: Replacing `on`/`onClass` on 
context-appliable annotations touches many call sites; plan for a single major 
release boundary and migration notes
 - **Incremental approach**: Each phase should be self-contained and buildable 
independently
diff --git a/todo/xapply-annotation-split.md b/todo/xapply-annotation-split.md
new file mode 100644
index 0000000000..1e4e4290e8
--- /dev/null
+++ b/todo/xapply-annotation-split.md
@@ -0,0 +1,230 @@
+# Split `on`/`onClass` into separate `@XApply` annotations
+
+## Goal
+
+Remove `on()` and `onClass()` from all context-appliable annotations and move 
them into companion `@XApply` annotations. This separates annotation 
**content** (what to configure) from **targeting** (where to apply it), 
enabling annotations like `@Schema` to live in `juneau-commons` while dynamic 
application machinery stays in `juneau-marshall`.
+
+## Background
+
+Today, annotations like `@Schema`, `@Bean`, `@Json`, etc. serve two roles:
+
+1. **Inline declaration** — placed directly on a class/method/field, no 
`on`/`onClass` set.
+2. **Dynamic application** — placed on a config class with `on`/`onClass` set, 
applied later via `BeanContext.Builder.applyAnnotations()`.
+
+The `@ContextApply` meta-annotation and `AnnotationApplier` machinery in 
marshall handles role 2. The applier checks `on`/`onClass`; if populated, it 
stores the annotation for later target matching.
+
+**Problem:** `on`/`onClass` and `@ContextApply` are **marshall-only** 
concepts, but they live on annotations that should be usable in 
`juneau-commons` or `juneau-rest-common` without pulling in marshall.
+
+## Design
+
+### Before (current)
+
+```java
+@Schema(on = "com.example.Foo", format = "date-time")
+@Bean(onClass = Foo.class, sort = true)
+public class MyConfig {}
+```
+
+### After (proposed)
+
+```java
+@SchemaApply(on = "com.example.Foo", value = @Schema(format = "date-time"))
+@BeanApply(onClass = Foo.class, value = @Bean(sort = true))
+public class MyConfig {}
+```
+
+- `@Schema` has **no** `on`, `onClass`, or `@ContextApply` — it is a pure data 
annotation.
+- `@SchemaApply` lives in marshall, carries `on`/`onClass` and 
`@ContextApply`, and wraps the annotation via a `value()` member.
+- Breaking change: existing `@Schema(on = ...)` usages must be rewritten.
+
+---
+
+## Annotations in scope
+
+### Group A: `org.apache.juneau.annotation` — both `on()` and `onClass()`
+
+These have the `AppliedOnClassAnnotationObject` builder base and target types 
(classes).
+
+| Annotation | Builder base | Applier target | Companion file |
+|---|---|---|---|
+| `@Schema` | `BuilderTMF` | `Context.Builder` | `SchemaAnnotation.java` |
+| `@Swap` | `BuilderTMF` | `BeanContext.Builder` | `SwapAnnotation.java` |
+| `@Bean` | `BuilderT` | `BeanContext.Builder` | `BeanAnnotation.java` |
+| `@BeanIgnore` | `BuilderTMFC` | `BeanContext.Builder` | 
`BeanIgnoreAnnotation.java` |
+| `@Uri` | `BuilderTMF` | `BeanContext.Builder` | `UriAnnotation.java` |
+| `@Marshalled` | `BuilderT` | `BeanContext.Builder` | 
`MarshalledAnnotation.java` |
+| `@Example` | `BuilderTMF` | `BeanContext.Builder` | `ExampleAnnotation.java` 
|
+
+### Group B: `org.apache.juneau.annotation` — `on()` only (no `onClass()`)
+
+These have the `AppliedAnnotationObject` builder base and target 
methods/fields/constructors only.
+
+| Annotation | Builder base | Companion file |
+|---|---|---|
+| `@Beanp` | `BuilderMF` | `BeanpAnnotation.java` |
+| `@Beanc` | `BuilderC` | `BeancAnnotation.java` |
+| `@ParentProperty` | `BuilderMF` | `ParentPropertyAnnotation.java` |
+| `@NameProperty` | `BuilderMF` | `NamePropertyAnnotation.java` |
+
+### Group C: Format-specific annotations (marshall subpackages) — both `on()` 
and `onClass()`
+
+| Annotation | Package |
+|---|---|
+| `@Json` | `json/annotation` |
+| `@Xml` | `xml/annotation` |
+| `@Html` | `html/annotation` |
+| `@HtmlLink` | `html/annotation` |
+| `@Csv` | `csv/annotation` |
+| `@Uon` | `uon/annotation` |
+| `@UrlEncoding` | `urlencoding/annotation` |
+| `@MsgPack` | `msgpack/annotation` |
+| `@OpenApi` | `oapi/annotation` |
+| `@PlainText` | `plaintext/annotation` |
+| `@SoapXml` | `soap/annotation` |
+| `@Cbor` | `cbor/annotation` |
+| `@Bson` | `bson/annotation` |
+| `@Proto` | `proto/annotation` |
+| `@Hjson` | `hjson/annotation` |
+| `@Hocon` | `hocon/annotation` |
+| `@Ini` | `ini/annotation` |
+| `@Markdown` | `markdown/annotation` |
+| `@Parquet` | `parquet/annotation` |
+
+### Group D: marshall-rdf
+
+| Annotation | Package |
+|---|---|
+| `@Rdf` | `jena/annotation` |
+
+### Group E: REST annotations (juneau-rest-server)
+
+These annotations also have `on()`/`onClass()` but use a different application 
mechanism (RestContext, not BeanContext). They are **out of scope** for this 
plan — their `on`/`onClass` serve a different purpose (REST method/class 
targeting) and do not need the `XApply` split.
+
+| Annotation |
+|---|
+| `@Rest`, `@RestGet`, `@RestPut`, `@RestPost`, `@RestDelete`, `@RestPatch`, 
`@RestOptions`, `@RestOp` |
+| `@RestStartCall`, `@RestPreCall`, `@RestPostCall`, `@RestEndCall`, 
`@RestInit`, `@RestPostInit`, `@RestDestroy` |
+
+Note: REST operation annotations (`@Rest` through `@RestOp`) have 
`@ContextApply` but their appliers configure the REST context, not 
`BeanContext`. Their `on()` identifies which REST class/method the annotation 
applies to. REST lifecycle annotations (`@RestStartCall` through 
`@RestDestroy`) have `on()` but no `@ContextApply`.
+
+---
+
+## Implementation plan
+
+### Step 1: Create the `@XApply` template pattern
+
+Define the structural pattern that all `@XApply` annotations will follow.
+
+For an annotation `@X` with both `on` and `onClass`:
+
+```java
+// In the same package as the original @X (stays in marshall)
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+@Repeatable(XApply.Array.class)
+@ContextApply(XApplyAnnotation.Applier.class)
+public @interface XApply {
+    X value();             // the wrapped annotation
+    String[] on() default {};
+    Class<?>[] onClass() default {};
+
+    @Documented
+    @Target(TYPE)
+    @Retention(RUNTIME)
+    public @interface Array {
+        XApply[] value();
+    }
+}
+```
+
+For `@X` with `on()` only (no `onClass`):
+
+```java
+@Documented
+@Target(TYPE)
+@Retention(RUNTIME)
+@Repeatable(XApply.Array.class)
+@ContextApply(XApplyAnnotation.Applier.class)
+public @interface XApply {
+    X value();
+    String[] on() default {};
+
+    @Documented @Target(TYPE) @Retention(RUNTIME)
+    public @interface Array { XApply[] value(); }
+}
+```
+
+### Step 2: Create `XApplyAnnotation` companion classes
+
+Each `@XApply` gets a companion `XApplyAnnotation.java` with:
+
+- Builder class extending the appropriate `AppliedAnnotationObject` or 
`AppliedOnClassAnnotationObject` builder base
+- `Applier` inner class that:
+  1. Reads `on`/`onClass` from the `@XApply`
+  2. Reads the nested `@X` annotation
+  3. Calls `b.annotations(...)` to register the nested annotation against the 
targets
+- `copy()` method for VarResolver resolution
+- `DEFAULT`, `create()`, `Array` container
+
+The applier logic is essentially the same as today's applier, just relocated 
from `XAnnotation.Apply` to `XApplyAnnotation.Applier`.
+
+### Step 3: Strip `on`/`onClass`/`@ContextApply` from `@X` annotations
+
+For each annotation in Groups A-D:
+
+1. Remove `String[] on() default {}` attribute
+2. Remove `Class<?>[] onClass() default {}` attribute (where present)
+3. Remove `@ContextApply(XAnnotation.Applier.class)` meta-annotation
+4. Remove the `Applier`/`Apply` inner class from `XAnnotation.java`
+5. Remove `on`/`onClass` from the `XAnnotation.java` Builder class
+6. Change builder base from `AppliedOnClassAnnotationObject.BuilderTMF` to a 
non-targeting base
+7. Change private Object class from `AppliedOnClassAnnotationObject` to base 
`AnnotationObject` or similar
+8. Keep `@Repeatable(XAnnotation.Array.class)` on `@X` itself (repeatability 
for inline use is still valid)
+
+### Step 4: Migrate all call sites
+
+For each usage of `@X(on = ..., ...)` or `@X(onClass = ..., ...)` in the 
codebase:
+
+1. **Declarative (annotation on class):** Rewrite to `@XApply(on = ..., value 
= @X(...))`
+2. **Programmatic (builder API):** Rewrite from 
`XAnnotation.create().on(...).build()` to 
`XApplyAnnotation.create().on(...).value(XAnnotation.create()...build()).build()`
+
+### Step 5: Update tests
+
+Every `*Annotation_Test.java` file needs updates for the new structure. Also 
update integration tests that use `on`/`onClass` declaratively.
+
+### Step 6: Update `AnnotationWorkList` and `Context.Builder`
+
+The annotation discovery machinery (`CONTEXT_APPLY_FILTER`, `traverse()`, 
`applyAnnotation()`) should work unchanged because `@XApply` carries 
`@ContextApply` and its applier handles the nesting. Verify this with tests.
+
+### Step 7: Release notes
+
+Document the breaking change with before/after examples for common patterns.
+
+---
+
+## Ordering
+
+This work is a **prerequisite** for Phase 2 of 
`decouple-rest-common-from-marshall.md` (moving `@Schema` to commons). However, 
it can be done incrementally:
+
+1. **Start with one annotation** (e.g. `@Schema`) as a proof-of-concept
+2. **Sweep remaining Group A** annotations
+3. **Sweep Group B** (`on()`-only annotations)
+4. **Sweep Group C** (format-specific annotations)
+5. **Sweep Group D** (`@Rdf`)
+
+Each step should compile and pass tests independently.
+
+---
+
+## Files affected (estimated)
+
+| Category | Count |
+|---|---|
+| New `@XApply` annotation files | ~30 |
+| New `XApplyAnnotation.java` companion files | ~30 |
+| Modified `@X` annotation files (strip `on`/`onClass`) | ~30 |
+| Modified `XAnnotation.java` files (remove Applier) | ~30 |
+| Migrated call sites (declarative `on`/`onClass` usage) | ~80 |
+| Updated test files | ~40 |
+| Release notes | 1 |

Reply via email to