Repository: networknt/json-schema-validator
Pull request #580 (2022-06-02 20:07:12)

Add support for draft 2020-12 (#380)
2020-12 really is not that different from 2019-09.  This set of changes allows basic validation for 2020-12 (in fact, it even borrows most of the 2019-09 unit tests).


Patch:
diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
index 4a9efcb..f28916f 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
@@ -251,6 +251,9 @@ public class JsonSchemaFactory {
     public static JsonSchemaVersion checkVersion(SpecVersion.VersionFlag versionFlag){
         JsonSchemaVersion jsonSchemaVersion = null;
         switch (versionFlag) {
+            case V202012:
+                jsonSchemaVersion = new Version202012();
+                break;
             case V201909:
                 jsonSchemaVersion = new Version201909();
                 break;
diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java
new file mode 100644
index 0000000..aec1839
--- /dev/null
+++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2022 Network New Technologies Inc.
+ *
+ * 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 com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.networknt.schema.walk.DefaultItemWalkListenerRunner;
+import com.networknt.schema.walk.WalkListenerRunner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+public class PrefixItemsValidator extends BaseJsonValidator implements JsonValidator {
+    private static final Logger logger = LoggerFactory.getLogger(PrefixItemsValidator.class);
+    private static final String PROPERTY_ITEMS = "items";
+
+    private final JsonSchema schema;
+    private final List<JsonSchema> tupleSchema;
+    private boolean additionalItems = true;
+    private final JsonSchema additionalSchema;
+    private WalkListenerRunner arrayItemWalkListenerRunner;
+
+    public PrefixItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
+                                ValidationContext validationContext) {
+        super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext);
+        tupleSchema = new ArrayList<JsonSchema>();
+        JsonSchema foundSchema = null;
+        JsonSchema foundAdditionalSchema = null;
+
+        if (schemaNode.isObject() || schemaNode.isBoolean()) {
+            foundSchema = new JsonSchema(validationContext, schemaPath, parentSchema.getCurrentUri(), schemaNode,
+                    parentSchema);
+        } else {
+            for (JsonNode s : schemaNode) {
+                tupleSchema.add(
+                        new JsonSchema(validationContext, schemaPath, parentSchema.getCurrentUri(), s, parentSchema));
+            }
+
+            JsonNode addItemNode = getParentSchema().getSchemaNode().get(PROPERTY_ITEMS);
+            if (addItemNode != null) {
+                if (addItemNode.isBoolean()) {
+                    additionalItems = addItemNode.asBoolean();
+                } else if (addItemNode.isObject()) {
+                    foundAdditionalSchema = new JsonSchema(validationContext, "#", parentSchema.getCurrentUri(), addItemNode, parentSchema);
+                }
+            }
+        }
+        arrayItemWalkListenerRunner = new DefaultItemWalkListenerRunner(validationContext.getConfig().getArrayItemWalkListeners());
+
+        this.validationContext = validationContext;
+
+        parseErrorCode(getValidatorType().getErrorCodeKey());
+
+        this.schema = foundSchema;
+        this.additionalSchema = foundAdditionalSchema;
+    }
+
+    public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
+        debug(logger, node, rootNode, at);
+
+        Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
+
+        if (!node.isArray() && !this.validationContext.getConfig().isTypeLoose()) {
+            // ignores non-arrays
+            return errors;
+        }
+        if (node.isArray()) {
+            int i = 0;
+            for (JsonNode n : node) {
+                doValidate(errors, i, n, rootNode, at);
+                i++;
+            }
+        } else {
+            doValidate(errors, 0, node, rootNode, at);
+        }
+        return Collections.unmodifiableSet(errors);
+    }
+
+    private void doValidate(Set<ValidationMessage> errors, int i, JsonNode node, JsonNode rootNode, String at) {
+        if (schema != null) {
+            // validate with item schema (the whole array has the same item
+            // schema)
+            errors.addAll(schema.validate(node, rootNode, at + "[" + i + "]"));
+        }
+
+        if (tupleSchema != null) {
+            if (i < tupleSchema.size()) {
+                // validate against tuple schema
+                errors.addAll(tupleSchema.get(i).validate(node, rootNode, at + "[" + i + "]"));
+            } else {
+                if (additionalSchema != null) {
+                    // validate against additional item schema
+                    errors.addAll(additionalSchema.validate(node, rootNode, at + "[" + i + "]"));
+                } else if (!additionalItems) {
+                    // no additional item allowed, return error
+                    errors.add(buildValidationMessage(at, "" + i));
+                }
+            }
+        }
+    }
+
+    @Override
+    public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
+        HashSet<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
+        if (node instanceof ArrayNode) {
+            ArrayNode arrayNode = (ArrayNode) node;
+            JsonNode defaultNode = null;
+            if (applyDefaultsStrategy.shouldApplyArrayDefaults() && schema != null) {
+                defaultNode = schema.getSchemaNode().get("default");
+            }
+            int i = 0;
+            for (JsonNode n : arrayNode) {
+                if (n.isNull() && defaultNode != null) {
+                    arrayNode.set(i, defaultNode);
+                    n = defaultNode;
+                }
+                doWalk(validationMessages, i, n, rootNode, at, shouldValidateSchema);
+                i++;
+            }
+        } else {
+            doWalk(validationMessages, 0, node, rootNode, at, shouldValidateSchema);
+        }
+        return validationMessages;
+    }
+
+    private void doWalk(HashSet<ValidationMessage> validationMessages, int i, JsonNode node, JsonNode rootNode,
+            String at, boolean shouldValidateSchema) {
+        if (schema != null) {
+            // Walk the schema.
+            walkSchema(schema, node, rootNode, at + "[" + i + "]", shouldValidateSchema, validationMessages);
+        }
+
+        if (tupleSchema != null) {
+            if (i < tupleSchema.size()) {
+                // walk tuple schema
+                walkSchema(tupleSchema.get(i), node, rootNode, at + "[" + i + "]", shouldValidateSchema,
+                        validationMessages);
+            } else {
+                if (additionalSchema != null) {
+                    // walk additional item schema
+                    walkSchema(additionalSchema, node, rootNode, at + "[" + i + "]", shouldValidateSchema,
+                            validationMessages);
+                }
+            }
+        }
+    }
+
+    private void walkSchema(JsonSchema walkSchema, JsonNode node, JsonNode rootNode, String at,
+                            boolean shouldValidateSchema, Set<ValidationMessage> validationMessages) {
+        boolean executeWalk = arrayItemWalkListenerRunner.runPreWalkListeners(ValidatorTypeCode.ITEMS.getValue(), node,
+                rootNode, at, walkSchema.getSchemaPath(), walkSchema.getSchemaNode(), walkSchema.getParentSchema(),
+                validationContext, validationContext.getJsonSchemaFactory());
+        if (executeWalk) {
+            validationMessages.addAll(walkSchema.walk(node, rootNode, at, shouldValidateSchema));
+        }
+        arrayItemWalkListenerRunner.runPostWalkListeners(ValidatorTypeCode.ITEMS.getValue(), node, rootNode, at,
+                walkSchema.getSchemaPath(), walkSchema.getSchemaNode(), walkSchema.getParentSchema(),
+                validationContext, validationContext.getJsonSchemaFactory(), validationMessages);
+
+    }
+
+    public List<JsonSchema> getTupleSchema() {
+        return this.tupleSchema;
+    }
+
+    public JsonSchema getSchema() {
+        return schema;
+    }
+
+    @Override
+    public void preloadJsonSchema() {
+        if (null != schema) {
+            schema.initializeValidators();
+        }
+        preloadJsonSchemas(tupleSchema);
+        if (null != additionalSchema) {
+            additionalSchema.initializeValidators();
+        }
+    }
+}
diff --git a/src/main/java/com/networknt/schema/SpecVersion.java b/src/main/java/com/networknt/schema/SpecVersion.java
index d672941..011d62a 100644
--- a/src/main/java/com/networknt/schema/SpecVersion.java
+++ b/src/main/java/com/networknt/schema/SpecVersion.java
@@ -25,7 +25,8 @@ public class SpecVersion {
         V4(1 << 0),
         V6(1 << 1),
         V7(1 << 2),
-        V201909(1 << 3);
+        V201909(1 << 3),
+        V202012(1 << 4);
 
 
         private final long versionFlagValue;
diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java
index e84c059..d6d1892 100644
--- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java
+++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java
@@ -21,61 +21,84 @@ import com.fasterxml.jackson.databind.JsonNode;
 import java.lang.reflect.Constructor;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 
+enum VersionCode {
+    AllVersions(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V4, SpecVersion.VersionFlag.V6, SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }),
+    MinV6(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V6, SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }),
+    MinV7(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }),
+    MaxV201909(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V4, SpecVersion.VersionFlag.V6, SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909 }),
+    MinV201909(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }),
+    MinV202012(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V202012 });
+
+    private static final SpecVersion specVersion = new SpecVersion();
+
+    private final SpecVersion.VersionFlag[] versionFlags;
+
+    VersionCode(SpecVersion.VersionFlag[] versionFlags) {
+        this.versionFlags = versionFlags;
+    }
+    long getValue() {
+        return specVersion.getVersionValue(new HashSet<>(Arrays.asList(this.versionFlags)));
+    }
+}
+
 public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
