You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2025/12/19 17:25:22 UTC

(juneau) branch master updated: juneau-bct improvements

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 94175423d6 juneau-bct improvements
94175423d6 is described below

commit 94175423d67b94361672faf5f81053abb2cadcdd
Author: James Bognar <ja...@salesforce.com>
AuthorDate: Fri Dec 19 12:25:17 2025 -0500

    juneau-bct improvements
---
 .../org/apache/juneau/bean/openapi3/MediaType.java |   4 +-
 .../org/apache/juneau/bean/openapi3/Parameter.java |   2 +-
 .../org/apache/juneau/bean/openapi3/PathItem.java  |   2 +-
 .../apache/juneau/bean/openapi3/SchemaInfo.java    |   4 +-
 .../org/apache/juneau/bean/swagger/HeaderInfo.java |   6 +-
 .../java/org/apache/juneau/bean/swagger/Items.java |   6 +-
 .../apache/juneau/bean/swagger/ParameterInfo.java  |   4 +-
 .../org/apache/juneau/bean/swagger/SchemaInfo.java |   6 +-
 juneau-core/juneau-bct/pom.xml                     |   3 +-
 .../org/apache/juneau/junit/bct/BctAssertions.java | 197 ++++--------
 .../apache/juneau/junit/bct/BctConfiguration.java  | 309 ++++++++++++++++++
 .../org/apache/juneau/junit/bct/Listifiers.java    |   6 +-
 .../juneau/junit/bct/annotations/BctConfig.java    |  90 ++++++
 .../junit/bct/annotations/BctConfigExtension.java  |  76 +++++
 .../org/apache/juneau/commons/lang/TriState.java   |  77 +++++
 .../docs/topics/07.01.00.JuneauBctBasics.md        |  48 ++-
 .../docs/topics/07.01.04.PropertyExtractors.md     |   7 +-
 ...Messages.md => 07.02.00.CustomErrorMessages.md} |   9 +-
 juneau-docs/docs/topics/07.03.00.Customization.md  | 351 +++++++++++++++++++++
 ...01.Stringifiers.md => 07.03.01.Stringifiers.md} |   7 +-
 ....01.02.Listifiers.md => 07.03.02.Listifiers.md} |   7 +-
 .../{07.01.03.Swappers.md => 07.03.03.Swappers.md} |   7 +-
 juneau-docs/sidebars.ts                            |  42 ++-
 .../java/org/apache/juneau/BeanSession_Test.java   |   2 -
 .../juneau/bean/openapi3/HeaderInfo_Test.java      |   2 +-
 .../apache/juneau/bean/openapi3/Items_Test.java    |   2 +-
 .../juneau/bean/openapi3/Parameter_Test.java       |   2 +-
 .../juneau/bean/openapi3/SchemaInfo_Test.java      |   2 +-
 .../juneau/bean/swagger/SchemaInfo_Test.java       |   2 +-
 .../juneau/commons/collections/Maps_Test.java      |   3 +
 .../juneau/commons/collections/Sets_Test.java      |   3 +
 .../juneau/junit/bct/BasicBeanConverter_Test.java  |   4 +
 .../juneau/junit/bct/BctAssertions_Test.java       |   3 +
 .../apache/juneau/junit/bct/BctConfig_Test.java    | 306 ++++++++++++++++++
 .../apache/juneau/junit/bct/Listifiers_Test.java   |   9 +-
 35 files changed, 1398 insertions(+), 212 deletions(-)

