Repository: networknt/json-schema-validator
Pull request #1060 (2024-06-12 06:10:08)

Refactor to make more fields final
Refactor to make more fields final to ensure they aren't inadvertently changed and possibly improve performance marginally.

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) Report
Attention: Patch coverage is `90.38462%` with `10 lines` in your changes missing coverage. Please review.
> Project coverage is 78.88%. Comparing base [(`48ca3c2`)](https://app.codecov.io/gh/networknt/json-schema-validator/commit/48ca3c22995e32797e9defdb8b77869f0d15caf3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) to head [(`85246f1`)](https://app.codecov.io/gh/networknt/json-schema-validator/commit/85246f1783c5e4a6b9091556efe2b2ec83f0ddd8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).
> Report is 25 commits behind head on master.

| [Files](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) | Patch % | Lines |
|---|---|---|
| [...com/networknt/schema/ValidationMessageHandler.java](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fcom%2Fnetworknt%2Fschema%2FValidationMessageHandler.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9WYWxpZGF0aW9uTWVzc2FnZUhhbmRsZXIuamF2YQ==) | 76.92% | [3 Missing and 3 partials :warning: ](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) |
| [...n/java/com/networknt/schema/MaxItemsValidator.java](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fcom%2Fnetworknt%2Fschema%2FMaxItemsValidator.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9NYXhJdGVtc1ZhbGlkYXRvci5qYXZh) | 50.00% | [3 Missing :warning: ](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) |
| [.../java/com/networknt/schema/MaxLengthValidator.java](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fcom%2Fnetworknt%2Fschema%2FMaxLengthValidator.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9NYXhMZW5ndGhWYWxpZGF0b3IuamF2YQ==) | 75.00% | [1 Missing :warning: ](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##             master    #1060      +/-   ##
============================================
- Coverage     78.90%   78.88%   -0.03%     
- Complexity     1965     2018      +53     
============================================
  Files           172      189      +17     
  Lines          6352     6530     +178     
  Branches       1255     1248       -7     
============================================
+ Hits           5012     5151     +139     
- Misses          867      907      +40     
+ Partials        473      472       -1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1060?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).


Patch:
diff --git a/README.md b/README.md
index 8c6c090..cfb0f92 100644
--- a/README.md
+++ b/README.md
@@ -157,14 +157,26 @@ The following are the optional dependencies that may be required for certain opt
 These are not automatically included and setting the relevant option without adding the library will result in a `ClassNotFoundException`.
 
 ```xml
-<!-- This is required when setting setEcma262Validator(true)  -->
+<!-- Either library is required when setting setEcma262Validator(true) or explicitly via setRegularExpressionFactory() -->
 <dependency>
     <!-- Used to validate ECMA 262 regular expressions -->
+    <!-- Approximately 2 MB in dependencies -->
+    <!-- JoniRegularExpressionFactory -->
     <groupId>org.jruby.joni</groupId>
     <artifactId>joni</artifactId>
     <version>${version.joni}</version>
     <optional>true</optional>
 </dependency>
+
+<dependency>
+    <!-- Used to validate ECMA 262 regular expressions -->
+    <!-- Approximately 50 MB in dependencies -->
+    <!-- GraalJSRegularExpressionFactory -->
+    <groupId>org.graalvm.js</groupId>
+    <artifactId>js</artifactId>
+    <version>${version.graaljs}</version>
+    <optional>true</optional>
+</dependency>
 ```
 
 ##### Excludable Dependencies
@@ -258,7 +270,7 @@ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
 // By default JSON Path is used for reporting the instance location and evaluation path
 config.setPathType(PathType.JSON_POINTER);
 // By default the JDK regular expression implementation which is not ECMA 262 compliant is used
-// Note that setting this to true requires including the optional joni dependency
+// Note that setting this to true requires including the optional joni or graaljs dependency
 // config.setEcma262Validator(true);
 
 // Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json.
@@ -293,7 +305,7 @@ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
 // By default JSON Path is used for reporting the instance location and evaluation path
 config.setPathType(PathType.JSON_POINTER);
 // By default the JDK regular expression implementation which is not ECMA 262 compliant is used
-// Note that setting this to true requires including the optional joni dependency
+// Note that setting this to true requires including the optional joni or graaljs dependency
 // config.setEcma262Validator(true);
 
 // Due to the mapping the meta-schema will be retrieved from the classpath at classpath:draft/2020-12/schema.
@@ -517,7 +529,7 @@ The following is sample output from the Hierarchical format.
 | Name                                  | Description                                                                                                                                                                                                                       | Default Value
 |---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------
 | `pathType`                            | The path type to use for reporting the instance location and evaluation path. Set to `PathType.JSON_POINTER` to use JSON Pointer.                                                                                                 | `PathType.DEFAULT`
-| `ecma262Validator`                    | Whether to use the ECMA 262 `joni` library to validate the `pattern` keyword. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown.                                      | `false`
+| `ecma262Validator`                    | Whether to use the ECMA 262 `joni` or `graaljs` library to validate the `pattern` keyword. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown.                         | `false`
 | `executionContextCustomizer`          | This can be used to customize the `ExecutionContext` generated by the `JsonSchema` for each validation run.                                                                                                                       | `null`
 | `schemaIdValidator`                   | This is used to customize how the `$id` values are validated. Note that the default implementation allows non-empty fragments where no base IRI is specified and also allows non-absolute IRI `$id` values in the root schema.    | `JsonSchemaIdValidator.DEFAULT`
 | `messageSource`                       | This is used to retrieve the locale specific messages.                                                                                                                                                                            | `DefaultMessageSource.getInstance()`
@@ -527,6 +539,7 @@ The following is sample output from the Hierarchical format.
 | `locale`                              | The locale to use for generating messages in the `ValidationMessage`.                                                                                                                                                             | `Locale.getDefault()`
 | `failFast`                            | Whether to return failure immediately when an assertion is generated.                                                                                                                                                             | `false`
 | `formatAssertionsEnabled`             | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior.                                        | `null`
+| `regularExpressionFactory`            | The factory to use to create regular expressions for instance `JoniRegularExpressionFactory` or `GraalJSRegularExpressionFactory`.                                                                                                | `JDKRegularExpressionFactory.getInstance()`
 
 ## Performance Considerations
 
diff --git a/pom.xml b/pom.xml
index 1959b65..1cc1393 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,8 +75,9 @@
     <version.itu>1.10.2</version.itu>
     <version.jackson>2.17.0</version.jackson>
     <version.joni>2.2.1</version.joni>
-    <version.logback>1.3.14</version.logback>
+    <version.logback>1.3.14</version.logback> <!-- 1.4.x and above is not Java 8 compatible -->
     <version.slf4j>2.0.13</version.slf4j>
+    <version.graaljs>21.3.10</version.graaljs> <!-- 22.x and above is not Java 8 compatible -->
 
     <version.hamcrest>2.2</version.hamcrest>
     <version.junit>5.10.2</version.junit>
@@ -138,12 +139,31 @@
 
     <dependency>
       <!-- Used to validate ECMA 262 regular expressions -->
+      <!-- Approximately 2 MB in dependencies -->
       <groupId>org.jruby.joni</groupId>
       <artifactId>joni</artifactId>
       <version>${version.joni}</version>
       <optional>true</optional>
     </dependency>
 
+    <dependency>
+      <!-- Used to validate ECMA 262 regular expressions -->
+      <!-- Approximately 50 MB in dependencies -->
+      <groupId>org.graalvm.js</groupId>
+      <artifactId>js</artifactId>
+      <version>${version.graaljs}</version>
+      <optional>true</optional>
+    </dependency>
+
+    <dependency>
+      <!-- Used to validate ECMA 262 regular expressions -->
+      <!-- Approximately 50 MB in dependencies -->
+      <groupId>org.graalvm.sdk</groupId>
+      <artifactId>graal-sdk</artifactId>
+      <version>${version.graaljs}</version>
+      <optional>true</optional>
+    </dependency>
+
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index 9576e6e..87e9311 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.networknt.schema.annotation.JsonNodeAnnotation;
 import com.networknt.schema.i18n.DefaultMessageSource;
+import com.networknt.schema.i18n.MessageSource;
 
 import org.slf4j.Logger;
 
@@ -29,12 +30,15 @@ import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 
+/**
+ * Base {@link JsonValidator}. 
+ */
 public abstract class BaseJsonValidator extends ValidationMessageHandler implements JsonValidator {
     protected final boolean suppressSubSchemaRetrieval;
 
     protected final JsonNode schemaNode;
 
-    protected ValidationContext validationContext;
+    protected final ValidationContext validationContext;
 
     public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
             JsonSchema parentSchema, ValidatorTypeCode validatorType, ValidationContext validationContext) {
@@ -60,15 +64,41 @@ public abstract class BaseJsonValidator extends ValidationMessageHandler impleme
     }
 
     /**
-     * Copy constructor.
-     * 
-     * @param copy to copy from
+     * Constructor to create a copy using fields.
+     *
+     * @param suppressSubSchemaRetrieval to suppress sub schema retrieval
+     * @param schemaNode the schema node
+     * @param validationContext the validation context
+     * @param errorMessageType the error message type
+     * @param customErrorMessagesEnabled whether custom error msessages are enabled
+     * @param messageSource the message source
+     * @param keyword the keyword
+     * @param parentSchema the parent schema
+     * @param schemaLocation the schema location
+     * @param evaluationPath the evaluation path
+     * @param evaluationParentSchema the evaluation parent schema
+     * @param errorMessage the error message
      */
-    protected BaseJsonValidator(BaseJsonValidator copy) {
-        super(copy);
-        this.suppressSubSchemaRetrieval = copy.suppressSubSchemaRetrieval;
-        this.schemaNode = copy.schemaNode;
-        this.validationContext = copy.validationContext;
+    protected BaseJsonValidator(
+            /* Below from BaseJsonValidator */
+            boolean suppressSubSchemaRetrieval,
+            JsonNode schemaNode,
+            ValidationContext validationContext,
+            /* Below from ValidationMessageHandler */
+            ErrorMessageType errorMessageType,
+            boolean customErrorMessagesEnabled,
+            MessageSource messageSource,
+            Keyword keyword,
+            JsonSchema parentSchema,
+            SchemaLocation schemaLocation,
+            JsonNodePath evaluationPath,
+            JsonSchema evaluationParentSchema,
+            Map<String, String> errorMessage) {
+        super(errorMessageType, customErrorMessagesEnabled, messageSource, keyword,
+                parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, errorMessage);
+        this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval;
+        this.schemaNode = schemaNode;
+        this.validationContext = validationContext;
     }
 
     private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) {
diff --git a/src/main/java/com/networknt/schema/CollectorContext.java b/src/main/java/com/networknt/schema/CollectorContext.java
index 2469fce..21b1d6a 100644
--- a/src/main/java/com/networknt/schema/CollectorContext.java
+++ b/src/main/java/com/networknt/schema/CollectorContext.java
@@ -28,16 +28,35 @@ public class CollectorContext {
     /**
      * Map for holding the name and {@link Collector} or a simple Object.
      */
-    private Map<String, Object> collectorMap = new HashMap<>();
+    private final Map<String, Object> collectorMap;
 
     /**
      * Map for holding the name and {@link Collector} class collect method output.
      */
-    private Map<String, Object> collectorLoadMap = new HashMap<>();
+    private final Map<String, Object> collectorLoadMap;
 
+    /**
+     * Default constructor will use an unsynchronized HashMap to store data. This is
+     * suitable if the collector context is not shared with multiple threads.
+     */
     public CollectorContext() {
+        this(new HashMap<>(), new HashMap<>());
     }
 
+    /**
+     * Constructor that creates the context using the specified instances to store
+     * data.
+     * <p>
+     * If for instance the collector context needs to be shared with multiple
+     * threads a ConcurrentHashMap can be used.
+     *
+     * @param collectorMap the collector map
+     * @param collectorLoadMap the collector load map
+     */
+    public CollectorContext(Map<String, Object> collectorMap, Map<String, Object> collectorLoadMap) {
+        this.collectorMap = collectorMap;
+        this.collectorLoadMap = collectorLoadMap;
+    }
 
     /**
      * Adds a collector with give name. Preserving this method for backward
diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java
index adcdcde..ebfde55 100644
--- a/src/main/java/com/networknt/schema/ContainsValidator.java
+++ b/src/main/java/com/networknt/schema/ContainsValidator.java
@@ -42,9 +42,9 @@ public class ContainsValidator extends BaseJsonValidator {
     private final JsonSchema schema;
     private final boolean isMinV201909;
 
-    private Integer min = null;
-    private Integer max = null;
-    
+    private final Integer min;
+    private final Integer max;
+
     private Boolean hasUnevaluatedItemsValidator = null;
 
     public ContainsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
@@ -55,19 +55,29 @@ public class ContainsValidator extends BaseJsonValidator {
         // slightly.
         this.isMinV201909 = MinV201909.getVersions().contains(this.validationContext.getMetaSchema().getSpecification());
 
+        Integer currentMax = null;
+        Integer currentMin = null;
         if (schemaNode.isObject() || schemaNode.isBoolean()) {
             this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema);
             JsonNode parentSchemaNode = parentSchema.getSchemaNode();
-            Optional.ofNullable(parentSchemaNode.get(ValidatorTypeCode.MAX_CONTAINS.getValue()))
-                    .filter(JsonNode::canConvertToExactIntegral)
-                    .ifPresent(node -> this.max = node.intValue());
+            Optional<JsonNode> maxNode = Optional
+                    .ofNullable(parentSchemaNode.get(ValidatorTypeCode.MAX_CONTAINS.getValue()))
+                    .filter(JsonNode::canConvertToExactIntegral);
+            if (maxNode.isPresent()) {
+                currentMax = maxNode.get().intValue();
+            }
 
-            Optional.ofNullable(parentSchemaNode.get(ValidatorTypeCode.MIN_CONTAINS.getValue()))
-                    .filter(JsonNode::canConvertToExactIntegral)
-                    .ifPresent(node -> this.min = node.intValue());
+            Optional<JsonNode> minNode = Optional
+                    .ofNullable(parentSchemaNode.get(ValidatorTypeCode.MIN_CONTAINS.getValue()))
+                    .filter(JsonNode::canConvertToExactIntegral);
+            if (minNode.isPresent()) {
+                currentMin = minNode.get().intValue();
+            }
         } else {
             this.schema = null;
         }
+        this.max = currentMax;
+        this.min = currentMin;
     }
 
     @Override
diff --git a/src/main/java/com/networknt/schema/ContentEncodingValidator.java b/src/main/java/com/networknt/schema/ContentEncodingValidator.java
index 6f2bbbb..5c9e296 100644
--- a/src/main/java/com/networknt/schema/ContentEncodingValidator.java
+++ b/src/main/java/com/networknt/schema/ContentEncodingValidator.java
@@ -32,7 +32,7 @@ import java.util.Set;
  */
 public class ContentEncodingValidator extends BaseJsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(ContentEncodingValidator.class);
-    private String contentEncoding;
+    private final String contentEncoding;
 
     /**
      * Constructor.
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 7257681..b110fe6 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -19,6 +19,7 @@ package com.networknt.schema;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.networknt.schema.SpecVersion.VersionFlag;
+import com.networknt.schema.i18n.MessageSource;
 import com.networknt.schema.utils.JsonNodes;
 import com.networknt.schema.utils.SetView;
 
@@ -31,6 +32,7 @@ import java.util.Comparator;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
@@ -50,17 +52,15 @@ import java.util.function.Consumer;
  */
 public class JsonSchema extends BaseJsonValidator {
     private static final long V201909_VALUE = VersionFlag.V201909.getVersionFlagValue();
+    private final String id;
 
     /**
      * The validators sorted and indexed by evaluation path.
      */
-    private List<JsonValidator> validators;
+    private List<JsonValidator> validators = null;
     private boolean validatorsLoaded = false;
     private boolean recursiveAnchor = false;
-
-    private TypeValidator typeValidator;
-
-    private final String id;
+    private TypeValidator typeValidator = null;
 
     static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) {
         return new JsonSchema(validationContext, schemaLocation, evaluationPath, schemaNode, parent, suppressSubSchemaRetrieval);
@@ -149,19 +149,56 @@ public class JsonSchema extends BaseJsonValidator {
         }
         getValidators();
     }
-    
+
     /**
-     * Copy constructor.
+     * Constructor to create a copy using fields.
      * 
-     * @param copy to copy from
+     * @param validators the validators
+     * @param validatorsLoaded whether the validators are preloaded
+     * @param recursiveAnchor whether this is has a recursive anchor
+     * @param typeValidator the type validator
+     * @param id the id
+     * @param suppressSubSchemaRetrieval to suppress sub schema retrieval
+     * @param schemaNode the schema node
+     * @param validationContext the validation context
+     * @param errorMessageType the error message type
+     * @param customErrorMessagesEnabled whether custom error msessages are enabled
+     * @param messageSource the message source
+     * @param keyword the keyword
+     * @param parentSchema the parent schema
+     * @param schemaLocation the schema location
+     * @param evaluationPath the evaluation path
+     * @param evaluationParentSchema the evaluation parent schema
+     * @param errorMessage the error message
      */
-    protected JsonSchema(JsonSchema copy) {
-        super(copy);
-        this.validators = copy.validators;
-        this.validatorsLoaded = copy.validatorsLoaded;
-        this.recursiveAnchor = copy.recursiveAnchor;
-        this.typeValidator = copy.typeValidator;
-        this.id = copy.id;
+    protected JsonSchema(
+            /* Below from JsonSchema */
+            List<JsonValidator> validators,
+            boolean validatorsLoaded,
+            boolean recursiveAnchor,
+            TypeValidator typeValidator,
+            String id,            
+            /* Below from BaseJsonValidator */
+            boolean suppressSubSchemaRetrieval,
+            JsonNode schemaNode,
+            ValidationContext validationContext,
+            /* Below from ValidationMessageHandler */
+            ErrorMessageType errorMessageType,
+            boolean customErrorMessagesEnabled,
+            MessageSource messageSource,
+            Keyword keyword,
+            JsonSchema parentSchema,
+            SchemaLocation schemaLocation,
+            JsonNodePath evaluationPath,
+            JsonSchema evaluationParentSchema,
+            Map<String, String> errorMessage) {
+        super(suppressSubSchemaRetrieval, schemaNode, validationContext, errorMessageType, customErrorMessagesEnabled, messageSource, keyword,
+                parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, errorMessage);
+        this.validators = validators;
+        this.validatorsLoaded = validatorsLoaded;
+        this.recursiveAnchor = recursiveAnchor;
+        this.typeValidator = typeValidator;
+        this.id = id;
     }
 
     /**
@@ -176,34 +213,68 @@ public class JsonSchema extends BaseJsonValidator {
      * @return the schema
      */
     public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath refEvaluationPath) {
-        JsonSchema copy = new JsonSchema(this);
-        copy.validationContext = new ValidationContext(copy.getValidationContext().getMetaSchema(),
-                copy.getValidationContext().getJsonSchemaFactory(),
+        ValidationContext validationContext = new ValidationContext(this.getValidationContext().getMetaSchema(),
+                this.getValidationContext().getJsonSchemaFactory(),
                 refEvaluationParentSchema.validationContext.getConfig(),
                 refEvaluationParentSchema.getValidationContext().getSchemaReferences(),
                 refEvaluationParentSchema.getValidationContext().getSchemaResources(),
                 refEvaluationParentSchema.getValidationContext().getDynamicAnchors());
-        copy.evaluationPath = refEvaluationPath;
-        copy.evaluationParentSchema = refEvaluationParentSchema;
+        JsonNodePath evaluationPath = refEvaluationPath;
+        JsonSchema evaluationParentSchema = refEvaluationParentSchema;
         // Validator state is reset due to the changes in evaluation path
-        copy.validatorsLoaded = false;
-        copy.typeValidator = null;
-        copy.validators = null;
-        return copy;
+        boolean validatorsLoaded = false;
+        TypeValidator typeValidator = null;
+        List<JsonValidator> validators = null;
+
+        return new JsonSchema(
+                /* Below from JsonSchema */
+                validators,
+                validatorsLoaded,
+                recursiveAnchor,
+                typeValidator,
+                id,            
+                /* Below from BaseJsonValidator */
+                suppressSubSchemaRetrieval,
+                schemaNode,
+                validationContext,
+                /* Below from ValidationMessageHandler */
+                errorMessageType, customErrorMessagesEnabled, messageSource,
+                keyword, parentSchema, schemaLocation, evaluationPath,
+                evaluationParentSchema, errorMessage);
     }
 
     public JsonSchema withConfig(SchemaValidatorsConfig config) {
         if (!this.getValidationContext().getConfig().equals(config)) {
-            JsonSchema copy = new JsonSchema(this);
-            copy.validationContext = new ValidationContext(copy.getValidationContext().getMetaSchema(),
-                    copy.getValidationContext().getJsonSchemaFactory(), config,
-                    copy.getValidationContext().getSchemaReferences(),
-                    copy.getValidationContext().getSchemaResources(),
-                    copy.getValidationContext().getDynamicAnchors());
-            copy.validatorsLoaded = false;
-            copy.typeValidator = null;
-            copy.validators = null;
-            return copy;
+            ValidationContext validationContext = new ValidationContext(this.getValidationContext().getMetaSchema(),
+                    this.getValidationContext().getJsonSchemaFactory(), config,
+                    this.getValidationContext().getSchemaReferences(),
+                    this.getValidationContext().getSchemaResources(),
+                    this.getValidationContext().getDynamicAnchors());
+            boolean validatorsLoaded = false;
+            TypeValidator typeValidator = null;
+            List<JsonValidator> validators = null;
+            return new JsonSchema(
+                    /* Below from JsonSchema */
+                    validators,
+                    validatorsLoaded,
+                    recursiveAnchor,
+                    typeValidator,
+                    id,            
+                    /* Below from BaseJsonValidator */
+                    suppressSubSchemaRetrieval,
+                    schemaNode,
+                    validationContext,
+                    /* Below from ValidationMessageHandler */
+                    errorMessageType,
+                    customErrorMessagesEnabled,
+                    messageSource,
+                    keyword,
+                    parentSchema,
+                    schemaLocation,
+                    evaluationPath,
+                    evaluationParentSchema,
+                    errorMessage);
+
         }
         return this;
     }
diff --git a/src/main/java/com/networknt/schema/JsonSchemaException.java b/src/main/java/com/networknt/schema/JsonSchemaException.java
index 2113d2e..d667e87 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaException.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaException.java
@@ -19,9 +19,12 @@ package com.networknt.schema;
 import java.util.Collections;
 import java.util.Set;
 
+/**
+ * Represents an error when processing the JsonSchema.
+ */
 public class JsonSchemaException extends RuntimeException {
     private static final long serialVersionUID = -7805792737596582110L;
-    private ValidationMessage validationMessage;
+    private final ValidationMessage validationMessage;
 
     public JsonSchemaException(ValidationMessage validationMessage) {
         this.validationMessage = validationMessage;
@@ -29,10 +32,12 @@ public class JsonSchemaException extends RuntimeException {
 
     public JsonSchemaException(String message) {
         super(message);
+        this.validationMessage = null;
     }
 
     public JsonSchemaException(Throwable throwable) {
         super(throwable);
+        this.validationMessage = null;
     }
 
     @Override
diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java
index 57b0c02..1f7748b 100644
--- a/src/main/java/com/networknt/schema/MaxItemsValidator.java
+++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java
@@ -30,12 +30,14 @@ public class MaxItemsValidator extends BaseJsonValidator implements JsonValidato
 
     private static final Logger logger = LoggerFactory.getLogger(MaxItemsValidator.class);
 
-    private int max = 0;
+    private final int max;
 
     public MaxItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_ITEMS, validationContext);
         if (schemaNode.canConvertToExactIntegral()) {
-            max = schemaNode.intValue();
+            this.max = schemaNode.intValue();
+        } else {
+            this.max = 0;
         }
     }
 
@@ -43,16 +45,16 @@ public class MaxItemsValidator extends BaseJsonValidator implements JsonValidato
         debug(logger, executionContext, node, rootNode, instanceLocation);
 
         if (node.isArray()) {
-            if (node.size() > max) {
+            if (node.size() > this.max) {
                 return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
                         .locale(executionContext.getExecutionConfig().getLocale())
-                        .failFast(executionContext.isFailFast()).arguments(max, node.size()).build());
+                        .failFast(executionContext.isFailFast()).arguments(this.max, node.size()).build());
             }
         } else if (this.validationContext.getConfig().isTypeLoose()) {
-            if (1 > max) {
+            if (1 > this.max) {
                 return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
                         .locale(executionContext.getExecutionConfig().getLocale())
-                        .failFast(executionContext.isFailFast()).arguments(max, 1).build());
+                        .failFast(executionContext.isFailFast()).arguments(this.max, 1).build());
             }
         }
 
diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java
index e304478..ec587fd 100644
--- a/src/main/java/com/networknt/schema/MaxLengthValidator.java
+++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java
@@ -29,13 +29,14 @@ import java.util.Set;
 public class MaxLengthValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(MaxLengthValidator.class);
 
-    private int maxLength;
+    private final int maxLength;
 
     public MaxLengthValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_LENGTH, validationContext);
-        maxLength = Integer.MAX_VALUE;
         if (schemaNode != null && schemaNode.canConvertToExactIntegral()) {
-            maxLength = schemaNode.intValue();
+            this.maxLength = schemaNode.intValue();
+        } else {
+            this.maxLength = Integer.MAX_VALUE;
         }
     }
 
@@ -47,10 +48,10 @@ public class MaxLengthValidator extends BaseJsonValidator implements JsonValidat
             // ignore no-string typs
             return Collections.emptySet();
         }