-    ADDITIONAL_PROPERTIES("additionalProperties", "1001", new MessageFormat(I18nSupport.getString("additionalProperties")), AdditionalPropertiesValidator.class, 15), // v4|v6|v7|v201909
-    ALL_OF("allOf", "1002", new MessageFormat(I18nSupport.getString("allOf")), AllOfValidator.class, 15),
-    ANY_OF("anyOf", "1003", new MessageFormat(I18nSupport.getString("anyOf")), AnyOfValidator.class, 15),
-    CROSS_EDITS("crossEdits", "1004", new MessageFormat(I18nSupport.getString("crossEdits")), null, 15),
-    DEPENDENCIES("dependencies", "1007", new MessageFormat(I18nSupport.getString("dependencies")), DependenciesValidator.class, 15),
-    EDITS("edits", "1005", new MessageFormat(I18nSupport.getString("edits")), null, 15),
-    ENUM("enum", "1008", new MessageFormat(I18nSupport.getString("enum")), EnumValidator.class, 15),
-    FORMAT("format", "1009", new MessageFormat(I18nSupport.getString("format")), null, 15) {
+    ADDITIONAL_PROPERTIES("additionalProperties", "1001", new MessageFormat(I18nSupport.getString("additionalProperties")), AdditionalPropertiesValidator.class, VersionCode.AllVersions),
+    ALL_OF("allOf", "1002", new MessageFormat(I18nSupport.getString("allOf")), AllOfValidator.class, VersionCode.AllVersions),
+    ANY_OF("anyOf", "1003", new MessageFormat(I18nSupport.getString("anyOf")), AnyOfValidator.class, VersionCode.AllVersions),
+    CROSS_EDITS("crossEdits", "1004", new MessageFormat(I18nSupport.getString("crossEdits")), null, VersionCode.AllVersions),
+    DEPENDENCIES("dependencies", "1007", new MessageFormat(I18nSupport.getString("dependencies")), DependenciesValidator.class, VersionCode.AllVersions),
+    EDITS("edits", "1005", new MessageFormat(I18nSupport.getString("edits")), null, VersionCode.AllVersions),
+    ENUM("enum", "1008", new MessageFormat(I18nSupport.getString("enum")), EnumValidator.class, VersionCode.AllVersions),
+    FORMAT("format", "1009", new MessageFormat(I18nSupport.getString("format")), null, VersionCode.AllVersions) {
         @Override
         public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)
                 throws Exception {
             throw new UnsupportedOperationException("Use FormatKeyword instead");
         }
     },
