CAMEL-24501: Only copy configured starter options onto the Camel target

The generated configuration classes materialise every catalog default as a
field initializer, so the starter customizer copied those defaults onto any
component, data format or language it was applied to, including a bean the
application registered and configured in Java. A value set in code that
differed from the catalog default was silently reverted at startup.

CamelPropertiesHelper now copies only the options Spring Boot recorded as
bound in BoundConfigurationProperties, including the entries of a map or
list option. The field initializers stay, so spring-configuration-metadata
and the generated documentation keep showing the defaults, and nothing
generated changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
diff --git a/components-starter/camel-gson-starter/src/test/java/org/apache/camel/component/gson/springboot/GsonDataFormatUserBeanTest.java b/components-starter/camel-gson-starter/src/test/java/org/apache/camel/component/gson/springboot/GsonDataFormatUserBeanTest.java
new file mode 100644
index 0000000..9f824d4
--- /dev/null
+++ b/components-starter/camel-gson-starter/src/test/java/org/apache/camel/component/gson/springboot/GsonDataFormatUserBeanTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.camel.component.gson.springboot;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.gson.GsonDataFormat;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The starter customizer is applied to a data format the application registered itself. Only the options configured
+ * through Spring Boot properties may be copied onto it, a catalog default must not overwrite what was set in code.
+ */
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = { CamelAutoConfiguration.class, GsonDataFormatAutoConfiguration.class,
+        GsonDataFormatUserBeanTest.TestConfiguration.class },
+                properties = { "camel.dataformat.gson.content-type-header = false" })
+public class GsonDataFormatUserBeanTest {
+
+    @Autowired
+    CamelContext context;
+
+    @Test
+    public void testCatalogDefaultDoesNotOverwriteUserBean() {
+        GsonDataFormat gson = (GsonDataFormat) context.resolveDataFormat("gson");
+
+        // set in code and never configured through properties, so the catalog default (false) must not win
+        assertTrue(gson.isPrettyPrint());
+        // configured through properties, so it is applied
+        assertFalse(gson.isContentTypeHeader());
+    }
+
+    @Configuration
+    public static class TestConfiguration {
+
+        @Bean("gson")
+        public GsonDataFormat gson() {
+            GsonDataFormat gson = new GsonDataFormat();
+            gson.setPrettyPrint(true);
+            return gson;
+        }
+    }
+}
diff --git a/components-starter/camel-http-starter/src/test/java/org/apache/camel/component/http/springboot/HttpComponentUserBeanTest.java b/components-starter/camel-http-starter/src/test/java/org/apache/camel/component/http/springboot/HttpComponentUserBeanTest.java
new file mode 100644
index 0000000..04fda28
--- /dev/null
+++ b/components-starter/camel-http-starter/src/test/java/org/apache/camel/component/http/springboot/HttpComponentUserBeanTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.camel.component.http.springboot;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.http.HttpComponent;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The starter customizer is applied to a component the application registered itself. Only the options configured
+ * through Spring Boot properties may be copied onto it, a catalog default must not overwrite what was set in code.
+ */
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = { CamelAutoConfiguration.class, HttpComponentConverter.class,
+        HttpComponentAutoConfiguration.class, HttpComponentUserBeanTest.TestConfiguration.class },
+                properties = { "camel.component.http.log-http-activity = true" })
+public class HttpComponentUserBeanTest {
+
+    @Autowired
+    CamelContext context;
+
+    @Test
+    public void testCatalogDefaultDoesNotOverwriteUserBean() {
+        HttpComponent http = context.getComponent("http", HttpComponent.class);
+
+        // set in code and never configured through properties, so the catalog default (false) must not win
+        assertTrue(http.isSkipRequestHeaders());
+        // configured through properties, so it is applied
+        assertTrue(http.isLogHttpActivity());
+    }
+
+    @Configuration
+    public static class TestConfiguration {
+
+        @Bean("http")
+        public HttpComponent http() {
+            HttpComponent http = new HttpComponent();
+            http.setSkipRequestHeaders(true);
+            return http;
+        }
+    }
+}
diff --git a/components-starter/camel-jsonpath-starter/src/test/java/org/apache/camel/component/jsonpath/springboot/test/JsonPathLanguageUserBeanTest.java b/components-starter/camel-jsonpath-starter/src/test/java/org/apache/camel/component/jsonpath/springboot/test/JsonPathLanguageUserBeanTest.java
new file mode 100644
index 0000000..5f207c5
--- /dev/null
+++ b/components-starter/camel-jsonpath-starter/src/test/java/org/apache/camel/component/jsonpath/springboot/test/JsonPathLanguageUserBeanTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.camel.component.jsonpath.springboot.test;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.jsonpath.JsonPathLanguage;
+import org.apache.camel.jsonpath.springboot.JsonPathLanguageAutoConfiguration;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The starter customizer is applied to a language the application registered itself. Only the options configured
+ * through Spring Boot properties may be copied onto it, a catalog default must not overwrite what was set in code.
+ */
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = { CamelAutoConfiguration.class, JsonPathLanguageAutoConfiguration.class,
+        JsonPathLanguageUserBeanTest.TestConfiguration.class },
+                properties = { "camel.language.jsonpath.write-as-string = true" })
+public class JsonPathLanguageUserBeanTest {
+
+    @Autowired
+    CamelContext context;
+
+    @Test
+    public void testCatalogDefaultDoesNotOverwriteUserBean() {
+        JsonPathLanguage jsonpath = (JsonPathLanguage) context.resolveLanguage("jsonpath");
+
+        // set in code and never configured through properties, so the catalog default (false) must not win
+        assertTrue(jsonpath.isSuppressExceptions());
+        // configured through properties, so it is applied
+        assertTrue(jsonpath.isWriteAsString());
+    }
+
+    @Configuration
+    public static class TestConfiguration {
+
+        @Bean("jsonpath")
+        public JsonPathLanguage jsonpath() {
+            JsonPathLanguage jsonpath = new JsonPathLanguage();
+            jsonpath.setSuppressExceptions(true);
+            return jsonpath;
+        }
+    }
+}
diff --git a/core/camel-spring-boot/src/main/docs/starter-configuration.adoc b/core/camel-spring-boot/src/main/docs/starter-configuration.adoc
index c98e54a..8bdba6f 100644
--- a/core/camel-spring-boot/src/main/docs/starter-configuration.adoc
+++ b/core/camel-spring-boot/src/main/docs/starter-configuration.adoc
@@ -60,6 +60,8 @@
 
 The `Bean` name has to be equal to that of the Component, Dataformat or Language your are configuring. If the `Bean` name isn't specified in the annotation it will be set to the method name.
 