-        if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) {
+        if (node.textValue().codePointCount(0, node.textValue().length()) > this.maxLength) {
             return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
                     .locale(executionContext.getExecutionConfig().getLocale())
-                    .failFast(executionContext.isFailFast()).arguments(maxLength).build());
+                    .failFast(executionContext.isFailFast()).arguments(this.maxLength).build());
         }
         return Collections.emptySet();
     }
diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java
index 261fa47..1bbef9e 100644
--- a/src/main/java/com/networknt/schema/MaximumValidator.java
+++ b/src/main/java/com/networknt/schema/MaximumValidator.java
@@ -34,7 +34,7 @@ public class MaximumValidator extends BaseJsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(MaximumValidator.class);
     private static final String PROPERTY_EXCLUSIVE_MAXIMUM = "exclusiveMaximum";
 
-    private boolean excludeEqual = false;
+    private final boolean excludeEqual;
 
     private final ThresholdMixin typedMaximum;
 
@@ -47,14 +47,16 @@ public class MaximumValidator extends BaseJsonValidator {
 
         JsonNode exclusiveMaximumNode = getParentSchema().getSchemaNode().get(PROPERTY_EXCLUSIVE_MAXIMUM);
         if (exclusiveMaximumNode != null && exclusiveMaximumNode.isBoolean()) {
-            excludeEqual = exclusiveMaximumNode.booleanValue();
+            this.excludeEqual = exclusiveMaximumNode.booleanValue();
+        } else {
+            this.excludeEqual = false;
         }
 
         final String maximumText = schemaNode.asText();
         if ((schemaNode.isLong() || schemaNode.isInt()) && (JsonType.INTEGER.toString().equals(getNodeFieldType()))) {
             // "integer", and within long range
             final long lm = schemaNode.asLong();
-            typedMaximum = new ThresholdMixin() {
+            this.typedMaximum = new ThresholdMixin() {
                 @Override
                 public boolean crossesThreshold(JsonNode node) {
                     if (node.isBigInteger()) {
@@ -78,7 +80,7 @@ public class MaximumValidator extends BaseJsonValidator {
                 }
             };
         } else {
-            typedMaximum = new ThresholdMixin() {
+            this.typedMaximum = new ThresholdMixin() {
                 @Override
                 public boolean crossesThreshold(JsonNode node) {
                     if (schemaNode.isDouble() && schemaNode.doubleValue() == Double.POSITIVE_INFINITY) {
@@ -115,11 +117,11 @@ public class MaximumValidator extends BaseJsonValidator {
             return Collections.emptySet();
         }
 
-        if (typedMaximum.crossesThreshold(node)) {
+        if (this.typedMaximum.crossesThreshold(node)) {
             return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
                     .locale(executionContext.getExecutionConfig().getLocale())
                     .failFast(executionContext.isFailFast())
-                    .arguments(typedMaximum.thresholdValue()).build());
+                    .arguments(this.typedMaximum.thresholdValue()).build());
         }
         return Collections.emptySet();
     }
diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java
index 3f35948..dc557a1 100644
--- a/src/main/java/com/networknt/schema/MinimumValidator.java
+++ b/src/main/java/com/networknt/schema/MinimumValidator.java
@@ -34,7 +34,7 @@ public class MinimumValidator extends BaseJsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(MinimumValidator.class);
     private static final String PROPERTY_EXCLUSIVE_MINIMUM = "exclusiveMinimum";
 
-    private boolean excludeEqual = false;
+    private final boolean excludeEqual;
 
     /**
      * In order to limit number of `if` statements in `validate` method, all the
@@ -51,14 +51,16 @@ public class MinimumValidator extends BaseJsonValidator {
 
         JsonNode exclusiveMinimumNode = getParentSchema().getSchemaNode().get(PROPERTY_EXCLUSIVE_MINIMUM);
         if (exclusiveMinimumNode != null && exclusiveMinimumNode.isBoolean()) {
-            excludeEqual = exclusiveMinimumNode.booleanValue();
+            this.excludeEqual = exclusiveMinimumNode.booleanValue();
+        } else {
+            this.excludeEqual = false;
         }
 
         final String minimumText = schemaNode.asText();
         if ((schemaNode.isLong() || schemaNode.isInt()) && JsonType.INTEGER.toString().equals(getNodeFieldType())) {
             // "integer", and within long range
             final long lmin = schemaNode.asLong();
-            typedMinimum = new ThresholdMixin() {
+            this.typedMinimum = new ThresholdMixin() {
                 @Override
                 public boolean crossesThreshold(JsonNode node) {
                     if (node.isBigInteger()) {
@@ -84,7 +86,7 @@ public class MinimumValidator extends BaseJsonValidator {
             };
 
         } else {
-            typedMinimum = new ThresholdMixin() {
+            this.typedMinimum = new ThresholdMixin() {
                 @Override
                 public boolean crossesThreshold(JsonNode node) {
                     // jackson's BIG_DECIMAL parsing is limited. see https://github.com/FasterXML/jackson-databind/issues/1770
@@ -122,11 +124,11 @@ public class MinimumValidator extends BaseJsonValidator {
             return Collections.emptySet();
         }
 
-        if (typedMinimum.crossesThreshold(node)) {
+        if (this.typedMinimum.crossesThreshold(node)) {
             return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
                     .locale(executionContext.getExecutionConfig().getLocale())
                     .failFast(executionContext.isFailFast())
-                    .arguments(typedMinimum.thresholdValue()).build());
+                    .arguments(this.typedMinimum.thresholdValue()).build());
         }
         return Collections.emptySet();
     }
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index 1240da3..3cd1303 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -19,6 +19,9 @@ package com.networknt.schema;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.networknt.schema.i18n.DefaultMessageSource;
 import com.networknt.schema.i18n.MessageSource;
+import com.networknt.schema.regex.ECMAScriptRegularExpressionFactory;
+import com.networknt.schema.regex.JDKRegularExpressionFactory;
+import com.networknt.schema.regex.RegularExpressionFactory;
 import com.networknt.schema.walk.DefaultItemWalkListenerRunner;
 import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner;
 import com.networknt.schema.walk.DefaultPropertyWalkListenerRunner;
@@ -32,6 +35,9 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 
+/**
+ * Configuration for validators. 
+ */
 public class SchemaValidatorsConfig {
     public static final int DEFAULT_PRELOAD_JSON_SCHEMA_REF_MAX_NESTING_DEPTH = 40;
 
@@ -59,9 +65,9 @@ public class SchemaValidatorsConfig {
     private ApplyDefaultsStrategy applyDefaultsStrategy = ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY;
 
     /**
-     * When set to true, use ECMA-262 compatible validator
+     * Used to create {@link com.networknt.schema.regex.RegularExpression}.
      */
-    private boolean ecma262Validator;
+    private RegularExpressionFactory regularExpressionFactory = JDKRegularExpressionFactory.getInstance();
 
     /**
      * When set to true, use Java-specific semantics rather than native JavaScript
@@ -78,7 +84,7 @@ public class SchemaValidatorsConfig {
      * When set to true, "messages" provided in schema are used for forming validation errors
      * else default messages are used
      */
-    private boolean isCustomMessageSupported = true;
+    private boolean customMessageSupported = true;
 
     /**
      * When set to true, support for discriminators is enabled for validations of
@@ -168,54 +174,52 @@ public class SchemaValidatorsConfig {
 
     /************************ START OF UNEVALUATED CHECKS **********************************/
 
-    // These are costly in terms of performance so we provide a way to disable them.
-    private boolean disableUnevaluatedItems = false;
-    private boolean disableUnevaluatedProperties = false;
-    
+    @Deprecated
     public SchemaValidatorsConfig disableUnevaluatedAnalysis() {
-        disableUnevaluatedItems();
-        disableUnevaluatedProperties();
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig disableUnevaluatedItems() {
-        this.disableUnevaluatedItems = true;
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig disableUnevaluatedProperties() {
-        this.disableUnevaluatedProperties = true;
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig enableUnevaluatedAnalysis() {
-        enableUnevaluatedItems();
-        enableUnevaluatedProperties();
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig enableUnevaluatedItems() {
-        this.disableUnevaluatedItems = false;
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig enableUnevaluatedProperties() {
-        this.disableUnevaluatedProperties = false;
         return this;
     }
 
+    @Deprecated
     public boolean isUnevaluatedItemsAnalysisDisabled() {
-        return this.disableUnevaluatedItems;
+        return false;
     }
 
+    @Deprecated
     public boolean isUnevaluatedItemsAnalysisEnabled() {
         return !isUnevaluatedItemsAnalysisDisabled();
     }
 
+    @Deprecated
     public boolean isUnevaluatedPropertiesAnalysisDisabled() {
-        return this.disableUnevaluatedProperties;
+        return false;
     }
 
+    @Deprecated
     public boolean isUnevaluatedPropertiesAnalysisEnabled() {
         return !isUnevaluatedPropertiesAnalysisDisabled();
     }
@@ -268,12 +272,55 @@ public class SchemaValidatorsConfig {
         this.handleNullableField = handleNullableField;
     }
 
+    /**
+     * Gets whether to use a ECMA-262 compliant regular expression validator.
+     * <p>
+     * This defaults to the false and setting true require inclusion of optional
+     * org.jruby.joni:joni or org.graalvm.js:js dependencies.
+     *
+     * @return true if ECMA-262 compliant
+     */
     public boolean isEcma262Validator() {
-        return this.ecma262Validator;
+        return !(this.regularExpressionFactory instanceof JDKRegularExpressionFactory);
     }
 
+    /**
+     * Sets whether to use a ECMA-262 compliant regular expression validator.
+     * <p>
+     * This defaults to the false and setting true require inclusion of optional
+     * org.jruby.joni:joni or org.graalvm.js:js dependencies.
+     *
+     * @param ecma262Validator true if ECMA-262 compliant
+     */
     public void setEcma262Validator(boolean ecma262Validator) {
-        this.ecma262Validator = ecma262Validator;
+        this.regularExpressionFactory = ecma262Validator ? ECMAScriptRegularExpressionFactory.getInstance()
+                : JDKRegularExpressionFactory.getInstance();
+    }
+
+    /**
+     * Gets the regular expression factory.
+     * <p>
+     * This defaults to the JDKRegularExpressionFactory and the implementations
+     * require inclusion of optional org.jruby.joni:joni or org.graalvm.js:js dependencies.
+     *
+     * @return the factory
+     */
+    public RegularExpressionFactory getRegularExpressionFactory() {
+        return regularExpressionFactory;
+    }
+
+    /**
+     * Sets the regular expression factory.
+     * <p>
+     * This defaults to the JDKRegularExpressionFactory and the implementations
+     * require inclusion of optional org.jruby.joni:joni or org.graalvm.js:js dependencies.
+     *
+     * @see JDKRegularExpressionFactory
+     * @see ECMAScriptRegularExpressionFactory
+     * @param regularExpressionFactory the factory
+     */
+    public void setRegularExpressionFactory(RegularExpressionFactory regularExpressionFactory) {
+        this.regularExpressionFactory = regularExpressionFactory;
     }
 
     public boolean isJavaSemantics() {
@@ -285,11 +332,11 @@ public class SchemaValidatorsConfig {
     }
 
     public boolean isCustomMessageSupported() {
-        return isCustomMessageSupported;
+        return customMessageSupported;
     }
 
     public void setCustomMessageSupported(boolean customMessageSupported) {
-        this.isCustomMessageSupported = customMessageSupported;
+        this.customMessageSupported = customMessageSupported;
     }
 
     public void addKeywordWalkListener(JsonSchemaWalkListener keywordWalkListener) {
@@ -358,13 +405,13 @@ public class SchemaValidatorsConfig {
         return this.itemWalkListenerRunner;
     }
     
-    private WalkListenerRunner keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(getKeywordWalkListenersMap());
+    private final WalkListenerRunner keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(getKeywordWalkListenersMap());
 
     WalkListenerRunner getKeywordWalkListenerRunner() {
         return this.keywordWalkListenerRunner;
     }
 
-    private WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(getPropertyWalkListeners());
+    private final WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(getPropertyWalkListeners());
 
     WalkListenerRunner getPropertyWalkListenerRunner() {
         return this.propertyWalkListenerRunner;
diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java
index 1bacaff..1183f4a 100644
--- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java
+++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java
@@ -9,48 +9,78 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 
+/**
+ * Validation message handler.
+ */
 public abstract class ValidationMessageHandler {
+    protected final ErrorMessageType errorMessageType;
+    protected final boolean customErrorMessagesEnabled;
     protected final MessageSource messageSource;
-    protected ErrorMessageType errorMessageType;
-
-    protected SchemaLocation schemaLocation;
-    protected JsonNodePath evaluationPath;
-    protected JsonSchema evaluationParentSchema;
-
-    protected JsonSchema parentSchema;
-
-    protected boolean customErrorMessagesEnabled;
-    protected Map<String, String> errorMessage;
-
-    protected Keyword keyword;
+    protected final Keyword keyword;
+    protected final JsonSchema parentSchema;
+    protected final SchemaLocation schemaLocation;
+    protected final JsonNodePath evaluationPath;
+    protected final JsonSchema evaluationParentSchema;
+    protected final Map<String, String> errorMessage;
 
     protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean customErrorMessagesEnabled,
             MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation,
             JsonNodePath evaluationPath) {
-        this.errorMessageType = errorMessageType;
+        ErrorMessageType currentErrorMessageType = errorMessageType;
         this.messageSource = messageSource;
         this.schemaLocation = Objects.requireNonNull(schemaLocation);
         this.evaluationPath = Objects.requireNonNull(evaluationPath);
         this.parentSchema = parentSchema;
+        this.evaluationParentSchema = null;
         this.customErrorMessagesEnabled = customErrorMessagesEnabled;
-        updateKeyword(keyword);
+        this.keyword = keyword;
+
+        Map<String, String> currentErrorMessage = null;
+
+        if (this.keyword != null) {
+            if (this.customErrorMessagesEnabled && keyword != null && parentSchema != null) {
+                currentErrorMessage = getErrorMessage(parentSchema.getSchemaNode(), keyword.getValue());
+            }
+            String errorCodeKey = getErrorCodeKey(keyword.getValue());
+            if (errorCodeKey != null && this.parentSchema != null) {
+                JsonNode errorCodeNode = this.parentSchema.getSchemaNode().get(errorCodeKey);
+                if (errorCodeNode != null && errorCodeNode.isTextual()) {
+                    String errorCodeText = errorCodeNode.asText();
+                    if (StringUtils.isNotBlank(errorCodeText)) {
+                        currentErrorMessageType = CustomErrorMessageType.of(errorCodeText);
+                    }
+                }
+            }
+        }
+        this.errorMessageType = currentErrorMessageType;
+        this.errorMessage = currentErrorMessage;
     }
 
     /**
-     * Copy constructor.
-     *
-     * @param copy to copy from
+     * Constructor to create a copy using fields.
+     * 
+     * @param errorMessageType the error message type
+     * @param customErrorMessagesEnabled whether custom error msessages are enabled
+     * @param messageSource the message source
+     * @param keyword the keyword
+     * @param parentSchema the parent schema
+     * @param schemaLocation the schema location
+     * @param evaluationPath the evaluation path
+     * @param evaluationParentSchema the evaluation parent schema
+     * @param errorMessage the error message
      */
-    protected ValidationMessageHandler(ValidationMessageHandler copy) {
-        this.messageSource = copy.messageSource;
-        this.errorMessageType = copy.errorMessageType;
-        this.schemaLocation = copy.schemaLocation;
-        this.evaluationPath = copy.evaluationPath;
-        this.parentSchema = copy.parentSchema;
-        this.evaluationParentSchema = copy.evaluationParentSchema;
-        this.customErrorMessagesEnabled = copy.customErrorMessagesEnabled;
-        this.errorMessage = copy.errorMessage;
-        this.keyword = copy.keyword;
+    protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean customErrorMessagesEnabled,
+            MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation,
+            JsonNodePath evaluationPath, JsonSchema evaluationParentSchema, Map<String, String> errorMessage) {
+        this.errorMessageType = errorMessageType;
+        this.customErrorMessagesEnabled = customErrorMessagesEnabled;
+        this.messageSource = messageSource;
+        this.keyword = keyword;
+        this.parentSchema = parentSchema;
+        this.schemaLocation = schemaLocation;
+        this.evaluationPath = evaluationPath;
+        this.evaluationParentSchema = evaluationParentSchema;
+        this.errorMessage = errorMessage;
     }
 
     protected MessageSourceValidationMessage.Builder message() {
@@ -67,37 +97,6 @@ public abstract class ValidationMessageHandler {
         return this.errorMessageType;
     }
 
-    protected void parseErrorCode(String errorCodeKey) {
-        if (errorCodeKey != null && this.parentSchema != null) {
-            JsonNode errorCodeNode = this.parentSchema.getSchemaNode().get(errorCodeKey);
-            if (errorCodeNode != null && errorCodeNode.isTextual()) {
-                String errorCodeText = errorCodeNode.asText();
-                if (StringUtils.isNotBlank(errorCodeText)) {
-                    this.errorMessageType = CustomErrorMessageType.of(errorCodeText);
-                }
-            }
-        }
-    }
-
-    protected void updateValidatorType(ValidatorTypeCode validatorTypeCode) {
-        updateKeyword(validatorTypeCode);
-        updateErrorMessageType(validatorTypeCode);
-    }
-
-    protected void updateErrorMessageType(ErrorMessageType errorMessageType) {
-        this.errorMessageType = errorMessageType;
-    }
-
-    protected void updateKeyword(Keyword keyword) {
-        this.keyword = keyword;
-        if (this.keyword != null) {
-            if (this.customErrorMessagesEnabled && keyword != null && parentSchema != null) {
-                this.errorMessage = getErrorMessage(parentSchema.getSchemaNode(), keyword.getValue());
-            }
-            parseErrorCode(getErrorCodeKey(keyword.getValue()));
-        }
-    }
-
     /**
      * Gets the custom error message to use.
      * 
diff --git a/src/main/java/com/networknt/schema/ValidationResult.java b/src/main/java/com/networknt/schema/ValidationResult.java
index 7c04196..74cd7ca 100644
--- a/src/main/java/com/networknt/schema/ValidationResult.java
+++ b/src/main/java/com/networknt/schema/ValidationResult.java
@@ -17,11 +17,14 @@ package com.networknt.schema;
 
 import java.util.Set;
 
+/**
+ * Represents a validation result.
+ */
 public class ValidationResult {
 
-    private Set<ValidationMessage> validationMessages;
+    private final Set<ValidationMessage> validationMessages;
 
-    private ExecutionContext executionContext;
+    private final ExecutionContext executionContext;
 
     public ValidationResult(Set<ValidationMessage> validationMessages, ExecutionContext executionContext) {
         super();
diff --git a/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java
new file mode 100644
index 0000000..72c5fec
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import com.networknt.schema.utils.Classes;
+
+/**
+ * ECMAScript regular expression factory that chooses between GraalJS or Joni
+ * implementations depending on which is on the classpath.
+ */
+public class ECMAScriptRegularExpressionFactory implements RegularExpressionFactory {
+    private static final boolean JONI_PRESENT = Classes.isPresent("org.joni.Regex",
+            ECMAScriptRegularExpressionFactory.class.getClassLoader());
+    private static final boolean GRAALJS_PRESENT = Classes.isPresent("com.oracle.truffle.js.parser.GraalJSEvaluator",
+            ECMAScriptRegularExpressionFactory.class.getClassLoader());
+
+    private static final RegularExpressionFactory DELEGATE = GRAALJS_PRESENT
+            ? GraalJSRegularExpressionFactory.getInstance()
+            : JoniRegularExpressionFactory.getInstance();
+    
+    public static final ECMAScriptRegularExpressionFactory INSTANCE = new ECMAScriptRegularExpressionFactory();
+
+    public static ECMAScriptRegularExpressionFactory getInstance() {
+        if (!JONI_PRESENT && !GRAALJS_PRESENT) {
+            throw new IllegalArgumentException(
+                    "Either org.jruby.joni:joni or org.graalvm.js:js needs to be present in the classpath");
+        }
+        return INSTANCE;
+    }
+
+    @Override
+    public RegularExpression getRegularExpression(String regex) {
+        return DELEGATE.getRegularExpression(regex);
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/GraalJSContextFactory.java b/src/main/java/com/networknt/schema/regex/GraalJSContextFactory.java
new file mode 100644
index 0000000..7e41669
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/GraalJSContextFactory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import org.graalvm.polyglot.Context;
+
+/**
+ * Factory for the js {@link Context}.
+ */
+public class GraalJSContextFactory {
+    /**
+     * The holder defers the classloading until it is used.
+     */
+    private static class Holder {
+        private static final Context INSTANCE = Context.newBuilder("js").option("engine.WarnInterpreterOnly", "false")
+                .build();
+    }
+
+    /**
+     * Gets the singleton instance of the Context.
+     * <p>
+     * This may need to be closed to release resources if no longer needed.
+     *
+     * @return the Context
+     */
+    public static Context getInstance() {
+        return Holder.INSTANCE;
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java
new file mode 100644
index 0000000..0ab33cd
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import org.graalvm.polyglot.Value;
+
+/**
+ * GraalJS {@link RegularExpression}.
+ * <p>
+ * This requires a dependency on org.graalvm.js:js which along with its
+ * dependency libraries are 50 mb.
+ */
+class GraalJSRegularExpression implements RegularExpression {
+    private final GraalJSRegularExpressionContext context;
+    private final Value function;
+    
+    GraalJSRegularExpression(String regex, GraalJSRegularExpressionContext context) {
+        this.context = context;
+        synchronized(context.getContext()) {
+            this.function = context.getRegExpBuilder().execute(regex);
+        }
+    }
+
+    @Override
+    public boolean matches(String value) {
+        synchronized(context.getContext()) {
+            return !function.execute(value).isNull();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java
new file mode 100644
index 0000000..14c63a8
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.Value;
+
+/**
+ * GraalJSRegularExpressionContext.
+ */
+public class GraalJSRegularExpressionContext {
+    private static final String SOURCE = "pattern => {\n"
+            + "    const regex = new RegExp(pattern, 'u');\n"
+            + "    return text => text.match(regex)\n"
+            + "};";
+
+    private final Context context;
+    private final Value regExpBuilder;
+
+    /**
+     * Constructor.
+     * <p>
+     * It is the caller's responsibility to release the context when it is no longer
+     * required.
+     * 
+     * @param context the context
+     */
+    public GraalJSRegularExpressionContext(Context context) {
+        this.context = context;
+        synchronized(this.context) {
+            this.regExpBuilder = this.context.eval("js", SOURCE);
+        }
+    }
+
+    /**
+     * Operations must synchronize on the {@link Context} as only a single thread
+     * can access the {@link Context} and {@link #getRegExpBuilder()} at one time.
+     * 
+     * @return the context
+     */
+    public Context getContext() {
+        return context;
+    }
+
+    /**
+     * Gets the RegExp builder.
+     * 
+     * @return the regexp builder
+     */
+    public Value getRegExpBuilder() {
+        return regExpBuilder;
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java
new file mode 100644
index 0000000..113da02
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import org.graalvm.polyglot.Context;
+
+/**
+ * GraalJS {@link RegularExpressionFactory}.
+ * <p>
+ * This requires a dependency on org.graalvm.js:js which along with its
+ * dependency libraries are 50 MB.
+ */
+public class GraalJSRegularExpressionFactory implements RegularExpressionFactory {
+    private static class Holder {
+        private static final GraalJSRegularExpressionFactory INSTANCE = new GraalJSRegularExpressionFactory();
+    }
+
+    private final GraalJSRegularExpressionContext context;
+
+    public static GraalJSRegularExpressionFactory getInstance() {
+        return Holder.INSTANCE;
+    }
+
+    /**
+     * Constructor.
+     * <p>
+     * This uses the context from {@link GraalJSContextFactory#getInstance()}.
+     * <p>
+     * It is the caller's responsibility to release the context when it is no longer
+     * required by using GraalJSContextFactory.getInstance().close().
+     */
+    public GraalJSRegularExpressionFactory() {
+        this(GraalJSContextFactory.getInstance());
+    }
+
+    /**
+     * Constructor.
+     * <p>
+     * It is the caller's responsibility to release the context when it is no longer
+     * required.
+     * 
+     * @param context the context
+     */
+    public GraalJSRegularExpressionFactory(Context context) {
+        this.context = new GraalJSRegularExpressionContext(context);
+    }
+
+    @Override
+    public RegularExpression getRegularExpression(String regex) {
+        return new GraalJSRegularExpression(regex, this.context);
+    }
+}
diff --git a/src/main/java/com/networknt/schema/JsonNodeLocation.java b/src/main/java/com/networknt/schema/regex/JDKRegularExpressionFactory.java
similarity index 54%
rename from src/main/java/com/networknt/schema/JsonNodeLocation.java
rename to src/main/java/com/networknt/schema/regex/JDKRegularExpressionFactory.java
index 7f888c1..0e773a3 100644
--- a/src/main/java/com/networknt/schema/JsonNodeLocation.java
+++ b/src/main/java/com/networknt/schema/regex/JDKRegularExpressionFactory.java
@@ -13,31 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.networknt.schema;
+package com.networknt.schema.regex;
 
 /**
- * JsonNode location information.
+ * JDK {@link RegularExpressionFactory}.
  */
-public class JsonNodeLocation {
-    private final int line;
-    private final int column;
-
-    public JsonNodeLocation(int line, int column) {
-        super();
-        this.line = line;
-        this.column = column;
-    }
-
-    public int getLine() {
-        return line;
-    }
-
-    public int getColumn() {
-        return column;
+public class JDKRegularExpressionFactory implements RegularExpressionFactory {
+    private static final JDKRegularExpressionFactory INSTANCE = new JDKRegularExpressionFactory();
+    
+    public static JDKRegularExpressionFactory getInstance() {
+        return INSTANCE;
     }
 
     @Override
-    public String toString() {
-        return "JsonNodeLocation [line=" + line + ", column=" + column + "]";
+    public RegularExpression getRegularExpression(String regex) {
+        return new JDKRegularExpression(regex);
     }
 }
diff --git a/src/main/java/com/networknt/schema/regex/JoniRegularExpression.java b/src/main/java/com/networknt/schema/regex/JoniRegularExpression.java
index 5e205e4..21cbdd1 100644
--- a/src/main/java/com/networknt/schema/regex/JoniRegularExpression.java
+++ b/src/main/java/com/networknt/schema/regex/JoniRegularExpression.java
@@ -7,18 +7,37 @@ import org.jcodings.specific.UTF8Encoding;
 import org.joni.Option;
 import org.joni.Regex;
 import org.joni.Syntax;
+import org.joni.constants.SyntaxProperties;
 import org.joni.exception.SyntaxException;
 
 /**
- * ECMAScript {@link RegularExpression}.
+ * Joni {@link RegularExpression}.
+ * <p>
+ * This requires a dependency on org.jruby.joni:joni which along with its
+ * dependency libraries are 2 MB.
  */
 class JoniRegularExpression implements RegularExpression {
     private final Regex pattern;
     private final Pattern INVALID_ESCAPE_PATTERN = Pattern.compile(
-            ".*\\\\([aeg-moqyzACE-OQ-RT-VX-Z1-9]|c$|[pP]([^{]|$)|u([^{0-9]|$)|x([0-9a-fA-F][^0-9a-fA-F]|[^0-9a-fA-F][0-9a-fA-F]|[^0-9a-fA-F][^0-9a-fA-F]|.?$)).*");
+            ".*\\\\([aeg-jl-moqyzACE-OQ-RT-VX-Z1-9]|k([^<]|$)|c$|[pP]([^{]|$)|u([^{0-9]|$)|x([0-9a-fA-F][^0-9a-fA-F]|[^0-9a-fA-F][0-9a-fA-F]|[^0-9a-fA-F][^0-9a-fA-F]|.?$)).*");
+    
+    /**
+     * This is a custom syntax as Syntax.ECMAScript doesn't seem to be correct.
+     * <p>
+     * 
+     * @see <a href=
+     *      "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group">OP2_QMARK_LT_NAMED_GROUP</a>
+     * @see <a href=
+     *      "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_backreference">OP2_ESC_K_NAMED_BACKREF</a>
+     */
+    private static final Syntax SYNTAX = new Syntax(Syntax.ECMAScript.name, Syntax.ECMAScript.op,
+            Syntax.ECMAScript.op2 | SyntaxProperties.OP2_QMARK_LT_NAMED_GROUP
+                    | SyntaxProperties.OP2_ESC_K_NAMED_BACKREF,
+            Syntax.ECMAScript.op3, Syntax.ECMAScript.behavior, Syntax.ECMAScript.options,
+            Syntax.ECMAScript.metaCharTable);;
 
     JoniRegularExpression(String regex) {
-        this(regex, Syntax.ECMAScript);
+        this(regex, SYNTAX);
     }
 
     JoniRegularExpression(String regex, Syntax syntax) {
diff --git a/src/main/java/com/networknt/schema/regex/JoniRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/JoniRegularExpressionFactory.java
new file mode 100644
index 0000000..2d451ae
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/JoniRegularExpressionFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+/**
+ * Joni {@link RegularExpressionFactory}.
+ * <p>
+ * This requires a dependency on org.jruby.joni:joni which along with its
+ * dependency libraries are 2 MB.
+ */
+public class JoniRegularExpressionFactory implements RegularExpressionFactory {
+    private static final JoniRegularExpressionFactory INSTANCE = new JoniRegularExpressionFactory();
+
+    public static JoniRegularExpressionFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public RegularExpression getRegularExpression(String regex) {
+        return new JoniRegularExpression(regex);
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/RegularExpression.java b/src/main/java/com/networknt/schema/regex/RegularExpression.java
index 83f4d22..8a3140b 100644
--- a/src/main/java/com/networknt/schema/regex/RegularExpression.java
+++ b/src/main/java/com/networknt/schema/regex/RegularExpression.java
@@ -11,9 +11,7 @@ public interface RegularExpression {
 
     static RegularExpression compile(String regex, ValidationContext validationContext) {
         if (null == regex) return s -> true;
-        return validationContext.getConfig().isEcma262Validator()
-            ? new JoniRegularExpression(regex)
-            : new JDKRegularExpression(regex);
+        return validationContext.getConfig().getRegularExpressionFactory().getRegularExpression(regex);
     }
 
 }
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java
new file mode 100644
index 0000000..5aca068
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+/**
+ * Factory for {@link RegularExpression}.
+ */
+public interface RegularExpressionFactory {
+    /**
+     * Gets a {@link RegularExpression}.
+     * 
+     * @param regex the regular expression text value
+     * @return the regular expression
+     */
+    RegularExpression getRegularExpression(String regex);
+}
diff --git a/src/main/java/com/networknt/schema/result/JsonNodeResults.java b/src/main/java/com/networknt/schema/result/JsonNodeResults.java
index 78b51f3..0b3f5e8 100644
--- a/src/main/java/com/networknt/schema/result/JsonNodeResults.java
+++ b/src/main/java/com/networknt/schema/result/JsonNodeResults.java
@@ -31,7 +31,7 @@ public class JsonNodeResults {
     /**
      * Stores the invalid results.
      */
-    private Map<JsonNodePath, List<JsonNodeResult>> values = new HashMap<>();
+    private final Map<JsonNodePath, List<JsonNodeResult>> values = new HashMap<>();
 
     public void setResult(JsonNodePath instanceLocation, SchemaLocation schemaLocation, JsonNodePath evaluationPath,
             boolean valid) {
diff --git a/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java
index eba7f05..fbd5c36 100644
--- a/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java
+++ b/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java
@@ -12,7 +12,7 @@ import java.util.Set;
 
 public class DefaultItemWalkListenerRunner extends AbstractWalkListenerRunner {
 
-    private List<JsonSchemaWalkListener> itemWalkListeners;
+    private final List<JsonSchemaWalkListener> itemWalkListeners;
 
     public DefaultItemWalkListenerRunner(List<JsonSchemaWalkListener> itemWalkListeners) {
         this.itemWalkListeners = itemWalkListeners;
diff --git a/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java
index 0435f53..fd75835 100644
--- a/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java
+++ b/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java
@@ -9,7 +9,7 @@ import java.util.Set;
 
 public class DefaultKeywordWalkListenerRunner extends AbstractWalkListenerRunner {
 
-    private Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap;
+    private final Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap;
 
     public DefaultKeywordWalkListenerRunner(Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap) {
         this.keywordWalkListenersMap = keywordWalkListenersMap;
diff --git a/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java
index e10253f..f0c101b 100644
--- a/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java
+++ b/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java
@@ -12,7 +12,7 @@ import java.util.Set;
 
 public class DefaultPropertyWalkListenerRunner extends AbstractWalkListenerRunner {
 
-    private List<JsonSchemaWalkListener> propertyWalkListeners;
+    private final List<JsonSchemaWalkListener> propertyWalkListeners;
 
     public DefaultPropertyWalkListenerRunner(List<JsonSchemaWalkListener> propertyWalkListeners) {
         this.propertyWalkListeners = propertyWalkListeners;
diff --git a/src/main/java/com/networknt/schema/walk/WalkFlow.java b/src/main/java/com/networknt/schema/walk/WalkFlow.java
index af2da7b..e4bc43f 100644
--- a/src/main/java/com/networknt/schema/walk/WalkFlow.java
+++ b/src/main/java/com/networknt/schema/walk/WalkFlow.java
@@ -8,9 +8,9 @@ public enum WalkFlow {
 
     CONTINUE("ContinueToWalk", "continue to invoke the walk method and other listeners");
 
-    private String name;
+    private final String name;
 
-    private String description;
+    private final String description;
 
     WalkFlow(String name, String description) {
         this.name = name;


Test patch:
diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java
index 3852f63..4e9e8f1 100644
--- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java
+++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java
@@ -18,6 +18,8 @@ package com.networknt.schema;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.networknt.schema.SpecVersion.VersionFlag;
+import com.networknt.schema.regex.JDKRegularExpressionFactory;
+import com.networknt.schema.regex.JoniRegularExpressionFactory;
 import com.networknt.schema.serialization.JsonMapperFactory;
 import com.networknt.schema.suite.TestCase;
 import com.networknt.schema.suite.TestSource;
@@ -206,7 +208,9 @@ public abstract class AbstractJsonSchemaTestSuite extends HTTPServiceSupport {
 
         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
         config.setTypeLoose(typeLoose);
-        config.setEcma262Validator(TestSpec.RegexKind.JDK != testSpec.getRegex());
+        config.setRegularExpressionFactory(
+                TestSpec.RegexKind.JDK == testSpec.getRegex() ? JDKRegularExpressionFactory.getInstance()
+                        : JoniRegularExpressionFactory.getInstance());
         testSpec.getStrictness().forEach(config::setStrict);
 
         if (testSpec.getConfig() != null) {
diff --git a/src/test/java/com/networknt/schema/CollectorContextTest.java b/src/test/java/com/networknt/schema/CollectorContextTest.java
index ea281e7..4d4b1e9 100644
--- a/src/test/java/com/networknt/schema/CollectorContextTest.java
+++ b/src/test/java/com/networknt/schema/CollectorContextTest.java
@@ -24,8 +24,12 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import java.io.IOException;
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 
 public class CollectorContextTest {
 
@@ -380,4 +384,19 @@ public class CollectorContextTest {
         map.put("sample3", "actual_value_added_to_context3");
         return map;
     }
+
+    @Test
+    void constructor() {
+        CollectorContext context = new CollectorContext();
+        assertTrue(context.getCollectorMap().isEmpty());
+        assertTrue(context.getAll().isEmpty());
+    }
+
+    @Test
+    void constructorWithMap() {
+        ConcurrentHashMap<String, Object> collectorMap = new ConcurrentHashMap<>();
+        ConcurrentHashMap<String, Object> collectorLoadMap = new ConcurrentHashMap<>();
+        CollectorContext context = new CollectorContext(collectorMap, collectorLoadMap);
+        assertSame(collectorMap, context.getCollectorMap());
+    }
 }
diff --git a/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java b/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java
index 22059d4..c21d7ef 100644
--- a/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java
+++ b/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java
@@ -19,6 +19,7 @@ package com.networknt.schema;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.networknt.schema.SpecVersion.VersionFlag;
 import com.networknt.schema.output.OutputUnit;
+import com.networknt.schema.regex.JoniRegularExpressionFactory;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -52,7 +53,7 @@ public class PatternPropertiesValidatorTest extends BaseJsonSchemaValidatorTest
     public void testInvalidPatternPropertiesValidatorECMA262() throws Exception {
         Assertions.assertThrows(JsonSchemaException.class, () -> {
             SchemaValidatorsConfig config = new SchemaValidatorsConfig();
-            config.setEcma262Validator(true);
+            config.setRegularExpressionFactory(JoniRegularExpressionFactory.getInstance());
             JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
             JsonSchema schema = factory.getSchema("{\"patternProperties\":6}", config);
 
diff --git a/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java b/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java
new file mode 100644
index 0000000..511eac9
--- /dev/null
+++ b/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import com.networknt.schema.regex.ECMAScriptRegularExpressionFactory;
+import com.networknt.schema.regex.JDKRegularExpressionFactory;
+
+/**
+ * Test for SchemaValidatorsConfig.
+ */
+class SchemaValidatorsConfigTest {
+    @Test
+    void defaultEcma262Validator() {
+        SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+        assertSame(JDKRegularExpressionFactory.getInstance(), config.getRegularExpressionFactory());
+        assertFalse(config.isEcma262Validator());
+    }
+
+    @Test
+    void setEcma262Validator() {
+        SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+
+        config.setEcma262Validator(true);
+        assertSame(ECMAScriptRegularExpressionFactory.getInstance(), config.getRegularExpressionFactory());
+        assertTrue(config.isEcma262Validator());
+
+        config.setEcma262Validator(false);
+        assertSame(JDKRegularExpressionFactory.getInstance(), config.getRegularExpressionFactory());
+        assertFalse(config.isEcma262Validator());
+    }
+
+}
diff --git a/src/test/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactoryTest.java
new file mode 100644
index 0000000..15eaad3
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactoryTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for ECMAScriptRegularExpressionFactory. 
+ */
+class ECMAScriptRegularExpressionFactoryTest {
+
+    @Test
+    void shouldPreferGraalJS() {
+        RegularExpression regularExpression = ECMAScriptRegularExpressionFactory.getInstance()
+                .getRegularExpression("[a-z]");
+        assertInstanceOf(GraalJSRegularExpression.class, regularExpression);
+    }
+
+}
diff --git a/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionFactoryTest.java
new file mode 100644
index 0000000..a1c4de0
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionFactoryTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.graalvm.polyglot.Context;
+import org.junit.jupiter.api.Test;
+
+/**
+ * GraalJSRegularExpressionFactoryTest.
+ */
+class GraalJSRegularExpressionFactoryTest {
+
+    @Test
+    void constructorContext() {
+        try (Context context = Context.newBuilder("js").option("engine.WarnInterpreterOnly", "false")
+                .build()) {
+            GraalJSRegularExpressionFactory factory = new GraalJSRegularExpressionFactory(context);
+            RegularExpression regex = factory.getRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+            assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+        }
+    }
+
+}
diff --git a/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionTest.java b/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionTest.java
new file mode 100644
index 0000000..1c1fd7d
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+/**
+ * Test for GraalJSRegularExpression.
+ */
+class GraalJSRegularExpressionTest {
+    private static GraalJSRegularExpressionContext CONTEXT = new GraalJSRegularExpressionContext(
+            GraalJSContextFactory.getInstance());
+
+    enum InvalidEscapeInput {
+        A("\\a"),
+        HELLOA("hello\\a"),
+        C("\\c"),
+        E("\\e"),
+        G("\\g"),
+        H("\\h"),
+        I("\\i"),
+        J("\\j"),
+        K("\\k"),
+        L("\\l"),
+        M("\\m"),
+        O("\\o"),
+        Q("\\q"),
+        U("\\u"),
+        X("\\x"),
+        X1("\\x1"),
+        XGG("\\xgg"),
+        X1G("\\x1g"),
+        Y("\\y"),
+        Z("\\z"),
+        _1("\\1"),
+        _2("\\2"),
+        _3("\\3"),
+        _4("\\4"),
+        _5("\\5"),
+        _6("\\6"),
+        _7("\\7"),
+        _8("\\8"),
+        _9("\\9");
+
+        String value;
+
+        InvalidEscapeInput(String value) {
+            this.value = value;
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(InvalidEscapeInput.class)
+    void invalidEscape(InvalidEscapeInput input) {
+        RuntimeException e = assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression(input.value, CONTEXT));
+        assertTrue(e.getMessage().startsWith("SyntaxError"));
+    }
+
+    enum ValidEscapeInput {
+        B("\\b"),
+        D("\\d"),
+        CAP_D("\\D"),
+        W("\\w"),
+        CAP_W("\\W"),
+        S("\\s"),
+        CAP_S("\\S"),
+        T("\\t"),
+        U1234("\\u1234"),
+        R("\\r"),
+        N("\\n"),
+        V("\\v"),
+        F("\\f"),
+        X12("\\x12"),
+        X1F("\\x1f"),
+        X1234("\\x1234"),
+        P("\\p{Letter}cole"), // unicode property
+        CAP_P("\\P{Letter}cole"), // unicode property
+        _0("\\0"),
+        CA("\\cA"), // control
+        CB("\\cB"), // control
+        CC("\\cC"), // control
+        CG("\\cG"); // control
+
+        String value;
+
+        ValidEscapeInput(String value) {
+            this.value = value;
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(ValidEscapeInput.class)
+    void validEscape(ValidEscapeInput input) {
+        assertDoesNotThrow(() -> new GraalJSRegularExpression(input.value, CONTEXT));
+    }
+    
+    @Test
+    void invalidPropertyName() {
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\p", CONTEXT));
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\P", CONTEXT));
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\pa", CONTEXT));
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\Pa", CONTEXT));
+    }
+    
+    @Test
+    void digit() {
+        RegularExpression regex = new GraalJSRegularExpression("\\d", CONTEXT);
+        assertTrue(regex.matches("1"));
+        assertFalse(regex.matches("a"));
+    }
+
+    @Test
+    void invalidEscape() {
+        RuntimeException e = assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\a", CONTEXT));
+        assertEquals("SyntaxError: Invalid escape", e.getMessage());
+    }
+
+    @Test
+    void namedCapturingGroup() {
+        RegularExpression regex = new GraalJSRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?", CONTEXT);
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void invalidNamedCapturingGroup() {
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("(?<name>)(?<name>)", CONTEXT));
+    }
+
+    @Test
+    void namedBackreference() {
+        RegularExpression regex = new GraalJSRegularExpression("title=(?<quote>[\"'])(.*?)\\k<quote>", CONTEXT);
+        assertTrue(regex.matches("title=\"Named capturing groups\\' advantages\""));
+    }
+
+    @Test
+    void concurrency() throws Exception {
+        RegularExpression regex = new GraalJSRegularExpression("\\d", CONTEXT);
+        Exception[] instance = new Exception[1];
+        CountDownLatch latch = new CountDownLatch(1);
+        List<Thread> threads = new ArrayList<>();
+        for (int i = 0; i < 50; ++i) {
+            Runnable runner = new Runnable() {
+                public void run() {
+                    try {
+                        latch.await();
+                    } catch (InterruptedException e) {
+                        throw new RuntimeException(e);
+                    }
+                    try {
+                        assertTrue(regex.matches("1"));
+                    } catch (RuntimeException e) {
+                        instance[0] = e;
+                    }
+                }
+            };
+            Thread thread = new Thread(runner, "Thread" + i);
+            thread.start();
+            threads.add(thread);
+        }
+        latch.countDown(); // Release the latch for threads to run concurrently
+        threads.forEach(t -> {
+            try {
+                t.join();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        });
+        if (instance[0] != null) {
+            throw instance[0];
+        }
+    }
+}
diff --git a/src/test/java/com/networknt/schema/regex/JDKRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/JDKRegularExpressionFactoryTest.java
new file mode 100644
index 0000000..76fcc5f
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/JDKRegularExpressionFactoryTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JDKRegularExpressionFactoryTest.
+ */
+class JDKRegularExpressionFactoryTest {
+
+    @Test
+    void getRegularExpression() {
+        JDKRegularExpressionFactory factory = new JDKRegularExpressionFactory();
+        RegularExpression regex = factory
+                .getRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void getInstance() {
+        assertSame(JDKRegularExpressionFactory.getInstance(), JDKRegularExpressionFactory.getInstance());
+    }
+}
diff --git a/src/test/java/com/networknt/schema/regex/JDKRegularExpressionTest.java b/src/test/java/com/networknt/schema/regex/JDKRegularExpressionTest.java
new file mode 100644
index 0000000..8cca8a7
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/JDKRegularExpressionTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for JDKRegularExpression.
+ */
+class JDKRegularExpressionTest {
+    @Test
+    void namedCapturingGroup() {
+        RegularExpression regex = new JDKRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void invalidNamedCapturingGroup() {
+        assertThrows(RuntimeException.class, () -> new JDKRegularExpression("(?<name>)(?<name>)"));
+    }
+
+    @Test
+    void namedBackreference() {
+        RegularExpression regex = new JDKRegularExpression("title=(?<quote>[\"'])(.*?)\\k<quote>");
+        assertTrue(regex.matches("title=\"Named capturing groups\\' advantages\""));
+    }
+}
diff --git a/src/test/java/com/networknt/schema/regex/JoniRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/JoniRegularExpressionFactoryTest.java
new file mode 100644
index 0000000..fb9c19b
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/JoniRegularExpressionFactoryTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JoniRegularExpressionFactoryTest.
+ */
+class JoniRegularExpressionFactoryTest {
+
+    @Test
+    void getRegularExpression() {
+        JoniRegularExpressionFactory factory = new JoniRegularExpressionFactory();
+        RegularExpression regex = factory
+                .getRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void getInstance() {
+        assertSame(JoniRegularExpressionFactory.getInstance(), JoniRegularExpressionFactory.getInstance());
+    }
+}
diff --git a/src/test/java/com/networknt/schema/regex/JoniRegularExpressionTest.java b/src/test/java/com/networknt/schema/regex/JoniRegularExpressionTest.java
index fc1f899..9a1d0cd 100644
--- a/src/test/java/com/networknt/schema/regex/JoniRegularExpressionTest.java
+++ b/src/test/java/com/networknt/schema/regex/JoniRegularExpressionTest.java
@@ -18,6 +18,7 @@ package com.networknt.schema.regex;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.joni.exception.SyntaxException;
 import org.junit.jupiter.api.Test;
@@ -111,7 +112,7 @@ class JoniRegularExpressionTest {
     void validEscape(ValidEscapeInput input) {
         assertDoesNotThrow(() -> new JoniRegularExpression(input.value));
     }
-    
+
     @Test
     void invalidPropertyName() {
         assertThrows(SyntaxException.class, () -> new JoniRegularExpression("\\p"));
@@ -119,4 +120,37 @@ class JoniRegularExpressionTest {
         assertThrows(SyntaxException.class, () -> new JoniRegularExpression("\\pa"));
         assertThrows(SyntaxException.class, () -> new JoniRegularExpression("\\Pa"));
     }
+
+    /**
+     * Named capturing group: (?<name>...).
+     * 
+     * @see org.joni.constants.SyntaxProperties#OP2_QMARK_LT_NAMED_GROUP.
+     * @see <a href=
+     *      "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group">Named
+     *      capturing group</a>
+     */
+    @Test
+    void namedCapturingGroup() {
+        RegularExpression regex = new JoniRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void invalidNamedCapturingGroup() {
+        assertThrows(RuntimeException.class, () -> new JoniRegularExpression("(?<name>)(?<name>)"));
+    }
+
+    /**
+     * Named capturing group: (?<name>...).
+     * 
+     * @see org.joni.constants.SyntaxProperties#OP2_ESC_K_NAMED_BACKREF.
+     * @see <a href=
+     *      "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_backreference">Named
+     *      backreference</a>
+     */
+    @Test
+    void namedBackreference() {
+        RegularExpression regex = new JoniRegularExpression("title=(?<quote>[\"'])(.*?)\\k<quote>");
+        assertTrue(regex.matches("title=\"Named capturing groups\\' advantages\""));
+    }
 }