-    ITEMS("items", "1010", new MessageFormat(I18nSupport.getString("items")), ItemsValidator.class, 15),
-    MAXIMUM("maximum", "1011", new MessageFormat(I18nSupport.getString("maximum")), MaximumValidator.class, 15),
-    MAX_ITEMS("maxItems", "1012", new MessageFormat(I18nSupport.getString("maxItems")), MaxItemsValidator.class, 15),
-    MAX_LENGTH("maxLength", "1013", new MessageFormat(I18nSupport.getString("maxLength")), MaxLengthValidator.class, 15),
-    MAX_PROPERTIES("maxProperties", "1014", new MessageFormat(I18nSupport.getString("maxProperties")), MaxPropertiesValidator.class, 15),
-    MINIMUM("minimum", "1015", new MessageFormat(I18nSupport.getString("minimum")), MinimumValidator.class, 15),
-    MIN_ITEMS("minItems", "1016", new MessageFormat(I18nSupport.getString("minItems")), MinItemsValidator.class, 15),
-    MIN_LENGTH("minLength", "1017", new MessageFormat(I18nSupport.getString("minLength")), MinLengthValidator.class, 15),
-    MIN_PROPERTIES("minProperties", "1018", new MessageFormat(I18nSupport.getString("minProperties")), MinPropertiesValidator.class, 15),
-    MULTIPLE_OF("multipleOf", "1019", new MessageFormat(I18nSupport.getString("multipleOf")), MultipleOfValidator.class, 15),
-    NOT_ALLOWED("notAllowed", "1033", new MessageFormat(I18nSupport.getString("notAllowed")), NotAllowedValidator.class, 15),
-    NOT("not", "1020", new MessageFormat(I18nSupport.getString("not")), NotValidator.class, 15),
-    ONE_OF("oneOf", "1022", new MessageFormat(I18nSupport.getString("oneOf")), OneOfValidator.class, 15),
-    PATTERN_PROPERTIES("patternProperties", "1024", new MessageFormat(I18nSupport.getString("patternProperties")), PatternPropertiesValidator.class, 15),
-    PATTERN("pattern", "1023", new MessageFormat(I18nSupport.getString("pattern")), PatternValidator.class, 15),
-    PROPERTIES("properties", "1025", new MessageFormat(I18nSupport.getString("properties")), PropertiesValidator.class, 15),
-    READ_ONLY("readOnly", "1032", new MessageFormat(I18nSupport.getString("readOnly")), ReadOnlyValidator.class, 15),
-    REF("$ref", "1026", new MessageFormat(I18nSupport.getString("$ref")), RefValidator.class, 15),
-    REQUIRED("required", "1028", new MessageFormat(I18nSupport.getString("required")), RequiredValidator.class, 15),
-    TYPE("type", "1029", new MessageFormat(I18nSupport.getString("type")), TypeValidator.class, 15),
-    UNION_TYPE("unionType", "1030", new MessageFormat(I18nSupport.getString("unionType")), UnionTypeValidator.class, 15),
-    UNIQUE_ITEMS("uniqueItems", "1031", new MessageFormat(I18nSupport.getString("uniqueItems")), UniqueItemsValidator.class, 15),
-    DATETIME("dateTime", "1034", new MessageFormat(I18nSupport.getString("dateTime")), null, 15),
-    UUID("uuid", "1035", new MessageFormat(I18nSupport.getString("uuid")), null, 15),
-    ID("id", "1036", new MessageFormat(I18nSupport.getString("id")), null, 15),
-    IF_THEN_ELSE("if", "1037", null, IfValidator.class, 12),  // V7|V201909
-    EXCLUSIVE_MAXIMUM("exclusiveMaximum", "1038", new MessageFormat(I18nSupport.getString("exclusiveMaximum")), ExclusiveMaximumValidator.class, 14),  // V6|V7|V201909
-    EXCLUSIVE_MINIMUM("exclusiveMinimum", "1039", new MessageFormat(I18nSupport.getString("exclusiveMinimum")), ExclusiveMinimumValidator.class, 14),
-    TRUE("true", "1040", null, TrueValidator.class, 14),
-    FALSE("false", "1041", new MessageFormat(I18nSupport.getString("false")), FalseValidator.class, 14),
-    CONST("const", "1042", new MessageFormat(I18nSupport.getString("const")), ConstValidator.class, 14),
-    CONTAINS("contains", "1043", new MessageFormat(I18nSupport.getString("contains")), ContainsValidator.class, 14),
-    PROPERTYNAMES("propertyNames", "1044", new MessageFormat(I18nSupport.getString("propertyNames")), PropertyNamesValidator.class, 14),
-    DEPENDENT_REQUIRED("dependentRequired", "1045", new MessageFormat(I18nSupport.getString("dependentRequired")), DependentRequired.class, 8), // V201909
-    DEPENDENT_SCHEMAS("dependentSchemas", "1046", new MessageFormat(I18nSupport.getString("dependentSchemas")), DependentSchemas.class, 8), // V201909
-    UNEVALUATED_PROPERTIES("unevaluatedProperties","1047",new MessageFormat(I18nSupport.getString("unevaluatedProperties")),UnEvaluatedPropertiesValidator.class,14);
+    ITEMS("items", "1010", new MessageFormat(I18nSupport.getString("items")), ItemsValidator.class, VersionCode.MaxV201909),
+    MAXIMUM("maximum", "1011", new MessageFormat(I18nSupport.getString("maximum")), MaximumValidator.class, VersionCode.AllVersions),
+    MAX_ITEMS("maxItems", "1012", new MessageFormat(I18nSupport.getString("maxItems")), MaxItemsValidator.class, VersionCode.AllVersions),
+    MAX_LENGTH("maxLength", "1013", new MessageFormat(I18nSupport.getString("maxLength")), MaxLengthValidator.class, VersionCode.AllVersions),
+    MAX_PROPERTIES("maxProperties", "1014", new MessageFormat(I18nSupport.getString("maxProperties")), MaxPropertiesValidator.class, VersionCode.AllVersions),
+    MINIMUM("minimum", "1015", new MessageFormat(I18nSupport.getString("minimum")), MinimumValidator.class, VersionCode.AllVersions),
+    MIN_ITEMS("minItems", "1016", new MessageFormat(I18nSupport.getString("minItems")), MinItemsValidator.class, VersionCode.AllVersions),
+    MIN_LENGTH("minLength", "1017", new MessageFormat(I18nSupport.getString("minLength")), MinLengthValidator.class, VersionCode.AllVersions),
+    MIN_PROPERTIES("minProperties", "1018", new MessageFormat(I18nSupport.getString("minProperties")), MinPropertiesValidator.class, VersionCode.AllVersions),
+    MULTIPLE_OF("multipleOf", "1019", new MessageFormat(I18nSupport.getString("multipleOf")), MultipleOfValidator.class, VersionCode.AllVersions),
+    NOT_ALLOWED("notAllowed", "1033", new MessageFormat(I18nSupport.getString("notAllowed")), NotAllowedValidator.class, VersionCode.AllVersions),
+    NOT("not", "1020", new MessageFormat(I18nSupport.getString("not")), NotValidator.class, VersionCode.AllVersions),
+    ONE_OF("oneOf", "1022", new MessageFormat(I18nSupport.getString("oneOf")), OneOfValidator.class, VersionCode.AllVersions),
+    PATTERN_PROPERTIES("patternProperties", "1024", new MessageFormat(I18nSupport.getString("patternProperties")), PatternPropertiesValidator.class, VersionCode.AllVersions),
+    PATTERN("pattern", "1023", new MessageFormat(I18nSupport.getString("pattern")), PatternValidator.class, VersionCode.AllVersions),
+    PREFIX_ITEMS("prefixItems", "1048", new MessageFormat(I18nSupport.getString("prefixItems")), PrefixItemsValidator.class, VersionCode.MinV202012),
+    PROPERTIES("properties", "1025", new MessageFormat(I18nSupport.getString("properties")), PropertiesValidator.class, VersionCode.AllVersions),
+    READ_ONLY("readOnly", "1032", new MessageFormat(I18nSupport.getString("readOnly")), ReadOnlyValidator.class, VersionCode.AllVersions),
+    REF("$ref", "1026", new MessageFormat(I18nSupport.getString("$ref")), RefValidator.class, VersionCode.AllVersions),
+    REQUIRED("required", "1028", new MessageFormat(I18nSupport.getString("required")), RequiredValidator.class, VersionCode.AllVersions),
+    TYPE("type", "1029", new MessageFormat(I18nSupport.getString("type")), TypeValidator.class, VersionCode.AllVersions),
+    UNION_TYPE("unionType", "1030", new MessageFormat(I18nSupport.getString("unionType")), UnionTypeValidator.class, VersionCode.AllVersions),
+    UNIQUE_ITEMS("uniqueItems", "1031", new MessageFormat(I18nSupport.getString("uniqueItems")), UniqueItemsValidator.class, VersionCode.AllVersions),
+    DATETIME("dateTime", "1034", new MessageFormat(I18nSupport.getString("dateTime")), null, VersionCode.AllVersions),
+    UUID("uuid", "1035", new MessageFormat(I18nSupport.getString("uuid")), null, VersionCode.AllVersions),
+    ID("id", "1036", new MessageFormat(I18nSupport.getString("id")), null, VersionCode.AllVersions),
+    IF_THEN_ELSE("if", "1037", null, IfValidator.class, VersionCode.MinV7),
+    EXCLUSIVE_MAXIMUM("exclusiveMaximum", "1038", new MessageFormat(I18nSupport.getString("exclusiveMaximum")), ExclusiveMaximumValidator.class, VersionCode.MinV6),
+    EXCLUSIVE_MINIMUM("exclusiveMinimum", "1039", new MessageFormat(I18nSupport.getString("exclusiveMinimum")), ExclusiveMinimumValidator.class, VersionCode.MinV6),
+    TRUE("true", "1040", null, TrueValidator.class, VersionCode.MinV6),
+    FALSE("false", "1041", new MessageFormat(I18nSupport.getString("false")), FalseValidator.class, VersionCode.MinV6),
+    CONST("const", "1042", new MessageFormat(I18nSupport.getString("const")), ConstValidator.class, VersionCode.MinV6),
+    CONTAINS("contains", "1043", new MessageFormat(I18nSupport.getString("contains")), ContainsValidator.class, VersionCode.MinV6),
+    PROPERTYNAMES("propertyNames", "1044", new MessageFormat(I18nSupport.getString("propertyNames")), PropertyNamesValidator.class, VersionCode.MinV6),
+    DEPENDENT_REQUIRED("dependentRequired", "1045", new MessageFormat(I18nSupport.getString("dependentRequired")), DependentRequired.class, VersionCode.MinV201909),
+    DEPENDENT_SCHEMAS("dependentSchemas", "1046", new MessageFormat(I18nSupport.getString("dependentSchemas")), DependentSchemas.class, VersionCode.MinV201909),
+    UNEVALUATED_PROPERTIES("unevaluatedProperties","1047",new MessageFormat(I18nSupport.getString("unevaluatedProperties")),UnEvaluatedPropertiesValidator.class, VersionCode.MinV6);
 
     private static Map<String, ValidatorTypeCode> constants = new HashMap<String, ValidatorTypeCode>();
     private static SpecVersion specVersion = new SpecVersion();