+External configuration is still applied on top of such a `Bean`: every `camel.[component|language|dataformat].[name].[parameter]` that is set in the external configuration is copied onto it. A parameter that is not set in the external configuration is left alone, so a value set on the `Bean` in Java is kept, even when it differs from the documented default of that parameter.
+
 Typical Camel Spring Boot projects will use a combination of external configuration and Beans to configure their application. For more complete examples on how to configure your Camel Spring Boot project, you can refer to our example https://github.com/apache/camel-spring-boot-examples[repository].
 
 All contributions are welcome!
diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/CamelPropertiesHelper.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/CamelPropertiesHelper.java
index 3bf909d..39caddd 100644
--- a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/CamelPropertiesHelper.java
+++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/CamelPropertiesHelper.java
@@ -35,9 +35,8 @@
 import org.apache.camel.util.StringHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.BoundConfigurationProperties;
 import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
-import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
-import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
 import org.springframework.context.ApplicationContext;
 
 /**
@@ -67,20 +66,22 @@
      * Copies the options from a generated Spring Boot configuration class onto the Camel component, data format or
      * language it configures.
      * <p/>
-     * The options that belong to the auto configuration layer itself (<tt>enabled</tt> and <tt>customizer</tt>) are
-     * removed first, as they are not options on the target bean.
+     * Only the options the application configured are copied, as recorded by Spring Boot in
+     * {@link BoundConfigurationProperties} when it bound the configuration class. The other options merely carry the
+     * default value the generator took from the Camel catalog as field initializer, and must not overwrite the value
+     * the target already holds, such as one set programmatically on a user supplied bean. The options that belong to
+     * the auto configuration layer itself (<tt>enabled</tt> and <tt>customizer</tt>) are never copied, as they are not
+     * options on the target bean.
      * <p/>
-     * An option that cannot be set on the target and that the application configured explicitly fails fast with an
-     * {@link IllegalArgumentException}, instead of being dropped without a trace. An option that cannot be set and
-     * that only carries its catalog default is logged at DEBUG, as there is nothing the application can do about it
-     * and the target keeps its own default. Set {@link #LENIENT_CONFIGURATION_BINDING} to <tt>true</tt> to log an
-     * explicitly configured option at WARN and continue, instead of failing.
+     * A configured option that cannot be set on the target fails fast with an {@link IllegalArgumentException},
+     * instead of being dropped without a trace. Set {@link #LENIENT_CONFIGURATION_BINDING} to <tt>true</tt> to log it
+     * at WARN and continue, instead of failing.
      *
      * @param camelContext
      *                           the CamelContext
      * @param applicationContext
-     *                           the Spring application context, used to tell an explicitly configured option from a
-     *                           catalog default
+     *                           the Spring application context, used to tell a configured option from a catalog
+     *                           default
      * @param propertyPrefix
      *                           the configuration prefix of the source, such as <tt>camel.component.http</tt>
      * @param source
@@ -96,6 +97,11 @@
 
         Map<String, Object> properties = getNonNullProperties(camelContext, source);
         properties.keySet().removeIf(key -> AUTO_CONFIGURATION_OPTIONS.contains(key.toLowerCase(Locale.US)));
+        // only the options the application configured are copied, the others merely carry their catalog default
+        Set<ConfigurationPropertyName> bound = boundProperties(applicationContext);
+        if (bound != null && propertyPrefix != null && !propertyPrefix.isEmpty()) {
+            properties.keySet().removeIf(key -> !isConfigured(bound, optionKey(propertyPrefix, key)));
+        }
 
         // the options that could be set are removed from the map, so what is left could not be set
         doSetCamelProperties(camelContext, target, properties, false, false);
@@ -106,19 +112,12 @@
         boolean lenient = isLenientBinding(applicationContext);
         List<String> failed = new ArrayList<>();
         for (Map.Entry<String, Object> entry : properties.entrySet()) {
-            String name = entry.getKey();
-            Object value = entry.getValue();
-            if (isExplicitlyConfigured(applicationContext, propertyPrefix, name)) {
-                if (lenient) {
-                    LOG.warn("Cannot configure option [{}] with value [{}] on [{}]. This option is ignored.",
-                            optionKey(propertyPrefix, name), value, ObjectHelper.classCanonicalName(target));
-                } else {
-                    failed.add(optionKey(propertyPrefix, name) + " = " + value);
-                }
+            String option = optionKey(propertyPrefix, entry.getKey());
+            if (lenient) {
+                LOG.warn("Cannot configure option [{}] with value [{}] on [{}]. This option is ignored.", option,
+                        entry.getValue(), ObjectHelper.classCanonicalName(target));
             } else {
-                // only the catalog default was carried, so the target keeps its own default
-                LOG.debug("Cannot configure option [{}] with default value [{}] on [{}]. This option is ignored.",
-                        optionKey(propertyPrefix, name), value, ObjectHelper.classCanonicalName(target));
+                failed.add(option + " = " + entry.getValue());
             }
         }
         if (!failed.isEmpty()) {
@@ -133,32 +132,42 @@
     }
 
     private static String optionKey(String propertyPrefix, String name) {
-        String dashed = StringHelper.camelCaseToDash(name);
+        String dashed = StringHelper.camelCaseToDash(name).toLowerCase(Locale.US);
         return propertyPrefix != null && !propertyPrefix.isEmpty() ? propertyPrefix + "." + dashed : dashed;
     }
 
     /**
-     * Whether the application configured the given option itself, as opposed to the option only carrying the default
-     * value the generator took from the Camel catalog.
+     * The properties Spring Boot bound onto the configuration classes of the application, or <tt>null</tt> when that
+     * cannot be determined, in which case every option is copied as before Camel 4.23.
      */
