You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ib...@apache.org on 2021/03/19 10:03:20 UTC

[ignite-3] branch main updated: IGNITE-14183 Reimplemented validation in new configuration framework. (#67)

This is an automated email from the ASF dual-hosted git repository.

ibessonov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 55359c9  IGNITE-14183 Reimplemented validation in new configuration framework. (#67)
55359c9 is described below

commit 55359c987014c3374c4315836bfae70b5f9c522b
Author: ibessonov <be...@gmail.com>
AuthorDate: Fri Mar 19 13:03:11 2021 +0300

    IGNITE-14183 Reimplemented validation in new configuration framework. (#67)
---
 .../processor/internal/Processor.java              | 100 ++++---
 .../configuration/processor/internal/Utils.java    |   2 +-
 .../internal/validation/ValidationGenerator.java   | 174 ------------
 .../internal/util/ConfigurationUtilTest.java       |   1 -
 .../internal/validation/ValidationUtilTest.java    | 174 ++++++++++++
 .../sample/AutoAdjustConfigurationSchema.java      |  13 +-
 .../sample/ConfigurationArrayTest.java             |  11 +-
 .../sample/ConstructableTreeNodeTest.java          |   3 -
 .../sample/DiscoveryConfigurationSchema.java       |   8 +-
 .../sample/NodeConfigurationSchema.java            |   7 +-
 .../sample/TraversableTreeNodeTest.java            |   3 -
 .../sample/storage/ConfigurationChangerTest.java   | 116 +++-----
 .../sample/validation/AutoAdjustValidator.java     |  41 ---
 .../sample/validation/AutoAdjustValidator2.java    |  41 ---
 .../sample/validation/NodeValidator.java           |  40 ---
 modules/configuration/pom.xml                      |   5 +
 .../ignite/configuration/ConfigurationChanger.java | 311 +++++++--------------
 .../configuration/ConfigurationRegistry.java       |  27 +-
 .../apache/ignite/configuration/Configurator.java  |  94 -------
 .../org/apache/ignite/configuration/RootKey.java   |   2 +-
 .../ignite/configuration/annotation/Validate.java  |  75 -----
 .../configuration/internal/ConfigurationNode.java  |   8 +-
 .../internal/DynamicConfiguration.java             |  28 +-
 .../configuration/internal/DynamicProperty.java    |   7 +-
 .../internal/NamedListConfiguration.java           |   5 +-
 .../ignite/configuration/internal/RootKeyImpl.java |   6 +-
 .../ignite/configuration/internal/SuperRoot.java   | 118 ++++++++
 .../internal/util/AnyNodeConfigurationVisitor.java |  49 ++++
 .../internal/util/ConfigurationUtil.java           | 178 +++++++-----
 .../util/KeysTrackingConfigurationVisitor.java     | 165 +++++++++++
 .../internal/validation/MaxValidator.java          |  29 +-
 .../internal/validation/MinValidator.java          |  29 +-
 .../internal/validation/NotNullValidator.java      |  41 ---
 .../internal/validation/ValidationContextImpl.java | 123 ++++++++
 .../internal/validation/ValidationUtil.java        | 178 ++++++++++++
 .../configuration/tree/ConstructableTreeNode.java  |   9 -
 .../ignite/configuration/tree/InnerNode.java       |  13 +-
 .../ignite/configuration/tree/NamedListNode.java   |   5 -
 .../ConfigurationValidationException.java          |   2 +
 .../configuration/validation/FieldValidator.java   |  46 ---
 .../validation/ValidationContext.java              |  67 +++++
 .../configuration/validation/ValidationIssue.java  |  18 ++
 .../ignite/configuration/validation/Validator.java |  40 +++
 .../java/org/apache/ignite/rest/RestModule.java    |   6 +-
 .../rest/presentation/json/JsonPresentation.java   |  11 +-
 parent/pom.xml                                     |   2 +-
 46 files changed, 1335 insertions(+), 1096 deletions(-)

diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
index 31924bc..81395d6 100644
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
+++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
@@ -39,8 +39,6 @@ import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.Filer;
-import javax.annotation.processing.ProcessingEnvironment;
 import javax.annotation.processing.RoundEnvironment;
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.Element;
@@ -66,7 +64,6 @@ import org.apache.ignite.configuration.annotation.Value;
 import org.apache.ignite.configuration.internal.DynamicConfiguration;
 import org.apache.ignite.configuration.internal.DynamicProperty;
 import org.apache.ignite.configuration.internal.NamedListConfiguration;
-import org.apache.ignite.configuration.processor.internal.validation.ValidationGenerator;
 import org.apache.ignite.configuration.tree.ConfigurationSource;
 import org.apache.ignite.configuration.tree.ConfigurationVisitor;
 import org.apache.ignite.configuration.tree.InnerNode;
@@ -95,9 +92,6 @@ public class Processor extends AbstractProcessor {
     /** Inherit doc javadoc. */
     private static final String INHERIT_DOC = "{@inheritDoc}";
 
-    /** Class file writer. */
-    private Filer filer;
-
     /**
      * Constructor.
      */
@@ -105,13 +99,6 @@ public class Processor extends AbstractProcessor {
     }
 
     /** {@inheritDoc} */
-    @Override public synchronized void init(ProcessingEnvironment processingEnv) {
-        super.init(processingEnv);
-
-        filer = processingEnv.getFiler();
-    }
-
-    /** {@inheritDoc} */
     @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
         final Elements elementUtils = processingEnv.getElementUtils();
 
@@ -167,7 +154,7 @@ public class Processor extends AbstractProcessor {
 
             TypeSpec.Builder configurationClassBuilder = TypeSpec.classBuilder(configClass)
                 .addSuperinterface(configInterface)
-                .addModifiers(PUBLIC, FINAL);
+                .addModifiers(FINAL);
 
             TypeSpec.Builder configurationInterfaceBuilder = TypeSpec.interfaceBuilder(configInterface)
                 .addModifiers(PUBLIC);
@@ -258,12 +245,10 @@ public class Processor extends AbstractProcessor {
 
                     configurationClassBuilder.addField(generatedField);
 
-                    final CodeBlock validatorsBlock = ValidationGenerator.generateValidators(field);
-
                     // Constructor statement
                     constructorBodyBuilder.addStatement(
-                        "add($L = new $T(keys, $S, rootKey, changer), $L)",
-                        fieldName, fieldType, fieldName, validatorsBlock
+                        "add($L = new $T(keys, $S, rootKey, changer))",
+                        fieldName, fieldType, fieldName
                     );
                 }
 
@@ -281,6 +266,12 @@ public class Processor extends AbstractProcessor {
                 TypeMirror storageType = null;
 
                 try {
+                    //  From JavaDocs: The annotation returned by this method could contain an element whose value is of type Class.
+                    //  This value cannot be returned directly: information necessary to locate and load a class
+                    //  (such as the class loader to use) is not available, and the class might not be loadable at all.
+                    //  Attempting to read a Class object by invoking the relevant method on the returned annotation will
+                    //  result in a MirroredTypeException, from which the corresponding TypeMirror may be extracted.
+                    //  Similarly, attempting to read a Class[]-valued element will result in a MirroredTypesException.
                     rootAnnotation.storage();
                 }
                 catch (MirroredTypesException e) {
@@ -291,7 +282,7 @@ public class Processor extends AbstractProcessor {
             }
 
             // Create constructors for configuration class
-            createConstructors(configurationClassBuilder, constructorBodyBuilder);
+            createConstructors(isRoot, configName, configurationClassBuilder, constructorBodyBuilder);
 
             // Write configuration interface
             buildClass(packageName, configurationInterfaceBuilder.build());
@@ -303,22 +294,23 @@ public class Processor extends AbstractProcessor {
     }
 
     /** */
-    private void createRootKeyField(ClassName configInterface,
+    private void createRootKeyField(
+        ClassName configInterface,
         TypeSpec.Builder configurationClassBuilder,
         ConfigurationDescription configDesc,
         TypeMirror storageType,
         ClassName schemaClassName
     ) {
-        ParameterizedTypeName fieldTypeName = ParameterizedTypeName.get(ClassName.get(RootKey.class), configInterface);
+        ClassName viewClassName = Utils.getViewName(schemaClassName);
+        ParameterizedTypeName fieldTypeName = ParameterizedTypeName.get(ClassName.get(RootKey.class), configInterface, viewClassName);
 
         ClassName nodeClassName = Utils.getNodeName(schemaClassName);
 
-        FieldSpec keyField = FieldSpec.builder(
-            fieldTypeName, "KEY", PUBLIC, STATIC, FINAL)
+        FieldSpec keyField = FieldSpec.builder(fieldTypeName, "KEY", PUBLIC, STATIC, FINAL)
             .initializer(
-                "$T.newRootKey($S, $T.class, $T::new, (rootKey, changer) -> new $T($T.emptyList(), $S, rootKey, changer))",
+                "$T.newRootKey($S, $T.class, $T::new, $T::new)",
                 ConfigurationRegistry.class, configDesc.getName(), storageType, nodeClassName,
-                Utils.getConfigurationName(schemaClassName), Collections.class, configDesc.getName()
+                Utils.getConfigurationName(schemaClassName)
             )
             .build();
 
@@ -483,24 +475,38 @@ public class Processor extends AbstractProcessor {
     /**
      * Create configuration class constructors.
      *
-     * @param configuratorClassName Configurator (configuration wrapper) class name.
-     * @param copyConstructorBodyBuilder Copy constructor body.
+     * @param isRoot Flag that indincates whether current configuration is root or not.
+     * @param configName Name of the root if configuration is root, {@code null} otherwise.
      * @param configurationClassBuilder Configuration class builder.
      * @param constructorBodyBuilder Constructor body.
      */
     private void createConstructors(
+        boolean isRoot,
+        String configName,
         TypeSpec.Builder configurationClassBuilder,
         CodeBlock.Builder constructorBodyBuilder
     ) {
-        final MethodSpec constructorWithName = MethodSpec.constructorBuilder()
-            .addModifiers(PUBLIC)
-            .addParameter(ParameterizedTypeName.get(List.class, String.class), "prefix")
-            .addParameter(String.class, "key")
-            .addParameter(ParameterizedTypeName.get(ClassName.get(RootKey.class), WILDCARD), "rootKey")
-            .addParameter(ConfigurationChanger.class, "changer")
-            .addStatement("super(prefix, key, rootKey, changer)")
+        MethodSpec.Builder builder = MethodSpec.constructorBuilder().addModifiers(PUBLIC);
+
+        if (!isRoot) {
+            builder
+                .addParameter(ParameterizedTypeName.get(List.class, String.class), "prefix")
+                .addParameter(String.class, "key");
+        }
+
+        builder
+            .addParameter(ParameterizedTypeName.get(ClassName.get(RootKey.class), WILDCARD, WILDCARD), "rootKey")
+            .addParameter(ConfigurationChanger.class, "changer");
+
+        if (isRoot)
+            builder.addStatement("super($T.emptyList(), $S, rootKey, changer)", Collections.class, configName);
+        else
+            builder.addStatement("super(prefix, key, rootKey, changer)");
+
+        MethodSpec constructorWithName = builder
             .addCode(constructorBodyBuilder.build())
             .build();
+
         configurationClassBuilder.addMethod(constructorWithName);
     }
 
@@ -553,7 +559,7 @@ public class Processor extends AbstractProcessor {
             .addModifiers(PUBLIC);
 
         TypeSpec.Builder nodeClsBuilder = TypeSpec.classBuilder(nodeClsName)
-            .addModifiers(PUBLIC, FINAL)
+            .addModifiers(FINAL)
             .superclass(ClassName.get(InnerNode.class))
             .addSuperinterface(viewClsName)
             .addSuperinterface(changeClsName)
@@ -627,25 +633,13 @@ public class Processor extends AbstractProcessor {
 
             boolean namedListField = field.getAnnotation(NamedConfigValue.class) != null;
 
-            TypeName viewFieldType = leafField ? schemaFieldType : ClassName.get(
-                ((ClassName)schemaFieldType).packageName(),
-                ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "View")
-            );
+            TypeName viewFieldType = leafField ? schemaFieldType : Utils.getViewName((ClassName)schemaFieldType);
 
-            TypeName changeFieldType = leafField ? schemaFieldType : ClassName.get(
-                ((ClassName)schemaFieldType).packageName(),
-                ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "Change")
-            );
+            TypeName changeFieldType = leafField ? schemaFieldType : Utils.getChangeName((ClassName)schemaFieldType);
 
-            TypeName initFieldType = leafField ? schemaFieldType : ClassName.get(
-                ((ClassName)schemaFieldType).packageName(),
-                ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "Init")
-            );
+            TypeName initFieldType = leafField ? schemaFieldType : Utils.getInitName((ClassName)schemaFieldType);
 
-            TypeName nodeFieldType = leafField ? schemaFieldType.box() : ClassName.get(
-                ((ClassName)schemaFieldType).packageName() + (leafField ? "" : ".impl"),
-                ((ClassName)schemaFieldType).simpleName().replace("ConfigurationSchema", "Node")
-            );
+            TypeName nodeFieldType = leafField ? schemaFieldType.box() : Utils.getNodeName((ClassName)schemaFieldType);
 
             TypeName namedListParamType = nodeFieldType;
 
@@ -849,7 +843,7 @@ public class Processor extends AbstractProcessor {
 
                     if (valAnnotation.hasDefault()) {
                         constructDefaultBuilder
-                            .addStatement("case $S: $L = _spec.$L", fieldName, fieldName, fieldName)
+                            .addStatement("case $S: $L = _spec.$L" + (isArray ? ".clone()" : ""), fieldName, fieldName, fieldName)
                             .addStatement(INDENT + "return true");
                     }
                     else
@@ -927,7 +921,7 @@ public class Processor extends AbstractProcessor {
             JavaFile.builder(packageName, cls)
                 .indent(INDENT)
                 .build()
-                .writeTo(filer);
+                .writeTo(processingEnv.getFiler());
         }
         catch (IOException e) {
             throw new ProcessorException("Failed to generate class " + packageName + "." + cls.name, e);
diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
index f11fe9b..b63d1b9 100644
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
+++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
@@ -146,7 +146,7 @@ public class Utils {
     /** */
     public static ClassName getNodeName(ClassName schemaClassName) {
         return ClassName.get(
-            schemaClassName.packageName() + ".impl",
+            schemaClassName.packageName(),
             schemaClassName.simpleName().replace("ConfigurationSchema", "Node")
         );
     }
diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/validation/ValidationGenerator.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/validation/ValidationGenerator.java
deleted file mode 100644
index 6b3cc44..0000000
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/validation/ValidationGenerator.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.configuration.processor.internal.validation;
-
-import com.squareup.javapoet.CodeBlock;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-import javax.lang.model.element.VariableElement;
-import javax.lang.model.type.MirroredTypesException;
-import javax.lang.model.type.TypeMirror;
-import javax.validation.constraints.Max;
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotNull;
-import org.apache.ignite.configuration.internal.DynamicConfiguration;
-import org.apache.ignite.configuration.annotation.Validate;
-import org.apache.ignite.configuration.internal.validation.MaxValidator;
-import org.apache.ignite.configuration.internal.validation.MinValidator;
-import org.apache.ignite.configuration.internal.validation.NotNullValidator;
-import org.apache.ignite.configuration.processor.internal.ProcessorException;
-
-/**
- * Class that handles validation generation.
- */
-public class ValidationGenerator {
-    /**
-     * Private constructor.
-     */
-    private ValidationGenerator() {
-    }
-
-    /**
-     * Generate validation block.
-     *
-     * @param variableElement Configuration field.
-     * @return Code block for field validation.
-     */
-    public static CodeBlock generateValidators(VariableElement variableElement) {
-        List<CodeBlock> validators = new ArrayList<>();
-
-        processMin(variableElement, validators);
-
-        processMax(variableElement, validators);
-
-        processNotNull(variableElement, validators);
-
-        processCustomValidations(variableElement, validators);
-
-        String text = validators.stream().map(v -> "$L").collect(Collectors.joining(","));
-
-        final CodeBlock validatorsArguments = CodeBlock.builder().add(text, validators.toArray()).build();
-
-        return CodeBlock.builder().add("$T.asList($L)", Arrays.class, validatorsArguments).build();
-    }
-
-    /**
-     * Process {@link Min} annotations.
-     * @param variableElement Field.
-     * @param validators Validators code blocks.
-     */
-    private static void processMin(VariableElement variableElement, List<CodeBlock> validators) {
-        final Min minAnnotation = variableElement.getAnnotation(Min.class);
-        if (minAnnotation != null) {
-            final long minValue = minAnnotation.value();
-            final String message = minAnnotation.message();
-            final CodeBlock build = CodeBlock.builder().add(
-                "new $T<$T<?, ?, ?>>($L, $S)", MinValidator.class, DynamicConfiguration.class, minValue, message
-            ).build();
-            validators.add(build);
-        }
-    }
-
-    /**
-     * Process {@link Max} annotations.
-     * @param variableElement Field.
-     * @param validators Validators code blocks.
-     */
-    private static void processMax(VariableElement variableElement, List<CodeBlock> validators) {
-        final Max maxAnnotation = variableElement.getAnnotation(Max.class);
-
-        if (maxAnnotation != null) {
-            final long maxValue = maxAnnotation.value();
-            final String message = maxAnnotation.message();
-
-            // new MaxValidator
-            final CodeBlock build = CodeBlock.builder().add(
-                "new $T<$T<?, ?, ?>>($L, $S)", MaxValidator.class, DynamicConfiguration.class, maxValue, message
-            ).build();
-
-            validators.add(build);
-        }
-    }
-
-    /**
-     * Process {@link NotNull} annotation.
-     * @param variableElement Field.
-     * @param validators Validators code blocks.
-     */
-    private static void processNotNull(VariableElement variableElement, List<CodeBlock> validators) {
-        final NotNull notNull = variableElement.getAnnotation(NotNull.class);
-
-        if (notNull != null) {
-            final String message = notNull.message();
-
-            final CodeBlock build = CodeBlock.builder().add(
-                "new $T<$T<?, ?, ?>>($S)", NotNullValidator.class, DynamicConfiguration.class, message
-            ).build();
-
-            validators.add(build);
-        }
-    }
-
-    /**
-     * Process custom validations from {@link Validate} annotation.
-     * @param variableElement Field.
-     * @param validators Validators code blocks.
-     */
-    private static void processCustomValidations(VariableElement variableElement, List<CodeBlock> validators) {
-        List<Validate> validateAnnotations = new ArrayList<>();
-
-        // There can repeatable Validate annotation, hence Validate.List
-        final Validate.List validateAnnotationsList = variableElement.getAnnotation(Validate.List.class);
-
-        if (validateAnnotationsList != null)
-            validateAnnotations.addAll(Arrays.asList(validateAnnotationsList.value()));
-
-        // Or there is single Validate annotation
-        final Validate validateAnnotationSingle = variableElement.getAnnotation(Validate.class);
-
-        if (validateAnnotationSingle != null)
-            validateAnnotations.add(validateAnnotationSingle);
-
-        for (Validate validateAnnotation : validateAnnotations) {
-            List<? extends TypeMirror> values = null;
-            try {
-                //  From JavaDocs: The annotation returned by this method could contain an element whose value is of type Class.
-                //  This value cannot be returned directly: information necessary to locate and load a class
-                //  (such as the class loader to use) is not available, and the class might not be loadable at all.
-                //  Attempting to read a Class object by invoking the relevant method on the returned annotation will
-                //  result in a MirroredTypeException, from which the corresponding TypeMirror may be extracted.
-                //  Similarly, attempting to read a Class[]-valued element will result in a MirroredTypesException.
-                validateAnnotation.value();
-            } catch (MirroredTypesException e) {
-                values = e.getTypeMirrors();
-            }
-
-            if (values == null)
-                throw new ProcessorException("Failed to retrieve Validate annotation value");
-
-            for (TypeMirror value : values) {
-                final String message = validateAnnotation.message();
-                final CodeBlock build = CodeBlock.builder().add("new $T($S)", value, message).build();
-                validators.add(build);
-            }
-        }
-    }
-
-}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/util/ConfigurationUtilTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/util/ConfigurationUtilTest.java
index 0a0fa7a..34f7f68 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/util/ConfigurationUtilTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/util/ConfigurationUtilTest.java
@@ -23,7 +23,6 @@ import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.internal.util.impl.ParentNode;
 import org.junit.jupiter.api.Test;
 
 import static java.util.Collections.singletonMap;
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/validation/ValidationUtilTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/validation/ValidationUtilTest.java
new file mode 100644
index 0000000..bf00949
--- /dev/null
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/internal/validation/ValidationUtilTest.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.internal.validation;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.ConfigValue;
+import org.apache.ignite.configuration.annotation.ConfigurationRoot;
+import org.apache.ignite.configuration.annotation.NamedConfigValue;
+import org.apache.ignite.configuration.annotation.Value;
+import org.apache.ignite.configuration.internal.SuperRoot;
+import org.apache.ignite.configuration.internal.util.ConfigurationUtil;
+import org.apache.ignite.configuration.sample.storage.TestConfigurationStorage;
+import org.apache.ignite.configuration.tree.NamedListView;
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/** */
+public class ValidationUtilTest {
+    /** */
+    @Target(FIELD)
+    @Retention(RUNTIME)
+    @interface LeafValidation {
+    }
+
+    /** */
+    @Target(FIELD)
+    @Retention(RUNTIME)
+    @interface InnerValidation {
+    }
+
+    /** */
+    @Target(FIELD)
+    @Retention(RUNTIME)
+    @interface NamedListValidation {
+    }
+
+    /** */
+    @ConfigurationRoot(rootName = "root", storage = TestConfigurationStorage.class)
+    public static class ValidatedRootConfigurationSchema {
+        /** */
+        @InnerValidation
+        @ConfigValue
+        public ValidatedChildConfigurationSchema child;
+
+        /** */
+        @NamedListValidation
+        @NamedConfigValue
+        public ValidatedChildConfigurationSchema elements;
+    }
+
+    /** */
+    @Config
+    public static class ValidatedChildConfigurationSchema {
+        /** */
+        @LeafValidation
+        @Value(hasDefault = true)
+        public String str = "foo";
+    }
+
+    /** */
+    private final ValidatedRootNode root = new ValidatedRootNode();
+
+    /** */
+    @BeforeEach
+    public void before() {
+        ConfigurationUtil.addDefaults(root, root);
+    }
+
+    /** */
+    @Test
+    public void validateLeafNode() throws Exception {
+        var rootsNode = new SuperRoot(emptyMap(), Map.of(ValidatedRootConfiguration.KEY, root));
+
+        Validator<LeafValidation, String> validator = new Validator<>() {
+            @Override public void validate(LeafValidation annotation, ValidationContext<String> ctx) {
+                assertEquals("root.child.str", ctx.currentKey());
+
+                assertEquals("foo", ctx.getOldValue());
+                assertEquals("foo", ctx.getNewValue());
+
+                ctx.addIssue(new ValidationIssue("bar"));
+            }
+        };
+
+        Map<Class<? extends Annotation>, Set<Validator<?, ?>>> validators = Map.of(LeafValidation.class, Set.of(validator));
+
+        List<ValidationIssue> issues = ValidationUtil.validate(rootsNode, rootsNode, null, new HashMap<>(), validators);
+
+        assertEquals(1, issues.size());
+
+        assertEquals("bar", issues.get(0).message());
+    }
+
+    /** */
+    @Test
+    public void validateInnerNode() throws Exception {
+        var rootsNode = new SuperRoot(emptyMap(), Map.of(ValidatedRootConfiguration.KEY, root));
+
+        Validator<InnerValidation, ValidatedChildView> validator = new Validator<>() {
+            @Override public void validate(InnerValidation annotation, ValidationContext<ValidatedChildView> ctx) {
+                assertEquals("root.child", ctx.currentKey());
+
+                assertEquals("foo", ctx.getOldValue().str());
+                assertEquals("foo", ctx.getNewValue().str());
+
+                ctx.addIssue(new ValidationIssue("bar"));
+            }
+        };
+
+        Map<Class<? extends Annotation>, Set<Validator<?, ?>>> validators = Map.of(InnerValidation.class, Set.of(validator));
+
+        List<ValidationIssue> issues = ValidationUtil.validate(rootsNode, rootsNode, null, new HashMap<>(), validators);
+
+        assertEquals(1, issues.size());
+
+        assertEquals("bar", issues.get(0).message());
+    }
+
+    /** */
+    @Test
+    public void validateNamedListNode() throws Exception {
+        var rootsNode = new SuperRoot(emptyMap(), Map.of(ValidatedRootConfiguration.KEY, root));
+
+        Validator<NamedListValidation, NamedListView<?>> validator = new Validator<>() {
+            @Override public void validate(NamedListValidation annotation, ValidationContext<NamedListView<?>> ctx) {
+                assertEquals("root.elements", ctx.currentKey());
+
+                assertEquals(emptySet(), ctx.getOldValue().namedListKeys());
+                assertEquals(emptySet(), ctx.getNewValue().namedListKeys());
+
+                ctx.addIssue(new ValidationIssue("bar"));
+            }
+        };
+
+        Map<Class<? extends Annotation>, Set<Validator<?, ?>>> validators = Map.of(NamedListValidation.class, Set.of(validator));
+
+        List<ValidationIssue> issues = ValidationUtil.validate(rootsNode, rootsNode, null, new HashMap<>(), validators);
+
+        assertEquals(1, issues.size());
+
+        assertEquals("bar", issues.get(0).message());
+    }
+}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/AutoAdjustConfigurationSchema.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/AutoAdjustConfigurationSchema.java
index 86245a5..954fc97 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/AutoAdjustConfigurationSchema.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/AutoAdjustConfigurationSchema.java
@@ -19,10 +19,7 @@ package org.apache.ignite.configuration.sample;
 
 import javax.validation.constraints.Min;
 import org.apache.ignite.configuration.annotation.Config;
-import org.apache.ignite.configuration.annotation.Validate;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.sample.validation.AutoAdjustValidator;
-import org.apache.ignite.configuration.sample.validation.AutoAdjustValidator2;
 
 /**
  * Test auto adjust configuration schema.
@@ -30,13 +27,11 @@ import org.apache.ignite.configuration.sample.validation.AutoAdjustValidator2;
 @Config
 public class AutoAdjustConfigurationSchema {
     /** Timeout. */
-    @Value
+    @Value(hasDefault = true)
     @Min(value = 0, message = "Minimal is 0")
-    @Validate(value = AutoAdjustValidator.class, message = "a")
-    @Validate(value = AutoAdjustValidator2.class, message = "b")
-    public long timeout;
+    public long timeout = 0L;
 
     /** Enabled. */
-    @Value
-    public boolean enabled;
+    @Value(hasDefault = true)
+    public boolean enabled = true;
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConfigurationArrayTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConfigurationArrayTest.java
index 4f9929e..961a8bb 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConfigurationArrayTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConfigurationArrayTest.java
@@ -17,14 +17,12 @@
 
 package org.apache.ignite.configuration.sample;
 
-import java.io.Serializable;
 import java.util.function.Supplier;
 import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.sample.impl.TestArrayNode;
-import org.apache.ignite.configuration.tree.ConfigurationVisitor;
 import org.junit.jupiter.api.Test;
 
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.leafNodeVisitor;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertNotSame;
@@ -95,11 +93,6 @@ public class ConfigurationArrayTest {
      * @return Array field value.
      */
     private String[] getArray(TestArrayNode arrayNode) {
-        return arrayNode.traverseChild("array", new ConfigurationVisitor<String[]>() {
-            /** {@inheritDoc} */
-            @Override public String[] visitLeafNode(String key, Serializable val) {
-                return (String[]) val;
-            }
-        });
+        return (String[])arrayNode.traverseChild("array", leafNodeVisitor());
     }
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConstructableTreeNodeTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConstructableTreeNodeTest.java
index 12f260d..3b8c274 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConstructableTreeNodeTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/ConstructableTreeNodeTest.java
@@ -19,9 +19,6 @@ package org.apache.ignite.configuration.sample;
 
 import java.util.Collections;
 import java.util.NoSuchElementException;
-import org.apache.ignite.configuration.sample.impl.ChildNode;
-import org.apache.ignite.configuration.sample.impl.NamedElementNode;
-import org.apache.ignite.configuration.sample.impl.ParentNode;
 import org.apache.ignite.configuration.tree.ConfigurationSource;
 import org.apache.ignite.configuration.tree.ConstructableTreeNode;
 import org.apache.ignite.configuration.tree.NamedListNode;
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/DiscoveryConfigurationSchema.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/DiscoveryConfigurationSchema.java
index e709c21..bf4d040 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/DiscoveryConfigurationSchema.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/DiscoveryConfigurationSchema.java
@@ -26,10 +26,10 @@ import org.apache.ignite.configuration.annotation.Value;
 @Config
 public class DiscoveryConfigurationSchema {
     /** Node failure detection timeout. */
-    @Value
-    public int failureDetectionTimeout;
+    @Value(hasDefault = true)
+    public int failureDetectionTimeout = 10_000;
 
     /** Node join timeout. */
-    @Value
-    public int joinTimeout;
+    @Value(hasDefault = true)
+    public int joinTimeout = 5_000;
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/NodeConfigurationSchema.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/NodeConfigurationSchema.java
index df21d02..0cadba1 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/NodeConfigurationSchema.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/NodeConfigurationSchema.java
@@ -19,9 +19,7 @@ package org.apache.ignite.configuration.sample;
 
 import javax.validation.constraints.NotNull;
 import org.apache.ignite.configuration.annotation.Config;
-import org.apache.ignite.configuration.annotation.Validate;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.sample.validation.NodeValidator;
 
 /**
  * Test node configuration schema.
@@ -38,7 +36,6 @@ public class NodeConfigurationSchema {
     public int port;
 
     /** Auto adjust enabled. */
-    @Value
-    @Validate(NodeValidator.class)
-    public boolean autoAdjustEnabled;
+    @Value(hasDefault = true)
+    public boolean autoAdjustEnabled = true;
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableTreeNodeTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableTreeNodeTest.java
index 6248654..7dbe609 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableTreeNodeTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/TraversableTreeNodeTest.java
@@ -25,9 +25,6 @@ import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.sample.impl.ChildNode;
-import org.apache.ignite.configuration.sample.impl.NamedElementNode;
-import org.apache.ignite.configuration.sample.impl.ParentNode;
 import org.apache.ignite.configuration.tree.ConfigurationVisitor;
 import org.apache.ignite.configuration.tree.InnerNode;
 import org.apache.ignite.configuration.tree.NamedListNode;
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java
index d35f4c2..57d54a2 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java
@@ -17,24 +17,28 @@
 package org.apache.ignite.configuration.sample.storage;
 
 import java.io.Serializable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import org.apache.ignite.configuration.ConfigurationChangeException;
 import org.apache.ignite.configuration.ConfigurationChanger;
-import org.apache.ignite.configuration.Configurator;
 import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
 import org.apache.ignite.configuration.annotation.NamedConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.sample.storage.impl.ANode;
-import org.apache.ignite.configuration.sample.storage.impl.DefaultsNode;
 import org.apache.ignite.configuration.storage.Data;
+import org.apache.ignite.configuration.validation.ValidationContext;
 import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
 import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
 
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.apache.ignite.configuration.sample.storage.AConfiguration.KEY;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -45,11 +49,18 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
  * Test configuration changer.
  */
 public class ConfigurationChangerTest {
+    /** Annotation used to test failing validation. */
+    @Target(FIELD)
+    @Retention(RUNTIME)
+    @interface MaybeInvalid {
+    }
+
     /** */
     @ConfigurationRoot(rootName = "key", storage = TestConfigurationStorage.class)
     public static class AConfigurationSchema {
         /** */
         @ConfigValue
+        @MaybeInvalid
         private BConfigurationSchema child;
 
         /** */
@@ -84,17 +95,12 @@ public class ConfigurationChangerTest {
     public void testSimpleConfigurationChange() throws Exception {
         final TestConfigurationStorage storage = new TestConfigurationStorage();
 
-        final ConfiguratorController configuratorController = new ConfiguratorController();
-        final Configurator<?> configurator = configuratorController.configurator();
-
         ANode data = new ANode()
             .initChild(init -> init.initIntCfg(1).initStrCfg("1"))
             .initElements(change -> change.create("a", init -> init.initStrCfg("1")));
 
         final ConfigurationChanger changer = new ConfigurationChanger(KEY);
-        changer.init(storage);
-
-        changer.registerConfiguration(KEY, configurator);
+        changer.register(storage);
 
         changer.change(Collections.singletonMap(KEY, data)).get(1, SECONDS);
 
@@ -112,9 +118,6 @@ public class ConfigurationChangerTest {
     public void testModifiedFromAnotherStorage() throws Exception {
         final TestConfigurationStorage storage = new TestConfigurationStorage();
 
-        final ConfiguratorController configuratorController = new ConfiguratorController();
-        final Configurator<?> configurator = configuratorController.configurator();
-
         ANode data1 = new ANode()
             .initChild(init -> init.initIntCfg(1).initStrCfg("1"))
             .initElements(change -> change.create("a", init -> init.initStrCfg("1")));
@@ -127,13 +130,10 @@ public class ConfigurationChangerTest {
             );
 
         final ConfigurationChanger changer1 = new ConfigurationChanger(KEY);
-        changer1.init(storage);
+        changer1.register(storage);
 
         final ConfigurationChanger changer2 = new ConfigurationChanger(KEY);
-        changer2.init(storage);
-
-        changer1.registerConfiguration(KEY, configurator);
-        changer2.registerConfiguration(KEY, configurator);
+        changer2.register(storage);
 
         changer1.change(Collections.singletonMap(KEY, data1)).get(1, SECONDS);
         changer2.change(Collections.singletonMap(KEY, data2)).get(1, SECONDS);
@@ -160,9 +160,6 @@ public class ConfigurationChangerTest {
     public void testModifiedFromAnotherStorageWithIncompatibleChanges() throws Exception {
         final TestConfigurationStorage storage = new TestConfigurationStorage();
 
-        final ConfiguratorController configuratorController = new ConfiguratorController();
-        final Configurator<?> configurator = configuratorController.configurator();
-
         ANode data1 = new ANode()
             .initChild(init -> init.initIntCfg(1).initStrCfg("1"))
             .initElements(change -> change.create("a", init -> init.initStrCfg("1")));
@@ -175,17 +172,18 @@ public class ConfigurationChangerTest {
             );
 
         final ConfigurationChanger changer1 = new ConfigurationChanger(KEY);
-        changer1.init(storage);
+        changer1.register(storage);
 
         final ConfigurationChanger changer2 = new ConfigurationChanger(KEY);
-        changer2.init(storage);
-
-        changer1.registerConfiguration(KEY, configurator);
-        changer2.registerConfiguration(KEY, configurator);
+        changer2.register(storage);
 
         changer1.change(Collections.singletonMap(KEY, data1)).get(1, SECONDS);
 
-        configuratorController.hasIssues(true);
+        changer2.addValidator(MaybeInvalid.class, new Validator<MaybeInvalid, Object>() {
+            @Override public void validate(MaybeInvalid annotation, ValidationContext<Object> ctx) {
+                ctx.addIssue(new ValidationIssue("foo"));
+            }
+        });
 
         assertThrows(ExecutionException.class, () -> changer2.change(Collections.singletonMap(KEY, data2)).get(1, SECONDS));
 
@@ -203,22 +201,17 @@ public class ConfigurationChangerTest {
     public void testFailedToWrite() {
         final TestConfigurationStorage storage = new TestConfigurationStorage();
 
-        final ConfiguratorController configuratorController = new ConfiguratorController();
-        final Configurator<?> configurator = configuratorController.configurator();
-
         ANode data = new ANode().initChild(child -> child.initIntCfg(1));
 
         final ConfigurationChanger changer = new ConfigurationChanger(KEY);
 
         storage.fail(true);
 
-        assertThrows(ConfigurationChangeException.class, () -> changer.init(storage));
+        assertThrows(ConfigurationChangeException.class, () -> changer.register(storage));
 
         storage.fail(false);
 
-        changer.init(storage);
-
-        changer.registerConfiguration(KEY, configurator);
+        changer.register(storage);
 
         storage.fail(true);
 
@@ -257,6 +250,10 @@ public class ConfigurationChangerTest {
         /** */
         @Value(hasDefault = true)
         public String defStr = "bar";
+
+        /** */
+        @Value(hasDefault = true)
+        public String[] arr = {"xyz"};
     }
 
     @Test
@@ -265,12 +262,15 @@ public class ConfigurationChangerTest {
 
         changer.addRootKey(DefaultsConfiguration.KEY);
 
-        changer.init(new TestConfigurationStorage());
+        changer.register(new TestConfigurationStorage());
+
+        changer.initialize(TestConfigurationStorage.class);
 
         DefaultsNode root = (DefaultsNode)changer.getRootNode(DefaultsConfiguration.KEY);
 
         assertEquals("foo", root.defStr());
         assertEquals("bar", root.child().defStr());
+        assertEquals(List.of("xyz"), Arrays.asList(root.child().arr()));
 
         // This is not init, move it to another test =(
         changer.change(Map.of(DefaultsConfiguration.KEY, new DefaultsNode().changeChildsList(childs ->
@@ -281,50 +281,4 @@ public class ConfigurationChangerTest {
 
         assertEquals("bar", root.childsList().get("name").defStr());
     }
-
-    /**
-     * Wrapper for Configurator mock to control validation.
-     */
-    private static class ConfiguratorController {
-        /** Configurator. */
-        final Configurator<?> configurator;
-
-        /** Whether validate method should return issues. */
-        private boolean hasIssues;
-
-        /** Constructor. */
-        private ConfiguratorController() {
-            this(false);
-        }
-
-        /** Constructor. */
-        private ConfiguratorController(boolean hasIssues) {
-            this.hasIssues = hasIssues;
-
-            configurator = Mockito.mock(Configurator.class);
-
-            Mockito.when(configurator.validateChanges(Mockito.any())).then(mock -> {
-                if (this.hasIssues)
-                    return Collections.singletonList(new ValidationIssue());
-
-                return Collections.emptyList();
-            });
-        }
-
-        /**
-         * Set has issues flag.
-         * @param hasIssues Has issues flag.
-         */
-        public void hasIssues(boolean hasIssues) {
-            this.hasIssues = hasIssues;
-        }
-
-        /**
-         * Get configurator.
-         * @return Configurator.
-         */
-        public Configurator<?> configurator() {
-            return configurator;
-        }
-    }
 }
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/AutoAdjustValidator.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/AutoAdjustValidator.java
deleted file mode 100644
index fdd2d58..0000000
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/AutoAdjustValidator.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.configuration.sample.validation;
-
-import org.apache.ignite.configuration.sample.LocalConfiguration;
-import org.apache.ignite.configuration.validation.ConfigurationValidationException;
-import org.apache.ignite.configuration.validation.FieldValidator;
-
-/**
- * Test validator for AutoAdjust.
- */
-public class AutoAdjustValidator extends FieldValidator<Number, LocalConfiguration> {
-    /** Constructor. */
-    public AutoAdjustValidator(String message) {
-        super(message);
-    }
-
-    /** {@inheritDoc} */
-    @Override public void validate(Number value, LocalConfiguration newRoot, LocalConfiguration oldRoot) throws ConfigurationValidationException {
-        final Boolean isEnabled = newRoot.baseline().autoAdjust().enabled().value();
-
-        if (value.longValue() > 0 && !isEnabled)
-            throw new ConfigurationValidationException(message);
-    }
-
-}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/AutoAdjustValidator2.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/AutoAdjustValidator2.java
deleted file mode 100644
index 5ab1bba..0000000
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/AutoAdjustValidator2.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.configuration.sample.validation;
-
-import org.apache.ignite.configuration.sample.LocalConfiguration;
-import org.apache.ignite.configuration.validation.ConfigurationValidationException;
-import org.apache.ignite.configuration.validation.FieldValidator;
-
-/**
- * Test validator for AutoAdjust (works the same as the other one).
- */
-public class AutoAdjustValidator2 extends FieldValidator<Number, LocalConfiguration> {
-    /** Constructor. */
-    public AutoAdjustValidator2(String message) {
-        super(message);
-    }
-
-    /** {@inheritDoc} */
-    @Override public void validate(Number value, LocalConfiguration newRoot, LocalConfiguration oldRoot) throws ConfigurationValidationException {
-        final Boolean isEnabled = newRoot.baseline().autoAdjust().enabled().value();
-
-        if (value.longValue() > 0 && !isEnabled)
-            throw new ConfigurationValidationException(message);
-    }
-
-}
\ No newline at end of file
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/NodeValidator.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/NodeValidator.java
deleted file mode 100644
index 5c99658..0000000
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/validation/NodeValidator.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.configuration.sample.validation;
-
-import org.apache.ignite.configuration.sample.LocalConfiguration;
-import org.apache.ignite.configuration.validation.ConfigurationValidationException;
-import org.apache.ignite.configuration.validation.FieldValidator;
-
-/**
- * Test Node configuration validator.
- */
-public class NodeValidator extends FieldValidator<Boolean, LocalConfiguration> {
-    /** Constructor. */
-    public NodeValidator(String message) {
-        super(message);
-    }
-
-    @Override public void validate(Boolean value, LocalConfiguration newRoot, LocalConfiguration oldRoot) throws ConfigurationValidationException {
-        if (value != null && value) {
-            if (!newRoot.baseline().autoAdjust().enabled().value()) {
-                throw new ConfigurationValidationException("");
-            }
-        }
-    }
-}
diff --git a/modules/configuration/pom.xml b/modules/configuration/pom.xml
index 68164f7..0da06b4 100644
--- a/modules/configuration/pom.xml
+++ b/modules/configuration/pom.xml
@@ -40,6 +40,11 @@
             <artifactId>validation-api</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+        </dependency>
+
         <!-- Test dependencies. -->
         <dependency>
             <groupId>log4j</groupId>
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
index 1934f72..67196ee 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
@@ -17,32 +17,34 @@
 package org.apache.ignite.configuration;
 
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
+import java.lang.annotation.Annotation;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ForkJoinPool;
 import java.util.stream.Collectors;
-import org.apache.ignite.configuration.internal.util.ConfigurationUtil;
+import org.apache.ignite.configuration.internal.SuperRoot;
+import org.apache.ignite.configuration.internal.validation.MemberKey;
+import org.apache.ignite.configuration.internal.validation.ValidationUtil;
 import org.apache.ignite.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.configuration.storage.Data;
 import org.apache.ignite.configuration.storage.StorageException;
-import org.apache.ignite.configuration.tree.ConfigurationSource;
-import org.apache.ignite.configuration.tree.ConfigurationVisitor;
 import org.apache.ignite.configuration.tree.InnerNode;
-import org.apache.ignite.configuration.tree.NamedListNode;
-import org.apache.ignite.configuration.tree.TraversableTreeNode;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
 import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
 
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.addDefaults;
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.fillFromPrefixMap;
 import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.nodeToFlatMap;
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.patch;
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.toPrefixMap;
 
 /**
  * Class that handles configuration changes, by validating them, passing to storage and listening to storage updates.
@@ -51,56 +53,66 @@ public class ConfigurationChanger {
     /** */
     private final ForkJoinPool pool = new ForkJoinPool(2);
 
-    /** Map of configurations' configurators. */
-    @Deprecated
-    private final Map<RootKey<?>, Configurator<?>> configurators = new HashMap<>();
-
     /** */
-    private final Set<RootKey<?>> rootKeys = new HashSet<>();
+    private final Map<String, RootKey<?, ?>> rootKeys = new TreeMap<>();
 
     /** Map that has all the trees in accordance to their storages. */
     private final Map<Class<? extends ConfigurationStorage>, StorageRoots> storagesRootsMap = new ConcurrentHashMap<>();
 
+    /** Annotation classes mapped to validator objects. */
+    private Map<Class<? extends Annotation>, Set<Validator<?, ?>>> validators = new HashMap<>();
+
     /**
      * Immutable data container to store version and all roots associated with the specific storage.
      */
-    public static class StorageRoots {
+    private static class StorageRoots {
         /** Immutable forest, so to say. */
-        private final Map<RootKey<?>, InnerNode> roots;
+        private final SuperRoot roots;
 
         /** Version associated with the currently known storage state. */
         private final long version;
 
         /** */
-        private StorageRoots(Map<RootKey<?>, InnerNode> roots, long version) {
-            this.roots = Collections.unmodifiableMap(roots);
+        private StorageRoots(SuperRoot roots, long version) {
+            this.roots = roots;
             this.version = version;
         }
     }
 
+    /** Lazy annotations cache for configuration schema fields. */
+    private final Map<MemberKey, Annotation[]> cachedAnnotations = new ConcurrentHashMap<>();
+
     /** Storage instances by their classes. Comes in handy when all you have is {@link RootKey}. */
     private final Map<Class<? extends ConfigurationStorage>, ConfigurationStorage> storageInstances = new HashMap<>();
 
     /** Constructor. */
-    public ConfigurationChanger(RootKey<?>... rootKeys) {
-        this.rootKeys.addAll(Arrays.asList(rootKeys));
+    public ConfigurationChanger(RootKey<?, ?>... rootKeys) {
+        for (RootKey<?, ?> rootKey : rootKeys)
+            this.rootKeys.put(rootKey.key(), rootKey);
     }
 
     /** */
-    public void addRootKey(RootKey<?> rootKey) {
+    public <A extends Annotation> void addValidator(Class<A> annotationType, Validator<A, ?> validator) {
+        validators
+            .computeIfAbsent(annotationType, a -> new HashSet<>())
+            .add(validator);
+    }
+
+    /** */
+    public void addRootKey(RootKey<?, ?> rootKey) {
         assert !storageInstances.containsKey(rootKey.getStorageType());
 
-        rootKeys.add(rootKey);
+        rootKeys.put(rootKey.key(), rootKey);
     }
 
     /**
-     * Initialize changer.
+     * Register changer.
      */
     // ConfigurationChangeException, really?
-    public void init(ConfigurationStorage configurationStorage) throws ConfigurationChangeException {
+    public void register(ConfigurationStorage configurationStorage) throws ConfigurationChangeException {
         storageInstances.put(configurationStorage.getClass(), configurationStorage);
 
-        Set<RootKey<?>> storageRootKeys = rootKeys.stream().filter(
+        Set<RootKey<?, ?>> storageRootKeys = rootKeys.values().stream().filter(
             rootKey -> configurationStorage.getClass() == rootKey.getStorageType()
         ).collect(Collectors.toSet());
 
@@ -113,130 +125,79 @@ public class ConfigurationChanger {
             throw new ConfigurationChangeException("Failed to initialize configuration: " + e.getMessage(), e);
         }
 
-        Map<RootKey<?>, InnerNode> storageRootsMap = new HashMap<>();
-        // Map to collect defaults for not initialized configurations.
-        Map<RootKey<?>, InnerNode> storageDefaultsMap = new HashMap<>();
+        SuperRoot superRoot = new SuperRoot(rootKeys);
 
-        Map<String, ?> dataValuesPrefixMap = ConfigurationUtil.toPrefixMap(data.values());
+        Map<String, ?> dataValuesPrefixMap = toPrefixMap(data.values());
 
-        for (RootKey<?> rootKey : storageRootKeys) {
+        for (RootKey<?, ?> rootKey : storageRootKeys) {
             Map<String, ?> rootPrefixMap = (Map<String, ?>)dataValuesPrefixMap.get(rootKey.key());
 
             InnerNode rootNode = rootKey.createRootNode();
 
             if (rootPrefixMap != null)
-                ConfigurationUtil.fillFromPrefixMap(rootNode, rootPrefixMap);
-
-            // Collecting defaults requires fresh new root.
-            InnerNode defaultsNode = rootKey.createRootNode();
+                fillFromPrefixMap(rootNode, rootPrefixMap);
 
-            addDefaults(rootNode, defaultsNode);
-
-            storageRootsMap.put(rootKey, rootNode);
-            storageDefaultsMap.put(rootKey, defaultsNode);
+            superRoot.addRoot(rootKey, rootNode);
         }
 
-        storagesRootsMap.put(configurationStorage.getClass(), new StorageRoots(storageRootsMap, data.version()));
+        StorageRoots storageRoots = new StorageRoots(superRoot, data.version());
+
+        storagesRootsMap.put(configurationStorage.getClass(), storageRoots);
 
         configurationStorage.addListener(changedEntries -> updateFromListener(
             configurationStorage.getClass(),
             changedEntries
         ));
+    }
+
+    /** */
+    public void initialize(Class<? extends ConfigurationStorage> storageType) {
+        ConfigurationStorage configurationStorage = storageInstances.get(storageType);
+
+        assert configurationStorage != null : storageType;
+
+        StorageRoots storageRoots = storagesRootsMap.get(storageType);
+
+        SuperRoot superRoot = storageRoots.roots;
+        SuperRoot defaultsNode = new SuperRoot(rootKeys);
+
+        addDefaults(superRoot, defaultsNode);
+
+        List<ValidationIssue> validationIssues = ValidationUtil.validate(
+            superRoot,
+            defaultsNode,
+            this::getRootNode,
+            cachedAnnotations,
+            validators
+        );
+
+        if (!validationIssues.isEmpty())
+            throw new ConfigurationValidationException(validationIssues);
 
-        // Do this strictly after adding listeners, otherwise we can lose these changes.
         try {
-            //TODO IGNITE-14183 Do not write defaults that have not been validated. This can ruin everything.
-            change(storageDefaultsMap).get();
+            change(defaultsNode, storageType).get();
         }
         catch (InterruptedException | ExecutionException e) {
-            throw new ConfigurationChangeException("too bad", e);
+            throw new ConfigurationChangeException(
+                "Failed to write defalut configuration values into the storage " + configurationStorage.getClass(), e
+            );
         }
     }
 
     /**
-     * Fill {@code dst} node with default values, required to complete {@code src} node.
-     * These two objects can be the same, this would mean that all {@code null} values of {@code scr} will be
-     * replaced with defaults if it's possible.
-     *
-     * @param src Source node.
-     * @param dst Destination node.
-     */
-    private void addDefaults(InnerNode src, InnerNode dst) {
-        src.traverseChildren(new ConfigurationVisitor<>() {
-            @Override public Object visitLeafNode(String key, Serializable val) {
-                // If source value is null then inititalise the same value on the destination node.
-                if (val == null)
-                    dst.constructDefault(key);
-
-                return null;
-            }
-
-            @Override public Object visitInnerNode(String key, InnerNode srcNode) {
-                // Instantiate field in destination node before doing something else.
-                // Not a big deal if it wasn't null.
-                dst.construct(key, new ConfigurationSource() {});
-
-                // Get that inner node from destination to continue the processing.
-                InnerNode dstNode = dst.traverseChild(key, new ConfigurationVisitor<>() {
-                    @Override public InnerNode visitInnerNode(String key, InnerNode dstNode) {
-                        return dstNode;
-                    }
-                });
-
-                // "dstNode" is guaranteed to not be null even if "src" and "dst" match.
-                // Null in "srcNode" means that we should initialize everything that we can in "dstNode"
-                // unconditionally. It's only possible if we pass it as a source as well.
-                addDefaults(srcNode == null ? dstNode : srcNode, dstNode);
-
-                return null;
-            }
-
-            @Override public <N extends InnerNode> Object visitNamedListNode(String key, NamedListNode<N> srcNamedList) {
-                // Here we don't need to preemptively initialise corresponsing field, because it can never be null.
-                NamedListNode<?> dstNamedList = dst.traverseChild(key, new ConfigurationVisitor<>() {
-                    @Override public <N extends InnerNode> NamedListNode<?> visitNamedListNode(String key, NamedListNode<N> dstNode) {
-                        return dstNode;
-                    }
-                });
-
-                for (String namedListKey : srcNamedList.namedListKeys()) {
-                    // But, in order to get non-null value from "dstNamedList.get(namedListKey)" we must explicitly
-                    // ensure its existance.
-                    dstNamedList.construct(namedListKey, new ConfigurationSource() {});
-
-                    addDefaults(srcNamedList.get(namedListKey), dstNamedList.get(namedListKey));
-                }
-
-                return null;
-            }
-        });
-    }
-
-    /**
-     * Add configurator.
-     * @param key Root configuration key of the configurator.
-     * @param configurator Configuration's configurator.
-     */
-    //TODO IGNITE-14183 Refactor, get rid of configurator and create some "validator".
-    @Deprecated
-    public void registerConfiguration(RootKey<?> key, Configurator<?> configurator) {
-        configurators.put(key, configurator);
-    }
-
-    /**
      * Get root node by root key. Subject to revisiting.
      *
      * @param rootKey Root key.
      */
-    public TraversableTreeNode getRootNode(RootKey<?> rootKey) {
-        return this.storagesRootsMap.get(rootKey.getStorageType()).roots.get(rootKey);
+    public InnerNode getRootNode(RootKey<?, ?> rootKey) {
+        return storagesRootsMap.get(rootKey.getStorageType()).roots.getRoot(rootKey);
     }
 
     /**
      * Change configuration.
      * @param changes Map of changes by root key.
      */
-    public CompletableFuture<Void> change(Map<RootKey<?>, ? extends TraversableTreeNode> changes) {
+    public CompletableFuture<Void> change(Map<RootKey<?, ?>, InnerNode> changes) {
         if (changes.isEmpty())
             return CompletableFuture.completedFuture(null);
 
@@ -252,8 +213,11 @@ public class ConfigurationChanger {
             );
         }
 
-        Class<? extends ConfigurationStorage> storageType = storagesTypes.iterator().next();
+        return change(new SuperRoot(rootKeys, changes), storagesTypes.iterator().next());
+    }
 
+    /** */
+    private CompletableFuture<Void> change(SuperRoot changes, Class<? extends ConfigurationStorage> storageType) {
         ConfigurationStorage storage = storageInstances.get(storageType);
 
         CompletableFuture<Void> fut = new CompletableFuture<>();
@@ -270,36 +234,30 @@ public class ConfigurationChanger {
      * @param fut Future, that must be completed after changes are written to the storage.
      */
     private void change0(
-        Map<RootKey<?>, ? extends TraversableTreeNode> changes,
+        SuperRoot changes,
         ConfigurationStorage storage,
         CompletableFuture<?> fut
     ) {
         StorageRoots storageRoots = storagesRootsMap.get(storage.getClass());
+        SuperRoot curRoots = storageRoots.roots;
 
         Map<String, Serializable> allChanges = new HashMap<>();
 
-        for (Map.Entry<RootKey<?>, ? extends TraversableTreeNode> entry : changes.entrySet()) {
-            RootKey<?> rootKey = entry.getKey();
-            TraversableTreeNode change = entry.getValue();
+        //TODO IGNITE-14180 single putAll + remove matching value, this way "allChanges" will be fair.
+        // These are changes explicitly provided by the client.
+        allChanges.putAll(nodeToFlatMap(curRoots, changes));
 
-            // It's important to get the root from "roots" object rather then "storageRootMap" or "getRootNode(...)".
-            InnerNode currentRootNode = storageRoots.roots.get(rootKey);
+        // It is necessary to reinitialize default values every time.
+        // Possible use case that explicitly requires it: creation of the same named list entry with slightly
+        // different set of values and different dynamic defaults at the same time.
+        SuperRoot patchedSuperRoot = patch(curRoots, changes);
 
-            // These are changes explicitly provided by the client.
-            allChanges.putAll(nodeToFlatMap(rootKey, currentRootNode, change));
+        SuperRoot defaultsNode = new SuperRoot(rootKeys);
 
-            // It is necessary to reinitialize default values every time.
-            // Possible use case that explicitly requires it: creation of the same named list entry with slightly
-            // different set of values and different dynamic defaults at the same time.
-            InnerNode patchedRootNode = ConfigurationUtil.patch(currentRootNode, change);
-            InnerNode defaultsNode = rootKey.createRootNode();
+        addDefaults(patchedSuperRoot, defaultsNode);
 
-            addDefaults(patchedRootNode, defaultsNode);
-
-            // These are default values for non-initialized values, required to complete the configuration.
-            //TODO IGNITE-14183 Take these defaults into account during validation.
-            allChanges.putAll(nodeToFlatMap(rootKey, patchedRootNode, defaultsNode));
-        }
+        // These are default values for non-initialized values, required to complete the configuration.
+        allChanges.putAll(nodeToFlatMap(patchedSuperRoot, defaultsNode));
 
         // Unlikely but still possible.
         if (allChanges.isEmpty()) {
@@ -308,9 +266,13 @@ public class ConfigurationChanger {
             return;
         }
 
-        ValidationResult validationResult = validate(storageRoots, changes);
-
-        List<ValidationIssue> validationIssues = validationResult.issues();
+        List<ValidationIssue> validationIssues = ValidationUtil.validate(
+            storageRoots.roots,
+            patch(patchedSuperRoot, defaultsNode),
+            this::getRootNode,
+            cachedAnnotations,
+            validators
+        );
 
         if (!validationIssues.isEmpty()) {
             fut.completeExceptionally(new ConfigurationValidationException(validationIssues));
@@ -341,25 +303,15 @@ public class ConfigurationChanger {
     ) {
         StorageRoots oldStorageRoots = this.storagesRootsMap.get(storageType);
 
-        Map<RootKey<?>, InnerNode> storageRootsMap = new HashMap<>(oldStorageRoots.roots);
-
-        Map<String, ?> dataValuesPrefixMap = ConfigurationUtil.toPrefixMap(changedEntries.values());
+        Map<String, ?> dataValuesPrefixMap = toPrefixMap(changedEntries.values());
 
         compressDeletedEntries(dataValuesPrefixMap);
 
-        for (RootKey<?> rootKey : oldStorageRoots.roots.keySet()) {
-            Map<String, ?> rootPrefixMap = (Map<String, ?>)dataValuesPrefixMap.get(rootKey.key());
-
-            if (rootPrefixMap != null) {
-                InnerNode rootNode = oldStorageRoots.roots.get(rootKey).copy();
-
-                ConfigurationUtil.fillFromPrefixMap(rootNode, rootPrefixMap);
+        SuperRoot newSuperRoot = oldStorageRoots.roots.copy();
 
-                storageRootsMap.put(rootKey, rootNode);
-            }
-        }
+        fillFromPrefixMap(newSuperRoot, dataValuesPrefixMap);
 
-        StorageRoots storageRoots = new StorageRoots(storageRootsMap, changedEntries.version());
+        StorageRoots storageRoots = new StorageRoots(newSuperRoot, changedEntries.version());
 
         storagesRootsMap.put(storageType, storageRoots);
 
@@ -391,59 +343,4 @@ public class ConfigurationChanger {
                 compressDeletedEntries((Map<String, ?>)value);
         }
     }
-
-    /**
-     * Validate configuration changes.
-     *
-     * @param storageRoots Storage roots.
-     * @param changes Configuration changes.
-     * @return Validation results.
-     */
-    // Will be used in the future, I promise (IGNITE-14183).
-    @SuppressWarnings("unused")
-    private ValidationResult validate(
-        StorageRoots storageRoots,
-        Map<RootKey<?>, ? extends TraversableTreeNode> changes
-    ) {
-        List<ValidationIssue> issues = new ArrayList<>();
-
-        for (Map.Entry<RootKey<?>, ? extends TraversableTreeNode> entry : changes.entrySet()) {
-            RootKey<?> rootKey = entry.getKey();
-            TraversableTreeNode changesForRoot = entry.getValue();
-
-            final Configurator<?> configurator = configurators.get(rootKey);
-
-            // TODO IGNITE-14183 This will be fixed later
-            if (configurator != null) {
-                List<ValidationIssue> list = configurator.validateChanges(changesForRoot);
-                issues.addAll(list);
-            }
-        }
-
-        return new ValidationResult(issues);
-    }
-
-    /**
-     * Results of the validation.
-     */
-    private static final class ValidationResult {
-        /** List of issues. */
-        private final List<ValidationIssue> issues;
-
-        /**
-         * Constructor.
-         * @param issues List of issues.
-         */
-        private ValidationResult(List<ValidationIssue> issues) {
-            this.issues = issues;
-        }
-
-        /**
-         * Get issues.
-         * @return Issues.
-         */
-        public List<ValidationIssue> issues() {
-            return issues;
-        }
-    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationRegistry.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationRegistry.java
index 5c0c7c5..8d4bcc5 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationRegistry.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationRegistry.java
@@ -17,15 +17,21 @@
 
 package org.apache.ignite.configuration;
 
+import java.lang.annotation.Annotation;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Supplier;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
 import org.apache.ignite.configuration.internal.DynamicConfiguration;
 import org.apache.ignite.configuration.internal.RootKeyImpl;
+import org.apache.ignite.configuration.internal.validation.MaxValidator;
+import org.apache.ignite.configuration.internal.validation.MinValidator;
 import org.apache.ignite.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.validation.Validator;
 
 /** */
 public class ConfigurationRegistry {
@@ -35,8 +41,14 @@ public class ConfigurationRegistry {
     /** */
     private final ConfigurationChanger changer = new ConfigurationChanger();
 
+    {
+        // Default vaildators implemented in current module.
+        changer.addValidator(Min.class, new MinValidator());
+        changer.addValidator(Max.class, new MaxValidator());
+    }
+
     /** */
-    public void registerRootKey(RootKey<?> rootKey) {
+    public void registerRootKey(RootKey<?, ?> rootKey) {
         changer.addRootKey(rootKey);
 
         configs.put(rootKey.key(), (DynamicConfiguration<?, ?, ?>)rootKey.createPublicRoot(changer));
@@ -45,12 +57,17 @@ public class ConfigurationRegistry {
     }
 
     /** */
+    public <A extends Annotation> void registerValidator(Class<A> annotationType, Validator<A, ?> validator) {
+        changer.addValidator(annotationType, validator);
+    }
+
+    /** */
     public void registerStorage(ConfigurationStorage configurationStorage) {
-        changer.init(configurationStorage);
+        changer.register(configurationStorage);
     }
 
     /** */
-    public <V, C, T extends ConfigurationTree<V, C>> T getConfiguration(RootKey<T> rootKey) {
+    public <V, C, T extends ConfigurationTree<V, C>> T getConfiguration(RootKey<T, V> rootKey) {
         return (T)configs.get(rootKey.key());
     }
 
@@ -63,11 +80,11 @@ public class ConfigurationRegistry {
      * @param rootSupplier Closure to instantiate internal configuration tree roots.
      * @param publicRootCreator Function to create public user-facing tree instance.
      */
-    public static <T extends ConfigurationTree<?, ?>> RootKey<T> newRootKey(
+    public static <T extends ConfigurationTree<V, ?>, V> RootKey<T, V> newRootKey(
         String rootName,
         Class<? extends ConfigurationStorage> storageType,
         Supplier<InnerNode> rootSupplier,
-        BiFunction<RootKey<T>, ConfigurationChanger, T> publicRootCreator
+        BiFunction<RootKey<T, V>, ConfigurationChanger, T> publicRootCreator
     ) {
         return new RootKeyImpl<>(rootName, storageType, rootSupplier, publicRootCreator);
     }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/Configurator.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/Configurator.java
index a3af965..05c1ebe 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/Configurator.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/Configurator.java
@@ -17,18 +17,7 @@
 
 package org.apache.ignite.configuration;
 
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
 import org.apache.ignite.configuration.internal.DynamicConfiguration;
-import org.apache.ignite.configuration.internal.DynamicProperty;
-import org.apache.ignite.configuration.internal.validation.MemberKey;
-import org.apache.ignite.configuration.tree.TraversableTreeNode;
-import org.apache.ignite.configuration.validation.FieldValidator;
-import org.apache.ignite.configuration.validation.ValidationIssue;
 
 /**
  * Convenient wrapper for configuration root. Provides access to configuration tree, stores validators, performs actions
@@ -36,87 +25,4 @@ import org.apache.ignite.configuration.validation.ValidationIssue;
  * @param <T> Type of configuration root.
  */
 public class Configurator<T extends DynamicConfiguration<?, ?, ?>> {
-    /** Root of the configuration tree. */
-    private final T root;
-
-    /** Configuration property validators. */
-    private final Map<MemberKey, List<FieldValidator<? extends Serializable, T>>> fieldValidators = new HashMap<>();
-
-    /**
-     *
-     * @param rootBuilder
-     * @param <VIEW>
-     * @param <INIT>
-     * @param <CHANGE>
-     * @param <CONF>
-     * @return
-     */
-    public static <VIEW, INIT, CHANGE, CONF extends DynamicConfiguration<VIEW, INIT, CHANGE>> Configurator<CONF> create(
-        Function<Configurator<CONF>, CONF> rootBuilder
-    ) {
-        return new Configurator<>(rootBuilder);
-    }
-
-    /**
-     * Constructor.
-     * @param rootBuilder Function, that creates configuration root.
-     */
-    private <VIEW, INIT, CHANGE, CONF extends DynamicConfiguration<VIEW, INIT, CHANGE>> Configurator(
-        Function<Configurator<CONF>, CONF> rootBuilder
-    ) {
-        final CONF built = rootBuilder.apply((Configurator<CONF>) this);
-
-        root = (T) built;
-    }
-
-    /**
-     *
-     * @param aClass
-     * @param key
-     * @param validators
-     * @param <PROP>
-     */
-    public <PROP extends Serializable> void addValidations(
-        Class<? extends ConfigurationTree<?, ?>> aClass,
-        String key,
-        List<FieldValidator<? super PROP, ? extends ConfigurationTree<?, ?>>> validators
-    ) {
-        fieldValidators.put(new MemberKey(aClass, key), (List) validators);
-    }
-
-    /**
-     * Get all validators for given member key (class + field).
-     * @param key Member key.
-     * @return Validators.
-     */
-    public List<FieldValidator<? extends Serializable, T>> validators(MemberKey key) {
-        return fieldValidators.getOrDefault(key, Collections.emptyList());
-    }
-
-    /**
-     * Get configuration root.
-     * @return Configuration root.
-     */
-    public T getRoot() {
-        return root;
-    }
-
-    /**
-     * Execute on property attached to configurator.
-     * @param property Property.
-     * @param <PROP> Type of the property.
-     */
-    public <PROP extends Serializable> void onAttached(DynamicProperty<PROP> property) {
-        property.addListener(new PropertyListener<PROP, PROP>() {
-            /** {@inheritDoc} */
-            @Override public void update(PROP newValue, ConfigurationProperty<PROP, PROP> modifier) {
-//                storage.save(key, newValue);
-            }
-        });
-//        storage.listen(key, property::setSilently);
-    }
-
-    public List<ValidationIssue> validateChanges(TraversableTreeNode changes) {
-        return Collections.emptyList();
-    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/RootKey.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/RootKey.java
index e49db7b..bd84c16 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/RootKey.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/RootKey.java
@@ -21,7 +21,7 @@ import org.apache.ignite.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.configuration.tree.InnerNode;
 
 /** */
-public abstract class RootKey<T extends ConfigurationTree<?, ?>> {
+public abstract class RootKey<T extends ConfigurationTree<VIEW, ?>, VIEW> {
     /** */
     public abstract String key();
 
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Validate.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Validate.java
deleted file mode 100644
index 2c2c34d..0000000
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Validate.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.configuration.annotation;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Repeatable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-import org.apache.ignite.configuration.validation.FieldValidator;
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-/**
- * This annotation applies custom validation to configuration field, for example:
- * <pre name="code" class="java">
- * public class ConfSchema {
- *     {@literal @}Validate(SomeCustomValidator.class)
- *     private String value;
- * }
- * </pre>
- *
- * If you need multiple custom validations:
- * <pre name="code" class="java">
- * public class ConfSchema {
- *     {@literal @}Validate(SomeCustomValidator.class),
- *     {@literal @}Validate(SomeOtherCustomValidator.class)
- *     private String value;
- * }
- * </pre>
- */
-@Target({ FIELD })
-@Retention(SOURCE)
-@Repeatable(Validate.List.class)
-@Documented
-public @interface Validate {
-    /**
-     * @return Validation class that is going to be instantiated for the field validation.
-     */
-    Class<? extends FieldValidator<?, ?>> value();
-
-    /**
-     * @return Validation error message.
-     */
-    String message() default "";
-
-    /**
-     * Defines several {@link Validate} annotations on the same element.
-     *
-     * @see Validate
-     */
-    @Target({ FIELD })
-    @Retention(SOURCE)
-    @Documented
-    @interface List {
-
-        Validate[] value();
-    }
-
-}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
index bcee71a..67f875c 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
@@ -37,7 +37,7 @@ public abstract class ConfigurationNode<VIEW> {
     protected final String key;
 
     /** Root key instance for the current trees root. */
-    protected final RootKey<?> rootKey;
+    protected final RootKey<?, ?> rootKey;
 
     /** Configuration changer instance to get latest value of the root. */
     protected final ConfigurationChanger changer;
@@ -64,7 +64,7 @@ public abstract class ConfigurationNode<VIEW> {
      * @param rootKey Root key.
      * @param changer Configuration changer.
      */
-    protected ConfigurationNode(List<String> prefix, String key, RootKey<?> rootKey, ConfigurationChanger changer) {
+    protected ConfigurationNode(List<String> prefix, String key, RootKey<?, ?> rootKey, ConfigurationChanger changer) {
         this.keys = ConfigurationUtil.appendKey(prefix, key);
         this.key = key;
         this.rootKey = rootKey;
@@ -131,5 +131,7 @@ public abstract class ConfigurationNode<VIEW> {
      *
      * @param newValue New configuration value.
      */
-    protected abstract void beforeRefreshValue(VIEW newValue);
+    protected void beforeRefreshValue(VIEW newValue) {
+        // No-op.
+    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
index 97c217f..e2ca703 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.configuration.internal;
 
-import java.io.Serializable;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -34,7 +33,6 @@ import org.apache.ignite.configuration.tree.ConfigurationSource;
 import org.apache.ignite.configuration.tree.ConstructableTreeNode;
 import org.apache.ignite.configuration.tree.InnerNode;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
-import org.apache.ignite.configuration.validation.FieldValidator;
 
 /**
  * This class represents configuration root or node.
@@ -54,7 +52,7 @@ public abstract class DynamicConfiguration<VIEW, INIT, CHANGE> extends Configura
     protected DynamicConfiguration(
         List<String> prefix,
         String key,
-        RootKey<?> rootKey,
+        RootKey<?, ?> rootKey,
         ConfigurationChanger changer
     ) {
         super(prefix, key, rootKey, changer);
@@ -65,29 +63,22 @@ public abstract class DynamicConfiguration<VIEW, INIT, CHANGE> extends Configura
      * @param member Configuration member (leaf or node).
      * @param <P> Type of member.
      */
-    protected <P extends ConfigurationProperty<?, ?>> void add(P member) {
+    protected final <P extends ConfigurationProperty<?, ?>> void add(P member) {
         members.put(member.key(), member);
     }
 
     /**
-     * Add new configuration member with validators.
+     * Add new configuration member.
+     *
      * @param member Configuration member (leaf or node).
-     * @param validators Validators for new member.
-     * @param <PROP> Type of {@link DynamicProperty}.
      * @param <M> Type of member.
      */
-    protected <PROP extends Serializable, M extends DynamicProperty<PROP>> void add(
-        M member,
-        List<FieldValidator<? super PROP, ? extends ConfigurationTree<?, ?>>> validators
-    ) {
+    protected final <M extends DynamicProperty<?>> void add(M member) {
         members.put(member.key(), member);
-
-        //TODO IGNITE-14183
-//        configurator.addValidations((Class<? extends ConfigurationTree<?, ?>>) getClass(), member.key(), validators);
     }
 
     /** {@inheritDoc} */
-    @Override public Future<Void> change(Consumer<CHANGE> change) throws ConfigurationValidationException {
+    @Override public final Future<Void> change(Consumer<CHANGE> change) throws ConfigurationValidationException {
         Objects.requireNonNull(change, "Configuration consumer cannot be null.");
 
         InnerNode rootNodeChange = ((RootKeyImpl)rootKey).createRootNode();
@@ -127,12 +118,7 @@ public abstract class DynamicConfiguration<VIEW, INIT, CHANGE> extends Configura
     }
 
     /** {@inheritDoc} */
-    @Override public Map<String, ConfigurationProperty<?, ?>> members() {
+    @Override public final Map<String, ConfigurationProperty<?, ?>> members() {
         return Collections.unmodifiableMap(members);
     }
-
-    /** {@inheritDoc} */
-    @Override protected void beforeRefreshValue(VIEW newValue) {
-        // No-op.
-    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
index 1beb83a..d74913a 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
@@ -51,7 +51,7 @@ public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T
     public DynamicProperty(
         List<String> prefix,
         String key,
-        RootKey<?> rootKey,
+        RootKey<?, ?> rootKey,
         ConfigurationChanger changer
     ) {
         super(prefix, key, rootKey, changer);
@@ -106,9 +106,4 @@ public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T
     @Override public String key() {
         return key;
     }
-
-    /** {@inheritDoc} */
-    @Override protected void beforeRefreshValue(T newValue) {
-        // No-op.
-    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
index 107f06f..9c9e53a 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
@@ -53,9 +53,10 @@ public class NamedListConfiguration<T extends ConfigurationProperty<VIEW, CHANGE
     public NamedListConfiguration(
         List<String> prefix,
         String key,
-        RootKey<?> rootKey,
+        RootKey<?, ?> rootKey,
         ConfigurationChanger changer,
-        BiFunction<List<String>, String, T> creator) {
+        BiFunction<List<String>, String, T> creator
+    ) {
         super(prefix, key, rootKey, changer);
         this.creator = creator;
     }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/RootKeyImpl.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/RootKeyImpl.java
index 66c04ea..8710f4d 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/RootKeyImpl.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/RootKeyImpl.java
@@ -26,7 +26,7 @@ import org.apache.ignite.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.configuration.tree.InnerNode;
 
 /** */
-public class RootKeyImpl<T extends ConfigurationTree<?, ?>> extends RootKey<T> {
+public class RootKeyImpl<T extends ConfigurationTree<VIEW, ?>, VIEW> extends RootKey<T, VIEW> {
     /** */
     private final String rootName;
 
@@ -37,14 +37,14 @@ public class RootKeyImpl<T extends ConfigurationTree<?, ?>> extends RootKey<T> {
     private final Supplier<InnerNode> rootSupplier;
 
     /** */
-    private final BiFunction<RootKey<T>, ConfigurationChanger, T> publicRootCreator;
+    private final BiFunction<RootKey<T, VIEW>, ConfigurationChanger, T> publicRootCreator;
 
     /** */
     public RootKeyImpl(
         String rootName,
         Class<? extends ConfigurationStorage> storageType,
         Supplier<InnerNode> rootSupplier,
-        BiFunction<RootKey<T>, ConfigurationChanger, T> publicRootCreator
+        BiFunction<RootKey<T, VIEW>, ConfigurationChanger, T> publicRootCreator
     ) {
         this.rootName = rootName;
         this.storageType = storageType;
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/SuperRoot.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/SuperRoot.java
new file mode 100644
index 0000000..a83852b
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/SuperRoot.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.internal;
+
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.tree.ConfigurationSource;
+import org.apache.ignite.configuration.tree.ConfigurationVisitor;
+import org.apache.ignite.configuration.tree.InnerNode;
+
+/** */
+public final class SuperRoot extends InnerNode {
+    /** */
+    private final SortedMap<String, InnerNode> roots = new TreeMap<>();
+
+    /** */
+    private final Map<String, RootKey<?, ?>> allRootKeys;
+
+    /** Copy constructor. */
+    private SuperRoot(SuperRoot superRoot) {
+        roots.putAll(superRoot.roots);
+
+        allRootKeys = superRoot.allRootKeys;
+    }
+
+    /** */
+    public SuperRoot(Map<String, RootKey<?, ?>> rootKeys) {
+        allRootKeys = rootKeys;
+    }
+
+    /** */
+    public SuperRoot(Map<String, RootKey<?, ?>> rootKeys, Map<RootKey<?, ?>, InnerNode> roots) {
+        allRootKeys = rootKeys;
+
+        for (Map.Entry<RootKey<?, ?>, InnerNode> entry : roots.entrySet())
+            this.roots.put(entry.getKey().key(), entry.getValue());
+    }
+
+    /** */
+    public void addRoot(RootKey<?, ?> rootKey, InnerNode root) {
+        assert !roots.containsKey(rootKey.key()) : rootKey.key() + " : " + roots;
+        assert allRootKeys.get(rootKey.key()) == rootKey : rootKey.key() + " : " + allRootKeys;
+
+        roots.put(rootKey.key(), root);
+    }
+
+    /** */
+    public InnerNode getRoot(RootKey<?, ?> rootKey) {
+        return roots.get(rootKey.key());
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> void traverseChildren(ConfigurationVisitor<T> visitor) {
+        for (Map.Entry<String, InnerNode> entry : roots.entrySet())
+            visitor.visitInnerNode(entry.getKey(), entry.getValue());
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T traverseChild(String key, ConfigurationVisitor<T> visitor) throws NoSuchElementException {
+        InnerNode root = roots.get(key);
+
+        if (root == null)
+            throw new NoSuchElementException(key);
+
+        return visitor.visitInnerNode(key, root);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void construct(String key, ConfigurationSource src) throws NoSuchElementException {
+        assert src != null;
+
+        RootKeyImpl<?, ?> rootKey = (RootKeyImpl<?, ?>)allRootKeys.get(key);
+
+        if (rootKey == null)
+            throw new NoSuchElementException(key);
+
+        InnerNode root = roots.get(key);
+
+        root = root == null ? rootKey.createRootNode() : root.copy();
+
+        roots.put(key, root);
+
+        src.descend(root);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean constructDefault(String key) throws NoSuchElementException {
+        throw new NoSuchElementException(key);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Class<?> schemaType() {
+        return Object.class;
+    }
+
+    /** {@inheritDoc} */
+    @Override public SuperRoot copy() {
+        return new SuperRoot(this);
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/AnyNodeConfigurationVisitor.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/AnyNodeConfigurationVisitor.java
new file mode 100644
index 0000000..c91fd03
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/AnyNodeConfigurationVisitor.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.internal.util;
+
+import java.io.Serializable;
+import org.apache.ignite.configuration.tree.ConfigurationVisitor;
+import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.tree.NamedListNode;
+
+/** Visitor with just a single method to override. */
+public abstract class AnyNodeConfigurationVisitor<T> implements ConfigurationVisitor<T> {
+    /** {@inheritDoc} */
+    @Override public final T visitLeafNode(String key, Serializable val) {
+        return visitNode(key, val);
+    }
+
+    /** {@inheritDoc} */
+    @Override public final T visitInnerNode(String key, InnerNode node) {
+        return visitNode(key, node);
+    }
+
+    /** {@inheritDoc} */
+    @Override public final <N extends InnerNode> T visitNamedListNode(String key, NamedListNode<N> node) {
+        return visitNode(key, node);
+    }
+
+    /**
+     * Visit tree node.
+     *
+     * @param key Name of the node.
+     * @param node {@link InnerNode}, {@link NamedListNode<?>} or {@link Serializable} leaf.
+     */
+    protected abstract T visitNode(String key, Object node);
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
index 1c09c97..53fd2f2 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
@@ -26,7 +26,7 @@ import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.RandomAccess;
 import java.util.stream.Collectors;
-import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.internal.SuperRoot;
 import org.apache.ignite.configuration.tree.ConfigurationSource;
 import org.apache.ignite.configuration.tree.ConfigurationVisitor;
 import org.apache.ignite.configuration.tree.ConstructableTreeNode;
@@ -282,99 +282,52 @@ public class ConfigurationUtil {
      * @return Map of changes.
      */
     public static Map<String, Serializable> nodeToFlatMap(
-        RootKey<?> rootKey,
-        TraversableTreeNode curRoot,
-        TraversableTreeNode updates
+        SuperRoot curRoots,
+        SuperRoot updates
     ) {
-        return updates.accept(rootKey.key(), new ConfigurationVisitor<>() {
-            /** Resulting flat map. */
-            private Map<String, Serializable> values = new HashMap<>();
-
-            /** Current key, aggregated by visitor. */
-            private StringBuilder currentKey = new StringBuilder();
-
-            /** Current keys list, almost the same as {@link #currentKey}. */
-            private List<String> currentPath = new ArrayList<>();
+        Map<String, Serializable> values = new HashMap<>();
 
+        updates.traverseChildren(new KeysTrackingConfigurationVisitor<>() {
             /** Write nulls instead of actual values. Makes sense for deletions from named lists. */
             private boolean writeNulls;
 
             /** {@inheritDoc} */
-            @Override public Map<String, Serializable> visitLeafNode(String key, Serializable val) {
+            @Override public Map<String, Serializable> doVisitLeafNode(String key, Serializable val) {
                 if (val != null)
-                    values.put(currentKey.toString() + key, writeNulls ? null : val);
+                    values.put(currentKey(), writeNulls ? null : val);
 
                 return values;
             }
 
             /** {@inheritDoc} */
-            @Override public Map<String, Serializable> visitInnerNode(String key, InnerNode node) {
+            @Override public Map<String, Serializable> doVisitInnerNode(String key, InnerNode node) {
                 if (node == null)
                     return null;
 
-                int previousKeyLength = startVisit(key, false);
-
                 node.traverseChildren(this);
 
-                endVisit(previousKeyLength);
-
                 return values;
             }
 
             /** {@inheritDoc} */
-            @Override public <N extends InnerNode> Map<String, Serializable> visitNamedListNode(String key, NamedListNode<N> node) {
-                int previousKeyLength = startVisit(key, false);
-
+            @Override public <N extends InnerNode> Map<String, Serializable> doVisitNamedListNode(String key, NamedListNode<N> node) {
                 for (String namedListKey : node.namedListKeys()) {
-                    int loopPreviousKeyLength = startVisit(namedListKey, true);
-
                     N namedElement = node.get(namedListKey);
 
-                    if (namedElement == null)
-                        visitDeletedNamedListElement();
-                    else
-                        namedElement.traverseChildren(this);
+                    withTracking(namedListKey, true, false, () -> {
+                        if (namedElement == null)
+                            visitDeletedNamedListElement();
+                        else
+                            namedElement.traverseChildren(this);
 
-                    endVisit(loopPreviousKeyLength);
+                        return null;
+                    });
                 }
 
-                endVisit(previousKeyLength);
-
                 return values;
             }
 
             /**
-             * Prepares values of {@link #currentKey} and {@link #currentPath} for further processing.
-             *
-             * @param key Key.
-             * @param escape Whether we need to escape the key before appending it to {@link #currentKey}.
-             * @return Previous length of {@link #currentKey} so it can be passed to {@link #endVisit(int)} later.
-             */
-            private int startVisit(String key, boolean escape) {
-                int previousKeyLength = currentKey.length();
-
-                currentKey.append(escape ? ConfigurationUtil.escape(key) : key).append('.');
-
-                if (!writeNulls)
-                    currentPath.add(key);
-
-                return previousKeyLength;
-            }
-
-            /**
-             * Puts {@link #currentKey} and {@link #currentPath} in the same state as they were before
-             * {@link #startVisit(String, boolean)}.
-             *
-             * @param previousKeyLength Value return by corresponding {@link #startVisit(String, boolean)} invocation.
-             */
-            private void endVisit(int previousKeyLength) {
-                currentKey.setLength(previousKeyLength);
-
-                if (!writeNulls)
-                    currentPath.remove(currentPath.size() - 1);
-            }
-
-            /**
              * Here we must list all joined keys belonging to deleted element. The only way to do it is to traverse
              * cached configuration tree and write {@code null} into all values.
              */
@@ -384,11 +337,13 @@ public class ConfigurationUtil {
 
                 Object originalNamedElement = null;
 
+                List<String> currentPath = currentPath();
+
                 try {
                     // This code can in fact be better optimized for deletion scenario,
                     // but there's no point in doing that, since the operation is so rare and it will
                     // complicate code even more.
-                    originalNamedElement = find(currentPath.subList(1, currentPath.size()), curRoot);
+                    originalNamedElement = find(currentPath, curRoots);
                 }
                 catch (KeyNotFoundException ignore) {
                     // May happen, not a big deal. This means that element never existed in the first place.
@@ -405,6 +360,8 @@ public class ConfigurationUtil {
                 }
             }
         });
+
+        return values;
     }
 
     /**
@@ -456,7 +413,94 @@ public class ConfigurationUtil {
         return copy;
     }
 
-    /** */
+    /**
+     * Fill {@code dst} node with default values, required to complete {@code src} node.
+     * These two objects can be the same, this would mean that all {@code null} values of {@code scr} will be
+     * replaced with defaults if it's possible.
+     *
+     * @param src Source node.
+     * @param dst Destination node.
+     */
+    public static void addDefaults(InnerNode src, InnerNode dst) {
+        assert src.getClass() == dst.getClass();
+
+        src.traverseChildren(new ConfigurationVisitor<>() {
+            @Override public Object visitLeafNode(String key, Serializable val) {
+                // If source value is null then inititalise the same value on the destination node.
+                if (val == null)
+                    dst.constructDefault(key);
+
+                return null;
+            }
+
+            @Override public Object visitInnerNode(String key, InnerNode srcNode) {
+                // Instantiate field in destination node before doing something else.
+                // Not a big deal if it wasn't null.
+                dst.construct(key, new ConfigurationSource() {});
+
+                // Get that inner node from destination to continue the processing.
+                InnerNode dstNode = dst.traverseChild(key, innerNodeVisitor());
+
+                // "dstNode" is guaranteed to not be null even if "src" and "dst" match.
+                // Null in "srcNode" means that we should initialize everything that we can in "dstNode"
+                // unconditionally. It's only possible if we pass it as a source as well.
+                addDefaults(srcNode == null ? dstNode : srcNode, dstNode);
+
+                return null;
+            }
+
+            @Override public <N extends InnerNode> Object visitNamedListNode(String key, NamedListNode<N> srcNamedList) {
+                // Here we don't need to preemptively initialise corresponsing field, because it can never be null.
+                NamedListNode<?> dstNamedList = dst.traverseChild(key, namedListNodeVisitor());
+
+                for (String namedListKey : srcNamedList.namedListKeys()) {
+                    // But, in order to get non-null value from "dstNamedList.get(namedListKey)" we must explicitly
+                    // ensure its existance.
+                    dstNamedList.construct(namedListKey, new ConfigurationSource() {});
+
+                    addDefaults(srcNamedList.get(namedListKey), dstNamedList.get(namedListKey));
+                }
+
+                return null;
+            }
+        });
+    }
+
+    /**
+     * @return Visitor that returns leaf value or {@code null} if node is not a leaf.
+     */
+    public static ConfigurationVisitor<Serializable> leafNodeVisitor() {
+        return new ConfigurationVisitor<>() {
+            @Override public Serializable visitLeafNode(String key, Serializable val) {
+                return val;
+            }
+        };
+    }
+
+    /**
+     * @return Visitor that returns inner node or {@code null} if node is not an inner node.
+     */
+    public static ConfigurationVisitor<InnerNode> innerNodeVisitor() {
+        return new ConfigurationVisitor<>() {
+            @Override public InnerNode visitInnerNode(String key, InnerNode node) {
+                return node;
+            }
+        };
+    }
+
+    /**
+     * @return Visitor that returns named list node or {@code null} if node is not a named list node.
+     */
+    public static ConfigurationVisitor<NamedListNode<?>> namedListNodeVisitor() {
+        return new ConfigurationVisitor<>() {
+            @Override
+            public <N extends InnerNode> NamedListNode<?> visitNamedListNode(String key, NamedListNode<N> node) {
+                return node;
+            }
+        };
+    }
+
+    /** @see #patch(ConstructableTreeNode, TraversableTreeNode) */
     private static class PatchLeafConfigurationSource implements ConfigurationSource {
         /** */
         private final Serializable val;
@@ -481,7 +525,7 @@ public class ConfigurationUtil {
         }
     }
 
-    /** */
+    /** @see #patch(ConstructableTreeNode, TraversableTreeNode) */
     private static class PatchInnerConfigurationSource implements ConfigurationSource {
         /** */
         private final InnerNode srcNode;
@@ -527,7 +571,7 @@ public class ConfigurationUtil {
         }
     }
 
-    /** */
+    /** @see #patch(ConstructableTreeNode, TraversableTreeNode) */
     private static class PatchNamedListConfigurationSource implements ConfigurationSource {
         /** */
         private final NamedListNode<?> srcNode;
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/KeysTrackingConfigurationVisitor.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/KeysTrackingConfigurationVisitor.java
new file mode 100644
index 0000000..76e1b8c
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/KeysTrackingConfigurationVisitor.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.internal.util;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+import org.apache.ignite.configuration.tree.ConfigurationVisitor;
+import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.tree.NamedListNode;
+
+/** Visitor that accumulates keys while descending. */
+public abstract class KeysTrackingConfigurationVisitor<T> implements ConfigurationVisitor<T> {
+    /** Current key, aggregated by visitor. */
+    private StringBuilder currentKey = new StringBuilder();
+
+    /** Current keys list, almost the same as {@link #currentKey}. */
+    private List<String> currentPath = new ArrayList<>();
+
+    /** {@inheritDoc} */
+    @Override public final T visitLeafNode(String key, Serializable val) {
+        int prevPos = startVisit(key, false, true);
+
+        try {
+            return doVisitLeafNode(key, val);
+        }
+        finally {
+            endVisit(prevPos);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public final T visitInnerNode(String key, InnerNode node) {
+        int prevPos = startVisit(key, false, false);
+
+        try {
+            return doVisitInnerNode(key, node);
+        }
+        finally {
+            endVisit(prevPos);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public final <N extends InnerNode> T visitNamedListNode(String key, NamedListNode<N> node) {
+        int prevPos = startVisit(key, false, false);
+
+        try {
+            return doVisitNamedListNode(key, node);
+        }
+        finally {
+            endVisit(prevPos);
+        }
+    }
+
+    /** To be used instead of {@link ConfigurationVisitor#visitLeafNode(String, Serializable)}. */
+    protected T doVisitLeafNode(String key, Serializable val) {
+        return null;
+    }
+
+    /** To be used instead of {@link ConfigurationVisitor#visitInnerNode(String, InnerNode)}. */
+    protected T doVisitInnerNode(String key, InnerNode node) {
+        node.traverseChildren(this);
+
+        return null;
+    }
+
+    /** To be used instead of {@link ConfigurationVisitor#visitNamedListNode(String, NamedListNode)}. */
+    protected <N extends InnerNode> T doVisitNamedListNode(String key, NamedListNode<N> node) {
+        for (String namedListKey : node.namedListKeys()) {
+            int prevPos = startVisit(namedListKey, true, false);
+
+            try {
+                doVisitInnerNode(namedListKey, node.get(namedListKey));
+            }
+            finally {
+                endVisit(prevPos);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Tracks passed key to reflect it in {@link #currentKey()} and {@link #currentPath()}.
+     *
+     * @param key Key itself.
+     * @param escape Whether the key needs escaping or not.
+     * @param leaf Add dot at the end of {@link #currentKey()} if {@code leaf} is {@code false}.
+     * @param closure Closure to execute when {@link #currentKey()} and {@link #currentPath()} have updated values.
+     */
+    protected final T withTracking(String key, boolean escape, boolean leaf, Supplier<T> closure) {
+        int prevPos = startVisit(key, escape, leaf);
+
+        try {
+            return closure.get();
+        }
+        finally {
+            endVisit(prevPos);
+        }
+    }
+
+    /**
+     * @return Current key, with a dot at the end if it's not a leaf.
+     */
+    protected final String currentKey() {
+        return currentKey.toString();
+    }
+
+    /**
+     * @return List representation of the current key.
+     */
+    protected final List<String> currentPath() {
+        return Collections.unmodifiableList(currentPath);
+    }
+
+    /**
+     * Prepares values of {@link #currentKey} and {@link #currentPath} for further processing.
+     *
+     * @param key Key.
+     * @param escape Whether we need to escape the key before appending it to {@link #currentKey}.
+     * @return Previous length of {@link #currentKey} so it can be passed to {@link #endVisit(int)} later.
+     */
+    private int startVisit(String key, boolean escape, boolean leaf) {
+        int previousKeyLength = currentKey.length();
+
+        currentKey.append(escape ? ConfigurationUtil.escape(key) : key);
+
+        if (!leaf)
+            currentKey.append('.');
+
+        currentPath.add(key);
+
+        return previousKeyLength;
+    }
+
+    /**
+     * Puts {@link #currentKey} and {@link #currentPath} in the same state as they were before
+     * {@link #startVisit(String, boolean)}.
+     *
+     * @param previousKeyLength Value return by corresponding {@link #startVisit(String, boolean)} invocation.
+     */
+    private void endVisit(int previousKeyLength) {
+        currentKey.setLength(previousKeyLength);
+
+        currentPath.remove(currentPath.size() - 1);
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/MaxValidator.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/MaxValidator.java
index 60620fe..48ff454 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/MaxValidator.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/MaxValidator.java
@@ -17,28 +17,21 @@
 
 package org.apache.ignite.configuration.internal.validation;
 
-import org.apache.ignite.configuration.internal.DynamicConfiguration;
-import org.apache.ignite.configuration.validation.ConfigurationValidationException;
-import org.apache.ignite.configuration.validation.FieldValidator;
+import javax.validation.constraints.Max;
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
 
 /**
  * Validate that field value is not greater than some maximum value.
- *
- * @param <C> Root configuration type.
  */
-public class MaxValidator<C extends DynamicConfiguration<?, ?, ?>> extends FieldValidator<Number, C> {
-    /** Maximum value. */
-    private final long maxValue;
-
-    /** Constructor. */
-    public MaxValidator(long maxValue, String message) {
-        super(message);
-        this.maxValue = maxValue;
-    }
-
+public class MaxValidator implements Validator<Max, Number> {
     /** {@inheritDoc} */
-    @Override public void validate(Number value, C newRoot, C oldRoot) throws ConfigurationValidationException {
-        if (value.longValue() > maxValue)
-            throw new ConfigurationValidationException(message);
+    @Override public void validate(Max annotation, ValidationContext<Number> ctx) {
+        if (ctx.getNewValue().longValue() > annotation.value()) {
+            ctx.addIssue(new ValidationIssue(
+                "Configuration value '" + ctx.currentKey() + "' must not be greater than " + annotation.value()
+            ));
+        }
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/MinValidator.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/MinValidator.java
index b94e65f..ba01c61 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/MinValidator.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/MinValidator.java
@@ -17,28 +17,21 @@
 
 package org.apache.ignite.configuration.internal.validation;
 
-import org.apache.ignite.configuration.internal.DynamicConfiguration;
-import org.apache.ignite.configuration.validation.ConfigurationValidationException;
-import org.apache.ignite.configuration.validation.FieldValidator;
+import javax.validation.constraints.Min;
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
 
 /**
  * Validate that field value is not less than some minimal value.
- *
- * @param <C> Root configuration type.
  */
-public class MinValidator<C extends DynamicConfiguration<?, ?, ?>> extends FieldValidator<Number, C> {
-    /** Minimal value. */
-    private final long minValue;
-
-    /** Constructor. */
-    public MinValidator(long minValue, String message) {
-        super(message);
-        this.minValue = minValue;
-    }
-
+public class MinValidator implements Validator<Min, Number> {
     /** {@inheritDoc} */
-    @Override public void validate(Number value, C newRoot, C oldRoot) throws ConfigurationValidationException {
-        if (value.longValue() < minValue)
-            throw new ConfigurationValidationException(message);
+    @Override public void validate(Min annotation, ValidationContext<Number> ctx) {
+        if (ctx.getNewValue().longValue() < annotation.value()) {
+            ctx.addIssue(new ValidationIssue(
+                "Configuration value '" + ctx.currentKey() + "' must not be less than " + annotation.value()
+            ));
+        }
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/NotNullValidator.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/NotNullValidator.java
deleted file mode 100644
index f060f53..0000000
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/NotNullValidator.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.configuration.internal.validation;
-
-import java.io.Serializable;
-import org.apache.ignite.configuration.internal.DynamicConfiguration;
-import org.apache.ignite.configuration.validation.ConfigurationValidationException;
-import org.apache.ignite.configuration.validation.FieldValidator;
-
-/**
- * Validate that field value is not null.
- *
- * @param <C> Root configuration type.
- */
-public class NotNullValidator<C extends DynamicConfiguration<?, ?, ?>> extends FieldValidator<Serializable, C> {
-    /** Constructor. */
-    public NotNullValidator(String message) {
-        super(message);
-    }
-
-    /** {@inheritDoc} */
-    @Override public void validate(Serializable value, C newRoot, C oldRoot) throws ConfigurationValidationException {
-        if (value == null)
-            throw new ConfigurationValidationException("");
-    }
-}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/ValidationContextImpl.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/ValidationContextImpl.java
new file mode 100644
index 0000000..ffb5aca
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/ValidationContextImpl.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.internal.validation;
+
+import java.util.List;
+import java.util.function.Function;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.internal.SuperRoot;
+import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.tree.TraversableTreeNode;
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.find;
+
+/**
+ * Validation context implementation.
+ */
+class ValidationContextImpl<VIEW> implements ValidationContext<VIEW> {
+    /** Cached storage roots with the current version of data. */
+    private final SuperRoot oldRoots;
+
+    /** Updated values that need to be validated. */
+    private final SuperRoot newRoots;
+
+    /** Provider for arbitrary roots that might not be accociated with the same storage. */
+    private final Function<RootKey<?, ?>, InnerNode> otherRoots;
+
+    /**
+     * Current node/configuration value.
+     *
+     * @see #getNewValue()
+     */
+    private final VIEW val;
+
+    /** */
+    private final String currentKey;
+
+    /** */
+    private final List<String> currentPath;
+
+    /** */
+    private final List<ValidationIssue> issues;
+
+    /**
+     * Constructor.
+     *  @param oldRoots Old roots.
+     * @param newRoots New roots.
+     * @param otherRoots Provider for arbitrary roots that might not be accociated with the same storage.
+     * @param val New value of currently validated configuration.
+     * @param currentKey Key corresponding to the value.
+     * @param currentPath List representation of {@code currentKey}.
+     * @param issues List of issues, should be used as a write-only collection.
+     */
+    ValidationContextImpl(
+        SuperRoot oldRoots,
+        SuperRoot newRoots,
+        Function<RootKey<?, ?>, InnerNode> otherRoots,
+        VIEW val,
+        String currentKey,
+        List<String> currentPath,
+        List<ValidationIssue> issues
+    ) {
+        this.oldRoots = oldRoots;
+        this.newRoots = newRoots;
+        this.otherRoots = otherRoots;
+        this.val = val;
+        this.currentKey = currentKey;
+        this.currentPath = currentPath;
+        this.issues = issues;
+
+        assert !currentPath.isEmpty();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String currentKey() {
+        return currentKey;
+    }
+
+    /** {@inheritDoc} */
+    @Override public VIEW getOldValue() {
+        return (VIEW)find(currentPath, oldRoots);
+    }
+
+    /** {@inheritDoc} */
+    @Override public VIEW getNewValue() {
+        return val;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <ROOT> ROOT getOldRoot(RootKey<?, ROOT> rootKey) {
+        InnerNode root = oldRoots.getRoot(rootKey);
+
+        return (ROOT)(root == null ? otherRoots.apply(rootKey) : root);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <ROOT> ROOT getNewRoot(RootKey<?, ROOT> rootKey) {
+        TraversableTreeNode root = newRoots.getRoot(rootKey);
+
+        return (ROOT)(root == null ? otherRoots.apply(rootKey) : root);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void addIssue(ValidationIssue issue) {
+        issues.add(issue);
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/ValidationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/ValidationUtil.java
new file mode 100644
index 0000000..b258b06
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/validation/ValidationUtil.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.internal.validation;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.internal.SuperRoot;
+import org.apache.ignite.configuration.internal.util.AnyNodeConfigurationVisitor;
+import org.apache.ignite.configuration.internal.util.KeysTrackingConfigurationVisitor;
+import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
+
+import static java.util.Collections.emptySet;
+import static org.apache.ignite.configuration.internal.util.ConfigurationUtil.appendKey;
+
+/** */
+public class ValidationUtil {
+    /**
+     * Validate configuration changes.
+     *
+     * @param oldRoots Old known roots.
+     * @param newRoots New roots.
+     * @param otherRoots Provider for arbitrary roots that might not be accociated with the same storage.
+     * @param memberAnnotationsCache Mutable map that contains annotations associated with corresponding member keys.
+     * @param validators Current validators map to look into.
+     * @return List of validation results.
+     */
+    public static List<ValidationIssue> validate(
+        SuperRoot oldRoots,
+        SuperRoot newRoots,
+        Function<RootKey<?, ?>, InnerNode> otherRoots,
+        Map<MemberKey, Annotation[]> memberAnnotationsCache,
+        Map<Class<? extends Annotation>, Set<Validator<?, ?>>> validators
+    ) {
+        List<ValidationIssue> issues = new ArrayList<>();
+
+        newRoots.traverseChildren(new KeysTrackingConfigurationVisitor<>() {
+            /** {@inheritDoc} */
+            @Override protected Object doVisitInnerNode(String key, InnerNode innerNode) {
+                assert innerNode != null;
+
+                innerNode.traverseChildren(new AnyNodeConfigurationVisitor<Void>() {
+                    @Override protected Void visitNode(String key, Object node) {
+                        validate(innerNode, key, node);
+
+                        return null;
+                    }
+                });
+
+                return super.doVisitInnerNode(key, innerNode);
+            }
+
+            /**
+             * Perform validation on the node's subnode.
+             *
+             * @param lastInnerNode Inner node that contains validated field.
+             * @param fieldName Name of the field.
+             * @param val Value of the field.
+             */
+            private void validate(InnerNode lastInnerNode, String fieldName, Object val) {
+                if (val == null) {
+                    String message = "'" + (currentKey() + fieldName) + "' configuration value is not initialized.";
+
+                    issues.add(new ValidationIssue(message));
+
+                    return;
+                }
+
+                MemberKey memberKey = new MemberKey(lastInnerNode.getClass(), fieldName);
+
+                Annotation[] fieldAnnotations = memberAnnotationsCache.computeIfAbsent(memberKey, k -> {
+                    try {
+                        Field field = lastInnerNode.schemaType().getDeclaredField(fieldName);
+
+                        return field.getDeclaredAnnotations();
+                    }
+                    catch (NoSuchFieldException e) {
+                        // Should be impossible.
+                        return new Annotation[0];
+                    }
+                });
+
+                if (fieldAnnotations.length == 0)
+                    return;
+
+                String currentKey = currentKey() + fieldName;
+                List<String> currentPath = appendKey(currentPath(), fieldName);
+
+                for (Annotation annotation : fieldAnnotations) {
+                    for (Validator<?, ?> validator : validators.getOrDefault(annotation.annotationType(), emptySet())) {
+                        // Making this a compile-time check would be too expensive to implement.
+                        assert assertValidatorTypesCoherence(validator.getClass(), annotation.annotationType(), val)
+                            : "Validator coherence is violated [" +
+                            "class=" + lastInnerNode.getClass().getCanonicalName() + ", " +
+                            "field=" + fieldName + ", " +
+                            "annotation=" + annotation.annotationType().getCanonicalName() + ", " +
+                            "validator=" + validator.getClass().getName() + ']';
+
+                        ValidationContextImpl<Object> ctx = new ValidationContextImpl<>(
+                            oldRoots,
+                            newRoots,
+                            otherRoots,
+                            val,
+                            currentKey,
+                            currentPath,
+                            issues
+                        );
+
+                        ((Validator<Annotation, Object>)validator).validate(annotation, ctx);
+                    }
+                }
+            }
+        });
+
+        return issues;
+    }
+
+    /** */
+    private static boolean assertValidatorTypesCoherence(
+        Class<?> validatorClass,
+        Class<? extends Annotation> annotationType,
+        Object val
+    ) {
+        // Find superclass that directly extends Validator.
+        if (!Arrays.asList(validatorClass.getInterfaces()).contains(Validator.class))
+            return assertValidatorTypesCoherence(validatorClass.getSuperclass(), annotationType, val);
+
+        Type genericSuperClass = Arrays.stream(validatorClass.getGenericInterfaces())
+            .filter(i -> i instanceof ParameterizedType && ((ParameterizedType)i).getRawType() == Validator.class)
+            .findAny()
+            .get();
+
+        if (!(genericSuperClass instanceof ParameterizedType))
+            return false;
+
+        ParameterizedType parameterizedSuperClass = (ParameterizedType)genericSuperClass;
+
+        Type[] actualTypeParameters = parameterizedSuperClass.getActualTypeArguments();
+
+        if (actualTypeParameters.length != 2)
+            return false;
+
+        if (actualTypeParameters[0] != annotationType)
+            return false;
+
+        Type sndParam = actualTypeParameters[1];
+
+        if (sndParam instanceof ParameterizedType)
+            sndParam = ((ParameterizedType)sndParam).getRawType();
+
+        return (sndParam instanceof Class) && (val == null || ((Class<?>)sndParam).isInstance(val));
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConstructableTreeNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConstructableTreeNode.java
index 2584006..44b77d8 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConstructableTreeNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/ConstructableTreeNode.java
@@ -32,15 +32,6 @@ public interface ConstructableTreeNode {
     void construct(String key,/* boolean canMutate, */ ConfigurationSource src) throws NoSuchElementException;
 
     /**
-     * Assigns default value to the corresponding leaf. Defaults are gathered from configuration schema class.
-     *
-     * @param key Field name to be initialized.
-     * @return {@code true} if default value has been assigned, {@code false} otherwise.
-     * @throws NoSuchElementException If there's no such field or it is not a leaf value.
-     */
-    boolean constructDefault(String key) throws NoSuchElementException;
-
-    /**
      * Public equivalent of {@link Object#clone()} method. Creates a copy with effectively the same content.
      * Helps to preserve trees immutability after construction is completed.
      *
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/InnerNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/InnerNode.java
index af8cf28..c898945 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/InnerNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/InnerNode.java
@@ -110,7 +110,18 @@ public abstract class InnerNode implements TraversableTreeNode, ConstructableTre
      */
     @Override public abstract void construct(String key, ConfigurationSource src) throws NoSuchElementException;
 
-    /** */
+    /**
+     * Assigns default value to the corresponding leaf. Defaults are gathered from configuration schema class.
+     *
+     * @param fieldName Field name to be initialized.
+     * @return {@code true} if default value has been assigned, {@code false} otherwise.
+     * @throws NoSuchElementException If there's no such field or it is not a leaf value.
+     */
+    public abstract boolean constructDefault(String fieldName) throws NoSuchElementException;
+
+    /**
+     * @return Class of corresponding configuration schema.
+     */
     public abstract Class<?> schemaType();
 
     /** {@inheritDoc} */
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
index 609abfa..530544c 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/tree/NamedListNode.java
@@ -124,11 +124,6 @@ public final class NamedListNode<N extends InnerNode> implements NamedListView<N
     }
 
     /** {@inheritDoc} */
-    @Override public boolean constructDefault(String key) {
-        return false;
-    }
-
-    /** {@inheritDoc} */
     @Override public NamedListNode<N> copy() {
         return new NamedListNode<>(this);
     }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
index 73af1f2..4ec7e05 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
@@ -32,6 +32,8 @@ public class ConfigurationValidationException extends RuntimeException {
     }
 
     public ConfigurationValidationException(List<ValidationIssue> issues) {
+        super(issues.toString());
+
         this.issues = issues;
     }
 
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/FieldValidator.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/FieldValidator.java
deleted file mode 100644
index 6705cf6..0000000
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/FieldValidator.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.configuration.validation;
-
-import java.io.Serializable;
-import org.apache.ignite.configuration.ConfigurationTree;
-
-/**
- * Base class for field validator. Contains exception message.
- * @param <T> Field type.
- * @param <C> Root configuration type.
- */
-public abstract class FieldValidator<T extends Serializable, C extends ConfigurationTree<?, ?>> {
-    /** Validation error message. */
-    protected final String message;
-
-    /** Constructor. */
-    protected FieldValidator(String message) {
-        this.message = message;
-    }
-
-    /**
-     * Validate field.
-     *
-     * @param value New value.
-     * @param newRoot New configuration root.
-     * @param oldRoot Old configuration root.
-     * @throws ConfigurationValidationException If validation failed.
-     */
-    public abstract void validate(T value, C newRoot, C oldRoot) throws ConfigurationValidationException;
-}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationContext.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationContext.java
new file mode 100644
index 0000000..6f9d84f
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationContext.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.validation;
+
+import java.lang.annotation.Annotation;
+import org.apache.ignite.configuration.RootKey;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Validation context for the validators.
+ *
+ * @see Validator#validate(Annotation, ValidationContext)
+ */
+public interface ValidationContext<VIEW> {
+    /** @return String representation of currently validated value, i.e. {@code root.config.node} */
+    String currentKey();
+
+    /** @return Previous value of the configuration. Might be null for leaves only. */
+    @Nullable VIEW getOldValue();
+
+    /** @return Updated value of the configuration. Cannot be null. */
+    @NotNull VIEW getNewValue();
+
+    /**
+     * @param rootKey Root key.
+     * @return Configuration root view before updates. Guaranteed to return valid value only if root belongs to the same
+     *      storage as currently validated value. Otherwise result of the method may very between invocations or even
+     *      be {@code null} if corresponding storage is not initialized.
+     *
+     * @param <ROOT> Root view type derived from the root key.
+     */
+    @Nullable <ROOT> ROOT getOldRoot(RootKey<?, ROOT> rootKey);
+
+    /**
+     * @param rootKey Root key.
+     * @return Configuration root view after updates. Guaranteed to return valid value only if root belongs to the same
+     *      storage as currently validated value. Otherwise result of the method may very between invocations or even
+     *      be {@code null} if corresponding storage is not initialized.
+     *
+     * @param <ROOT> Root view type derived from the root key.
+     */
+    @Nullable <ROOT> ROOT getNewRoot(RootKey<?, ROOT> rootKey);
+
+    /**
+     * Signifies that there's something wrong. Values will be accumulated and passed to the user later.
+     *
+     * @param issue Validation issue object.
+     * @see ConfigurationValidationException
+     */
+    void addIssue(ValidationIssue issue);
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
index 4594a53..28dd677 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
@@ -16,5 +16,23 @@
  */
 package org.apache.ignite.configuration.validation;
 
+/** */
 public class ValidationIssue {
+    /** */
+    private String message;
+
+    /** */
+    public ValidationIssue(String message) {
+        this.message = message;
+    }
+
+    /** */
+    public String message() {
+        return message;
+    }
+
+    /** */
+    @Override public String toString() {
+        return "ValidationIssue [message=" + message + ']';
+    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/Validator.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/Validator.java
new file mode 100644
index 0000000..47b57ab
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/Validator.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.configuration.validation;
+
+import java.lang.annotation.Annotation;
+import org.apache.ignite.configuration.ConfigurationRegistry;
+
+/**
+ * Interface for all configuration validators. Recommended to be a stateless class.
+ *
+ * It is mandatory that all direct implementations of the interface explicitly specify types {@code A} and {@code VIEW}.
+ *
+ * @param <A> Type of the annotation that puts current validator to the field.
+ * @param <VIEW> Upper bound for field types that can be validated with this validator.
+ * @see ConfigurationRegistry#registerValidator(Class, Validator)
+ */
+public interface Validator<A extends Annotation, VIEW> {
+    /**
+     * Perform validation. All validation issues must be put into {@link ValidationContext#addIssue(ValidationIssue)}.
+     *
+     * @param annotation Specific annotation from currently validated value.
+     * @param ctx Validation context.
+     */
+    void validate(A annotation, ValidationContext<VIEW> ctx);
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
index c3a8326..5d6a270 100644
--- a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
@@ -23,7 +23,7 @@ import java.io.Reader;
 import java.util.Collections;
 import org.apache.ignite.configuration.ConfigurationRegistry;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
-import org.apache.ignite.rest.configuration.RestConfigurationImpl;
+import org.apache.ignite.rest.configuration.RestConfiguration;
 import org.apache.ignite.rest.presentation.ConfigurationPresentation;
 import org.apache.ignite.rest.presentation.FormatConverter;
 import org.apache.ignite.rest.presentation.json.JsonConverter;
@@ -128,8 +128,8 @@ public class RestModule {
 
     /** */
     private Javalin startRestEndpoint() {
-        Integer port = sysConf.getConfiguration(RestConfigurationImpl.KEY).port().value();
-        Integer portRange = sysConf.getConfiguration(RestConfigurationImpl.KEY).portRange().value();
+        Integer port = sysConf.getConfiguration(RestConfiguration.KEY).port().value();
+        Integer portRange = sysConf.getConfiguration(RestConfiguration.KEY).portRange().value();
 
         Javalin app = null;
 
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/presentation/json/JsonPresentation.java b/modules/rest/src/main/java/org/apache/ignite/rest/presentation/json/JsonPresentation.java
index 15b46fe..46de08b 100644
--- a/modules/rest/src/main/java/org/apache/ignite/rest/presentation/json/JsonPresentation.java
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/presentation/json/JsonPresentation.java
@@ -17,8 +17,8 @@
 
 package org.apache.ignite.rest.presentation.json;
 
+import java.util.Collections;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.apache.ignite.configuration.Configurator;
 import org.apache.ignite.configuration.internal.DynamicConfiguration;
 import org.apache.ignite.rest.presentation.ConfigurationPresentation;
@@ -38,10 +38,11 @@ public class JsonPresentation implements ConfigurationPresentation<String> {
 
     /** {@inheritDoc} */
     @Override public String represent() {
-        Map<String, ?> preparedMap = configsMap.entrySet().stream().collect(Collectors.toMap(
-            e -> e.getKey(),
-            e -> e.getValue().getRoot().value()
-        ));
+        Map<String, ?> preparedMap = Collections.emptyMap();
+//        configsMap.entrySet().stream().collect(Collectors.toMap(
+//            e -> e.getKey(),
+//            e -> e.getValue().getRoot().value()
+//        ));
 
         return converter.convertTo(preparedMap);
     }
diff --git a/parent/pom.xml b/parent/pom.xml
index 0c321b5..3eeee99 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -71,7 +71,7 @@
         <mockito.junit.jupiter.version>3.3.3</mockito.junit.jupiter.version>
         <picocli.version>4.5.2</picocli.version>
         <slf4j.version>1.7.30</slf4j.version>
-        <spoon.framework.version>8.3.0</spoon.framework.version>
+        <spoon.framework.version>8.4.0-beta-18</spoon.framework.version>
         <typesafe.version>1.4.1</typesafe.version>
         <hamcrest.version>2.2</hamcrest.version>
         <scalecube.version>2.6.6</scalecube.version>