@@ -95,13 +118,13 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
     private final long versionCode;
 
 
-    private ValidatorTypeCode(String value, String errorCode, MessageFormat messageFormat, Class<?> validator, long versionCode) {
+    private ValidatorTypeCode(String value, String errorCode, MessageFormat messageFormat, Class<?> validator, VersionCode versionCode) {
         this.value = value;
         this.errorCode = errorCode;
         this.messageFormat = messageFormat;
         this.errorCodeKey = value + "ErrorCode";
         this.validator = validator;
-        this.versionCode = versionCode;
+        this.versionCode = versionCode.getValue();
         this.customMessage = null;
     }
 
diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java
new file mode 100644
index 0000000..498865f
--- /dev/null
+++ b/src/main/java/com/networknt/schema/Version202012.java
@@ -0,0 +1,40 @@
+package com.networknt.schema;
+
+import java.util.Arrays;
+
+public class Version202012 extends JsonSchemaVersion {
+    private static String URI = "https://json-schema.org/draft/2020-12/schema";
+    private static final String ID = "$id";
+
+    static {
+        // add version specific formats here.
+        //BUILTIN_FORMATS.add(pattern("phone", "^\\+(?:[0-9] ?){6,14}[0-9]$"));
+    }
+
+    @Override
+    JsonMetaSchema getInstance() {
+        return new JsonMetaSchema.Builder(URI)
+                .idKeyword(ID)
+                .addFormats(BUILTIN_FORMATS)
+                .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V202012))
+                // keywords that may validly exist, but have no validation aspect to them
+                .addKeywords(Arrays.asList(
+                        new NonValidationKeyword("$schema"),
+                        new NonValidationKeyword("$id"),
+                        new NonValidationKeyword("title"),
+                        new NonValidationKeyword("description"),
+                        new NonValidationKeyword("default"),
+                        new NonValidationKeyword("definitions"),
+                        new NonValidationKeyword("$comment"),
+                        new NonValidationKeyword("$defs"),
+                        new NonValidationKeyword("$anchor"),
+                        new NonValidationKeyword("deprecated"),
+                        new NonValidationKeyword("contentMediaType"),
+                        new NonValidationKeyword("contentEncoding"),
+                        new NonValidationKeyword("examples"),
+                        new NonValidationKeyword("then"),
+                        new NonValidationKeyword("unevaluatedItems")
+                ))
+                .build();
+    }
+}
diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties
index 6c7be2f..01d4f1e 100644
--- a/src/main/resources/jsv-messages.properties
+++ b/src/main/resources/jsv-messages.properties
@@ -31,6 +31,7 @@ notAllowed = {0}.{1}: is not allowed but it is in the data
 oneOf = {0}: should be valid to one and only one of schema, but more than one are valid: {1}
 pattern = {0}: does not match the regex pattern {1}
 patternProperties = {0}: has some error with 'pattern properties'