-    private static boolean isExplicitlyConfigured(ApplicationContext applicationContext, String propertyPrefix,
-            String name) {
-        if (applicationContext == null || propertyPrefix == null || propertyPrefix.isEmpty()) {
-            return false;
+    private static Set<ConfigurationPropertyName> boundProperties(ApplicationContext applicationContext) {
+        if (applicationContext == null) {
+            return null;
         }
         try {
-            ConfigurationPropertyName key
-                    = ConfigurationPropertyName.of(optionKey(propertyPrefix, name).toLowerCase(Locale.US));
-            for (ConfigurationPropertySource source : ConfigurationPropertySources
-                    .get(applicationContext.getEnvironment())) {
-                if (source.getConfigurationProperty(key) != null) {
-                    return true;
-                }
-            }
+            return BoundConfigurationProperties.get(applicationContext).getAll().keySet();
         } catch (Exception e) {
-            // returning false downgrades a hard error to an ignored option, so this must not stay quiet
-            LOG.warn("Cannot determine whether {} was configured due to: {}. Treating it as not configured.",
-                    optionKey(propertyPrefix, name), e.getMessage(), e);
+            LOG.warn("Cannot determine which options were configured due to: {}. Copying all options.",
+                    e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * Whether the application configured the given option, as opposed to the option only carrying the default value
+     * the generator took from the Camel catalog. Spring Boot records every property it bound, including the entries
+     * of a map or list option, so the option is configured when that property or any property below it was bound.
+     */
+    private static boolean isConfigured(Set<ConfigurationPropertyName> bound, String optionKey) {
+        ConfigurationPropertyName option = ConfigurationPropertyName.ofIfValid(optionKey);
+        if (option == null) {
+            // not a name Spring Boot could have bound, so it cannot be told apart from a default
+            return true;
+        }
+        for (ConfigurationPropertyName name : bound) {
+            if (option.equals(name) || option.isAncestorOf(name)) {
+                return true;
+            }
         }
         return false;
     }
diff --git a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperLenientBindingTest.java b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperLenientBindingTest.java
index 3104890..58c0f1e 100644
--- a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperLenientBindingTest.java
+++ b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperLenientBindingTest.java
@@ -22,6 +22,7 @@
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.context.ApplicationContext;
 import org.springframework.test.annotation.DirtiesContext;
@@ -37,7 +38,9 @@
                 classes = { CamelPropertiesHelperLenientBindingTest.class },
                 properties = {
                         "camel.springboot.lenient-configuration-binding = true",
+                        "camel.test.my-config.name = Donald Duck",
                         "camel.test.my-config.no-such-option-on-the-target = bar" })
+@EnableConfigurationProperties(CamelPropertiesHelperTest.MyDriftedConfiguration.class)
 public class CamelPropertiesHelperLenientBindingTest {
 
     @Autowired
@@ -46,14 +49,13 @@
     @Autowired
     CamelContext camelContext;
 
+    @Autowired
+    CamelPropertiesHelperTest.MyDriftedConfiguration config;
+
     @Test
     public void testConfiguredOptionThatCannotBeSetIsIgnoredWhenLenient() {
         CamelPropertiesHelperTest.MyClass target = new CamelPropertiesHelperTest.MyClass();
 
-        CamelPropertiesHelperTest.MyDriftedConfiguration config = new CamelPropertiesHelperTest.MyDriftedConfiguration();
-        config.setName("Donald Duck");
-        config.setNoSuchOptionOnTheTarget("bar");
-
         CamelPropertiesHelper.copyConfigurationProperties(camelContext, applicationContext,
                 CamelPropertiesHelperTest.PREFIX, config, target);
 
diff --git a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperTest.java b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperTest.java
index d47d3eb..17dbac2 100644
--- a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperTest.java
+++ b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperTest.java
@@ -26,6 +26,8 @@
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
@@ -37,7 +39,9 @@
 @SpringBootApplication
 @SpringBootTest(
                 classes = { CamelPropertiesHelperTest.TestConfiguration.class },
-                properties = { "camel.test.my-config.no-such-option-on-the-target = bar" })
+                properties = { "camel.test.my-config.name = Donald Duck",
+                        "camel.test.my-config.verify-hostname = true",
+                        "camel.test.my-config.no-such-option-on-the-target = bar" })
 public class CamelPropertiesHelperTest {
 
     static final String PREFIX = "camel.test.my-config";
@@ -48,7 +52,14 @@
     @Autowired
     CamelContext camelContext;
 
+    /**
+     * Bound by Spring Boot from the test properties, like a generated configuration class is.
+     */
+    @Autowired
+    MyDriftedConfiguration config;
+
     @Configuration
+    @EnableConfigurationProperties(MyDriftedConfiguration.class)
     static class TestConfiguration {
         @Bean(name = "myCoolOption")
         MyOption myCoolBean() {
@@ -61,12 +72,15 @@
 
     /**
      * Mimics a generated {@code *ComponentConfiguration} class: the auto configuration layer options
-     * (enabled/customizer) are inherited and are not options on the Camel target bean.
+     * (enabled/customizer) are inherited and are not options on the Camel target bean, and the catalog defaults are
+     * field initializers.
      */
     public static class MyConfiguration extends ComponentConfigurationPropertiesCommon {
 
         private String name;
         private MyOption option;
+        private Boolean secure = false;
+        private Boolean verifyHostname = true;
 
         public String getName() {
             return name;
@@ -83,16 +97,33 @@
         public void setOption(MyOption option) {
             this.option = option;
         }
+
+        public Boolean getSecure() {
+            return secure;
+        }
+
+        public void setSecure(Boolean secure) {
+            this.secure = secure;
+        }
+
+        public Boolean getVerifyHostname() {
+            return verifyHostname;
+        }
+
+        public void setVerifyHostname(Boolean verifyHostname) {
+            this.verifyHostname = verifyHostname;
+        }
     }
 
     /**
      * A configuration class holding an option that does not exist on the target bean, which is what generator or
      * catalog drift looks like at runtime.
      */
+    @ConfigurationProperties(prefix = PREFIX)
     public static class MyDriftedConfiguration extends MyConfiguration {
 
         private String noSuchOptionOnTheTarget;
-        private String anotherOptionOnlyCarryingItsDefault;
+        private String anotherOptionOnlyCarryingItsDefault = "false";
 
         public String getNoSuchOptionOnTheTarget() {
             return noSuchOptionOnTheTarget;
@@ -118,11 +149,29 @@
         private MyOption option;
         private CamelContext camelContext;
         private MyFooClass myFooClass;
+        private boolean secure;
+        private boolean verifyHostname;
 
         public int getId() {
             return id;
         }
 
+        public boolean isSecure() {
+            return secure;
+        }
+
+        public void setSecure(boolean secure) {
+            this.secure = secure;
+        }
+
+        public boolean isVerifyHostname() {
+            return verifyHostname;
+        }
+
+        public void setVerifyHostname(boolean verifyHostname) {
+            this.verifyHostname = verifyHostname;
+        }
+
         public void setId(int id) {
             this.id = id;
         }
@@ -287,30 +336,22 @@
     public void testCopyConfigurationPropertiesIgnoresAutoConfigurationOptions() {
         MyClass target = new MyClass();
 
-        MyConfiguration config = new MyConfiguration();
-        config.setName("Donald Duck");
-        config.setOption(context.getBean("myCoolOption", MyOption.class));
-
         // enabled and customizer are always set on a generated configuration class, and must not be
         // attempted on the target bean
         Assertions.assertTrue(config.isEnabled());
         Assertions.assertNotNull(config.getCustomizer());
 
-        CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, config, target);
+        CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, configWithoutDrift(),
+                target);
 
         Assertions.assertEquals("Donald Duck", target.getName());
-        Assertions.assertSame(context.getBean("myCoolOption"), target.getOption());
     }
 
     @Test
     public void testCopyConfigurationPropertiesFailsOnConfiguredOptionThatCannotBeSet() {
         MyClass target = new MyClass();
 
-        MyDriftedConfiguration config = new MyDriftedConfiguration();
-        config.setName("Donald Duck");
         // camel.test.my-config.no-such-option-on-the-target is set on the test application
-        config.setNoSuchOptionOnTheTarget("bar");
-
         IllegalArgumentException e = Assertions.assertThrows(IllegalArgumentException.class,
                 () -> CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, config,
                         target));
@@ -324,17 +365,52 @@
     public void testCopyConfigurationPropertiesIgnoresDefaultThatCannotBeSet() {
         MyClass target = new MyClass();
 
-        MyDriftedConfiguration config = new MyDriftedConfiguration();
-        config.setName("Donald Duck");
-        // nothing configured this one, so it only carries a catalog default and must not break startup
-        config.setAnotherOptionOnlyCarryingItsDefault("false");
-
-        CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, config, target);
+        // nothing configured anotherOptionOnlyCarryingItsDefault, so it only carries a catalog default and must
+        // not break startup
+        Assertions.assertEquals("false", config.getAnotherOptionOnlyCarryingItsDefault());
+        CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, configWithoutDrift(),
+                target);
 
         Assertions.assertEquals("Donald Duck", target.getName());
     }
 
     @Test
+    public void testCopyConfigurationPropertiesDoesNotOverwriteWithCatalogDefault() {
+        MyClass target = new MyClass();
+        target.setSecure(true);
+
+        // secure is not set on the test application, so it only carries the catalog default false
+        Assertions.assertEquals(false, config.getSecure());
+        CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, configWithoutDrift(),
+                target);
+
+        Assertions.assertTrue(target.isSecure(), "The catalog default must not overwrite the value set on the target");
+    }
+
+    @Test
+    public void testCopyConfigurationPropertiesAppliesOptionConfiguredToItsDefault() {
+        MyClass target = new MyClass();
+        target.setVerifyHostname(false);
+
+        // verify-hostname is set on the test application to the same value as its catalog default
+        CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, configWithoutDrift(),
+                target);
+
+        Assertions.assertTrue(target.isVerifyHostname(), "A configured option must be applied whatever its value");
+    }
+
+    /**
+     * The bound configuration without the option that cannot be set, for the tests that are not about that failure.
+     */
+    private MyConfiguration configWithoutDrift() {
+        MyConfiguration answer = new MyConfiguration();
+        answer.setName(config.getName());
+        answer.setSecure(config.getSecure());
+        answer.setVerifyHostname(config.getVerifyHostname());
+        return answer;
+    }
+
+    @Test
     public void testSetCamelPropertiesUnknownOptionIgnore() throws Exception {
         MyClass target = new MyClass();