diff --git a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/MediaType.java b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/MediaType.java
index 603524e5a7..e5b8d6c18a 100644
--- a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/MediaType.java
+++ b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/MediaType.java
@@ -182,10 +182,10 @@ public class MediaType extends OpenApiElement {
 	public Set<String> keySet() {
 		// @formatter:off
 		var s = setb(String.class)
-			.addIf(nn(schema), "schema")
-			.addIf(nn(example), "x-example")
 			.addIf(isNotEmpty(encoding), "encoding")
 			.addIf(isNotEmpty(examples), "examples")
+			.addIf(nn(schema), "schema")
+			.addIf(nn(example), "x-example")
 			.build();
 		// @formatter:on
 		return new MultiSet<>(s, super.keySet());
diff --git a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/Parameter.java b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/Parameter.java
index e19989ae90..f932b39284 100644
--- a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/Parameter.java
+++ b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/Parameter.java
@@ -234,8 +234,8 @@ public class Parameter extends OpenApiElement {
 		var s = setb(String.class)
 			.addIf(nn(allowEmptyValue), "allowEmptyValue")
 			.addIf(nn(allowReserved), "allowReserved")
-			.addIf(nn(description), "description")
 			.addIf(nn(deprecated), "deprecated")
+			.addIf(nn(description), "description")
 			.addIf(nn(example), "example")
 			.addIf(nn(examples), "examples")
 			.addIf(nn(explode), "explode")
diff --git a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/PathItem.java b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/PathItem.java
index 2cbefe48a3..c6f97e0465 100644
--- a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/PathItem.java
+++ b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/PathItem.java
@@ -235,8 +235,8 @@ public class PathItem extends OpenApiElement {
 			.addIf(nn(get), "get")
 			.addIf(nn(head), "head")
 			.addIf(nn(options), "options")
-			.addIf(nn(patch), "patch")
 			.addIf(nn(parameters), "parameters")
+			.addIf(nn(patch), "patch")
 			.addIf(nn(post), "post")
 			.addIf(nn(put), "put")
 			.addIf(nn(servers), "servers")
diff --git a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/SchemaInfo.java b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/SchemaInfo.java
index 0d8348eb66..fc87313b4f 100644
--- a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/SchemaInfo.java
+++ b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/SchemaInfo.java
@@ -654,14 +654,14 @@ public class SchemaInfo extends OpenApiElement {
 			.addIf(nn(externalDocs), "externalDocs")
 			.addIf(nn(format), "format")
 			.addIf(nn(items), "items")
-			.addIf(nn(maximum), "maximum")
 			.addIf(nn(maxItems), "maxItems")
 			.addIf(nn(maxLength), "maxLength")
 			.addIf(nn(maxProperties), "maxProperties")
-			.addIf(nn(minimum), "minimum")
+			.addIf(nn(maximum), "maximum")
 			.addIf(nn(minItems), "minItems")
 			.addIf(nn(minLength), "minLength")
 			.addIf(nn(minProperties), "minProperties")
+			.addIf(nn(minimum), "minimum")
 			.addIf(nn(multipleOf), "multipleOf")
 			.addIf(nn(not), "not")
 			.addIf(nn(nullable), "nullable")
diff --git a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/HeaderInfo.java b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/HeaderInfo.java
index 9d9ea2a833..dfc7af071e 100644
--- a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/HeaderInfo.java
+++ b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/HeaderInfo.java
@@ -351,6 +351,7 @@ public class HeaderInfo extends SwaggerElement {
 	public Set<String> keySet() {
 		// @formatter:off
 		var s = setb(String.class)
+			.addIf(nn(ref), "$ref")
 			.addIf(nn(collectionFormat), "collectionFormat")
 			.addIf(nn(default_), "default")
 			.addIf(nn(description), "description")
@@ -360,15 +361,14 @@ public class HeaderInfo extends SwaggerElement {
 			.addIf(nn(exclusiveMinimum), "exclusiveMinimum")
 			.addIf(nn(format), "format")
 			.addIf(nn(items), "items")
-			.addIf(nn(maximum), "maximum")
 			.addIf(nn(maxItems), "maxItems")
 			.addIf(nn(maxLength), "maxLength")
-			.addIf(nn(minimum), "minimum")
+			.addIf(nn(maximum), "maximum")
 			.addIf(nn(minItems), "minItems")
 			.addIf(nn(minLength), "minLength")
+			.addIf(nn(minimum), "minimum")
 			.addIf(nn(multipleOf), "multipleOf")
 			.addIf(nn(pattern), "pattern")
-			.addIf(nn(ref), "$ref")
 			.addIf(nn(type), "type")
 			.addIf(nn(uniqueItems), "uniqueItems")
 			.build();
diff --git a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Items.java b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Items.java
index 0fba5a81e3..44e909c794 100644
--- a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Items.java
+++ b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Items.java
@@ -328,6 +328,7 @@ public class Items extends SwaggerElement {
 	public Set<String> keySet() {
 		// @formatter:off
 		var s = setb(String.class)
+			.addIf(nn(ref), "$ref")
 			.addIf(nn(collectionFormat), "collectionFormat")
 			.addIf(nn(default_), "default")
 			.addIf(isNotEmpty(enum_), "enum")
@@ -335,15 +336,14 @@ public class Items extends SwaggerElement {
 			.addIf(nn(exclusiveMinimum), "exclusiveMinimum")
 			.addIf(nn(format), "format")
 			.addIf(nn(items), "items")
-			.addIf(nn(maximum), "maximum")
 			.addIf(nn(maxItems), "maxItems")
 			.addIf(nn(maxLength), "maxLength")
-			.addIf(nn(minimum), "minimum")
+			.addIf(nn(maximum), "maximum")
 			.addIf(nn(minItems), "minItems")
 			.addIf(nn(minLength), "minLength")
+			.addIf(nn(minimum), "minimum")
 			.addIf(nn(multipleOf), "multipleOf")
 			.addIf(nn(pattern), "pattern")
-			.addIf(nn(ref), "$ref")
 			.addIf(nn(type), "type")
 			.addIf(nn(uniqueItems), "uniqueItems")
 			.build();
diff --git a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/ParameterInfo.java b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/ParameterInfo.java
index f622b6ef6e..f00dbaa152 100644
--- a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/ParameterInfo.java
+++ b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/ParameterInfo.java
@@ -550,12 +550,12 @@ public class ParameterInfo extends SwaggerElement {
 			.addIf(nn(format), "format")
 			.addIf(nn(in), "in")
 			.addIf(nn(items), "items")
-			.addIf(nn(maximum), "maximum")
 			.addIf(nn(maxItems), "maxItems")
 			.addIf(nn(maxLength), "maxLength")
-			.addIf(nn(minimum), "minimum")
+			.addIf(nn(maximum), "maximum")
 			.addIf(nn(minItems), "minItems")
 			.addIf(nn(minLength), "minLength")
+			.addIf(nn(minimum), "minimum")
 			.addIf(nn(multipleOf), "multipleOf")
 			.addIf(nn(name), "name")
 			.addIf(nn(pattern), "pattern")
diff --git a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/SchemaInfo.java b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/SchemaInfo.java
index 3450de04ca..4c5d244271 100644
--- a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/SchemaInfo.java
+++ b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/SchemaInfo.java
@@ -557,6 +557,7 @@ public class SchemaInfo extends SwaggerElement {
 	public Set<String> keySet() {
 		// @formatter:off
 		var s = setb(String.class)
+			.addIf(nn(ref), "$ref")
 			.addIf(nn(additionalProperties), "additionalProperties")
 			.addIf(isNotEmpty(allOf), "allOf")
 			.addIf(nn(default_), "default")
@@ -569,19 +570,18 @@ public class SchemaInfo extends SwaggerElement {
 			.addIf(nn(externalDocs), "externalDocs")
 			.addIf(nn(format), "format")
 			.addIf(nn(items), "items")
-			.addIf(nn(maximum), "maximum")
 			.addIf(nn(maxItems), "maxItems")
 			.addIf(nn(maxLength), "maxLength")
 			.addIf(nn(maxProperties), "maxProperties")
-			.addIf(nn(minimum), "minimum")
+			.addIf(nn(maximum), "maximum")
 			.addIf(nn(minItems), "minItems")
 			.addIf(nn(minLength), "minLength")
 			.addIf(nn(minProperties), "minProperties")
+			.addIf(nn(minimum), "minimum")
 			.addIf(nn(multipleOf), "multipleOf")
 			.addIf(nn(pattern), "pattern")
 			.addIf(isNotEmpty(properties), "properties")
 			.addIf(nn(readOnly), "readOnly")
-			.addIf(nn(ref), "$ref")
 			.addIf(nn(required), "required")
 			.addIf(isNotEmpty(requiredProperties), "requiredProperties")
 			.addIf(nn(title), "title")
diff --git a/juneau-core/juneau-bct/pom.xml b/juneau-core/juneau-bct/pom.xml
index 460f27b870..25f1489736 100644
--- a/juneau-core/juneau-bct/pom.xml
+++ b/juneau-core/juneau-bct/pom.xml
@@ -43,7 +43,8 @@
 		<dependency>
 			<groupId>org.junit.jupiter</groupId>
 			<artifactId>junit-jupiter</artifactId>
-			<version>6.0.1</version>
+			<version>${junit.version}</version>
+			<scope>provided</scope>
 		</dependency>
 		<dependency>
 			<groupId>org.opentest4j</groupId>
diff --git a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
index 28707af99d..2189b3210f 100644
--- a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
+++ b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctAssertions.java
@@ -27,7 +27,6 @@ import java.util.*;
 import java.util.function.*;
 import java.util.stream.*;
 
-import org.apache.juneau.commons.function.*;
 import org.apache.juneau.commons.utils.*;
 import org.opentest4j.*;
 
@@ -118,53 +117,53 @@ import org.opentest4j.*;
  *       <js>"prop1,prop2"</js>, <js>"value1,value2"</js>);
  * </p>
  *
-	 * <h5 class='section'>Customizing the Default Converter:</h5>
-	 * <p>The default bean converter can be customized on a per-thread basis using:</p>
-	 * <ul>
-	 *    <li><b>{@link #setConverter(BeanConverter)}:</b> Set a custom converter for the current thread</li>
-	 *    <li><b>{@link #resetConverter()}:</b> Reset to the system default converter</li>
-	 * </ul>
-	 *
-	 * <p class='bjava'>
-	 *    <jc>// Example: Set custom converter in @BeforeEach method</jc>
-	 *    <ja>@BeforeEach</ja>
-	 *    <jk>void</jk> <jsm>setUp</jsm>() {
-	 *       <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
-	 *          .defaultSettings()
-	 *          .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
-	 *          .addStringifier(MyType.<jk>class</jk>, <jp>obj</jp> -> <jp>obj</jp>.customFormat())
-	 *          .build();
-	 *       BctAssertions.<jsm>setConverter</jsm>(<jv>customConverter</jv>);
-	 *    }
-	 *
-	 *    <jc>// All assertions in this test class now use the custom converter</jc>
-	 *    <ja>@Test</ja>
-	 *    <jk>void</jk> <jsm>testWithCustomConverter</jsm>() {
-	 *       <jsm>assertBean</jsm>(<jv>myObject</jv>, <js>"date,property"</js>, <js>"2023-12-01,value"</js>);
-	 *    }
-	 *
-	 *    <jc>// Clean up in @AfterEach method</jc>
-	 *    <ja>@AfterEach</ja>
-	 *    <jk>void</jk> <jsm>tearDown</jsm>() {
-	 *       BctAssertions.<jsm>resetConverter</jsm>();
-	 *    }
-	 * </p>
-	 *
-	 * <p class='bjava'>
-	 *    <jc>// Example: Per-test method converter override</jc>
-	 *    <ja>@Test</ja>
-	 *    <jk>void</jk> <jsm>testSpecificFormat</jsm>() {
-	 *       <jk>var</jk> <jv>dateConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
-	 *          .defaultSettings()
-	 *          .addStringifier(LocalDateTime.<jk>class</jk>, <jp>dt</jp> -> <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_DATE_TIME</jsf>))
-	 *          .build();
-	 *       BctAssertions.<jsm>setConverter</jsm>(<jv>dateConverter</jv>);
-	 *       <jkt>try</jkt> {
-	 *          <jsm>assertBean</jsm>(<jv>event</jv>, <js>"timestamp"</js>, <js>"2023-12-01T10:30:00"</js>);
-	 *       } <jkt>finally</jkt> {
-	 *          BctAssertions.<jsm>resetConverter</jsm>();
-	 *       }
-	 *    }
+ * <h5 class='section'>Customizing the Default Converter:</h5>
+ * <p>The default bean converter can be customized on a per-thread basis using:</p>
+ * <ul>
+ *    <li><b>{@link #setConverter(BeanConverter)}:</b> Set a custom converter for the current thread</li>
+ *    <li><b>{@link #resetConverter()}:</b> Reset to the system default converter</li>
+ * </ul>
+ *
+ * <p class='bjava'>
+ *    <jc>// Example: Set custom converter in @BeforeEach method</jc>
+ *    <ja>@BeforeEach</ja>
+ *    <jk>void</jk> <jsm>setUp</jsm>() {
+ *       <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
+ *          .defaultSettings()
+ *          .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
+ *          .addStringifier(MyType.<jk>class</jk>, <jp>obj</jp> -> <jp>obj</jp>.customFormat())
+ *          .build();
+ *       BctAssertions.<jsm>setConverter</jsm>(<jv>customConverter</jv>);
+ *    }
+ *
+ *    <jc>// All assertions in this test class now use the custom converter</jc>
+ *    <ja>@Test</ja>
+ *    <jk>void</jk> <jsm>testWithCustomConverter</jsm>() {
+ *       <jsm>assertBean</jsm>(<jv>myObject</jv>, <js>"date,property"</js>, <js>"2023-12-01,value"</js>);
+ *    }
+ *
+ *    <jc>// Clean up in @AfterEach method</jc>
+ *    <ja>@AfterEach</ja>
+ *    <jk>void</jk> <jsm>tearDown</jsm>() {
+ *       BctAssertions.<jsm>resetConverter</jsm>();
+ *    }
+ * </p>
+ *
+ * <p class='bjava'>
+ *    <jc>// Example: Per-test method converter override</jc>
+ *    <ja>@Test</ja>
+ *    <jk>void</jk> <jsm>testSpecificFormat</jsm>() {
+ *       <jk>var</jk> <jv>dateConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
+ *          .defaultSettings()
+ *          .addStringifier(LocalDateTime.<jk>class</jk>, <jp>dt</jp> -> <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_DATE_TIME</jsf>))
+ *          .build();
+ *       BctAssertions.<jsm>setConverter</jsm>(<jv>dateConverter</jv>);
+ *       <jkt>try</jkt> {
+ *          <jsm>assertBean</jsm>(<jv>event</jv>, <js>"timestamp"</js>, <js>"2023-12-01T10:30:00"</js>);
+ *       } <jkt>finally</jkt> {
+ *          BctAssertions.<jsm>resetConverter</jsm>();
+ *       }
+ *    }
  * </p>
  *
  * <h5 class='section'>Performance and Thread Safety:</h5>
@@ -183,12 +182,6 @@ import org.opentest4j.*;
  */
 public class BctAssertions {
 
-	// Thread-local memoized supplier for default converter (defaults to BasicBeanConverter.DEFAULT)
-	private static final ThreadLocal<ResettableSupplier<BeanConverter>> CONVERTER_SUPPLIER = ThreadLocal.withInitial(() -> memoizeResettable(() -> BasicBeanConverter.DEFAULT));
-
-	// Thread-local override for method-level converter customization
-	private static final ThreadLocal<BeanConverter> CONVERTER_OVERRIDE = new ThreadLocal<>();
-
 	/**
 	 * Asserts that the fields/properties on the specified bean are the specified values after being converted to strings.
 	 *
@@ -391,7 +384,7 @@ public class BctAssertions {
 		assertNotNull(actual, "Actual was null.");
 		assertArgNotNull("fields", fields);
 		assertArgNotNull("expected", expected);
-		var converter = getConverter();
+		var converter = BctConfiguration.getConverter();
 		assertEquals(expected, tokenize(fields).stream().map(x -> converter.getNested(actual, x)).collect(joining(",")), composeMessage(message, "Bean assertion failed."));
 	}
 
@@ -473,7 +466,7 @@ public class BctAssertions {
 		assertArgNotNull("fields", fields);
 		assertArgNotNull("expected", expected);
 
-		var converter = getConverter();
+		var converter = BctConfiguration.getConverter();
 		var tokens = tokenize(fields);
 		var errors = new ArrayList<AssertionFailedError>();
 		List<Object> actualList = converter.listify(actual);
@@ -549,7 +542,7 @@ public class BctAssertions {
 		assertArgNotNull("actual", actual);
 		assertNotNull(actual, "Value was null.");
 
-		var a = getConverter().stringify(actual);
+		var a = BctConfiguration.getConverter().stringify(actual);
 		assertTrue(a.contains(expected), composeMessage(message, "String did not contain expected substring.  ==> expected: <{0}> but was: <{1}>", expected, a));
 	}
 
@@ -596,7 +589,7 @@ public class BctAssertions {
 		assertArgNotNull("expected", expected);
 		assertNotNull(actual, "Value was null.");
 
-		var a = getConverter().stringify(actual);
+		var a = BctConfiguration.getConverter().stringify(actual);
 		var errors = new ArrayList<AssertionFailedError>();
 
 		for (var e : expected) {
@@ -683,7 +676,7 @@ public class BctAssertions {
 	 */
 	public static void assertEmpty(Supplier<String> message, Object value) {
 		assertNotNull(value, "Value was null.");
-		var size = getConverter().size(value);
+		var size = BctConfiguration.getConverter().size(value);
 		assertEquals(0, size, composeMessage(message, "Value was not empty. Size=<{0}>", size));
 	}
 
@@ -761,7 +754,7 @@ public class BctAssertions {
 		assertArgNotNull("expected", expected);
 		assertArgNotNull("actual", actual);
 
-		var converter = getConverter();
+		var converter = BctConfiguration.getConverter();
 		List<Object> list = converter.listify(actual);
 		var errors = new ArrayList<AssertionFailedError>();
 
@@ -990,7 +983,7 @@ public class BctAssertions {
 		assertArgNotNull("pattern", pattern);
 		assertNotNull(value, "Value was null.");
 
-		var v = getConverter().stringify(value);
+		var v = BctConfiguration.getConverter().stringify(value);
 		var m = StringUtils.getGlobMatchPattern(pattern).matcher(v);
 		assertTrue(m.matches(), composeMessage(message, "Pattern didn''t match. ==> pattern: <{0}> but was: <{1}>", pattern, v));
 	}
@@ -1056,7 +1049,7 @@ public class BctAssertions {
 	 */
 	public static void assertNotEmpty(Supplier<String> message, Object value) {
 		assertNotNull(value, "Value was null.");
-		int size = getConverter().size(value);
+		int size = BctConfiguration.getConverter().size(value);
 		assertTrue(size > 0, composeMessage(message, "Value was empty."));
 	}
 
@@ -1102,7 +1095,7 @@ public class BctAssertions {
 	 */
 	public static void assertSize(Supplier<String> message, int expected, Object actual) {
 		assertNotNull(actual, "Value was null.");
-		var size = getConverter().size(actual);
+		var size = BctConfiguration.getConverter().size(actual);
 		assertEquals(expected, size, composeMessage(message, "Value not expected size."));
 	}
 
@@ -1153,67 +1146,7 @@ public class BctAssertions {
 		assertNotNull(actual, "Value was null.");
 
 		var messageSupplier = message != null ? message : fs("");
-		assertEquals(expected, getConverter().stringify(actual), messageSupplier);
-	}
-
-	/**
-	 * Resets the bean converter for the current thread to the system default.
-	 *
-	 * <p>This method clears any thread-local converter override set via {@link #setConverter(BeanConverter)},
-	 * restoring the default converter ({@link BasicBeanConverter#DEFAULT}) for subsequent assertions.</p>
-	 *
-	 * <p>This is typically called in test teardown methods (e.g., {@code @AfterEach}) to clean up after tests
-	 * that set a custom converter.</p>
-	 *
-	 * <h5 class='section'>Usage Example:</h5>
-	 * <p class='bjava'>
-	 *    <jc>// In @AfterEach method</jc>
-	 *    BctAssertions.<jsm>resetConverter</jsm>();
-	 * </p>
-	 *
-	 * <h5 class='section'>Thread Safety:</h5>
-	 * <p>This method is thread-safe and only affects the current thread's converter.</p>
-	 *
-	 * @see #setConverter(BeanConverter)
-	 */
-	public static void resetConverter() {
-		CONVERTER_OVERRIDE.remove();
-		CONVERTER_SUPPLIER.get().reset();
-	}
-
-	/**
-	 * Sets a custom bean converter for the current thread.
-	 *
-	 * <p>This method allows you to override the default converter for all assertions in the current test method.
-	 * The converter will be used by all assertion methods in the current thread.</p>
-	 *
-	 * <p>This is particularly useful in test setup methods (e.g., {@code @BeforeEach}) to configure a custom converter
-	 * for all tests in a test class or method.</p>
-	 *
-	 * <h5 class='section'>Usage Example:</h5>
-	 * <p class='bjava'>
-	 *    <jc>// In @BeforeEach method</jc>
-	 *    <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
-	 *       .defaultSettings()
-	 *       .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
-	 *       .build();
-	 *    BctAssertions.<jsm>setConverter</jsm>(<jv>customConverter</jv>);
-	 *
-	 *    <jc>// All subsequent assertions in this test method will use the custom converter</jc>
-	 *    <jsm>assertBean</jsm>(<jv>event</jv>, <js>"date"</js>, <js>"2023-12-01"</js>);
-	 * </p>
-	 *
-	 * <h5 class='section'>Thread Safety:</h5>
-	 * <p>This method is thread-safe and uses thread-local storage. Each test method running in parallel
-	 * will have its own converter instance, preventing cross-thread interference.</p>
-	 *
-	 * @param converter The bean converter to use for the current thread. Must not be <jk>null</jk>.
-	 * @throws IllegalArgumentException If converter is <jk>null</jk>.
-	 * @see #resetConverter()
-	 */
-	public static void setConverter(BeanConverter converter) {
-		assertArgNotNull("converter", converter);
-		CONVERTER_OVERRIDE.set(converter);
+		assertEquals(expected, BctConfiguration.getConverter().stringify(actual), messageSupplier);
 	}
 
 	/**
@@ -1234,21 +1167,5 @@ public class BctAssertions {
 		return fs("{0}, Caused by: {1}", customMessage.get(), f(defaultMessage, defaultArgs));
 	}
 
-	/**
-	 * Gets the bean converter for the current thread.
-	 *
-	 * <p>Returns the thread-local converter override if set, otherwise returns the memoized default converter.
-	 * This method is used internally by all assertion methods to get the current thread-local converter.</p>
-	 *
-	 * @return The bean converter to use for the current thread.
-	 */
-	private static BeanConverter getConverter() {
-		var override = CONVERTER_OVERRIDE.get();
-		if (override != null) {
-			return override;
-		}
-		return CONVERTER_SUPPLIER.get().get();
-	}
-
 	private BctAssertions() {}
 }
\ No newline at end of file
diff --git a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctConfiguration.java b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctConfiguration.java
new file mode 100644
index 0000000000..47dc749466
--- /dev/null
+++ b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/BctConfiguration.java
@@ -0,0 +1,309 @@
+/*
+ * 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.junit.bct;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import org.apache.juneau.commons.function.*;
+import org.apache.juneau.commons.settings.*;
+
+/**
+ * Configuration utility for Bean-Centric Testing (BCT) framework.
+ *
+ * <p>
+ * This class provides thread-local configuration settings for BCT assertions, allowing test-specific
+ * customization of behavior such as map sorting, collection sorting, and bean converter selection.
+ *
+ * <p>
+ * Configuration is managed using thread-local storage, ensuring that parallel test execution doesn't
+ * interfere with each other's settings. Settings are typically configured via the {@link org.apache.juneau.junit.bct.annotations.BctConfig @BctConfig}
+ * annotation, but can also be set programmatically.
+ *
+ * <h5 class='section'>Configuration Properties:</h5>
+ * <ul>
+ * 	<li><b>{@link #BCT_SORT_MAPS}:</b> Enable sorting of maps in assertions</li>
+ * 	<li><b>{@link #BCT_SORT_COLLECTIONS}:</b> Enable sorting of collections in assertions</li>
+ * 	<li><b>Bean Converter:</b> Custom bean converter instance for property access and conversion</li>
+ * </ul>
+ *
+ * <h5 class='section'>Usage via Annotation:</h5>
+ * <p class='bjava'>
+ * 	<ja>@BctConfig</ja>(sortMaps=TriState.<jsf>TRUE</jsf>, beanConverter=MyConverter.<jk>class</jk>)
+ * 	<jk>class</jk> MyTest {
+ * 		<ja>@Test</ja>
+ * 		<ja>@BctConfig</ja>(sortCollections=TriState.<jsf>TRUE</jsf>)
+ * 		<jk>void</jk> testSomething() {
+ * 			<jc>// sortMaps=true (from class), sortCollections=true (from method),</jc>
+ * 			<jc>// beanConverter=MyConverter (from class)</jc>
+ * 		}
+ * 	}
+ * </p>
+ *
+ * <h5 class='section'>Usage via Programmatic API:</h5>
+ * <p class='bjava'>
+ * 	<ja>@BeforeEach</ja>
+ * 	<jk>void</jk> setUp() {
+ * 		BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>true</jk>);
+ * 		BctConfiguration.<jsm>set</jsm>(<jk>new</jk> MyCustomConverter());
+ * 	}
+ *
+ * 	<ja>@AfterEach</ja>
+ * 	<jk>void</jk> tearDown() {
+ * 		BctConfiguration.<jsm>clear</jsm>();
+ * 	}
+ * </p>
+ *
+ * <h5 class='section'>Thread Safety:</h5>
+ * <p>
+ * All methods in this class are thread-safe. Configuration is stored in thread-local storage,
+ * ensuring that each test thread has its own isolated configuration that doesn't interfere
+ * with other concurrently running tests.
+ * </p>
+ *
+ * @see org.apache.juneau.junit.bct.annotations.BctConfig
+ * @see org.apache.juneau.junit.bct.BctAssertions
+ */
+public class BctConfiguration {
+
+	// Thread-local memoized supplier for default converter (defaults to BasicBeanConverter.DEFAULT)
+	private static final ThreadLocal<ResettableSupplier<BeanConverter>> CONVERTER_SUPPLIER = ThreadLocal.withInitial(() -> memoizeResettable(() -> BasicBeanConverter.DEFAULT));
+
+	/**
+	 * Configuration property name for enabling map sorting in BCT assertions.
+	 *
+	 * <p>
+	 * When set to <jk>true</jk>, maps will be sorted by key before comparison in assertions.
+	 * This ensures consistent ordering regardless of the map's internal ordering.
+	 *
+	 * <h5 class='section'>Example:</h5>
+	 * <p class='bjava'>
+	 * 	BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>true</jk>);
+	 * </p>
+	 */
+	public static final String BCT_SORT_MAPS = "Bct.sortMaps";
+
+	/**
+	 * Configuration property name for enabling collection sorting in BCT assertions.
+	 *
+	 * <p>
+	 * When set to <jk>true</jk>, collections will be sorted before comparison in assertions.
+	 * This ensures consistent ordering regardless of the collection's internal ordering.
+	 *
+	 * <h5 class='section'>Example:</h5>
+	 * <p class='bjava'>
+	 * 	BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_COLLECTIONS</jsf>, <jk>true</jk>);
+	 * </p>
+	 */
+	public static final String BCT_SORT_COLLECTIONS = "Bct.sortCollections";
+
+	// Thread-local override for method-level converter customization
+	private static final ThreadLocal<BeanConverter> CONVERTER_OVERRIDE = new ThreadLocal<>();
+
+	private static final Settings SETTINGS = Settings.create().build();
+
+	/**
+	 * Sets a thread-local configuration value.
+	 *
+	 * <p>
+	 * The value is stored in thread-local storage and will only affect the current thread.
+	 * This is the recommended way to set configuration for individual tests.
+	 *
+	 * <h5 class='section'>Example:</h5>
+	 * <p class='bjava'>
+	 * 	BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>true</jk>);
+	 * </p>
+	 *
+	 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}).
+	 * @param value The value to set.
+	 * @see #get(String)
+	 * @see #clear()
+	 */
+	public static void set(String name, Object value) {
+		SETTINGS.setLocal(name, s(value));
+	}
+
+	/**
+	 * Sets a global configuration value.
+	 *
+	 * <p>
+	 * Global values apply to all threads and persist until explicitly cleared.
+	 * Use with caution in multi-threaded test environments.
+	 *
+	 * <h5 class='section'>Example:</h5>
+	 * <p class='bjava'>
+	 * 	BctConfiguration.<jsm>setGlobal</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>true</jk>);
+	 * </p>
+	 *
+	 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}).
+	 * @param value The value to set.
+	 * @see #get(String)
+	 * @see #clearGlobal()
+	 */
+	public static void setGlobal(String name, Object value) {
+		SETTINGS.setGlobal(name, s(value));
+	}
+
+	/**
+	 * Gets a configuration setting by name.
+	 *
+	 * <p>
+	 * Returns the setting object which can be used to check if a value is set and retrieve it.
+	 * Thread-local values take precedence over global values.
+	 *
+	 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}).
+	 * @return The setting object, or <jk>null</jk> if not set.
+	 * @see #get(String, Object)
+	 * @see #set(String, Object)
+	 */
+	public static StringSetting get(String name) {
+		return SETTINGS.get(name);
+	}
+
+	/**
+	 * Gets a configuration value by name with a default value.
+	 *
+	 * <p>
+	 * Returns the configured value if set, otherwise returns the provided default value.
+	 * Thread-local values take precedence over global values.
+	 *
+	 * <h5 class='section'>Example:</h5>
+	 * <p class='bjava'>
+	 * 	<jk>boolean</jk> <jv>sortMaps</jv> = BctConfiguration.<jsm>get</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>false</jk>);
+	 * </p>
+	 *
+	 * @param <T> The type of the value.
+	 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}).
+	 * @param def The default value to return if not set.
+	 * @return The configured value, or the default if not set.
+	 * @see #get(String)
+	 * @see #set(String, Object)
+	 */
+	public static <T> T get(String name, T def) {
+		return SETTINGS.get(name, def);
+	}
+
+	/**
+	 * Clears all thread-local configuration settings.
+	 *
+	 * <p>
+	 * This method removes all thread-local settings including:
+	 * <ul>
+	 * 	<li>Configuration properties (e.g., {@link #BCT_SORT_MAPS}, {@link #BCT_SORT_COLLECTIONS})</li>
+	 * 	<li>Bean converter override</li>
+	 * </ul>
+	 *
+	 * <p>
+	 * This is typically called in test teardown methods (e.g., {@code @AfterEach}) to ensure
+	 * a clean state for subsequent tests. The {@link org.apache.juneau.junit.bct.annotations.BctConfigExtension BctConfigExtension}
+	 * automatically calls this method after each test.
+	 *
+	 * <h5 class='section'>Example:</h5>
+	 * <p class='bjava'>
+	 * 	<ja>@AfterEach</ja>
+	 * 	<jk>void</jk> tearDown() {
+	 * 		BctConfiguration.<jsm>clear</jsm>();
+	 * 	}
+	 * </p>
+	 *
+	 * @see #clearGlobal()
+	 * @see #set(String, Object)
+	 */
+	public static void clear() {
+		SETTINGS.clearLocal();
+		CONVERTER_OVERRIDE.remove();
+	}
+
+	/**
+	 * Clears all global configuration settings.
+	 *
+	 * <p>
+	 * This method removes all global settings. Use with caution as this affects all threads.
+	 *
+	 * @see #clear()
+	 * @see #setGlobal(String, Object)
+	 */
+	public static void clearGlobal() {
+		SETTINGS.clearGlobal();
+	}
+
+	/**
+	 * Sets a custom bean converter for the current thread.
+	 *
+	 * <p>
+	 * This method allows you to override the default converter ({@link BasicBeanConverter#DEFAULT}) for all
+	 * assertions in the current test method. The converter will be used by all assertion methods in the current thread.
+	 *
+	 * <p>
+	 * This is particularly useful when you need custom property access, stringification, or conversion logic
+	 * for specific tests. The converter can be configured via the {@link org.apache.juneau.junit.bct.annotations.BctConfig @BctConfig}
+	 * annotation or set programmatically.
+	 *
+	 * <h5 class='section'>Usage Example:</h5>
+	 * <p class='bjava'>
+	 * 	<ja>@BeforeEach</ja>
+	 * 	<jk>void</jk> setUp() {
+	 * 		<jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
+	 * 			.defaultSettings()
+	 * 			.addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
+	 * 			.build();
+	 * 		BctConfiguration.<jsm>set</jsm>(<jv>customConverter</jv>);
+	 * 	}
+	 *
+	 * 	<ja>@Test</ja>
+	 * 	<jk>void</jk> testWithCustomConverter() {
+	 * 		<jc>// All assertions use the custom converter</jc>
+	 * 		<jsm>assertBean</jsm>(<jv>event</jv>, <js>"date"</js>, <js>"2023-12-01"</js>);
+	 * 	}
+	 *
+	 * 	<ja>@AfterEach</ja>
+	 * 	<jk>void</jk> tearDown() {
+	 * 		BctConfiguration.<jsm>clear</jsm>(); <jc>// Also clears converter override</jc>
+	 * 	}
+	 * </p>
+	 *
+	 * <h5 class='section'>Thread Safety:</h5>
+	 * <p>
+	 * This method is thread-safe and uses thread-local storage. Each test method running in parallel
+	 * will have its own converter instance, preventing cross-thread interference.
+	 * </p>
+	 *
+	 * @param converter The bean converter to use for the current thread. Must not be <jk>null</jk>.
+	 * @throws IllegalArgumentException If converter is <jk>null</jk>.
+	 * @see #clear()
+	 * @see org.apache.juneau.junit.bct.annotations.BctConfig#beanConverter()
+	 * @see BeanConverter
+	 * @see BasicBeanConverter
+	 */
+	public static void set(BeanConverter converter) {
+		assertArgNotNull("converter", converter);
+		CONVERTER_OVERRIDE.set(converter);
+	}
+
+
+	/**
+	 * Gets the bean converter for the current thread.
+	 *
+	 * <p>Returns the thread-local converter override if set, otherwise returns the memoized default converter.
+	 * This method is used internally by all assertion methods to get the current thread-local converter.</p>
+	 *
+	 * @return The bean converter to use for the current thread.
+	 */
+	static BeanConverter getConverter() {
+		return opt(BctConfiguration.CONVERTER_OVERRIDE.get()).orElseGet(CONVERTER_SUPPLIER.get());
+	}
+}
diff --git a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Listifiers.java b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Listifiers.java
index 35bd0045dd..917917dfd9 100644
--- a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Listifiers.java
+++ b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/Listifiers.java
@@ -20,6 +20,7 @@ import static java.util.Collections.*;
 import static java.util.Spliterators.*;
 import static java.util.stream.StreamSupport.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctConfiguration.*;
 
 import java.util.*;
 import java.util.stream.*;
@@ -134,7 +135,7 @@ public class Listifiers {
 	@SuppressWarnings("unchecked")
 	public static Listifier<Collection> collectionListifier() {
 		return (bc, collection) -> {
-			if (collection instanceof Set && ! (collection instanceof SortedSet) && ! (collection instanceof LinkedHashSet)) {
+			if (collection instanceof Set && ! (collection instanceof SortedSet) && ! (collection instanceof LinkedHashSet) && BctConfiguration.get(BCT_SORT_COLLECTIONS, false)) {
 				// TODO - This is too unreliable.
 				var collection2 = new TreeSet<>(flexibleComparator(bc));
 				collection2.addAll(collection);
@@ -314,8 +315,7 @@ public class Listifiers {
 	@SuppressWarnings("unchecked")
 	public static Listifier<Map> mapListifier() {
 		return (bc, map) -> {
-			if (! (map instanceof SortedMap) && ! (map instanceof LinkedHashMap)) {
-				// TODO - This is too unreliable.
+			if (! (map instanceof SortedMap) && ! (map instanceof LinkedHashMap) && BctConfiguration.get(BCT_SORT_MAPS, false)) {
 				var map2 = new TreeMap<>(flexibleComparator(bc));
 				map2.putAll(map);
 				map = map2;
diff --git a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfig.java b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfig.java
new file mode 100644
index 0000000000..e3224c4f4b
--- /dev/null
+++ b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfig.java
@@ -0,0 +1,90 @@
+/*
+ * 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.junit.bct.annotations;
+
+import java.lang.annotation.*;
+
+import org.apache.juneau.commons.lang.*;
+import org.apache.juneau.junit.bct.*;
+import org.junit.jupiter.api.extension.*;
+
+/**
+ * Annotation for configuring BCT settings in JUnit 5 tests.
+ *
+ * <p>
+ * This annotation can be applied to test classes or test methods to automatically configure
+ * BCT settings before tests run and clear them after tests complete.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * 	<ja>@BctConfig</ja>(sortMaps=TriState.<jsf>TRUE</jsf>)
+ * 	<jk>class</jk> MyTest {
+ * 		<ja>@Test</ja>
+ * 		<ja>@BctConfig</ja>(sortCollections=TriState.<jsf>TRUE</jsf>)
+ * 		<jk>void</jk> testFoo() {
+ * 			<jc>// sortMaps and sortCollections are both enabled</jc>
+ * 		}
+ * 	}
+ * </p>
+ *
+ * <p>
+ * Method-level annotations override class-level annotations.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(BctConfigExtension.class)
+public @interface BctConfig {
+
+	/**
+	 * Enable sorting of maps in BCT assertions.
+	 *
+	 * @return {@link TriState#TRUE} to enable map sorting, {@link TriState#FALSE} to disable,
+	 * 	or {@link TriState#UNSET} to inherit from class-level annotation.
+	 */
+	TriState sortMaps() default TriState.UNSET;
+
+	/**
+	 * Enable sorting of collections in BCT assertions.
+	 *
+	 * @return {@link TriState#TRUE} to enable collection sorting, {@link TriState#FALSE} to disable,
+	 * 	or {@link TriState#UNSET} to inherit from class-level annotation.
+	 */
+	TriState sortCollections() default TriState.UNSET;
+
+	/**
+	 * Custom bean converter class to use for BCT assertions.
+	 *
+	 * <p>
+	 * If specified (not the default {@link BeanConverter}), the class will be instantiated using
+	 * its no-arg constructor and set as the converter for the current thread.
+	 *
+	 * <h5 class='section'>Example:</h5>
+	 * <p class='bjava'>
+	 * 	<ja>@BctConfiguration</ja>(beanConverter=MyCustomConverter.<jk>class</jk>)
+	 * 	<jk>class</jk> MyTest {
+	 * 		<ja>@Test</ja>
+	 * 		<jk>void</jk> testFoo() {
+	 * 			<jc>// Uses MyCustomConverter for all assertions</jc>
+	 * 		}
+	 * 	}
+	 * </p>
+	 *
+	 * @return The bean converter class to instantiate and use, or {@link BeanConverter} to use the default.
+	 */
+	Class<? extends BeanConverter> beanConverter() default BeanConverter.class;
+}
+
diff --git a/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfigExtension.java b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfigExtension.java
new file mode 100644
index 0000000000..c606f3081b
--- /dev/null
+++ b/juneau-core/juneau-bct/src/main/java/org/apache/juneau/junit/bct/annotations/BctConfigExtension.java
@@ -0,0 +1,76 @@
+/*
+ * 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.junit.bct.annotations;
+
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+import static org.apache.juneau.junit.bct.BctConfiguration.*;
+import static org.apache.juneau.commons.lang.TriState.*;
+
+import java.util.*;
+
+import org.apache.juneau.junit.bct.*;
+import org.junit.jupiter.api.extension.*;
+
+/**
+ * JUnit 5 extension that processes {@link BctConfig} annotations.
+ *
+ * <p>
+ * This extension automatically sets BCT settings before test execution and clears them after.
+ * It supports both class-level and method-level annotations, with method-level taking precedence.
+ */
+public class BctConfigExtension implements BeforeEachCallback, AfterEachCallback {
+
+	@Override
+	public void beforeEach(ExtensionContext context) throws Exception {
+		var al = getAnnotations(context);
+
+		if (al.isEmpty())
+			return;
+
+		clear();
+
+		al.stream().map(x -> x.sortMaps()).filter(x -> ne(x, UNSET)).findFirst().ifPresent(x -> set(BCT_SORT_MAPS, x == TRUE));
+		al.stream().map(x -> x.sortCollections()).filter(x -> ne(x, UNSET)).findFirst().ifPresent(x -> set(BCT_SORT_COLLECTIONS, x == TRUE));
+		al.stream().map(x -> x.beanConverter()).filter(x -> ne(x, BeanConverter.class)).findFirst().map(x -> eq(x, BasicBeanConverter.class) ? null : x).ifPresent(x -> setConverter(x));
+	}
+
+	private static void setConverter(Class<? extends BeanConverter> x) {
+		safe(()->{
+			var c = x.getDeclaredConstructor();
+			c.setAccessible(true);
+			set(c.newInstance());
+		}, e -> rex(e, "Failed to instantiate BeanConverter: {0}. It must have a no-arg constructor.", x.getName()));
+	}
+
+	@Override
+	public void afterEach(ExtensionContext context) throws Exception {
+		var al = getAnnotations(context);
+
+		if (al.isEmpty())
+			return;
+
+		clear();
+	}
+
+	private static List<BctConfig> getAnnotations(ExtensionContext context) {
+		var l = new ArrayList<BctConfig>();
+		context.getTestMethod().map(x -> x.getAnnotation(BctConfig.class)).ifPresent(x -> l.add(x));
+		context.getTestClass().map(x -> x.getAnnotation(BctConfig.class)).ifPresent(x -> l.add(x));
+		return l;
+	}
+}
diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/lang/TriState.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/lang/TriState.java
new file mode 100644
index 0000000000..6b668f96fb
--- /dev/null
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/lang/TriState.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.lang;
+
+/**
+ * Three-state enumeration for boolean-like values that can be explicitly set or unset.
+ *
+ * <p>
+ * This enum is useful in scenarios where you need to distinguish between:
+ * <ul>
+ * 	<li>A value that is explicitly set to <jk>true</jk></li>
+ * 	<li>A value that is explicitly set to <jk>false</jk></li>
+ * 	<li>A value that is not set (should inherit from a parent/default)</li>
+ * </ul>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * 	<jc>// In an annotation</jc>
+ * 	<jk>public @interface</jk> MyConfig {
+ * 		TriState <jsm>enabled</jsm>() <jk>default</jk> TriState.<jsf>UNSET</jsf>;
+ * 	}
+ *
+ * 	<jc>// Usage</jc>
+ * 	<ja>@MyConfig</ja>(enabled = TriState.<jsf>TRUE</jsf>)  <jc>// Explicitly enabled</jc>
+ * 	<ja>@MyConfig</ja>(enabled = TriState.<jsf>FALSE</jsf>) <jc>// Explicitly disabled</jc>
+ * 	<ja>@MyConfig</ja>                                    <jc>// Not set, inherits default</jc>
+ * </p>
+ */
+public enum TriState {
+	/** Explicitly set to <jk>true</jk>. */
+	TRUE,
+
+	/** Explicitly set to <jk>false</jk>. */
+	FALSE,
+
+	/** Not set - should inherit from parent or use default value. */
+	UNSET;
+
+	/**
+	 * Converts this TriState to a boolean value.
+	 *
+	 * <p>
+	 * If this is {@link #UNSET}, returns the provided default value.
+	 *
+	 * @param defaultValue The default value to use if this is {@link #UNSET}.
+	 * @return <jk>true</jk> if this is {@link #TRUE}, <jk>false</jk> if this is {@link #FALSE},
+	 * 	or the provided default if this is {@link #UNSET}.
+	 */
+	public boolean toBoolean(boolean defaultValue) {
+		return this == UNSET ? defaultValue : (this == TRUE);
+	}
+
+	/**
+	 * Converts a boolean to a TriState.
+	 *
+	 * @param value The boolean value.
+	 * @return {@link #TRUE} if <jk>true</jk>, {@link #FALSE} if <jk>false</jk>.
+	 */
+	public static TriState fromBoolean(boolean value) {
+		return value ? TRUE : FALSE;
+	}
+}
+
diff --git a/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md b/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
index e8187f5ae2..9a593e8408 100644
--- a/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
+++ b/juneau-docs/docs/topics/07.01.00.JuneauBctBasics.md
@@ -18,11 +18,12 @@ A powerful and intuitive testing framework that extends JUnit with streamlined a
 
 ## 📚 Advanced Topics
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Custom string conversions
-- [Listifiers](07.01.02.Listifiers.md) - Custom list conversions
-- [Swappers](07.01.03.Swappers.md) - Object transformation
+- [Custom Error Messages](07.02.00.CustomErrorMessages.md) - Enhanced error reporting
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Custom string conversions
+  - [Listifiers](07.03.02.Listifiers.md) - Custom list conversions
+  - [Swappers](07.03.03.Swappers.md) - Object transformation
 - [Property Extractors](07.01.04.PropertyExtractors.md) - Custom property access
-- [Custom Error Messages](07.01.05.CustomErrorMessages.md) - Enhanced error reporting
 
 ## Overview
 
@@ -536,9 +537,33 @@ assertBean(user, "orders{size,#{total}}", "{3,[{99.99},{149.99},{79.99}]}");
 
 BCT provides several advanced configuration options for customizing assertion behavior:
 
+### @BctConfig Annotation
+
+The `@BctConfig` annotation provides a declarative way to configure BCT settings for your tests. It can be applied at the class level or method level, with method-level annotations taking precedence.
+
+```java
+@BctConfig(sortMaps=TriState.TRUE, beanConverter=MyCustomConverter.class)
+class MyTest {
+    @Test
+    @BctConfig(sortCollections=TriState.TRUE)
+    void testSomething() {
+        // sortMaps=true (from class), sortCollections=true (from method),
+        // beanConverter=MyCustomConverter (from class)
+        assertBean(order, "items{0{name}}", "{{Laptop}}");
+    }
+}
+```
+
+The annotation supports:
+- **sortMaps**: Enable sorting of map entries before comparison
+- **sortCollections**: Enable sorting of collection elements before comparison
+- **beanConverter**: Specify a custom bean converter class to use
+
+For more details, see [Customization](07.03.00.Customization.md).
+
 ### Custom Bean Converters
 
-The default bean converter can be customized on a per-thread basis using `setConverter()` and `resetConverter()`. This is particularly useful in test setup methods to configure a custom converter for all tests in a test class or method.
+The default bean converter can be customized on a per-thread basis using `BctConfiguration.set(BeanConverter)` or via the `@BctConfig` annotation. This is particularly useful in test setup methods to configure a custom converter for all tests in a test class or method.
 
 ```java
 // Set custom converter in @BeforeEach method
@@ -551,7 +576,7 @@ var converter = BasicBeanConverter.builder()
     .addStringifier(Money.class, money -> 
         money.getAmount().toPlainString())
     .build();
-    BctAssertions.setConverter(converter);
+    BctConfiguration.set(converter);
 }
 
 // All assertions now use the custom converter
@@ -563,7 +588,7 @@ void testWithCustomConverter() {
 // Reset in @AfterEach method
 @AfterEach
 void tearDown() {
-    BctAssertions.resetConverter();
+    BctConfiguration.clear();
 }
 ```
 
@@ -573,11 +598,12 @@ void tearDown() {
 
 For detailed information on extending and customizing BCT, see:
 
-- **[Stringifiers](07.01.01.Stringifiers.md)** - Define how objects are converted to strings for comparison
-- **[Listifiers](07.01.02.Listifiers.md)** - Convert collection-like objects to lists for testing
-- **[Swappers](07.01.03.Swappers.md)** - Transform objects before processing (unwrap Optional, Supplier, etc.)
+- **[Custom Error Messages](07.02.00.CustomErrorMessages.md)** - Add contextual information to assertion failures
+- **[Customization](07.03.00.Customization.md)** - Configure BCT via @BctConfig annotation and BctConfiguration class
+  - **[Stringifiers](07.03.01.Stringifiers.md)** - Define how objects are converted to strings for comparison
+  - **[Listifiers](07.03.02.Listifiers.md)** - Convert collection-like objects to lists for testing
+  - **[Swappers](07.03.03.Swappers.md)** - Transform objects before processing (unwrap Optional, Supplier, etc.)
 - **[Property Extractors](07.01.04.PropertyExtractors.md)** - Define custom property access logic for non-standard objects
-- **[Custom Error Messages](07.01.05.CustomErrorMessages.md)** - Add contextual information to assertion failures
 
 ## Migration Examples
 
diff --git a/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md b/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
index 4f24da3162..1a9752c23f 100644
--- a/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
+++ b/juneau-docs/docs/topics/07.01.04.PropertyExtractors.md
@@ -480,8 +480,9 @@ try {
 
 ## See Also
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Converting objects to strings
-- [Listifiers](07.01.02.Listifiers.md) - Converting collection-like objects to lists
-- [Swappers](07.01.03.Swappers.md) - Transforming objects before processing
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Converting objects to strings
+  - [Listifiers](07.03.02.Listifiers.md) - Converting collection-like objects to lists
+  - [Swappers](07.03.03.Swappers.md) - Transforming objects before processing
 
diff --git a/juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md b/juneau-docs/docs/topics/07.02.00.CustomErrorMessages.md
similarity index 96%
rename from juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md
rename to juneau-docs/docs/topics/07.02.00.CustomErrorMessages.md
index 119ddd2e4a..0a73632cbd 100644
--- a/juneau-docs/docs/topics/07.01.05.CustomErrorMessages.md
+++ b/juneau-docs/docs/topics/07.02.00.CustomErrorMessages.md
@@ -398,9 +398,10 @@ void testOrderIntegration() {
 
 ## See Also
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Converting objects to strings
-- [Listifiers](07.01.02.Listifiers.md) - Converting collection-like objects to lists
-- [Swappers](07.01.03.Swappers.md) - Transforming objects before processing
-- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property access logic
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Converting objects to strings
+  - [Listifiers](07.03.02.Listifiers.md) - Converting collection-like objects to lists
+  - [Swappers](07.03.03.Swappers.md) - Transforming objects before processing
+- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property access logic
 
diff --git a/juneau-docs/docs/topics/07.03.00.Customization.md b/juneau-docs/docs/topics/07.03.00.Customization.md
new file mode 100644
index 0000000000..43b1db7ac8
--- /dev/null
+++ b/juneau-docs/docs/topics/07.03.00.Customization.md
@@ -0,0 +1,351 @@
+---
+title: "Customization"
+slug: Customization
+---
+
+# Customization
+
+BCT provides flexible customization options through the `@BctConfig` annotation and the `BctConfiguration` class. These tools allow you to configure assertion behavior, customize property access, and extend BCT's capabilities to match your testing needs.
+
+## 📋 Table of Contents
+
+- [@BctConfig Annotation](#bctconfig-annotation)
+- [BctConfiguration Class](#bctconfiguration-class)
+- [Configuration Properties](#configuration-properties)
+- [Custom Bean Converters](#custom-bean-converters)
+- [Thread Safety](#thread-safety)
+- [Advanced Customization Topics](#advanced-customization-topics)
+
+## @BctConfig Annotation
+
+The `@BctConfig` annotation provides a declarative way to configure BCT settings for your tests. It can be applied at both the class level and method level, with method-level annotations taking precedence over class-level annotations.
+
+### Basic Usage
+
+```java
+import org.apache.juneau.junit.bct.annotations.BctConfig;
+import org.apache.juneau.commons.lang.TriState;
+
+@BctConfig(sortMaps=TriState.TRUE)
+class MyTest {
+    @Test
+    @BctConfig(sortCollections=TriState.TRUE)
+    void testSomething() {
+        // sortMaps=true (from class), sortCollections=true (from method)
+        assertBean(order, "items{0{name}}", "{{Laptop}}");
+    }
+}
+```
+
+### Annotation Properties
+
+#### sortMaps
+
+Controls whether map entries are sorted by key before comparison in assertions.
+
+```java
+@BctConfig(sortMaps=TriState.TRUE)
+class MyTest {
+    @Test
+    void testMap() {
+        // Map entries will be sorted before comparison
+        Map<String, String> map = Map.of("zebra", "animal", "apple", "fruit");
+        assertBean(map, "apple,zebra", "fruit,animal");
+    }
+}
+```
+
+**Values:**
+- `TriState.TRUE` - Enable map sorting
+- `TriState.FALSE` - Disable map sorting
+- `TriState.UNSET` - Inherit from class-level annotation (default for method-level)
+
+#### sortCollections
+
+Controls whether collection elements are sorted before comparison in assertions.
+
+```java
+@BctConfig(sortCollections=TriState.TRUE)
+class MyTest {
+    @Test
+    void testList() {
+        // List elements will be sorted before comparison
+        List<String> list = Arrays.asList("zebra", "apple", "banana");
+        assertList(list, "apple", "banana", "zebra");
+    }
+}
+```
+
+**Values:**
+- `TriState.TRUE` - Enable collection sorting
+- `TriState.FALSE` - Disable collection sorting
+- `TriState.UNSET` - Inherit from class-level annotation (default for method-level)
+
+#### beanConverter
+
+Specifies a custom bean converter class to use for all assertions in the test.
+
+```java
+public static class MyCustomConverter extends BasicBeanConverter {
+    public MyCustomConverter() {
+        super(BasicBeanConverter.builder()
+            .defaultSettings()
+            .addStringifier(LocalDate.class, date -> 
+                date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+            .build());
+    }
+}
+
+@BctConfig(beanConverter=MyCustomConverter.class)
+class MyTest {
+    @Test
+    void testWithCustomConverter() {
+        // All assertions use MyCustomConverter
+        assertBean(event, "date", "2023-12-01");
+    }
+}
+```
+
+**Requirements:**
+- The converter class must have a no-arg constructor
+- The constructor will be made accessible if needed (for nested classes, etc.)
+- If instantiation fails, the test will fail with a descriptive error message
+
+### Class-Level vs Method-Level
+
+Method-level annotations override class-level annotations for the same property. Properties not specified in the method annotation inherit from the class annotation.
+
+```java
+@BctConfig(sortMaps=TriState.TRUE, beanConverter=MyConverter.class)
+class MyTest {
+    @Test
+    @BctConfig(sortMaps=TriState.FALSE, sortCollections=TriState.TRUE)
+    void testMethod() {
+        // sortMaps=false (from method, overrides class)
+        // sortCollections=true (from method)
+        // beanConverter=MyConverter (from class, inherited)
+    }
+    
+    @Test
+    void testAnotherMethod() {
+        // sortMaps=true (from class)
+        // sortCollections=UNSET (default, not set)
+        // beanConverter=MyConverter (from class)
+    }
+}
+```
+
+### Using Default Converter
+
+To explicitly use the default converter when a class-level converter is set, use `BasicBeanConverter.class`:
+
+```java
+@BctConfig(beanConverter=MyConverter.class)
+class MyTest {
+    @Test
+    @BctConfig(beanConverter=BasicBeanConverter.class)
+    void testWithDefaultConverter() {
+        // Uses default converter, not MyConverter
+    }
+}
+```
+
+## BctConfiguration Class
+
+The `BctConfiguration` class provides a programmatic API for configuring BCT settings. It's useful when you need dynamic configuration or want to set up converters in test setup methods.
+
+### Setting Configuration Values
+
+```java
+import static org.apache.juneau.junit.bct.BctConfiguration.*;
+
+@BeforeEach
+void setUp() {
+    // Enable map sorting
+    set(BCT_SORT_MAPS, true);
+    
+    // Enable collection sorting
+    set(BCT_SORT_COLLECTIONS, true);
+    
+    // Set custom converter
+    var converter = BasicBeanConverter.builder()
+        .defaultSettings()
+        .addStringifier(LocalDate.class, date -> 
+            date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+        .build();
+    set(converter);
+}
+
+@AfterEach
+void tearDown() {
+    // Clear all thread-local settings
+    clear();
+}
+```
+
+### Getting Configuration Values
+
+```java
+// Get a setting with a default value
+boolean sortMaps = BctConfiguration.get(BCT_SORT_MAPS, false);
+
+// Get a setting object (can check if set)
+StringSetting setting = BctConfiguration.get(BCT_SORT_MAPS);
+if (setting != null && setting.asBoolean()) {
+    // Map sorting is enabled
+}
+```
+
+### Global vs Thread-Local Settings
+
+Settings can be set globally (affecting all threads) or thread-locally (affecting only the current thread). Thread-local settings take precedence.
+
+```java
+// Set global setting (affects all threads)
+BctConfiguration.setGlobal(BCT_SORT_MAPS, true);
+
+// Set thread-local setting (overrides global for this thread)
+BctConfiguration.set(BCT_SORT_MAPS, false);
+
+// Clear thread-local settings
+BctConfiguration.clear();
+
+// Clear global settings
+BctConfiguration.clearGlobal();
+```
+
+## Configuration Properties
+
+### BCT_SORT_MAPS
+
+Property name: `"Bct.sortMaps"`
+
+When enabled, map entries are sorted by key before comparison, ensuring consistent assertion results regardless of map implementation or insertion order.
+
+```java
+BctConfiguration.set(BctConfiguration.BCT_SORT_MAPS, true);
+```
+
+### BCT_SORT_COLLECTIONS
+
+Property name: `"Bct.sortCollections"`
+
+When enabled, collection elements are sorted before comparison, ensuring consistent assertion results regardless of collection implementation or insertion order.
+
+```java
+BctConfiguration.set(BctConfiguration.BCT_SORT_COLLECTIONS, true);
+```
+
+## Custom Bean Converters
+
+Bean converters control how objects are accessed and converted to strings for comparison. BCT uses `BasicBeanConverter.DEFAULT` by default, but you can provide custom converters for specialized needs.
+
+### Creating a Custom Converter
+
+```java
+public static class MyCustomConverter extends BasicBeanConverter {
+    public MyCustomConverter() {
+        super(BasicBeanConverter.builder()
+            .defaultSettings()
+            // Add custom stringifier for LocalDate
+            .addStringifier(LocalDate.class, date -> 
+                date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+            // Add custom stringifier for Money
+            .addStringifier(Money.class, money -> 
+                money.getAmount().toPlainString())
+            .build());
+    }
+}
+```
+
+### Using via Annotation
+
+```java
+@BctConfig(beanConverter=MyCustomConverter.class)
+class MyTest {
+    @Test
+    void testWithCustomConverter() {
+        assertBean(order, "date,total", "2023-12-01,99.99");
+    }
+}
+```
+
+### Using via Programmatic API
+
+```java
+@BeforeEach
+void setUp() {
+    var converter = BasicBeanConverter.builder()
+        .defaultSettings()
+        .addStringifier(LocalDate.class, date -> 
+            date.format(DateTimeFormatter.ISO_LOCAL_DATE))
+        .build();
+    BctConfiguration.set(converter);
+}
+
+@AfterEach
+void tearDown() {
+    BctConfiguration.clear(); // Also clears converter
+}
+```
+
+### Converter Requirements
+
+- Must have a no-arg constructor
+- Constructor will be made accessible if needed (for nested classes, private constructors, etc.)
+- If instantiation fails, a `RuntimeException` is thrown with a descriptive message
+
+## Thread Safety
+
+All configuration in BCT is thread-safe and uses thread-local storage. This ensures that:
+
+- **Parallel test execution** doesn't interfere with each other's settings
+- **Each test thread** has its own isolated configuration
+- **Settings are automatically cleared** after each test when using `@BctConfig`
+
+### Thread-Local Storage
+
+```java
+// Thread 1
+BctConfiguration.set(BCT_SORT_MAPS, true);
+
+// Thread 2 (running in parallel)
+BctConfiguration.set(BCT_SORT_MAPS, false);
+
+// Each thread has its own setting - no interference
+```
+
+### Automatic Cleanup
+
+When using `@BctConfig`, settings are automatically cleared after each test method:
+
+```java
+@BctConfig(sortMaps=TriState.TRUE)
+class MyTest {
+    @Test
+    void test1() {
+        // sortMaps is enabled
+    }
+    
+    @Test
+    void test2() {
+        // sortMaps is still enabled (from class annotation)
+        // But if test1 had method-level annotation, it would be cleared
+    }
+}
+```
+
+## Advanced Customization Topics
+
+For more advanced customization options, see:
+
+- **[Stringifiers](07.03.01.Stringifiers.md)** - Define how objects are converted to strings for comparison
+- **[Listifiers](07.03.02.Listifiers.md)** - Convert collection-like objects to lists for testing
+- **[Swappers](07.03.03.Swappers.md)** - Transform objects before processing (unwrap Optional, Supplier, etc.)
+
+## See Also
+
+- [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Custom Error Messages](07.02.00.CustomErrorMessages.md) - Enhanced error reporting
+- [Property Extractors](07.01.04.PropertyExtractors.md) - Custom property access logic
+
diff --git a/juneau-docs/docs/topics/07.01.01.Stringifiers.md b/juneau-docs/docs/topics/07.03.01.Stringifiers.md
similarity index 95%
rename from juneau-docs/docs/topics/07.01.01.Stringifiers.md
rename to juneau-docs/docs/topics/07.03.01.Stringifiers.md
index ac587d3b58..dda0ec41eb 100644
--- a/juneau-docs/docs/topics/07.01.01.Stringifiers.md
+++ b/juneau-docs/docs/topics/07.03.01.Stringifiers.md
@@ -190,8 +190,9 @@ try {
 
 ## See Also
 
-- [Listifiers](07.01.02.Listifiers.md) - Converting collection-like objects to lists
-- [Swappers](07.01.03.Swappers.md) - Transforming objects before processing
-- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property access logic
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig and BctConfiguration
+  - [Listifiers](07.03.02.Listifiers.md) - Converting collection-like objects to lists
+  - [Swappers](07.03.03.Swappers.md) - Transforming objects before processing
+- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property access logic
 
diff --git a/juneau-docs/docs/topics/07.01.02.Listifiers.md b/juneau-docs/docs/topics/07.03.02.Listifiers.md
similarity index 96%
rename from juneau-docs/docs/topics/07.01.02.Listifiers.md
rename to juneau-docs/docs/topics/07.03.02.Listifiers.md
index 6f9de86b2d..d3bed0b58e 100644
--- a/juneau-docs/docs/topics/07.01.02.Listifiers.md
+++ b/juneau-docs/docs/topics/07.03.02.Listifiers.md
@@ -259,8 +259,9 @@ Listifier<DataSource> safeListifier = (conv, source) -> {
 
 ## See Also
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Converting objects to strings
-- [Swappers](07.01.03.Swappers.md) - Transforming objects before processing
-- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property access logic
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Converting objects to strings
+  - [Swappers](07.03.03.Swappers.md) - Transforming objects before processing
+- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property access logic
 
diff --git a/juneau-docs/docs/topics/07.01.03.Swappers.md b/juneau-docs/docs/topics/07.03.03.Swappers.md
similarity index 97%
rename from juneau-docs/docs/topics/07.01.03.Swappers.md
rename to juneau-docs/docs/topics/07.03.03.Swappers.md
index ee060bd61b..df5429e2b4 100644
--- a/juneau-docs/docs/topics/07.01.03.Swappers.md
+++ b/juneau-docs/docs/topics/07.03.03.Swappers.md
@@ -355,8 +355,9 @@ Swapper<Validation> validationSwapper = (conv, validation) -> {
 
 ## See Also
 
-- [Stringifiers](07.01.01.Stringifiers.md) - Converting objects to strings
-- [Listifiers](07.01.02.Listifiers.md) - Converting collection-like objects to lists
-- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property access logic
 - [juneau-bct Basics](07.01.00.JuneauBctBasics.md) - Main BCT documentation
+- [Customization](07.03.00.Customization.md) - Configuration via @BctConfig and BctConfiguration
+  - [Stringifiers](07.03.01.Stringifiers.md) - Converting objects to strings
+  - [Listifiers](07.03.02.Listifiers.md) - Converting collection-like objects to lists
+- [PropertyExtractors](07.01.04.PropertyExtractors.md) - Custom property access logic
 
diff --git a/juneau-docs/sidebars.ts b/juneau-docs/sidebars.ts
index f54e0c96f2..4013176709 100644
--- a/juneau-docs/sidebars.ts
+++ b/juneau-docs/sidebars.ts
@@ -863,29 +863,41 @@ const sidebars: SidebarsConfig = {
         },
         {
           type: 'doc',
-          id: 'topics/07.01.01.Stringifiers',
-          label: '7.1.1. Stringifiers',
+          id: 'topics/07.02.00.CustomErrorMessages',
+          label: '7.2. Custom Error Messages',
         },
         {
-          type: 'doc',
-          id: 'topics/07.01.02.Listifiers',
-          label: '7.1.2. Listifiers',
-        },
-        {
-          type: 'doc',
-          id: 'topics/07.01.03.Swappers',
-          label: '7.1.3. Swappers',
+          type: 'category',
+          label: '7.3. Customization',
+          collapsed: true,
+          items: [
+            {
+              type: 'doc',
+              id: 'topics/07.03.00.Customization',
+              label: '7.3. Customization',
+            },
+            {
+              type: 'doc',
+              id: 'topics/07.03.01.Stringifiers',
+              label: '7.3.1. Stringifiers',
+            },
+            {
+              type: 'doc',
+              id: 'topics/07.03.02.Listifiers',
+              label: '7.3.2. Listifiers',
+            },
+            {
+              type: 'doc',
+              id: 'topics/07.03.03.Swappers',
+              label: '7.3.3. Swappers',
+            },
+          ],
         },
         {
           type: 'doc',
           id: 'topics/07.01.04.PropertyExtractors',
           label: '7.1.4. Property Extractors',
         },
-        {
-          type: 'doc',
-          id: 'topics/07.01.05.CustomErrorMessages',
-          label: '7.1.5. Custom Error Messages',
-        },
       ],
     },
     {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/BeanSession_Test.java b/juneau-utest/src/test/java/org/apache/juneau/BeanSession_Test.java
index 7ab35555c5..657af8756b 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/BeanSession_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/BeanSession_Test.java
@@ -16,8 +16,6 @@
  */
 package org.apache.juneau;
 
-import org.junit.jupiter.api.*;
-
 class BeanSession_Test extends TestBase {
 	// Test class for BeanSession
 }
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/HeaderInfo_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/HeaderInfo_Test.java
index 9f39d8fe08..fbfd02f2e3 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/HeaderInfo_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/HeaderInfo_Test.java
@@ -221,7 +221,7 @@ class HeaderInfo_Test extends TestBase {
 		}
 
 		@Test void c07_keySet() {
-			assertList(TESTER.bean().keySet(), "allowEmptyValue", "allowReserved", "deprecated", "description", "examples", "explode", "required", "schema", "style", "x-example", "x1", "x2");
+			assertList(TESTER.bean().keySet(), "allowEmptyValue", "allowReserved", "deprecated", "description", "examples", "explode", "required", "schema", "x-example", "style", "x1", "x2");
 		}
 
 		@Test void c08_get() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Items_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Items_Test.java
index a9cd1c6c1d..cd43cb75ad 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Items_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Items_Test.java
@@ -271,7 +271,7 @@ class Items_Test extends TestBase {
 		}
 
 		@Test void c07_keySet() {
-			assertList(TESTER.bean().keySet(), "additionalItems", "allOf", "collectionFormat", "default", "discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "pattern", "properties", "readOnly", "required", "title", "type", "uniqueItems", "x1", "x2", "xml");
+			assertList(TESTER.bean().keySet(), "collectionFormat", "default", "enum", "exclusiveMaximum", "exclusiveMinimum", "format", "items", "maxItems", "maxLength", "maximum", "minItems", "minLength", "minimum", "multipleOf", "pattern", "type", "uniqueItems", "additionalItems", "allOf", "discriminator", "example", "externalDocs", "maxProperties", "minProperties", "properties", "readOnly", "required", "title", "xml", "x1", "x2");
 		}
 
 		@Test void c08_get() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Parameter_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Parameter_Test.java
index 6f6b65acf4..e6ccd983f6 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Parameter_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/Parameter_Test.java
@@ -228,7 +228,7 @@ class Parameter_Test extends TestBase {
 		}
 
 		@Test void c07_keySet() {
-			assertList(TESTER.bean().keySet(), "allowEmptyValue", "allowReserved", "content", "deprecated", "description", "example", "examples", "explode", "in", "name", "required", "schema", "style", "x1", "x2");
+			assertList(TESTER.bean().keySet(), "allowEmptyValue", "allowReserved", "deprecated", "description", "example", "examples", "explode", "in", "name", "required", "schema", "style", "content", "x1", "x2");
 		}
 
 		@Test void c08_get() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
index 7e99125e2e..62bc12efc6 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/openapi3/SchemaInfo_Test.java
@@ -277,7 +277,7 @@ class SchemaInfo_Test extends TestBase {
 		}
 
 		@Test void c07_keySet() {
-			assertList(TESTER.bean().keySet(), "additionalItems", "additionalProperties", "allOf", "anyOf", "default", "description", "discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "not", "nullable", "oneOf", "pattern", "properties", "readOnly", "required", "title", "type", "uniqueItems", "writeOnly", "x1", "x2", "xml");
+			assertList(TESTER.bean().keySet(), "additionalProperties", "allOf", "anyOf", "default", "description", "discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "not", "nullable", "oneOf", "pattern", "properties", "readOnly", "required", "title", "type", "uniqueItems", "writeOnly", "xml", "additionalItems", "x1", "x2");
 		}
 
 		@Test void c08_get() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/swagger/SchemaInfo_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/swagger/SchemaInfo_Test.java
index b4c92e38f1..b4ac43d8c0 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/bean/swagger/SchemaInfo_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/swagger/SchemaInfo_Test.java
@@ -306,7 +306,7 @@ class SchemaInfo_Test extends TestBase {
 		}
 
 		@Test void c07_keySet() {
-			assertList(TESTER.bean().keySet(), "$ref", "additionalProperties", "allOf", "default", "description", "discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "pattern", "properties", "readOnly", "required", "requiredProperties", "title", "type", "uniqueItems", "x1", "x2", "xml");
+			assertList(TESTER.bean().keySet(), "$ref", "additionalProperties", "allOf", "default", "description", "discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "pattern", "properties", "readOnly", "required", "requiredProperties", "title", "type", "uniqueItems", "xml", "x1", "x2");
 		}
 
 		@Test void c08_get() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Maps_Test.java b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Maps_Test.java
index 150139c615..47478b3c68 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Maps_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Maps_Test.java
@@ -16,12 +16,14 @@
  */
 package org.apache.juneau.commons.collections;
 
+import static org.apache.juneau.commons.lang.TriState.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 
 class Maps_Test extends TestBase {
@@ -92,6 +94,7 @@ class Maps_Test extends TestBase {
 	//-----------------------------------------------------------------------------------------------------------------
 
 	@Test
+	@BctConfig(sortMaps = TRUE)
 	void b01_addPairs() {
 		var map = Maps.create(String.class, String.class)
 			.addPairs("host", "localhost", "port", "8080", "protocol", "https")
diff --git a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
index 4d6db8e676..b394fcab34 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Sets_Test.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.commons.collections;
 
+import static org.apache.juneau.commons.lang.TriState.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
@@ -23,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import java.util.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 
 class Sets_Test extends TestBase {
@@ -849,6 +851,7 @@ class Sets_Test extends TestBase {
 	}
 
 	@Test
+	@BctConfig(sortCollections = TRUE)
 	void u02_filtered_multipleFilters() {
 		var set = Sets.create(Integer.class)
 			.filtered(v -> v != null)
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BasicBeanConverter_Test.java b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BasicBeanConverter_Test.java
index 2409c601ba..a3e9d6aa32 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BasicBeanConverter_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BasicBeanConverter_Test.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.junit.bct;
 
+import static org.apache.juneau.commons.lang.TriState.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 import static org.apache.juneau.junit.bct.BasicBeanConverter.*;
@@ -29,6 +30,7 @@ import java.util.*;
 import java.util.concurrent.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 
 /**
@@ -172,6 +174,7 @@ class BasicBeanConverter_Test extends TestBase {
 
 		@Test
 		@DisplayName("b04_stringify() handles collections")
+		@BctConfig(sortCollections = TRUE)
 		void b04_stringify_handlesCollections() {
 			assertEquals("[1,2,3]", converter.stringify(l(1, 2, 3)));
 			assertEquals("[]", converter.stringify(Collections.emptyList()));
@@ -216,6 +219,7 @@ class BasicBeanConverter_Test extends TestBase {
 
 		@Test
 		@DisplayName("b09_listify() handles collections")
+		@BctConfig(sortCollections = TRUE)
 		void b09_listify_handlesCollections() {
 			var set = Set.of("z", "a", "m");
 			var result = converter.listify(set);
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
index 92961f8778..cff53d2307 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctAssertions_Test.java
@@ -20,12 +20,14 @@ import static org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.apache.juneau.commons.lang.TriState.*;
 
 import java.util.*;
 import java.util.function.*;
 import java.util.stream.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 import org.opentest4j.*;
 
@@ -392,6 +394,7 @@ class BctAssertions_Test extends TestBase {
 	// ====================================================================================================
 
 	@Nested
+	@BctConfig(sortMaps = TRUE)
 	class H_assertMap extends TestBase {
 
 		@Test
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctConfig_Test.java b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctConfig_Test.java
new file mode 100644
index 0000000000..2a3dbaa560
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/BctConfig_Test.java
@@ -0,0 +1,306 @@
+/*
+ * 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 "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.junit.bct;
+
+import static org.apache.juneau.commons.lang.TriState.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.apache.juneau.junit.bct.BctConfiguration.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for {@link BctConfig} annotation and extension.
+ */
+class BctConfig_Test extends TestBase {
+
+	// ====================================================================================================
+	// Class-level annotation tests
+	// ====================================================================================================
+
+	@Nested
+	@BctConfig(sortMaps = TRUE)
+	class A_classLevelAnnotation extends TestBase {
+
+		@Test
+		void a01_sortMapsEnabled() {
+			// Verify sortMaps is enabled from class-level annotation
+			assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+		}
+
+		@Test
+		void a02_sortCollectionsNotEnabled() {
+			// Verify sortCollections is not enabled (default UNSET)
+			assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+	}
+
+	@Nested
+	@BctConfig(sortMaps = TRUE, sortCollections = TRUE)
+	class B_classLevelBothEnabled extends TestBase {
+
+		@Test
+		void b01_bothEnabled() {
+			// Verify both are enabled from class-level annotation
+			assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+			assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+	}
+
+	// ====================================================================================================
+	// Method-level annotation tests
+	// ====================================================================================================
+
+	@Nested
+	class C_methodLevelAnnotation extends TestBase {
+
+		@Test
+		@BctConfig(sortMaps = TRUE)
+		void c01_sortMapsEnabled() {
+			// Verify sortMaps is enabled from method-level annotation
+			assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+		}
+
+		@Test
+		@BctConfig(sortCollections = TRUE)
+		void c02_sortCollectionsEnabled() {
+			// Verify sortCollections is enabled from method-level annotation
+			assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+
+		@Test
+		void c03_noAnnotation() {
+			// Verify no settings are enabled when no annotation is present
+			assertFalse(BctConfiguration.get(BCT_SORT_MAPS, false));
+			assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+	}
+
+	// ====================================================================================================
+	// Method-level overriding class-level tests
+	// ====================================================================================================
+
+	@Nested
+	@BctConfig(sortMaps = TRUE, sortCollections = TRUE)
+	class D_methodOverridesClass extends TestBase {
+
+		@Test
+		@BctConfig(sortMaps = FALSE)
+		void d01_methodOverridesSortMaps() {
+			// Method-level should override class-level for sortMaps
+			assertFalse(BctConfiguration.get(BCT_SORT_MAPS, false));
+			// sortCollections should still be true from class-level (UNSET in method inherits from class)
+			assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+
+		@Test
+		@BctConfig(sortCollections = FALSE)
+		void d02_methodOverridesSortCollections() {
+			// sortMaps should still be true from class-level (UNSET in method inherits from class)
+			assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+			// Method-level should override class-level for sortCollections
+			assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+
+		@Test
+		@BctConfig(sortMaps = FALSE, sortCollections = FALSE)
+		void d03_methodOverridesBoth() {
+			// Method-level should override both from class-level
+			assertFalse(BctConfiguration.get(BCT_SORT_MAPS, false));
+			assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+
+		@Test
+		void d04_inheritsFromClass() {
+			// No method annotation, should inherit from class
+			assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+			assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+	}
+
+	// ====================================================================================================
+	// BeanConverter annotation tests
+	// ====================================================================================================
+
+	/**
+	 * Custom converter for testing.
+	 */
+	static class TestBeanConverter extends BasicBeanConverter {
+		public TestBeanConverter() {
+			super(BasicBeanConverter.builder().defaultSettings());
+		}
+	}
+
+	@Nested
+	class E_beanConverterAnnotation extends TestBase {
+
+		@Test
+		@BctConfig(beanConverter = TestBeanConverter.class)
+		void e01_customConverterSet() {
+			// Verify custom converter is set by using it in an assertion
+			// The converter should be used by assertBean
+			var person = new TestPerson("Alice", 25);
+			assertDoesNotThrow(() -> assertBean(person, "name", "Alice"));
+			// Verify it's actually the custom converter by checking the class
+			var converter = BctConfiguration.getConverter();
+			assertNotNull(converter);
+			assertEquals(TestBeanConverter.class, converter.getClass());
+		}
+
+		@Test
+		void e02_defaultConverter() {
+			// Verify default converter is used when no annotation
+			var converter = BctConfiguration.getConverter();
+			assertNotNull(converter);
+			assertEquals(BasicBeanConverter.class, converter.getClass());
+		}
+	}
+
+	@Nested
+	@BctConfig(beanConverter = TestBeanConverter.class)
+	class F_classLevelBeanConverter extends TestBase {
+
+		@Test
+		void f01_inheritsConverterFromClass() {
+			// Verify converter is inherited from class-level annotation
+			var converter = BctConfiguration.getConverter();
+			assertNotNull(converter);
+			assertEquals(TestBeanConverter.class, converter.getClass());
+		}
+
+		@Test
+		@BctConfig(beanConverter = BeanConverter.class)
+		void f02_methodFallsBackToClassConverter() {
+			// Method-level with BeanConverter.class (default) falls back to class-level converter
+			var converter = BctConfiguration.getConverter();
+			assertNotNull(converter);
+			assertEquals(TestBeanConverter.class, converter.getClass());
+		}
+
+		@Test
+		@BctConfig(beanConverter = BasicBeanConverter.class)
+		void f03_methodExplicitlyUsesDefaultConverter() {
+			// Method-level with BasicBeanConverter.class explicitly uses default converter,
+			// overriding class-level TestBeanConverter
+			var converter = BctConfiguration.getConverter();
+			assertNotNull(converter);
+			assertEquals(BasicBeanConverter.class, converter.getClass());
+		}
+	}
+
+	// ====================================================================================================
+	// Combined settings tests
+	// ====================================================================================================
+
+	@Nested
+	@BctConfig(sortMaps = TRUE, beanConverter = TestBeanConverter.class)
+	class G_combinedSettings extends TestBase {
+
+		@Test
+		void g01_bothSettingsApplied() {
+			// Verify both sortMaps and converter are set
+			assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+			var converter = BctConfiguration.getConverter();
+			assertEquals(TestBeanConverter.class, converter.getClass());
+		}
+
+		@Test
+		@BctConfig(sortCollections = TRUE)
+		void g02_methodAddsToClass() {
+			// Method adds sortCollections, inherits sortMaps and converter from class
+			assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+			assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+			var converter = BctConfiguration.getConverter();
+			assertEquals(TestBeanConverter.class, converter.getClass());
+		}
+	}
+
+	// ====================================================================================================
+	// Clearing after test tests
+	// ====================================================================================================
+
+	@Nested
+	class H_clearingAfterTest extends TestBase {
+
+		@Test
+		@BctConfig(sortMaps = TRUE, sortCollections = TRUE)
+		void h01_settingsClearedAfterTest() {
+			// Settings should be active during test
+			assertTrue(BctConfiguration.get(BCT_SORT_MAPS, false));
+			assertTrue(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+
+		@AfterEach
+		void verifyCleared() {
+			// After test, settings should be cleared
+			// Note: This runs after the extension's afterEach, so we need to check in the next test
+		}
+
+		@Test
+		void h02_verifyPreviousTestCleared() {
+			// This test runs after h01, so settings should be cleared
+			assertFalse(BctConfiguration.get(BCT_SORT_MAPS, false));
+			assertFalse(BctConfiguration.get(BCT_SORT_COLLECTIONS, false));
+		}
+	}
+
+	// ====================================================================================================
+	// Error handling tests
+	// ====================================================================================================
+
+	@Nested
+	class I_errorHandling extends TestBase {
+
+		/**
+		 * Converter without no-arg constructor (should fail).
+		 */
+		static class InvalidConverter extends BasicBeanConverter {
+			public InvalidConverter(String arg) {
+				super(BasicBeanConverter.builder().defaultSettings());
+			}
+		}
+	}
+
+	// ====================================================================================================
+	// Test helper classes
+	// ====================================================================================================
+
+	/**
+	 * Test person class for testing.
+	 */
+	static class TestPerson {
+		private final String name;
+		private final int age;
+
+		TestPerson(String name, int age) {
+			this.name = name;
+			this.age = age;
+		}
+
+		String getName() { return name; }
+
+		int getAge() { return age; }
+
+		@Override
+		public String toString() {
+			return "TestPerson{name='" + name + "', age=" + age + "}";
+		}
+	}
+}
+
diff --git a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/Listifiers_Test.java b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/Listifiers_Test.java
index 7088d5454f..811625c79b 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/junit/bct/Listifiers_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/junit/bct/Listifiers_Test.java
@@ -16,6 +16,7 @@
  */
 package org.apache.juneau.junit.bct;
 
+import static org.apache.juneau.commons.lang.TriState.*;
 import static org.apache.juneau.commons.utils.CollectionUtils.*;
 import static org.apache.juneau.commons.utils.Utils.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
@@ -25,6 +26,7 @@ import java.util.*;
 import java.util.stream.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.junit.bct.annotations.*;
 import org.junit.jupiter.api.*;
 
 /**
@@ -46,20 +48,21 @@ class Listifiers_Test extends TestBase {
 		}
 
 		@Test
+		@BctConfig(sortCollections = TRUE)
 		void a02_listifySet() {
 			var listifier = Listifiers.collectionListifier();
 			var input = Set.of("z", "a", "m"); // Unordered input
 			var result = listifier.apply(null, input);
-
-			// TreeSet conversion ensures natural ordering
 			assertList(result, "a", "m", "z");
 		}
 
 		@Test
+		@BctConfig(sortCollections = TRUE)
 		void a02a_listifySetTypes() {
 			var listifier = Listifiers.collectionListifier();
 
 			// HashSet (unordered) -> converted to TreeSet for natural ordering
+			BctConfiguration.set(BctConfiguration.BCT_SORT_MAPS, true);
 			var hashSet = new HashSet<>(l("z", "a", "m"));
 			var hashResult = listifier.apply(null, hashSet);
 			assertList(hashResult, "a", "m", "z");
@@ -298,6 +301,7 @@ class Listifiers_Test extends TestBase {
 	class F_mapListifier extends TestBase {
 
 		@Test
+		@BctConfig(sortMaps = TRUE)
 		void f01_listifyMap() {
 			var listifier = Listifiers.mapListifier();
 			var input = m("z", "value1", "a", "value2"); // Unordered input
@@ -314,6 +318,7 @@ class Listifiers_Test extends TestBase {
 		}
 
 		@Test
+		@BctConfig(sortMaps = TRUE)
 		void f01a_listifyMapTypes() {
 			var listifier = Listifiers.mapListifier();