+prefixItems = {0}[{1}]: no validator found at this index
 properties = {0}: has an error with 'properties'
 propertyNames = Property name {0} is not valid for validation: {1}
 readOnly = {0}: is a readonly field, it cannot be changed


Test patch:
diff --git a/src/test/java/com/networknt/schema/V202012JsonSchemaTest.java b/src/test/java/com/networknt/schema/V202012JsonSchemaTest.java
new file mode 100644
index 0000000..1b9c276
--- /dev/null
+++ b/src/test/java/com/networknt/schema/V202012JsonSchemaTest.java
@@ -0,0 +1,335 @@
+package com.networknt.schema;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author jabbott
+ */
+public class V202012JsonSchemaTest extends BaseSuiteJsonSchemaTest {
+    public V202012JsonSchemaTest() { super(SpecVersion.VersionFlag.V202012); }
+
+    @Test
+    public void testOptionalBignumValidator() throws Exception {
+        runTestFile("draft2019-09/optional/bignum.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalContentValidator() throws Exception {
+        runTestFile("draft2019-09/optional/content.json");
+    }
+
+    @Test
+    @Disabled
+    public void testEcmascriptRegexValidator() throws Exception {
+        runTestFile("draft2019-09/optional/ecmascript-regex.json");
+    }
+
+    @Test
+    @Disabled
+    public void testZeroTerminatedFloatsValidator() throws Exception {
+        runTestFile("draft2019-09/optional/zeroTerminatedFloats.json");
+    }
+
+    @Test
+    public void testOptionalFormatDateValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/date.json");
+    }
+
+    @Test
+    public void testOptionalFormatDateTimeValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/date-time.json");
+    }
+
+    @Test
+    public void testOptionalFormatEmailValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/email.json");
+    }
+
+    @Test
+    public void testOptionalFormatHostnameValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/hostname.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatIdnEmailValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/idn-email.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatIdnHostnameValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/idn-hostname.json");
+    }
+
+    @Test
+    public void testOptionalFormatIpv4Validator() throws Exception {
+        runTestFile("draft2019-09/optional/format/ipv4.json");
+    }
+
+    @Test
+    public void testOptionalFormatIpv6Validator() throws Exception {
+        runTestFile("draft2019-09/optional/format/ipv6.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatIriValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/iri.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatIriReferenceValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/iri-reference.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatJsonPointerValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/json-pointer.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatRegexValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/regex.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatRelativeJsonPointerValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/relative-json-pointer.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatTimeValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/time.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatUriValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/uri.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatUriReferenceValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/uri-reference.json");
+    }
+
+    @Test
+    @Disabled
+    public void testOptionalFormatUriTemplateValidator() throws Exception {
+        runTestFile("draft2019-09/optional/format/uri-template.json");
+    }
+
+    @Test
+    public void testItemsValidator() throws Exception {
+        runTestFile("draft2020-12/items.json");
+    }
+
+    @Test
+    public void testAdditionalPropertiesValidator() throws Exception {
+        runTestFile("draft2019-09/additionalProperties.json");
+    }
+
+    @Test
+    public void testAllOfValidator() throws Exception {
+        runTestFile("draft2019-09/allOf.json");
+    }
+
+    @Test
+    @Disabled
+    public void testAnchorValidator() throws Exception {
+        runTestFile("draft2019-09/anchor.json");
+    }
+
+    @Test
+    public void testAnyOfValidator() throws Exception {
+        runTestFile("draft2019-09/anyOf.json");
+    }
+
+    @Test
+    public void testBooleanSchemaValidator() throws Exception {
+        runTestFile("draft2019-09/boolean_schema.json");
+    }
+
+    @Test
+    public void testConstValidator() throws Exception {
+        runTestFile("draft2019-09/const.json");
+    }
+
+    @Test
+    public void testContainsValidator() throws Exception {
+        runTestFile("draft2019-09/contains.json");
+    }
+
+    @Test
+    public void testDefaultValidator() throws Exception {
+        runTestFile("draft2019-09/default.json");
+    }
+
+    @Test
+    @Disabled
+    public void testDefsValidator() throws Exception {
+        runTestFile("draft2019-09/defs.json");
+    }
+
+    @Test
+    public void testDependenciesValidator() throws Exception {
+        runTestFile("draft2019-09/dependencies.json");
+    }
+
+    @Test
+    public void testEnumValidator() throws Exception {
+        runTestFile("draft2019-09/enum.json");
+    }
+
+    @Test
+    public void testExclusiveMaximumValidator() throws Exception {
+        runTestFile("draft2019-09/exclusiveMaximum.json");
+    }
+
+    @Test
+    public void testExclusiveMinimumValidator() throws Exception {
+        runTestFile("draft2019-09/exclusiveMinimum.json");
+    }
+
+    @Test
+    public void testFormatValidator() throws Exception {
+        runTestFile("draft2019-09/format.json");
+    }
+
+    @Test
+    public void testIfThenElseValidator() throws Exception {
+        runTestFile("draft2019-09/if-then-else.json");
+    }
+
+    @Test
+    public void testMaximumValidator() throws Exception {
+        runTestFile("draft2019-09/maximum.json");
+    }
+
+    @Test
+    public void testMaxItemsValidator() throws Exception {
+        runTestFile("draft2019-09/maxItems.json");
+    }
+
+    @Test
+    public void testMaxLengthValidator() throws Exception {
+        runTestFile("draft2019-09/maxLength.json");
+    }
+
+    @Test
+    public void testMaxPropertiesValidator() throws Exception {
+        runTestFile("draft2019-09/maxProperties.json");
+    }
+
+    @Test
+    public void testMinimumValidator() throws Exception {
+        runTestFile("draft2019-09/minimum.json");
+    }
+
+    @Test
+    public void testMinItemsValidator() throws Exception {
+        runTestFile("draft2019-09/minItems.json");
+    }
+
+    @Test
+    public void testMinLengthValidator() throws Exception {
+        runTestFile("draft2019-09/minLength.json");
+    }
+
+    @Test
+    public void testMinPropertiesValidator() throws Exception {
+        runTestFile("draft2019-09/minProperties.json");
+    }
+
+    @Test
+    public void testMultipleOfValidator() throws Exception {
+        runTestFile("draft2019-09/multipleOf.json");
+    }
+
+    @Test
+    public void testNotValidator() throws Exception {
+        runTestFile("draft2019-09/not.json");
+    }
+
+    @Test
+    public void testOneOfValidator() throws Exception {
+        runTestFile("draft2019-09/oneOf.json");
+    }
+
+    @Test
+    public void testPatternValidator() throws Exception {
+        runTestFile("draft2019-09/pattern.json");
+    }
+
+    @Test
+    public void testPatternPropertiesValidator() throws Exception {
+        runTestFile("draft2019-09/patternProperties.json");
+    }
+
+    @Test
+    public void testPrefixItemsValidator() throws Exception {
+        runTestFile("draft2020-12/prefixItems.json");
+    }
+
+    @Test
+    public void testPropertiesValidator() throws Exception {
+        runTestFile("draft2019-09/properties.json");
+    }
+
+    @Test
+    public void testPropertyNamesValidator() throws Exception {
+        runTestFile("draft2019-09/propertyNames.json");
+    }
+
+    @Test
+    @Disabled
+    public void testRefValidator() throws Exception {
+        runTestFile("draft2019-09/ref.json");
+    }
+
+    @Test
+    public void testRefRemoteValidator() throws Exception {
+        runTestFile("draft2020-12/refRemote.json");
+    }
+
+    @Test
+    @Disabled
+    public void testRefRemoteValidator_Ignored() throws Exception {
+        runTestFile("draft2019-09/refRemote_ignored.json");
+    }
+
+    @Test
+    public void testRequiredValidator() throws Exception {
+        runTestFile("draft2019-09/required.json");
+    }
+
+    @Test
+    public void testTypeValidator() throws Exception {
+        runTestFile("draft2019-09/type.json");
+    }
+
+    @Test
+    public void testUniqueItemsValidator() throws Exception {
+        runTestFile("draft2020-12/uniqueItems.json");
+    }
+
+    @Test
+    public void testDependentRequiredValidator() throws Exception {
+        runTestFile("draft2019-09/dependentRequired.json");
+    }
+
+    @Test
+    public void testDependentSchemasValidator() throws Exception {
+        runTestFile("draft2019-09/dependentSchemas.json");
+    }
+
+}
diff --git a/src/test/resources/draft2020-12/items.json b/src/test/resources/draft2020-12/items.json
new file mode 100644
index 0000000..28f1c5a
--- /dev/null
+++ b/src/test/resources/draft2020-12/items.json
@@ -0,0 +1,142 @@
+[
+  {
+    "description": "additionalItems as schema",
+    "schema": {
+      "prefixItems": [
+        {}
+      ],
+      "items": {
+        "type": "integer"
+      }
+    },
+    "tests": [
+      {
+        "description": "additional items match schema",
+        "data": [
+          null,
+          2,
+          3,
+          4
+        ],
+        "valid": true
+      },
+      {
+        "description": "additional items do not match schema",
+        "data": [
+          null,
+          2,
+          3,
+          "foo"
+        ],
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "items is schema, no additionalItems",
+    "schema": {
+      "prefixItems": {},
+      "items": false
+    },
+    "tests": [
+      {
+        "description": "all items match schema",
+        "data": [
+          1,
+          2,
+          3,
+          4,
+          5
+        ],
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "array of items with no additionalItems",
+    "schema": {
+      "prefixItems": [
+        {},
+        {},
+        {}
+      ],
+      "items": false
+    },
+    "tests": [
+      {
+        "description": "fewer number of items present",
+        "data": [
+          1,
+          2
+        ],
+        "valid": true
+      },
+      {
+        "description": "equal number of items present",
+        "data": [
+          1,
+          2,
+          3
+        ],
+        "valid": true
+      },
+      {
+        "description": "additional items are not permitted",
+        "data": [
+          1,
+          2,
+          3,
+          4
+        ],
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "additionalItems as false without items",
+    "schema": {
+      "items": false
+    },
+    "tests": [
+      {
+        "description": "items defaults to empty schema so everything is valid",
+        "data": [
+          1,
+          2,
+          3,
+          4,
+          5
+        ],
+        "valid": true
+      },
+      {
+        "description": "ignores non-arrays",
+        "data": {
+          "foo": "bar"
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "additionalItems are allowed by default",
+    "schema": {
+      "prefixItems": [
+        {
+          "type": "integer"
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "only the first item is validated",
+        "data": [
+          1,
+          "foo",
+          false
+        ],
+        "valid": true
+      }
+    ]
+  }
+]
diff --git a/src/test/resources/draft2020-12/prefixItems.json b/src/test/resources/draft2020-12/prefixItems.json
new file mode 100644
index 0000000..7316063
--- /dev/null
+++ b/src/test/resources/draft2020-12/prefixItems.json
@@ -0,0 +1,506 @@
+[
+  {
+    "description": "a schema given for items",
+    "schema": {
+      "prefixItems": {
+        "type": "integer"
+      }
+    },
+    "tests": [
+      {
+        "description": "valid items",
+        "data": [
+          1,
+          2,
+          3
+        ],
+        "valid": true
+      },
+      {
+        "description": "wrong type of items",
+        "data": [
+          1,
+          "x"
+        ],
+        "valid": false
+      },
+      {
+        "description": "ignores non-arrays",
+        "data": {
+          "foo": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "JavaScript pseudo-array is valid",
+        "data": {
+          "0": "invalid",
+          "length": 1
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "an array of schemas for items",
+    "schema": {
+      "prefixItems": [
+        {
+          "type": "integer"
+        },
+        {
+          "type": "string"
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "correct types",
+        "data": [
+          1,
+          "foo"
+        ],
+        "valid": true
+      },
+      {
+        "description": "wrong types",
+        "data": [
+          "foo",
+          1
+        ],
+        "valid": false
+      },
+      {
+        "description": "incomplete array of items",
+        "data": [
+          1
+        ],
+        "valid": true
+      },
+      {
+        "description": "array with additional items",
+        "data": [
+          1,
+          "foo",
+          true
+        ],
+        "valid": true
+      },
+      {
+        "description": "empty array",
+        "data": [],
+        "valid": true
+      },
+      {
+        "description": "JavaScript pseudo-array is valid",
+        "data": {
+          "0": "invalid",
+          "1": "valid",
+          "length": 2
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "items with boolean schema (true)",
+    "schema": {
+      "items": true
+    },
+    "tests": [
+      {
+        "description": "any array is valid",
+        "data": [
+          1,
+          "foo",
+          true
+        ],
+        "valid": true
+      },
+      {
+        "description": "empty array is valid",
+        "data": [],
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "items with boolean schema (false)",
+    "schema": {
+      "prefixItems": false
+    },
+    "tests": [
+      {
+        "description": "any non-empty array is invalid",
+        "data": [
+          1,
+          "foo",
+          true
+        ],
+        "valid": false
+      },
+      {
+        "description": "empty array is valid",
+        "data": [],
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "items with boolean schemas",
+    "schema": {
+      "prefixItems": [
+        true,
+        false
+      ]
+    },
+    "tests": [
+      {
+        "description": "array with one item is valid",
+        "data": [
+          1
+        ],
+        "valid": true
+      },
+      {
+        "description": "array with two items is invalid",
+        "data": [
+          1,
+          "foo"
+        ],
+        "valid": false
+      },
+      {
+        "description": "empty array is valid",
+        "data": [],
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "items and subitems",
+    "schema": {
+      "$defs": {
+        "item": {
+          "type": "array",
+          "items": false,
+          "prefixItems": [
+            {
+              "$ref": "#/$defs/sub-item"
+            },
+            {
+              "$ref": "#/$defs/sub-item"
+            }
+          ]
+        },
+        "sub-item": {
+          "type": "object",
+          "required": [
+            "foo"
+          ]
+        }
+      },
+      "type": "array",
+      "items": false,
+      "prefixItems": [
+        {
+          "$ref": "#/$defs/item"
+        },
+        {
+          "$ref": "#/$defs/item"
+        },
+        {
+          "$ref": "#/$defs/item"
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "valid items",
+        "data": [
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ]
+        ],
+        "valid": true
+      },
+      {
+        "description": "too many items",
+        "data": [
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ]
+        ],
+        "valid": false
+      },
+      {
+        "description": "too many sub-items",
+        "data": [
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ]
+        ],
+        "valid": false
+      },
+      {
+        "description": "wrong item",
+        "data": [
+          {
+            "foo": null
+          },
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ]
+        ],
+        "valid": false
+      },
+      {
+        "description": "wrong sub-item",
+        "data": [
+          [
+            {},
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            },
+            {
+              "foo": null
+            }
+          ]
+        ],
+        "valid": false
+      },
+      {
+        "description": "fewer items is valid",
+        "data": [
+          [
+            {
+              "foo": null
+            }
+          ],
+          [
+            {
+              "foo": null
+            }
+          ]
+        ],
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "nested items",
+    "schema": {
+      "type": "array",
+      "prefixItems": {
+        "type": "array",
+        "prefixItems": {
+          "type": "array",
+          "prefixItems": {
+            "type": "array",
+            "prefixItems": {
+              "type": "number"
+            }
+          }
+        }
+      }
+    },
+    "tests": [
+      {
+        "description": "valid nested array",
+        "data": [
+          [
+            [
+              [
+                1
+              ]
+            ],
+            [
+              [
+                2
+              ],
+              [
+                3
+              ]
+            ]
+          ],
+          [
+            [
+              [
+                4
+              ],
+              [
+                5
+              ],
+              [
+                6
+              ]
+            ]
+          ]
+        ],
+        "valid": true
+      },
+      {
+        "description": "nested array with invalid type",
+        "data": [
+          [
+            [
+              [
+                "1"
+              ]
+            ],
+            [
+              [
+                2
+              ],
+              [
+                3
+              ]
+            ]
+          ],
+          [
+            [
+              [
+                4
+              ],
+              [
+                5
+              ],
+              [
+                6
+              ]
+            ]
+          ]
+        ],
+        "valid": false
+      },
+      {
+        "description": "not deep enough",
+        "data": [
+          [
+            [
+              1
+            ],
+            [
+              2
+            ],
+            [
+              3
+            ]
+          ],
+          [
+            [
+              4
+            ],
+            [
+              5
+            ],
+            [
+              6
+            ]
+          ]
+        ],
+        "valid": false
+      }
+    ]
+  }
+]
diff --git a/src/test/resources/draft2020-12/refRemote.json b/src/test/resources/draft2020-12/refRemote.json
new file mode 100644
index 0000000..69b2361
--- /dev/null
+++ b/src/test/resources/draft2020-12/refRemote.json
@@ -0,0 +1,166 @@
+[
+  {
+    "description": "remote ref",
+    "schema": {
+      "$ref": "http://localhost:1234/integer.json"
+    },
+    "tests": [
+      {
+        "description": "remote ref valid",
+        "data": 1,
+        "valid": true
+      },
+      {
+        "description": "remote ref invalid",
+        "data": "a",
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "fragment within remote ref",
+    "schema": {
+      "$ref": "http://localhost:1234/subSchemas.json#/integer"
+    },
+    "tests": [
+      {
+        "description": "remote fragment valid",
+        "data": 1,
+        "valid": true
+      },
+      {
+        "description": "remote fragment invalid",
+        "data": "a",
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "ref within remote ref",
+    "schema": {
+      "$ref": "http://localhost:1234/subSchemas.json#/refToInteger"
+    },
+    "tests": [
+      {
+        "description": "ref within ref valid",
+        "data": 1,
+        "valid": true
+      },
+      {
+        "description": "ref within ref invalid",
+        "data": "a",
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "base URI change",
+    "schema": {
+      "$id": "http://localhost:1234/",
+      "prefixItems": {
+        "$id": "folder/",
+        "prefixItems": {
+          "$ref": "folderInteger.json"
+        }
+      }
+    },
+    "tests": [
+      {
+        "description": "base URI change ref valid",
+        "data": [
+          [
+            1
+          ]
+        ],
+        "valid": true
+      },
+      {
+        "description": "base URI change ref invalid",
+        "data": [
+          [
+            "a"
+          ]
+        ],
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "base URI change - change folder",
+    "schema": {
+      "$id": "http://localhost:1234/scope_change_defs1.json",
+      "type": "object",
+      "properties": {
+        "list": {
+          "$ref": "#/$defs/baz"
+        }
+      },
+      "$defs": {
+        "baz": {
+          "$id": "folder/",
+          "type": "array",
+          "prefixItems": {
+            "$ref": "folderInteger.json"
+          }
+        }
+      }
+    },
+    "tests": [
+      {
+        "description": "number is valid",
+        "data": {
+          "list": [
+            1
+          ]
+        },
+        "valid": true
+      },
+      {
+        "description": "string is invalid",
+        "data": {
+          "list": [
+            "a"
+          ]
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "root ref in remote ref",
+    "schema": {
+      "$id": "http://localhost:1234/object",
+      "type": "object",
+      "properties": {
+        "name": {
+          "$ref": "name-defs.json#/$defs/orNull"
+        }
+      }
+    },
+    "tests": [
+      {
+        "description": "string is valid",
+        "data": {
+          "name": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "null is valid",
+        "data": {
+          "name": null
+        },
+        "valid": true
+      },
+      {
+        "description": "object is invalid",
+        "data": {
+          "name": {
+            "name": null
+          }
+        },
+        "valid": false
+      }
+    ]
+  }
+]
diff --git a/src/test/resources/draft2020-12/uniqueItems.json b/src/test/resources/draft2020-12/uniqueItems.json
new file mode 100644
index 0000000..fcc0969
--- /dev/null
+++ b/src/test/resources/draft2020-12/uniqueItems.json
@@ -0,0 +1,328 @@
+[
+  {
+    "description": "uniqueItems validation",
+    "schema": {
+      "uniqueItems": true
+    },
+    "tests": [
+      {
+        "description": "unique array of integers is valid",
+        "data": [
+          1,
+          2
+        ],
+        "valid": true
+      },
+      {
+        "description": "non-unique array of integers is invalid",
+        "data": [
+          1,
+          1
+        ],
+        "valid": false
+      },
+      {
+        "description": "numbers are unique if mathematically unequal",
+        "data": [
+          1.0,
+          1.00,
+          1
+        ],
+        "valid": false
+      },
+      {
+        "description": "false is not equal to zero",
+        "data": [
+          0,
+          false
+        ],
+        "valid": true
+      },
+      {
+        "description": "true is not equal to one",
+        "data": [
+          1,
+          true
+        ],
+        "valid": true
+      },
+      {
+        "description": "unique array of objects is valid",
+        "data": [
+          {
+            "foo": "bar"
+          },
+          {
+            "foo": "baz"
+          }
+        ],
+        "valid": true
+      },
+      {
+        "description": "non-unique array of objects is invalid",
+        "data": [
+          {
+            "foo": "bar"
+          },
+          {
+            "foo": "bar"
+          }
+        ],
+        "valid": false
+      },
+      {
+        "description": "unique array of nested objects is valid",
+        "data": [
+          {
+            "foo": {
+              "bar": {
+                "baz": true
+              }
+            }
+          },
+          {
+            "foo": {
+              "bar": {
+                "baz": false
+              }
+            }
+          }
+        ],
+        "valid": true
+      },
+      {
+        "description": "non-unique array of nested objects is invalid",
+        "data": [
+          {
+            "foo": {
+              "bar": {
+                "baz": true
+              }
+            }
+          },
+          {
+            "foo": {
+              "bar": {
+                "baz": true
+              }
+            }
+          }
+        ],
+        "valid": false
+      },
+      {
+        "description": "unique array of arrays is valid",
+        "data": [
+          [
+            "foo"
+          ],
+          [
+            "bar"
+          ]
+        ],
+        "valid": true
+      },
+      {
+        "description": "non-unique array of arrays is invalid",
+        "data": [
+          [
+            "foo"
+          ],
+          [
+            "foo"
+          ]
+        ],
+        "valid": false
+      },
+      {
+        "description": "1 and true are unique",
+        "data": [
+          1,
+          true
+        ],
+        "valid": true
+      },
+      {
+        "description": "0 and false are unique",
+        "data": [
+          0,
+          false
+        ],
+        "valid": true
+      },
+      {
+        "description": "unique heterogeneous types are valid",
+        "data": [
+          {},
+          [
+            1
+          ],
+          true,
+          null,
+          1
+        ],
+        "valid": true
+      },
+      {
+        "description": "non-unique heterogeneous types are invalid",
+        "data": [
+          {},
+          [
+            1
+          ],
+          true,
+          null,
+          {},
+          1
+        ],
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "uniqueItems with an array of items",
+    "schema": {
+      "prefixItems": [
+        {
+          "type": "boolean"
+        },
+        {
+          "type": "boolean"
+        }
+      ],
+      "uniqueItems": true
+    },
+    "tests": [
+      {
+        "description": "[false, true] from items array is valid",
+        "data": [
+          false,
+          true
+        ],
+        "valid": true
+      },
+      {
+        "description": "[true, false] from items array is valid",
+        "data": [
+          true,
+          false
+        ],
+        "valid": true
+      },
+      {
+        "description": "[false, false] from items array is not valid",
+        "data": [
+          false,
+          false
+        ],
+        "valid": false
+      },
+      {
+        "description": "[true, true] from items array is not valid",
+        "data": [
+          true,
+          true
+        ],
+        "valid": false
+      },
+      {
+        "description": "unique array extended from [false, true] is valid",
+        "data": [
+          false,
+          true,
+          "foo",
+          "bar"
+        ],
+        "valid": true
+      },
+      {
+        "description": "unique array extended from [true, false] is valid",
+        "data": [
+          true,
+          false,
+          "foo",
+          "bar"
+        ],
+        "valid": true
+      },
+      {
+        "description": "non-unique array extended from [false, true] is not valid",
+        "data": [
+          false,
+          true,
+          "foo",
+          "foo"
+        ],
+        "valid": false
+      },
+      {
+        "description": "non-unique array extended from [true, false] is not valid",
+        "data": [
+          true,
+          false,
+          "foo",
+          "foo"
+        ],
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "uniqueItems with an array of items and additionalItems=false",
+    "schema": {
+      "prefixItems": [
+        {
+          "type": "boolean"
+        },
+        {
+          "type": "boolean"
+        }
+      ],
+      "uniqueItems": true,
+      "items": false
+    },
+    "tests": [
+      {
+        "description": "[false, true] from items array is valid",
+        "data": [
+          false,
+          true
+        ],
+        "valid": true
+      },
+      {
+        "description": "[true, false] from items array is valid",
+        "data": [
+          true,
+          false
+        ],
+        "valid": true
+      },
+      {
+        "description": "[false, false] from items array is not valid",
+        "data": [
+          false,
+          false
+        ],
+        "valid": false
+      },
+      {
+        "description": "[true, true] from items array is not valid",
+        "data": [
+          true,
+          true
+        ],
+        "valid": false
+      },
+      {
+        "description": "extra items are invalid even if unique",
+        "data": [
+          false,
+          true,
+          null
+        ],
+        "valid": false
+      }
+    ]
+  }
+]