diff --git a/.gitignore b/.gitignore
index 24fbd6346..27e2aa33f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,7 @@ build-logic/*/bin/
 /format/*/.apt_generated_tests/
 /format/*/.checkstyle
 /format/*/.settings/
-/extra/*/build/
+/extra/**/build/
 /extra/*/out/
 /extra/*/bin/
 /extra/*/.factorypath
diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java
index 5f8d2e5f1..6ca44b573 100644
--- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java
+++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java
@@ -316,6 +316,44 @@ default <A extends Annotation> Builder addProcessor(final Class<A> definition, f
              */
             <A extends Annotation, T> Builder addProcessor(Class<A> definition, Class<T> valueType, Processor.Factory<A, T> factory);
 
+            /**
+             * Register a {@link Processor} that will process fields after write.
+             * The difference between an AdvancedFactory and a Factory is that
+             * an AdvancedFactory has access to all the annotations on the
+             * field, which makes more advanced processors possible.
+             *
+             * <p>Processors registered without a specific data type should be
+             * able to operate on any value type.</p>
+             *
+             * @param definition annotation providing data
+             * @param factory factory for callback function
+             * @param <A> annotation type
+             * @return this builder
+             * @since 4.0.0
+             */
+            default <A extends Annotation> Builder addProcessor(final Class<A> definition, final Processor.AdvancedFactory<A, Object> factory) {
+                return addProcessor(definition, Object.class, factory);
+            }
+
+            /**
+             * Register a {@link Processor} that will process fields after write.
+             * The difference between an AdvancedFactory and a Factory is that
+             * an AdvancedFactory has access to all the annotations on the
+             * field, which makes more advanced processors possible.
+             *
+             * <p>All value types will be tested against types normalized to
+             * their boxed variants.</p>
+             *
+             * @param definition annotation providing data
+             * @param valueType value types the processor will handle
+             * @param factory factory for callback function
+             * @param <A> annotation type
+             * @param <T> data type
+             * @return this builder
+             * @since 4.0.0
+             */
+            <A extends Annotation, T> Builder addProcessor(Class<A> definition, Class<T> valueType, Processor.AdvancedFactory<A, T> factory);
+
             /**
              * Register a {@link Constraint} that will be used to validate fields.
              *
diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java
index 2426c87e4..f95849916 100644
--- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java
+++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java
@@ -72,7 +72,7 @@ protected boolean removeEldestEntry(final Map.Entry<Type, ObjectMapper<?>> eldes
     private final List<NodeResolver.Factory> resolverFactories;
     private final List<FieldDiscoverer<?>> fieldDiscoverers;
     private final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends Constraint.Factory<?, ?>>>> constraints;
-    private final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends Processor.Factory<?, ?>>>> processors;
+    private final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>>>> processors;
     private final List<PostProcessor.Factory> postProcessors;
 
     ObjectMapperFactoryImpl(final Builder builder) {
@@ -97,7 +97,7 @@ protected boolean removeEldestEntry(final Map.Entry<Type, ObjectMapper<?>> eldes
         this.constraints.values().forEach(Collections::reverse);
 
         this.processors = new HashMap<>();
-        for (final Definition<?, ?, ? extends Processor.Factory<?, ?>> def : builder.processors) {
+        for (final Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>> def : builder.processors) {
             this.processors.computeIfAbsent(def.annotation(), k -> new ArrayList<>()).add(def);
         }
         this.processors.values().forEach(Collections::reverse);
@@ -206,11 +206,11 @@ private <I, O> void makeData(final List<FieldData<I, O>> fields, final String na
                 }
             }
 
-            final List<Definition<?, ?, ? extends Processor.Factory<?, ?>>> processorDefs = this.processors.get(annotation.annotationType());
+            final List<Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>>> processorDefs = this.processors.get(annotation.annotationType());
             if (processorDefs != null) {
-                for (final Definition<?, ?, ? extends Processor.Factory<?, ?>> processorDef : processorDefs) {
+                for (final Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>> processorDef : processorDefs) {
                     if (isSuperType(processorDef.type(), normalizedType)) {
-                        processors.add(((Processor.Factory) processorDef.factory()).make(annotation, type.getType()));
+                        processors.add(((Processor.AdvancedFactory) processorDef.factory()).make(annotation, type.getType(), container));
                     }
                 }
             }
@@ -356,7 +356,7 @@ static class Builder implements ObjectMapper.Factory.Builder {
         private final List<NodeResolver.Factory> resolvers = new ArrayList<>();
         private final List<FieldDiscoverer<?>> discoverer = new ArrayList<>();
         private final List<Definition<?, ?, ? extends Constraint.Factory<?, ?>>> constraints = new ArrayList<>();
-        private final List<Definition<?, ?, ? extends Processor.Factory<?, ?>>> processors = new ArrayList<>();
+        private final List<Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>>> processors = new ArrayList<>();
         private final List<PostProcessor.Factory> postProcessors = new ArrayList<>();
 
         @Override
@@ -384,6 +384,13 @@ public <A extends Annotation, T> Builder addProcessor(final Class<A> definition,
             return this;
         }
 
+        @Override
+        public <A extends Annotation, T> Builder addProcessor(final Class<A> definition, final Class<T> valueType,
+                final Processor.AdvancedFactory<A, T> factory) {
+            this.processors.add(Definition.of(definition, valueType, factory));
+            return this;
+        }
+
         @Override
         public <A extends Annotation, T> Builder addConstraint(final Class<A> definition, final Class<T> valueType,
                 final Constraint.Factory<A, T> factory) {
diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Comment.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Comment.java
index 1b0a969dc..468620cd3 100644
--- a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Comment.java
+++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Comment.java
@@ -33,7 +33,7 @@
  *
  * @since 4.0.0
  */
-@Target({ElementType.FIELD, ElementType.PARAMETER})
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
 @Retention(RetentionPolicy.RUNTIME)
 public @interface Comment {
 
diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Matches.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Matches.java
index 824b99632..beaf41ab2 100644
--- a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Matches.java
+++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Matches.java
@@ -35,7 +35,7 @@
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
+@Target({ElementType.FIELD, ElementType.METHOD})
 public @interface Matches {
 
     /**
diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Processor.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Processor.java
index be223b150..4ca2f6cf2 100644
--- a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Processor.java
+++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Processor.java
@@ -20,6 +20,7 @@
 import org.spongepowered.configurate.ConfigurationNode;
 
 import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Type;
 import java.util.ResourceBundle;
 
@@ -40,6 +41,31 @@ public interface Processor<V> {
      */
     void process(V value, ConfigurationNode destination);
 
+    /**
+     * Provider to, given an annotation instance and the type it's on,
+     * create a {@link Processor}. If you don't need access to the other
+     * annotations on the field, you can also choose the simpler {@link Factory}.
+     *
+     * @param <A> annotation type
+     * @param <T> handled value type
+     * @since 4.0.0
+     */
+    @FunctionalInterface
+    interface AdvancedFactory<A extends Annotation, T> {
+
+        /**
+         * Create a new processor given the annotation and data type.
+         *
+         * @param data annotation type on record field
+         * @param value declared field type
+         * @param container container holding the field, with its annotations
+         * @return new processor
+         * @since 4.0.0
+         */
+        Processor<T> make(A data, Type value, AnnotatedElement container);
+
+    }
+
     /**
      * Provider to, given an annotation instance and the type it's on,
      * create a {@link Processor}.
@@ -49,7 +75,7 @@ public interface Processor<V> {
      * @since 4.0.0
      */
     @FunctionalInterface
-    interface Factory<A extends Annotation, T> {
+    interface Factory<A extends Annotation, T> extends AdvancedFactory<A, T> {
 
         /**
          * Create a new processor given the annotation and data type.
@@ -61,6 +87,10 @@ interface Factory<A extends Annotation, T> {
          */
         Processor<T> make(A data, Type value);
 
+        @Override
+        default Processor<T> make(A data, Type value, AnnotatedElement element) {
+            return make(data, value);
+        }
     }
 
     /**
diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Required.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Required.java
index c1354016d..95758e6b7 100644
--- a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Required.java
+++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Required.java
@@ -34,7 +34,7 @@
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
+@Target({ElementType.FIELD, ElementType.METHOD})
 @SubtypeOf(NonNull.class)
 public @interface Required {
 }
diff --git a/extra/interface/ap/build.gradle b/extra/interface/ap/build.gradle
new file mode 100644
index 000000000..9b4dc7e91
--- /dev/null
+++ b/extra/interface/ap/build.gradle
@@ -0,0 +1,29 @@
+plugins {
+    id "org.spongepowered.configurate.build.component"
+}
+
+description = "Annotation processor for Configurate to generate an implementation for config interfaces"
+
+dependencies {
+    implementation projects.core
+    implementation projects.extra.extraInterface
+    implementation libs.javapoet
+    implementation libs.auto.service
+    annotationProcessor libs.auto.service
+
+    testImplementation libs.compile.testing
+}
+
+// there is no javadoc
+tasks.withType(Javadoc).configureEach { enabled = false }
+
+tasks.withType(Test).configureEach {
+    doFirst {
+        // See: https://github.com/google/compile-testing/issues/222
+        if (javaLauncher.get().metadata.languageVersion >= JavaLanguageVersion.of(9)) {
+            jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED'
+            jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED'
+            jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED'
+        }
+    }
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationDefaults.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationDefaults.java
new file mode 100644
index 000000000..0ae7e3e33
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationDefaults.java
@@ -0,0 +1,135 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import static org.spongepowered.configurate.interfaces.processor.Utils.annotation;
+
+import com.google.auto.common.MoreTypes;
+import com.squareup.javapoet.AnnotationSpec;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
+
+import java.lang.annotation.Annotation;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.lang.model.AnnotatedConstruct;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+final class AnnotationDefaults implements AnnotationProcessor {
+
+    static final AnnotationDefaults INSTANCE = new AnnotationDefaults();
+
+    private AnnotationDefaults() {}
+
+    @Override
+    public Set<Class<? extends Annotation>> processes() {
+        return new HashSet<>(Arrays.asList(DefaultBoolean.class, DefaultDecimal.class, DefaultNumeric.class, DefaultString.class));
+    }
+
+    @Override
+    public void process(
+            final TypeElement targetInterface,
+            final ExecutableElement element,
+            final TypeMirror nodeType,
+            final FieldSpecBuilderTracker fieldSpec
+    ) throws IllegalStateException {
+        // there are two types of default values, one using annotations and one using the default value of a default method
+
+        // first, handle default value of a default method getter
+        if (element.isDefault() && element.getParameters().isEmpty() && hasNoAnnotationDefaults(element)) {
+            fieldSpec.initializer("$T.super.$L()", targetInterface, element.getSimpleName());
+            return;
+        }
+
+        // if it's not using the default value of a default method, use the annotations
+        final @Nullable DefaultBoolean defaultBoolean = annotation(element, DefaultBoolean.class);
+        final @Nullable DefaultDecimal defaultDecimal = annotation(element, DefaultDecimal.class);
+        final @Nullable DefaultNumeric defaultNumeric = annotation(element, DefaultNumeric.class);
+        final @Nullable DefaultString defaultString = annotation(element, DefaultString.class);
+        final boolean hasDefault = defaultBoolean != null || defaultDecimal != null || defaultNumeric != null || defaultString != null;
+
+        @Nullable Class<? extends Annotation> annnotationType = null;
+        @Nullable Object value = null;
+        if (hasDefault) {
+            if (MoreTypes.isTypeOf(Boolean.TYPE, nodeType)) {
+                if (defaultBoolean == null) {
+                    throw new IllegalStateException("A default value of the incorrect type was provided for " + element);
+                }
+                annnotationType = DefaultBoolean.class;
+                value = defaultBoolean.value();
+
+            } else if (Utils.isDecimal(nodeType)) {
+                if (defaultDecimal == null) {
+                    throw new IllegalStateException("A default value of the incorrect type was provided for " + element);
+                }
+                annnotationType = DefaultDecimal.class;
+                value = defaultDecimal.value();
+
+            } else if (Utils.isNumeric(nodeType)) {
+                if (defaultNumeric == null) {
+                    throw new IllegalStateException("A default value of the incorrect type was provided for " + element);
+                }
+                annnotationType = DefaultNumeric.class;
+                value = defaultNumeric.value();
+
+            } else if (MoreTypes.isTypeOf(String.class, nodeType)) {
+                if (defaultString == null) {
+                    throw new IllegalStateException("A default value of the incorrect type was provided for " + element);
+                }
+                annnotationType = DefaultString.class;
+                value = defaultString.value();
+            }
+        }
+
+        if (annnotationType == null) {
+            return;
+        }
+
+        final boolean isString = value instanceof String;
+
+        // special cases are floats and longs, because the default for decimals
+        // is double and for numerics it's int.
+        if (MoreTypes.isTypeOf(Float.TYPE, nodeType)) {
+            value = value + "F";
+        } else if (MoreTypes.isTypeOf(Long.TYPE, nodeType)) {
+            value = value + "L";
+        }
+
+        fieldSpec.addAnnotation(
+                AnnotationSpec.builder(annnotationType)
+                        .addMember("value", isString ? "$S" : "$L", value)
+        );
+        fieldSpec.initializer(isString ? "$S" : "$L", value);
+    }
+
+    static boolean hasNoAnnotationDefaults(final AnnotatedConstruct construct) {
+        for (Class<? extends Annotation> defaultAnnotation : INSTANCE.processes()) {
+            if (annotation(construct, defaultAnnotation) != null) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationHidden.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationHidden.java
new file mode 100644
index 000000000..b55cf42b4
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationHidden.java
@@ -0,0 +1,63 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import static org.spongepowered.configurate.interfaces.processor.Utils.hasAnnotation;
+
+import org.spongepowered.configurate.interfaces.meta.Hidden;
+
+import java.lang.annotation.Annotation;
+import java.util.Collections;
+import java.util.Set;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+final class AnnotationHidden implements AnnotationProcessor {
+
+    static final AnnotationHidden INSTANCE = new AnnotationHidden();
+
+    private AnnotationHidden() {}
+
+    @Override
+    public Set<Class<? extends Annotation>> processes() {
+        // the purpose of this class is to warn people, not to add the annotation.
+        // AnnotationOthers can do that just fine
+        return Collections.emptySet();
+    }
+
+    @Override
+    public void process(
+            final TypeElement targetInterface,
+            final ExecutableElement element,
+            final TypeMirror nodeType,
+            final FieldSpecBuilderTracker fieldSpec
+    ) throws IllegalStateException {
+        if (!element.isDefault()) {
+            return;
+        }
+
+        // throw exception to prevent unexpected behaviour during runtime
+        if (hasAnnotation(element, Hidden.class) && AnnotationDefaults.hasNoAnnotationDefaults(element)) {
+            throw new IllegalStateException(
+                    "Due to limitations there is no support for methods that use the default value and Hidden. Method: " + element
+            );
+        }
+    }
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationOthers.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationOthers.java
new file mode 100644
index 000000000..1452f96d6
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationOthers.java
@@ -0,0 +1,83 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import static org.spongepowered.configurate.interfaces.processor.Utils.annotation;
+
+import com.google.auto.common.MoreElements;
+import com.squareup.javapoet.AnnotationSpec;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+final class AnnotationOthers implements AnnotationProcessor {
+
+    static final AnnotationOthers INSTANCE = new AnnotationOthers();
+
+    private AnnotationOthers() {}
+
+    @Override
+    public Set<Class<? extends Annotation>> processes() {
+        return new HashSet<>();
+    }
+
+    @Override
+    public void process(
+            final TypeElement targetInterface,
+            final ExecutableElement element,
+            final TypeMirror nodeType,
+            final FieldSpecBuilderTracker fieldSpec) {
+        for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
+            //noinspection UnstableApiUsage
+            final TypeElement annotationType = MoreElements.asType(annotationMirror.getAnnotationType().asElement());
+
+            // only handle not yet processed annotations
+            if (fieldSpec.isProcessed(annotationType)) {
+                continue;
+            }
+
+            final @Nullable Target target = annotation(annotationType, Target.class);
+            final boolean isCompatible = target == null || Arrays.stream(target.value()).anyMatch(elementType -> ElementType.FIELD == elementType);
+            // an annotation is only compatible if it supports fields, if it has no target it supports everything
+            if (!isCompatible) {
+                continue;
+            }
+
+            final @Nullable Retention retention = annotation(annotationType, Retention.class);
+            final boolean hasRuntimeRetention = retention != null && RetentionPolicy.RUNTIME == retention.value();
+            // not needed to add an annotation if it has no runtime retention
+            if (!hasRuntimeRetention) {
+                continue;
+            }
+
+            fieldSpec.addAnnotation(AnnotationSpec.get(annotationMirror));
+        }
+    }
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessor.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessor.java
new file mode 100644
index 000000000..c4468c704
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessor.java
@@ -0,0 +1,51 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import java.lang.annotation.Annotation;
+import java.util.Set;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+interface AnnotationProcessor {
+
+    /**
+     * A set of annotations this processor will process.
+     *
+     * @return a set of annotations this processor will process.
+     */
+    Set<Class<? extends Annotation>> processes();
+
+    /**
+     * Process a method.
+     * There is no guarantee that one of the {@link #processes()} annotations is present on this element.
+     *
+     * @param element the method that is being processed
+     * @param nodeType the type of the field that is being generated
+     * @param fieldSpec the builder of the field that is being generated
+     * @throws IllegalStateException when something goes wrong
+     */
+    void process(
+            TypeElement targetInterface,
+            ExecutableElement element,
+            TypeMirror nodeType,
+            FieldSpecBuilderTracker fieldSpec
+    ) throws IllegalStateException;
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessorHandler.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessorHandler.java
new file mode 100644
index 000000000..b16cd6a30
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessorHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import com.squareup.javapoet.FieldSpec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+
+final class AnnotationProcessorHandler {
+
+    private static final List<AnnotationProcessor> HANDLERS = new ArrayList<>();
+
+    static {
+        HANDLERS.add(AnnotationDefaults.INSTANCE);
+        HANDLERS.add(AnnotationHidden.INSTANCE);
+        // always add others as last because it adds all annotations that have not been processed
+        HANDLERS.add(AnnotationOthers.INSTANCE);
+    }
+
+    private AnnotationProcessorHandler() {}
+
+    static void handle(
+            final TypeElement targetInterface,
+            final ExecutableElement element,
+            final TypeMirror nodeType,
+            final FieldSpec.Builder fieldSpec) {
+        final FieldSpecBuilderTracker fieldTracker = new FieldSpecBuilderTracker(fieldSpec);
+
+        for (AnnotationProcessor handler : HANDLERS) {
+            handler.process(targetInterface, element, nodeType, fieldTracker);
+            fieldTracker.processed(handler.processes());
+        }
+    }
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerator.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerator.java
new file mode 100644
index 000000000..2283ac939
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerator.java
@@ -0,0 +1,287 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import static org.spongepowered.configurate.interfaces.processor.Utils.hasAnnotation;
+
+import com.google.auto.common.MoreTypes;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.interfaces.meta.Exclude;
+import org.spongepowered.configurate.interfaces.meta.Field;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.PostProcess;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeMirror;
+
+class ConfigImplementationGenerator {
+
+    private final ConfigImplementationGeneratorProcessor processor;
+    private final TypeElement source;
+
+    ConfigImplementationGenerator(
+        final ConfigImplementationGeneratorProcessor processor,
+        final TypeElement source
+    ) {
+        this.processor = processor;
+        this.source = source;
+    }
+
+    /**
+     * Returns the generated class or null if something went wrong during
+     * generation.
+     */
+    public TypeSpec.@Nullable Builder generate() {
+        final ClassName className = ClassName.get(this.source);
+
+        final TypeSpec.Builder spec = TypeSpec
+            .classBuilder(className.simpleName() + "Impl")
+            .addSuperinterface(className)
+            .addModifiers(Modifier.FINAL)
+            .addAnnotation(ConfigSerializable.class)
+            .addJavadoc("Automatically generated implementation of the config");
+
+        final TypeSpecBuilderTracker tracker = new TypeSpecBuilderTracker();
+        if (!gatherElementSpec(tracker, this.source)) {
+            return null;
+        }
+        tracker.writeTo(spec);
+
+        final String qualifiedName = className.reflectionName();
+        final String qualifiedImplName = qualifiedName.replace("$", "Impl$") + "Impl";
+        this.processor.generatedClasses().put(qualifiedName, qualifiedImplName);
+
+        return spec;
+    }
+
+    /**
+     * Returns true if successful, otherwise false.
+     */
+    private boolean gatherElementSpec(
+        final TypeSpecBuilderTracker spec,
+        final TypeElement type
+    ) {
+        return gatherElementSpec(spec, type, new HashSet<>());
+    }
+
+    /**
+     * Returns true if successful, otherwise false.
+     *
+     * @param excludedElements a set of all elements a superclass has annotated with {@link Exclude}.
+     */
+    private boolean gatherElementSpec(
+        final TypeSpecBuilderTracker spec,
+        final TypeElement type,
+        final Set<Name> excludedElements
+    ) {
+        // first handle own elements
+        // If this interface is noted as ConfigSerializable, then its element order should override previous configs
+        final boolean hasConfigSerializable = hasAnnotation(type, ConfigSerializable.class);
+
+        for (final Element enclosedElement : type.getEnclosedElements()) {
+            final ElementKind kind = enclosedElement.getKind();
+
+            if (kind == ElementKind.INTERFACE && hasAnnotation(enclosedElement, ConfigSerializable.class)) {
+                final TypeSpec.@Nullable Builder generated =
+                        new ConfigImplementationGenerator(this.processor, (TypeElement) enclosedElement)
+                                .generate();
+
+                // if something went wrong in the child class, the parent can't complete normally either
+                if (generated == null) {
+                    return false;
+                }
+
+                spec.add(enclosedElement.getSimpleName().toString(), generated.addModifiers(Modifier.STATIC));
+                continue;
+            }
+
+            if (kind != ElementKind.METHOD) {
+                continue;
+            }
+
+            final ExecutableElement element = (ExecutableElement) enclosedElement;
+
+            if (hasAnnotation(element, PostProcess.class)) {
+                // A postprocess annotated method is not a config node
+                continue;
+            }
+
+            if (excludedElements.contains(element.getSimpleName())) {
+                continue;
+            }
+            final boolean excluded = hasAnnotation(element, Exclude.class);
+            if (excluded) {
+                if (element.isDefault()) {
+                    // Do not add setters to the exclusion list as they will not be serialized anyway.
+                    if (element.getParameters().isEmpty()) {
+                        excludedElements.add(element.getSimpleName());
+                    }
+                    continue;
+                }
+                this.processor.printError(
+                        "Cannot make config due to method %s, which is an excluded method that has no implementation!",
+                        element
+                );
+                return false;
+            }
+
+            // all methods are either setters or getters past this point
+
+            final List<? extends VariableElement> parameters = element.getParameters();
+            if (parameters.size() > 1) {
+                this.processor.printError("Setters cannot have more than one parameter! Method: " + element);
+                return false;
+            }
+
+            final String simpleName = element.getSimpleName().toString();
+            TypeMirror nodeType = element.getReturnType();
+
+            if (parameters.size() == 1) {
+                // setter
+                final VariableElement parameter = parameters.get(0);
+                final boolean success = handleSetter(element, simpleName, parameter, nodeType, spec);
+                if (!success) {
+                    return false;
+                }
+                nodeType = parameter.asType();
+            } else {
+                handleGetter(element, simpleName, nodeType, spec);
+            }
+
+            final FieldSpec.Builder fieldSpec = FieldSpec.builder(TypeName.get(nodeType), simpleName, Modifier.PRIVATE);
+
+            final boolean isField = hasAnnotation(element, Field.class);
+            if (isField) {
+                fieldSpec.addModifiers(Modifier.TRANSIENT);
+            }
+
+            // set a default value for config subsections
+            final TypeElement nodeTypeElement = Utils.toBoxedTypeElement(nodeType, this.processor.typeUtils);
+            if (!isField && !element.isDefault() && hasAnnotation(nodeTypeElement, ConfigSerializable.class)) {
+                ClassName configClass = ClassName.get(nodeTypeElement);
+                if (nodeTypeElement.getKind().isInterface()) {
+                    // first find the generated class for given type
+                    String implName = this.processor.generatedClasses().getProperty(configClass.reflectionName());
+                    if (implName == null) {
+                        this.processor.printError("Could not determine an implementation type for method " + element.getSimpleName());
+                        return false;
+                    }
+                    // make it canonical and replace superinterface type with source interface type if present
+                    implName = implName.replace('$', '.').replace(type.getQualifiedName(), this.source.getQualifiedName());
+                    configClass = ClassName.bestGuess(implName);
+                }
+                fieldSpec.initializer("new $T()", configClass);
+            }
+
+            //todo add tests for hidden in both ap and interfaces and defaults in interfaces
+            AnnotationProcessorHandler.handle(this.source, element, nodeType, fieldSpec);
+
+            // If this is a getter and ConfigSerializable, then it should define where in the config
+            // this element should go.
+            spec.add(simpleName, fieldSpec, hasConfigSerializable && element.getParameters().isEmpty());
+        }
+
+        // then handle parent elements
+        for (final TypeMirror parent : type.getInterfaces()) {
+            gatherElementSpec(spec, (TypeElement) this.processor.typeUtils.asElement(parent), excludedElements);
+        }
+        return true;
+    }
+
+    private boolean handleSetter(
+            final ExecutableElement element,
+            final String simpleName,
+            final VariableElement parameter,
+            final TypeMirror returnType,
+            final TypeSpecBuilderTracker spec
+    ) {
+        final MethodSpec.Builder method = MethodSpec.overriding(element);
+
+        // we have two main branches of setters, default non-void setters and non-default any setters
+        if (element.isDefault()) {
+            if (MoreTypes.isTypeOf(Void.TYPE, returnType)) {
+                this.processor.printError("A default setter cannot have void as return type. Method: " + element);
+                return false;
+            }
+
+            method.addStatement(
+                    "this.$N = $T.super.$L($N)",
+                    simpleName,
+                    element.getEnclosingElement(),
+                    simpleName,
+                    parameter.getSimpleName()
+            );
+        } else {
+            method.addStatement("this.$N = $N", simpleName, parameter.getSimpleName());
+        }
+
+        // if it's not void
+        if (!MoreTypes.isTypeOf(Void.TYPE, returnType)) {
+            // the return type can be a parent type of parameter, but it has to be assignable
+            if (!this.processor.typeUtils.isAssignable(parameter.asType(), returnType)) {
+                this.processor.printError(
+                        "Cannot create a setter with return type %s for argument type %s. Method: %s",
+                        returnType,
+                        parameter.asType(),
+                        element
+                );
+                return false;
+            }
+            method.addStatement("return this.$N", simpleName);
+        }
+
+        spec.add(simpleName + "#" + parameter.getSimpleName(), method);
+        return true;
+    }
+
+    private void handleGetter(
+            final ExecutableElement element,
+            final String simpleName,
+            final TypeMirror nodeType,
+            final TypeSpecBuilderTracker spec
+    ) {
+        // voids aren't valid
+        if (MoreTypes.isTypeOf(Void.TYPE, nodeType)) {
+            this.processor.printError(
+                    "Cannot create a getter with return type void for method %s, did you forget to @Exclude this method?",
+                    element
+            );
+        }
+
+        spec.add(
+                simpleName,
+                MethodSpec.overriding(element)
+                        .addStatement("return $N", element.getSimpleName())
+        );
+    }
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGeneratorProcessor.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGeneratorProcessor.java
new file mode 100644
index 000000000..ba1dc9f8a
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGeneratorProcessor.java
@@ -0,0 +1,150 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import static org.spongepowered.configurate.interfaces.processor.Utils.isNestedConfig;
+
+import com.google.auto.service.AutoService;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.JavaFile;
+import com.squareup.javapoet.TypeSpec;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.interfaces.Constants;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.Types;
+import javax.tools.Diagnostic.Kind;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Generates an implementation for a given interface based config,
+ * which then can be read by Configurate.
+ *
+ * @since 4.2.0
+ */
+@AutoService(Processor.class)
+public final class ConfigImplementationGeneratorProcessor extends AbstractProcessor {
+
+    private final Properties mappings = new Properties();
+    Types typeUtils;
+    private Filer filer;
+    private Messager messager;
+
+    @Override
+    @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel")
+    public synchronized void init(final ProcessingEnvironment processingEnv) {
+        super.init(processingEnv);
+        this.typeUtils = processingEnv.getTypeUtils();
+        this.filer = processingEnv.getFiler();
+        this.messager = processingEnv.getMessager();
+    }
+
+    @Override
+    public Set<String> getSupportedAnnotationTypes() {
+        return Collections.singleton(ConfigSerializable.class.getCanonicalName());
+    }
+
+    @Override
+    public boolean process(final Set<? extends TypeElement> ignored, final RoundEnvironment env) {
+        if (env.processingOver()) {
+            if (!env.errorRaised()) {
+                writeMappings();
+            }
+            return false;
+        }
+
+        for (final Element element : env.getElementsAnnotatedWith(ConfigSerializable.class)) {
+            if (element.getKind() != ElementKind.INTERFACE) {
+                continue;
+            }
+            final TypeElement typeElement = (TypeElement) element;
+
+            // nested classes are handled in their containing interfaces
+            if (isNestedConfig(typeElement)) {
+                continue;
+            }
+
+            try {
+                processInterface(typeElement);
+            } catch (final IOException exception) {
+                printError(exception.getMessage());
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Generate a class for the given interface.
+     */
+    private void processInterface(final TypeElement type) throws IOException {
+        final ClassName className = ClassName.get(type);
+
+        final TypeSpec.@Nullable Builder generated = new ConfigImplementationGenerator(this, type).generate();
+        if (generated == null) {
+            return;
+        }
+
+        JavaFile.builder(className.packageName(), generated.build())
+            .build()
+            .writeTo(this.filer);
+    }
+
+    @Override
+    public SourceVersion getSupportedSourceVersion() {
+        return SourceVersion.latest();
+    }
+
+    private void writeMappings() {
+        final FileObject resource;
+        try {
+            resource = this.filer.createResource(StandardLocation.CLASS_OUTPUT, "", Constants.MAPPING_FILE);
+            try (Writer writer = resource.openWriter()) {
+                this.mappings.store(writer, null);
+            }
+        } catch (final IOException exception) {
+            throw new RuntimeException("Failed to write interface mappings!", exception);
+        }
+    }
+
+    Properties generatedClasses() {
+        return this.mappings;
+    }
+
+    void printError(final String message, final Object... arguments) {
+        this.messager.printMessage(Kind.ERROR, String.format(Locale.ROOT, message, arguments));
+    }
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/FieldSpecBuilderTracker.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/FieldSpecBuilderTracker.java
new file mode 100644
index 000000000..22578ee33
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/FieldSpecBuilderTracker.java
@@ -0,0 +1,66 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.FieldSpec;
+
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.lang.model.element.TypeElement;
+
+final class FieldSpecBuilderTracker {
+
+    private final Set<ClassName> processed = new HashSet<>();
+    private final FieldSpec.Builder builder;
+
+    FieldSpecBuilderTracker(final FieldSpec.Builder builder) {
+        this.builder = builder;
+    }
+
+    void addAnnotation(final AnnotationSpec annotation) {
+        this.processed.add(((ClassName) annotation.type));
+        this.builder.addAnnotation(annotation);
+    }
+
+    void addAnnotation(final AnnotationSpec.Builder annotationBuilder) {
+        addAnnotation(annotationBuilder.build());
+    }
+
+    void initializer(final String format, final Object... args) {
+        this.builder.initializer(format, args);
+    }
+
+    boolean isProcessed(final Class<? extends Annotation> annotation) {
+        return this.processed.contains(ClassName.get(annotation));
+    }
+
+    boolean isProcessed(final TypeElement annotationType) {
+        return this.processed.contains(ClassName.get(annotationType));
+    }
+
+    void processed(final Collection<Class<? extends Annotation>> annotations) {
+        for (Class<? extends Annotation> annotation : annotations) {
+            this.processed.add(ClassName.get(annotation));
+        }
+    }
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/TypeSpecBuilderTracker.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/TypeSpecBuilderTracker.java
new file mode 100644
index 000000000..da3cd230a
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/TypeSpecBuilderTracker.java
@@ -0,0 +1,101 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.TypeSpec;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link TypeSpec.Builder} does not keep track of duplicates, resulting in failures to compile.
+ * This will only allow a single definition of a given method/field
+ */
+final class TypeSpecBuilderTracker {
+
+    private final Map<String, FieldSpec.Builder> fieldSpecs = new LinkedHashMap<>();
+    private final Map<String, MethodSpec.Builder> methodSpecs = new LinkedHashMap<>();
+    private final Map<String, TypeSpec> typeSpecs = new LinkedHashMap<>();
+
+    void add(final String fieldIdentifier, final FieldSpec.Builder builder, final boolean override) {
+        final FieldSpec.Builder existing = override ? this.fieldSpecs.remove(fieldIdentifier)
+                : this.fieldSpecs.get(fieldIdentifier);
+        if (existing != null) {
+            final FieldSpec existingBuild = existing.build();
+            final FieldSpec builderBuild = builder.build();
+            // copy initializer of the builder to the existing one if the existing one doesn't have an initializer
+            if (existingBuild.initializer.isEmpty() && !builderBuild.initializer.isEmpty()) {
+                existing.initializer(builderBuild.initializer);
+            }
+            existing.addAnnotations(pickNewAnnotations(existingBuild.annotations, builderBuild.annotations));
+            if (override) {
+                this.fieldSpecs.put(fieldIdentifier, existing);
+            }
+            return;
+        }
+        this.fieldSpecs.put(fieldIdentifier, builder);
+    }
+
+    void add(final String methodIdentifier, final MethodSpec.Builder builder) {
+        final MethodSpec.Builder existing = this.methodSpecs.get(methodIdentifier);
+        if (existing != null) {
+            existing.addAnnotations(pickNewAnnotations(existing.build().annotations, builder.build().annotations));
+            return;
+        }
+        this.methodSpecs.put(methodIdentifier, builder);
+    }
+
+    void add(final String typeIdentifier, final TypeSpec.Builder builder) {
+        if (this.typeSpecs.putIfAbsent(typeIdentifier, builder.build()) != null) {
+            throw new IllegalStateException(
+                "Cannot have multiple nested types with the same name! Name: " + typeIdentifier);
+        }
+    }
+
+    void writeTo(final TypeSpec.Builder builder) {
+        for (FieldSpec.Builder field : this.fieldSpecs.values()) {
+            builder.addField(field.build());
+        }
+        for (MethodSpec.Builder method : this.methodSpecs.values()) {
+            builder.addMethod(method.build());
+        }
+        this.typeSpecs.values().forEach(builder::addType);
+    }
+
+    private List<AnnotationSpec> pickNewAnnotations(
+        final List<AnnotationSpec> existing,
+        final List<AnnotationSpec> newOne
+    ) {
+        final List<AnnotationSpec> result = new ArrayList<>();
+        // only add annotations if they don't already exist
+        outer: for (AnnotationSpec spec : newOne) {
+            for (AnnotationSpec existingSpec : existing) {
+                if (existingSpec.type.equals(spec.type)) {
+                    break outer;
+                }
+            }
+            result.add(spec);
+        }
+        return result;
+    }
+
+}
diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/Utils.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/Utils.java
new file mode 100644
index 000000000..8f9143499
--- /dev/null
+++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/Utils.java
@@ -0,0 +1,78 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import com.google.auto.common.MoreTypes;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+import java.lang.annotation.Annotation;
+
+import javax.lang.model.AnnotatedConstruct;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Types;
+
+final class Utils {
+
+    private Utils() {}
+
+    static boolean hasAnnotation(final AnnotatedConstruct element, final Class<? extends Annotation> annotation) {
+        return annotation(element, annotation) != null;
+    }
+
+    /**
+     * The same as {@link AnnotatedConstruct#getAnnotation(Class)} except that
+     * you don't have to suppress the ConstantValue warning everywhere.
+     */
+    @SuppressWarnings("DataFlowIssue")
+    static <T extends Annotation> @Nullable T annotation(final AnnotatedConstruct construct, final Class<T> annotation) {
+        return construct.getAnnotation(annotation);
+    }
+
+    static boolean isNestedConfig(final TypeElement type) {
+        if (!type.getNestingKind().isNested()) {
+            return false;
+        }
+
+        Element current = type;
+        while (current.getKind() == ElementKind.INTERFACE && hasAnnotation(current, ConfigSerializable.class)) {
+            current = current.getEnclosingElement();
+        }
+        return current.getKind() == ElementKind.PACKAGE;
+    }
+
+    static boolean isDecimal(final TypeMirror typeMirror) {
+        return MoreTypes.isTypeOf(Float.TYPE, typeMirror) || MoreTypes.isTypeOf(Double.TYPE, typeMirror);
+    }
+
+    static boolean isNumeric(final TypeMirror typeMirror) {
+        return MoreTypes.isTypeOf(Byte.TYPE, typeMirror) || MoreTypes.isTypeOf(Character.TYPE, typeMirror)
+            || MoreTypes.isTypeOf(Short.TYPE, typeMirror) || MoreTypes.isTypeOf(Integer.TYPE, typeMirror)
+            || MoreTypes.isTypeOf(Long.TYPE, typeMirror);
+    }
+
+    public static TypeElement toBoxedTypeElement(final TypeMirror mirror, final Types typeUtils) {
+        if (mirror.getKind().isPrimitive()) {
+            return typeUtils.boxedClass(MoreTypes.asPrimitiveType(mirror));
+        }
+        return MoreTypes.asTypeElement(mirror);
+    }
+
+}
diff --git a/extra/interface/ap/src/main/resources/META-INF/gradle/incremental.annotation.processors b/extra/interface/ap/src/main/resources/META-INF/gradle/incremental.annotation.processors
new file mode 100644
index 000000000..287495b9a
--- /dev/null
+++ b/extra/interface/ap/src/main/resources/META-INF/gradle/incremental.annotation.processors
@@ -0,0 +1 @@
+org.spongepowered.configurate.interfaces.processor.ConfigImplementationGeneratorProcessor,aggregating
diff --git a/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerationTest.java b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerationTest.java
new file mode 100644
index 000000000..e09b6e295
--- /dev/null
+++ b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerationTest.java
@@ -0,0 +1,48 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import static org.spongepowered.configurate.interfaces.processor.TestUtils.testCompilation;
+
+import org.junit.experimental.runners.Enclosed;
+import org.junit.jupiter.api.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(Enclosed.class)
+class ConfigImplementationGenerationTest {
+
+    @Test
+    void testBasicCompilation() {
+        testCompilation("structure/BasicConfig");
+    }
+
+    @Test
+    void testExtendedCompilation() {
+        testCompilation("structure/ExtendedConfig");
+    }
+
+    @Test
+    void testMultiLayerCompilation() {
+        testCompilation("structure/MultiLayerConfig");
+    }
+
+    @Test
+    void testAnnotationOthersCompilation() {
+        testCompilation("test/OtherAnnotations");
+    }
+
+}
diff --git a/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/DefaultValueTest.java b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/DefaultValueTest.java
new file mode 100644
index 000000000..2e69455ed
--- /dev/null
+++ b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/DefaultValueTest.java
@@ -0,0 +1,35 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import static org.spongepowered.configurate.interfaces.processor.TestUtils.testCompilation;
+
+import org.junit.jupiter.api.Test;
+
+class DefaultValueTest {
+
+    @Test
+    void testCorrectDefaults() {
+        testCompilation("defaults/CorrectDefaults");
+    }
+
+    @Test
+    void testMultipleDefaults() {
+        testCompilation("defaults/MultipleDefaults");
+    }
+
+}
diff --git a/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/TestUtils.java b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/TestUtils.java
new file mode 100644
index 000000000..8d103c44a
--- /dev/null
+++ b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/TestUtils.java
@@ -0,0 +1,104 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+import static com.google.testing.compile.Compiler.javac;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+
+import com.google.common.io.Resources;
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.JavaFileObjects;
+import org.spongepowered.configurate.interfaces.Constants;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import javax.tools.StandardLocation;
+
+final class TestUtils {
+
+    private TestUtils() {
+    }
+
+    /**
+     * Tests whether the compilation is successful, that the correct mappings
+     * have been made and that the generated impl matches the expected impl.
+     */
+    static Compilation testCompilation(final String sourceResourceName) {
+        final Compilation compilation =
+            javac()
+                .withProcessors(new ConfigImplementationGeneratorProcessor())
+                .compile(JavaFileObjects.forResource(sourceResourceName + ".java"));
+
+        final String targetResourceName = sourceResourceName + "Impl";
+        final String targetSourceName = targetResourceName.replace('/', '.');
+
+        assertThat(compilation).succeeded();
+        assertThat(compilation)
+            .generatedSourceFile(targetSourceName)
+            .hasSourceEquivalentTo(JavaFileObjects.forResource(targetResourceName + ".java"));
+
+        try {
+
+            final String actualContent = compilation
+                .generatedFile(StandardLocation.CLASS_OUTPUT, Constants.MAPPING_FILE)
+                .orElseThrow(() -> new IllegalStateException("Expected the interface mappings file to be created"))
+                .getCharContent(false)
+                .toString();
+
+            final List<String> expectedLines = readOrGenerateMappings(sourceResourceName, targetResourceName);
+
+            assertIterableEquals(expectedLines, removeComments(actualContent));
+        } catch (final IOException exception) {
+            throw new RuntimeException(exception);
+        }
+
+        return compilation;
+    }
+
+    private static List<String> removeComments(final String content) {
+        return Arrays.stream(content.split(System.lineSeparator()))
+            .filter(line -> !line.startsWith("#"))
+            .collect(Collectors.toList());
+    }
+
+    private static List<String> readOrGenerateMappings(final String sourceResourceName, final String targetResourceName) {
+        try {
+            final URL localMappings = Resources.getResource(sourceResourceName + ".properties");
+            return Resources.asCharSource(localMappings, StandardCharsets.UTF_8).readLines();
+        } catch (final IllegalArgumentException ignored) {
+            // we only support generating simple (not nested) configs,
+            // for complexer configs we need a mappings file
+            return Collections.singletonList(String.format(
+                Locale.ROOT,
+                "%s=%s",
+                sourceResourceName.replace('/', '.'),
+                targetResourceName.replace('/', '.')
+            ));
+        } catch (final IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
diff --git a/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/util/AnnotationOthersAnnotations.java b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/util/AnnotationOthersAnnotations.java
new file mode 100644
index 000000000..97c0fb763
--- /dev/null
+++ b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/util/AnnotationOthersAnnotations.java
@@ -0,0 +1,40 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.processor.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+public final class AnnotationOthersAnnotations {
+
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface AnnotationNoField {}
+
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface AnnotationNoTarget {}
+
+    @Target({ElementType.METHOD, ElementType.FIELD})
+    @Retention(RetentionPolicy.CLASS)
+    public @interface AnnotationOtherRetention {}
+
+    @Target({ElementType.METHOD, ElementType.FIELD})
+    public @interface AnnotationNoRetention {}
+
+}
diff --git a/extra/interface/ap/src/test/resources/defaults/CorrectDefaults.java b/extra/interface/ap/src/test/resources/defaults/CorrectDefaults.java
new file mode 100644
index 000000000..e52b15958
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/defaults/CorrectDefaults.java
@@ -0,0 +1,78 @@
+package defaults;
+
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+@ConfigSerializable
+public interface CorrectDefaults {
+    @DefaultBoolean
+    boolean apple();
+
+    @DefaultBoolean(true)
+    boolean blueberry();
+
+    @DefaultDecimal
+    float cherry();
+
+    @DefaultDecimal(123.5)
+    float dragonfruit();
+
+    @DefaultDecimal
+    double eggplant();
+
+    @DefaultDecimal(234.23)
+    double fig();
+
+    @DefaultNumeric
+    byte grape();
+
+    @DefaultNumeric(127)
+    byte huckleberry();
+
+    @DefaultNumeric
+    char italianPrunePlum();
+
+    @DefaultNumeric(126)
+    char jackfruit();
+
+    @DefaultNumeric('c')
+    char kiwi();
+
+    @DefaultNumeric
+    short lemon();
+
+    @DefaultNumeric(1341)
+    short mango();
+
+    @DefaultNumeric
+    int nectarine();
+
+    @DefaultNumeric(1231241)
+    int orange();
+
+    @DefaultNumeric
+    long pineapple();
+
+    @DefaultNumeric(24524524521L)
+    long quince();
+
+    @DefaultString
+    String raspberry();
+
+    @DefaultString("Hello world!")
+    String strawberry();
+
+    @DefaultString("Hi")
+    void tamarillo(String value);
+
+    default String ugli() {
+        return "A fruit";
+    }
+
+    default int velvetApple() {
+        return 500;
+    }
+}
diff --git a/extra/interface/ap/src/test/resources/defaults/CorrectDefaultsImpl.java b/extra/interface/ap/src/test/resources/defaults/CorrectDefaultsImpl.java
new file mode 100644
index 000000000..3e6b9486b
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/defaults/CorrectDefaultsImpl.java
@@ -0,0 +1,188 @@
+package defaults;
+
+import java.lang.Override;
+import java.lang.String;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+/**
+ * Automatically generated implementation of the config */
+@ConfigSerializable
+final class CorrectDefaultsImpl implements CorrectDefaults {
+    @DefaultBoolean(false)
+    private boolean apple = false;
+
+    @DefaultBoolean(true)
+    private boolean blueberry = true;
+
+    @DefaultDecimal(0.0F)
+    private float cherry = 0.0F;
+
+    @DefaultDecimal(123.5F)
+    private float dragonfruit = 123.5F;
+
+    @DefaultDecimal(0.0D)
+    private double eggplant = 0.0D;
+
+    @DefaultDecimal(234.23D)
+    private double fig = 234.23D;
+
+    @DefaultNumeric(0)
+    private byte grape = 0;
+
+    @DefaultNumeric(127)
+    private byte huckleberry = 127;
+
+    @DefaultNumeric(0)
+    private char italianPrunePlum = 0;
+
+    @DefaultNumeric(126)
+    private char jackfruit = 126;
+
+    @DefaultNumeric(99)
+    private char kiwi = 99;
+
+    @DefaultNumeric(0)
+    private short lemon = 0;
+
+    @DefaultNumeric(1341)
+    private short mango = 1341;
+
+    @DefaultNumeric(0)
+    private int nectarine = 0;
+
+    @DefaultNumeric(1231241)
+    private int orange = 1231241;
+
+    @DefaultNumeric(0L)
+    private long pineapple = 0L;
+
+    @DefaultNumeric(24524524521L)
+    private long quince = 24524524521L;
+
+    @DefaultString("")
+    private String raspberry = "";
+
+    @DefaultString("Hello world!")
+    private String strawberry = "Hello world!";
+
+    @DefaultString("Hi")
+    private String tamarillo = "Hi";
+
+    private String ugli = CorrectDefaults.super.ugli();
+
+    private int velvetApple = CorrectDefaults.super.velvetApple();
+
+    @Override
+    public boolean apple() {
+        return apple;
+    }
+
+    @Override
+    public boolean blueberry() {
+        return blueberry;
+    }
+
+    @Override
+    public float cherry() {
+        return cherry;
+    }
+
+    @Override
+    public float dragonfruit() {
+        return dragonfruit;
+    }
+
+    @Override
+    public double eggplant() {
+        return eggplant;
+    }
+
+    @Override
+    public double fig() {
+        return fig;
+    }
+
+    @Override
+    public byte grape() {
+        return grape;
+    }
+
+    @Override
+    public byte huckleberry() {
+        return huckleberry;
+    }
+
+    @Override
+    public char italianPrunePlum() {
+        return italianPrunePlum;
+    }
+
+    @Override
+    public char jackfruit() {
+        return jackfruit;
+    }
+
+    @Override
+    public char kiwi() {
+        return kiwi;
+    }
+
+    @Override
+    public short lemon() {
+        return lemon;
+    }
+
+    @Override
+    public short mango() {
+        return mango;
+    }
+
+    @Override
+    public int nectarine() {
+        return nectarine;
+    }
+
+    @Override
+    public int orange() {
+        return orange;
+    }
+
+    @Override
+    public long pineapple() {
+        return pineapple;
+    }
+
+    @Override
+    public long quince() {
+        return quince;
+    }
+
+    @Override
+    public String raspberry() {
+        return raspberry;
+    }
+
+    @Override
+    public String strawberry() {
+        return strawberry;
+    }
+
+    @Override
+    public void tamarillo(String value) {
+        this.tamarillo = value;
+    }
+
+    @Override
+    public String ugli() {
+        return ugli;
+    }
+
+    @Override
+    public int velvetApple() {
+        return velvetApple;
+    }
+}
diff --git a/extra/interface/ap/src/test/resources/defaults/MultipleDefaults.java b/extra/interface/ap/src/test/resources/defaults/MultipleDefaults.java
new file mode 100644
index 000000000..a6d7948e3
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/defaults/MultipleDefaults.java
@@ -0,0 +1,22 @@
+package defaults;
+
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+@ConfigSerializable
+public interface MultipleDefaults {
+    @DefaultBoolean(true)
+    @DefaultDecimal(2)
+    @DefaultNumeric(3)
+    @DefaultString("Hi!")
+    int multipleSingle();
+
+    @DefaultString("Hey!")
+    String multipleOverride();
+
+    @DefaultString("Hello!")
+    void multipleOverride(String x);
+}
diff --git a/extra/interface/ap/src/test/resources/defaults/MultipleDefaultsImpl.java b/extra/interface/ap/src/test/resources/defaults/MultipleDefaultsImpl.java
new file mode 100644
index 000000000..384241bdc
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/defaults/MultipleDefaultsImpl.java
@@ -0,0 +1,33 @@
+package defaults;
+
+import java.lang.Override;
+import java.lang.String;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+/**
+ * Automatically generated implementation of the config */
+@ConfigSerializable
+final class MultipleDefaultsImpl implements MultipleDefaults {
+    @DefaultNumeric(3)
+    private int multipleSingle = 3;
+
+    @DefaultString("Hey!")
+    private String multipleOverride = "Hey!"; // this is expected as "Hey!" is handled before "Hello!"
+
+    @Override
+    public int multipleSingle() {
+        return multipleSingle;
+    }
+
+    @Override
+    public String multipleOverride() {
+        return multipleOverride;
+    }
+
+    @Override
+    public void multipleOverride(String x) {
+        this.multipleOverride = x;
+    }
+}
diff --git a/extra/interface/ap/src/test/resources/structure/BasicConfig.java b/extra/interface/ap/src/test/resources/structure/BasicConfig.java
new file mode 100644
index 000000000..483ba767c
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/structure/BasicConfig.java
@@ -0,0 +1,16 @@
+package structure;
+
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+@ConfigSerializable
+public interface BasicConfig {
+    String hello();
+
+    void hi(String value);
+
+    String hello(String value);
+
+    default String hey(String value) {
+        return "Hello";
+    }
+}
diff --git a/extra/interface/ap/src/test/resources/structure/BasicConfigImpl.java b/extra/interface/ap/src/test/resources/structure/BasicConfigImpl.java
new file mode 100644
index 000000000..9f850914e
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/structure/BasicConfigImpl.java
@@ -0,0 +1,38 @@
+package structure;
+
+import java.lang.Override;
+import java.lang.String;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+/**
+ * Automatically generated implementation of the config */
+@ConfigSerializable
+final class BasicConfigImpl implements BasicConfig {
+    private String hello;
+
+    private String hi;
+
+    private String hey;
+
+    @Override
+    public String hello() {
+        return hello;
+    }
+
+    @Override
+    public void hi(String value) {
+        this.hi = value;
+    }
+
+    @Override
+    public String hello(String value) {
+        this.hello = value;
+        return this.hello;
+    }
+
+    @Override
+    public String hey(String value) {
+        this.hey = BasicConfig.super.hey(value);
+        return this.hey;
+    }
+}
diff --git a/extra/interface/ap/src/test/resources/structure/ExtendedConfig.java b/extra/interface/ap/src/test/resources/structure/ExtendedConfig.java
new file mode 100644
index 000000000..78ba9a1c3
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/structure/ExtendedConfig.java
@@ -0,0 +1,10 @@
+package structure;
+
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+@ConfigSerializable
+public interface ExtendedConfig extends BasicConfig {
+    String hi();
+
+    Number hey(int value);
+}
diff --git a/extra/interface/ap/src/test/resources/structure/ExtendedConfigImpl.java b/extra/interface/ap/src/test/resources/structure/ExtendedConfigImpl.java
new file mode 100644
index 000000000..a2fc4e63b
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/structure/ExtendedConfigImpl.java
@@ -0,0 +1,44 @@
+package structure;
+
+import java.lang.Number;
+import java.lang.Override;
+import java.lang.String;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+/**
+ * Automatically generated implementation of the config */
+@ConfigSerializable
+final class ExtendedConfigImpl implements ExtendedConfig {
+    private String hi;
+
+    private int hey;
+
+    private String hello;
+
+    @Override
+    public String hi() {
+        return hi;
+    }
+
+    @Override
+    public Number hey(int value) {
+        this.hey = value;
+        return this.hey;
+    }
+
+    @Override
+    public String hello() {
+        return hello;
+    }
+
+    @Override
+    public void hi(String value) {
+        this.hi = value;
+    }
+
+    @Override
+    public String hello(String value) {
+        this.hello = value;
+        return this.hello;
+    }
+}
diff --git a/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.java b/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.java
new file mode 100644
index 000000000..33de1c0eb
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.java
@@ -0,0 +1,15 @@
+package structure;
+
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+@ConfigSerializable
+public interface MultiLayerConfig {
+    String test();
+    SecondLayer second();
+
+    @ConfigSerializable
+    public interface SecondLayer {
+        String test();
+        String test2();
+    }
+}
diff --git a/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.properties b/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.properties
new file mode 100644
index 000000000..a437a7fe5
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.properties
@@ -0,0 +1,2 @@
+structure.MultiLayerConfig=structure.MultiLayerConfigImpl
+structure.MultiLayerConfig$SecondLayer=structure.MultiLayerConfigImpl$SecondLayerImpl
diff --git a/extra/interface/ap/src/test/resources/structure/MultiLayerConfigImpl.java b/extra/interface/ap/src/test/resources/structure/MultiLayerConfigImpl.java
new file mode 100644
index 000000000..15f6056eb
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/structure/MultiLayerConfigImpl.java
@@ -0,0 +1,43 @@
+package structure;
+
+import java.lang.Override;
+import java.lang.String;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+/**
+ * Automatically generated implementation of the config */
+@ConfigSerializable
+final class MultiLayerConfigImpl implements MultiLayerConfig {
+    private String test;
+
+    private MultiLayerConfig.SecondLayer second = new SecondLayerImpl();
+
+    @Override
+    public String test() {
+        return test;
+    }
+
+    @Override
+    public MultiLayerConfig.SecondLayer second() {
+        return second;
+    }
+
+    /**
+     * Automatically generated implementation of the config */
+    @ConfigSerializable
+    static final class SecondLayerImpl implements MultiLayerConfig.SecondLayer {
+        private String test;
+
+        private String test2;
+
+        @Override
+        public String test() {
+            return test;
+        }
+
+        @Override
+        public String test2() {
+            return test2;
+        }
+    }
+}
diff --git a/extra/interface/ap/src/test/resources/test/OtherAnnotations.java b/extra/interface/ap/src/test/resources/test/OtherAnnotations.java
new file mode 100644
index 000000000..dc7c7ef5b
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/test/OtherAnnotations.java
@@ -0,0 +1,30 @@
+package test;
+
+import org.spongepowered.configurate.interfaces.processor.util.AnnotationOthersAnnotations;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Comment;
+import org.spongepowered.configurate.objectmapping.meta.Matches;
+import org.spongepowered.configurate.objectmapping.meta.Required;
+
+@ConfigSerializable
+public interface OtherAnnotations {
+    @Comment(value = "Hello!", override = true)
+    @Matches(value = "abc", failureMessage = "ohno!")
+    @Required
+    String hello();
+
+    @Comment("Hi!")
+    String hi();
+
+    @AnnotationOthersAnnotations.AnnotationNoField
+    String noField();
+
+    @AnnotationOthersAnnotations.AnnotationNoTarget
+    String noTarget();
+
+    @AnnotationOthersAnnotations.AnnotationOtherRetention
+    String otherRetention();
+
+    @AnnotationOthersAnnotations.AnnotationNoRetention
+    String noRetention();
+}
diff --git a/extra/interface/ap/src/test/resources/test/OtherAnnotationsImpl.java b/extra/interface/ap/src/test/resources/test/OtherAnnotationsImpl.java
new file mode 100644
index 000000000..de0bdb968
--- /dev/null
+++ b/extra/interface/ap/src/test/resources/test/OtherAnnotationsImpl.java
@@ -0,0 +1,62 @@
+package test;
+
+import java.lang.Override;
+import java.lang.String;
+
+import org.spongepowered.configurate.interfaces.processor.util.AnnotationOthersAnnotations;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Comment;
+import org.spongepowered.configurate.objectmapping.meta.Matches;
+import org.spongepowered.configurate.objectmapping.meta.Required;
+
+/**
+ * Automatically generated implementation of the config */
+@ConfigSerializable
+final class OtherAnnotationsImpl implements OtherAnnotations {
+    @Comment(value = "Hello!", override = true)
+    @Matches(value = "abc", failureMessage = "ohno!")
+    @Required
+    private String hello;
+
+    @Comment("Hi!")
+    private String hi;
+
+    private String noField;
+
+    @AnnotationOthersAnnotations.AnnotationNoTarget
+    private String noTarget;
+
+    private String otherRetention;
+
+    private String noRetention;
+
+    @Override
+    public String hello() {
+        return hello;
+    }
+
+    @Override
+    public String hi() {
+        return hi;
+    }
+
+    @Override
+    public String noField() {
+        return noField;
+    }
+
+    @Override
+    public String noTarget() {
+        return noTarget;
+    }
+
+    @Override
+    public String otherRetention() {
+        return otherRetention;
+    }
+
+    @Override
+    public String noRetention() {
+        return noRetention;
+    }
+}
diff --git a/extra/interface/build.gradle b/extra/interface/build.gradle
new file mode 100644
index 000000000..5c6236119
--- /dev/null
+++ b/extra/interface/build.gradle
@@ -0,0 +1,10 @@
+plugins {
+    id "org.spongepowered.configurate.build.component"
+}
+
+description = "Utility classes for generated config interface implementations"
+
+dependencies {
+    api projects.core
+    testAnnotationProcessor projects.extra.extraInterface.extraInterfaceAp
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/Constants.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/Constants.java
new file mode 100644
index 000000000..67371395c
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/Constants.java
@@ -0,0 +1,35 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces;
+
+/**
+ * Constants that are used in multiple files. Meant to be used internally.
+ *
+ * @since 4.2.0
+ */
+public final class Constants {
+
+    private Constants() {}
+
+    /**
+     * The file location of the interface mappings.
+     *
+     * @since 4.2.0
+     */
+    public static final String MAPPING_FILE = "org/spongepowered/configurate/interfaces/interface_mappings.properties";
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceDefaultOptions.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceDefaultOptions.java
new file mode 100644
index 000000000..b99f3d820
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceDefaultOptions.java
@@ -0,0 +1,84 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces;
+
+import org.spongepowered.configurate.ConfigurationOptions;
+import org.spongepowered.configurate.objectmapping.ObjectMapper;
+import org.spongepowered.configurate.serialize.TypeSerializerCollection;
+
+import java.util.function.Consumer;
+
+/**
+ * This class has the default {@link ConfigurationOptions}
+ * with {@link InterfaceTypeSerializer} added to the serializers.
+ *
+ * @since 4.2.0
+ */
+public final class InterfaceDefaultOptions {
+
+    private static final TypeSerializerCollection DEFAULTS =
+                TypeSerializerCollection.builder()
+                    .registerAnnotated(InterfaceTypeSerializer::applicable, InterfaceTypeSerializer.INSTANCE)
+                    .registerAnnotatedObjects(InterfaceMiddleware.buildObjectMapperWithMiddleware().build())
+                    .build();
+
+    private InterfaceDefaultOptions() {
+    }
+
+    /**
+     * The default ConfigurationOptions with {@link InterfaceTypeSerializer} added to the serializers.
+     *
+     * @return the default ConfigurationOptions with {@link InterfaceTypeSerializer} added to the serializers.
+     * @since 4.2.0
+     */
+    public static ConfigurationOptions defaults() {
+        return addTo(ConfigurationOptions.defaults());
+    }
+
+    /**
+     * Sets the default configuration options to be used by the resultant loader
+     * by providing a function which takes interface's default serializer
+     * collection and applies any desired changes.
+     *
+     * @param options to transform the existing default options
+     * @return the default options with the applied changes
+     * @since 4.2.0
+     */
+    public static ConfigurationOptions addTo(final ConfigurationOptions options) {
+        // This creates a new TypeSerializerCollection with 'options' as parent. Child takes priority over parent.
+        return options.serializers(serializers -> serializers.registerAll(DEFAULTS));
+    }
+
+    /**
+     * {@link #addTo(ConfigurationOptions)} with an option to customize the {@link ObjectMapper.Factory}.
+     *
+     * @param options to transform the existing default options
+     * @param objectMapperConsumer to transform the ObjectMapper factory
+     * @return the default options with the applied changes
+     * @since 4.2.0
+     */
+    public static ConfigurationOptions addTo(final ConfigurationOptions options,
+                             final Consumer<ObjectMapper.Factory.Builder> objectMapperConsumer) {
+        return options.serializers(serializers -> {
+            final ObjectMapper.Factory.Builder builder = InterfaceMiddleware.buildObjectMapperWithMiddleware();
+            objectMapperConsumer.accept(builder);
+            serializers.registerAnnotated(InterfaceTypeSerializer::applicable, InterfaceTypeSerializer.INSTANCE)
+                    .registerAnnotatedObjects(builder.build());
+        });
+    }
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceMiddleware.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceMiddleware.java
new file mode 100644
index 000000000..6b744188e
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceMiddleware.java
@@ -0,0 +1,145 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.interfaces.meta.Hidden;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
+import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;
+import org.spongepowered.configurate.interfaces.meta.range.DecimalRange;
+import org.spongepowered.configurate.interfaces.meta.range.NumericRange;
+import org.spongepowered.configurate.interfaces.meta.range.StringRange;
+import org.spongepowered.configurate.objectmapping.ObjectMapper;
+import org.spongepowered.configurate.objectmapping.meta.Constraint;
+import org.spongepowered.configurate.objectmapping.meta.Processor;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.util.Locale;
+
+final class InterfaceMiddleware {
+
+    private InterfaceMiddleware() {
+    }
+
+    static ObjectMapper.Factory.Builder buildObjectMapperWithMiddleware() {
+        return ObjectMapper.factoryBuilder()
+            .addConstraint(DecimalRange.class, Number.class, decimalRange())
+            .addConstraint(NumericRange.class, Number.class, numericRange())
+            .addConstraint(StringRange.class, String.class, stringRange())
+            .addProcessor(Hidden.class, hiddenProcessor());
+    }
+
+    private static Constraint.Factory<DecimalRange, Number> decimalRange() {
+        return (data, type) -> number -> {
+            // null requirement is part of @Required
+            if (number == null) {
+                return;
+            }
+
+            final double value = number.doubleValue();
+            if (!(data.from() >= value && data.to() <= value)) {
+                throw new SerializationException(String.format(
+                    Locale.ROOT,
+                    "'%s' is not in the allowed range of from: %s, to: %s!",
+                    value, data.from(), data.to()
+                ));
+            }
+        };
+    }
+
+    private static Constraint.Factory<NumericRange, Number> numericRange() {
+        return (data, type) -> number -> {
+            // null requirement is part of @Required
+            if (number == null) {
+                return;
+            }
+
+            final long value = number.longValue();
+            if (!(data.from() >= value && data.to() <= value)) {
+                throw new SerializationException(String.format(
+                    Locale.ROOT,
+                    "'%s' is not in the allowed range of from: %s, to: %s!",
+                    value, data.from(), data.to()
+                ));
+            }
+        };
+    }
+
+    private static Constraint.Factory<StringRange, String> stringRange() {
+        return (data, type) -> string -> {
+            // null requirement is part of @Required
+            if (string == null) {
+                return;
+            }
+
+            final int length = string.length();
+            if (!(data.from() >= length && data.to() <= length)) {
+                throw new SerializationException(String.format(
+                    Locale.ROOT,
+                    "'%s' is not in the allowed string length range of from: %s, to: %s!",
+                    length, data.from(), data.to()
+                ));
+            }
+        };
+    }
+
+    @SuppressWarnings("ConstantValue")
+    private static Processor.AdvancedFactory<Hidden, Object> hiddenProcessor() {
+        return (ignored, fieldType, element) -> {
+            // prefetch everything we can
+            final @Nullable DefaultBoolean defaultBoolean = element.getAnnotation(DefaultBoolean.class);
+            final @Nullable DefaultDecimal defaultDecimal = element.getAnnotation(DefaultDecimal.class);
+            final @Nullable DefaultNumeric defaultNumeric = element.getAnnotation(DefaultNumeric.class);
+            final @Nullable DefaultString defaultString = element.getAnnotation(DefaultString.class);
+            final boolean isBoolean = TypeUtils.isBoolean(fieldType);
+            final boolean isDecimal = TypeUtils.isDecimal(fieldType);
+            final boolean isNumeric = TypeUtils.isNumeric(fieldType);
+            final boolean isString = String.class == fieldType;
+
+            // unfortunately default methods cannot be supported in combination with Hidden in Configurate
+
+            return (value, destination) -> {
+                // hidden fields are only absent from the config if the value is the default value, so we check that below
+                if (isBoolean && defaultBoolean != null) {
+                    if (!value.equals(defaultBoolean.value())) {
+                        return;
+                    }
+                } else if (isDecimal && defaultDecimal != null) {
+                    if (((Number) value).doubleValue() != defaultDecimal.value()) {
+                        return;
+                    }
+                } else if (isNumeric && defaultNumeric != null) {
+                    if (((Number) value).longValue() != defaultNumeric.value()) {
+                        return;
+                    }
+                } else if (isString && defaultString != null) {
+                    if (!defaultString.value().equals(value)) {
+                        return;
+                    }
+                }
+
+                // as long as it uses the naming scheme-based resolver parent should be the object holding the field and
+                // key the field type
+                //noinspection DataFlowIssue
+                destination.parent().removeChild(destination.key());
+            };
+        };
+    }
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializer.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializer.java
new file mode 100644
index 000000000..c76576dfc
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializer.java
@@ -0,0 +1,115 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces;
+
+import static io.leangen.geantyref.GenericTypeReflector.erase;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.Type;
+import java.net.URL;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.StringJoiner;
+
+final class InterfaceTypeSerializer implements TypeSerializer<Object> {
+
+    public static final InterfaceTypeSerializer INSTANCE = new InterfaceTypeSerializer();
+
+    private final Properties interfaceMappings = new Properties();
+
+    public static boolean applicable(final AnnotatedType type) {
+        return type.isAnnotationPresent(ConfigSerializable.class) && erase(type.getType()).isInterface();
+    }
+
+    private InterfaceTypeSerializer() {
+        final @Nullable URL mappingsUrl = getClass().getClassLoader().getResource(Constants.MAPPING_FILE);
+        if (mappingsUrl == null) {
+            return;
+        }
+
+        try (InputStream stream = mappingsUrl.openStream()) {
+            this.interfaceMappings.load(stream);
+        } catch (final IOException exception) {
+            throw new RuntimeException("Could not load interface mappings!", exception);
+        }
+    }
+
+    @Override
+    public Object deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
+        final String canonicalName = erase(type).getTypeName();
+        final @Nullable String typeImpl = this.interfaceMappings.getProperty(canonicalName);
+        if (typeImpl == null) {
+            throw new SerializationException(String.format(
+                Locale.ROOT,
+                "No mapping found for type %s. Available mappings: %s",
+                canonicalName, availableMappings()
+            ));
+        }
+
+        final Class<?> implClass;
+        try {
+            implClass = Class.forName(typeImpl, true, erase(type).getClassLoader());
+        } catch (final ClassNotFoundException exception) {
+            throw new SerializationException(String.format(
+                Locale.ROOT,
+                "Could not find implementation class %s for type %s!",
+                typeImpl, canonicalName
+            ));
+        }
+
+        final @Nullable TypeSerializer<?> serializer = node.options().serializers().get(implClass);
+        if (serializer == null) {
+            throw new SerializationException("No serializer found for implementation class " + implClass);
+        }
+        return serializer.deserialize(implClass, node);
+    }
+
+    @Override
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public void serialize(
+        final Type type,
+        final @Nullable Object obj,
+        final ConfigurationNode node
+    ) throws SerializationException {
+        // don't determine serialize from type, because that might be incorrect for subsections
+        if (obj == null) {
+            node.set(null);
+            return;
+        }
+
+        final @Nullable TypeSerializer serializer = node.options().serializers().get(obj.getClass());
+        if (serializer == null) {
+            throw new SerializationException("No serializer found for implementation class " + obj.getClass());
+        }
+        serializer.serialize(obj.getClass(), obj, node);
+    }
+
+    private String availableMappings() {
+        final StringJoiner joiner = new StringJoiner(", ");
+        this.interfaceMappings.keySet().forEach((key) -> joiner.add((CharSequence) key));
+        return joiner.toString();
+    }
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/TypeUtils.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/TypeUtils.java
new file mode 100644
index 000000000..b87b203d4
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/TypeUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces;
+
+import static io.leangen.geantyref.GenericTypeReflector.box;
+
+import java.lang.reflect.Type;
+
+final class TypeUtils {
+
+    private TypeUtils() {}
+
+    static boolean isNumeric(final Type type) {
+        final Type boxed = box(type);
+        return Byte.class.equals(boxed) || Character.class.equals(boxed) || Short.class.equals(boxed)
+            || Integer.class.equals(boxed) || Long.class.equals(boxed);
+    }
+
+    static boolean isDecimal(final Type type) {
+        final Type boxed = box(type);
+        return Float.class.equals(boxed) || Double.class.equals(boxed);
+    }
+
+    static boolean isBoolean(final Type type) {
+        return Boolean.class.equals(box(type));
+    }
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Exclude.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Exclude.java
new file mode 100644
index 000000000..5dc4c871d
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Exclude.java
@@ -0,0 +1,34 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Adding this will cause the implementation generator to completely ignore
+ * this method. This is practically only used for default methods, as normal
+ * interface methods need to have an implementation.
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.METHOD)
+public @interface Exclude {
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Field.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Field.java
new file mode 100644
index 000000000..e474aaf04
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Field.java
@@ -0,0 +1,33 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Creates a field which makes the annotated method act as a simple getter /
+ * setter without being handled as a config node.
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface Field {
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Hidden.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Hidden.java
new file mode 100644
index 000000000..4ac1c83bc
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Hidden.java
@@ -0,0 +1,42 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation allows you to read the config node as normal, but it only
+ * writes the node when the value is not the default value. This is to ensure
+ * that when a user manually adds the entry, it remains there (as long as it's
+ * not the default value.)
+ *
+ * <p>Without a default value the annotated node will be read, but will never
+ * be written even if the user explicitly added it to their config.</p>
+ *
+ * <b>Note that Hidden doesn't work with default method getters due to a
+ * limitation, and Hidden will function like it doesn't have a default
+ * value.</b>
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.FIELD})
+public @interface Hidden {
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultBoolean.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultBoolean.java
new file mode 100644
index 000000000..f49b0a333
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultBoolean.java
@@ -0,0 +1,44 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta.defaults;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation provides a default value for the annotated method.
+ * Because of annotation limits, there is an annotation for:
+ * {@link DefaultBoolean booleans}, {@link DefaultDecimal decimals},
+ * {@link DefaultNumeric numerics} and {@link DefaultString Strings}.
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.FIELD})
+public @interface DefaultBoolean {
+
+    /**
+     * The default value for the annotated method.
+     *
+     * @return the default value for the annotated method.
+     * @since 4.2.0
+     */
+    boolean value() default false;
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultDecimal.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultDecimal.java
new file mode 100644
index 000000000..9efda5816
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultDecimal.java
@@ -0,0 +1,44 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta.defaults;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation provides a default value for the annotated method.
+ * Because of annotation limits, there is an annotation for:
+ * {@link DefaultBoolean booleans}, {@link DefaultDecimal decimals},
+ * {@link DefaultNumeric numerics} and {@link DefaultString Strings}.
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.FIELD})
+public @interface DefaultDecimal {
+
+    /**
+     * The default value for the annotated method.
+     *
+     * @return the default value for the annotated method.
+     * @since 4.2.0
+     */
+    double value() default 0.0;
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultNumeric.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultNumeric.java
new file mode 100644
index 000000000..c6f47f5cd
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultNumeric.java
@@ -0,0 +1,44 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta.defaults;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation provides a default value for the annotated method.
+ * Because of annotation limits, there is an annotation for:
+ * {@link DefaultBoolean booleans}, {@link DefaultDecimal decimals},
+ * {@link DefaultNumeric numerics} and {@link DefaultString Strings}.
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.FIELD})
+public @interface DefaultNumeric {
+
+    /**
+     * The default value for the annotated method.
+     *
+     * @return the default value for the annotated method.
+     * @since 4.2.0
+     */
+    long value() default 0;
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultString.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultString.java
new file mode 100644
index 000000000..f99cec6f7
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultString.java
@@ -0,0 +1,44 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta.defaults;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation provides a default value for the annotated method.
+ * Because of annotation limits, there is an annotation for:
+ * {@link DefaultBoolean booleans}, {@link DefaultDecimal decimals},
+ * {@link DefaultNumeric numerics} and {@link DefaultString Strings}.
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.FIELD})
+public @interface DefaultString {
+
+    /**
+     * The default value for the annotated method.
+     *
+     * @return the default value for the annotated method.
+     * @since 4.2.0
+     */
+    String value() default "";
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/DecimalRange.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/DecimalRange.java
new file mode 100644
index 000000000..fad986dc6
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/DecimalRange.java
@@ -0,0 +1,51 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta.range;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation limits the values that a config node can have.
+ * Because of annotation limits, there is an annotation for:
+ * {@link DecimalRange decimals}, {@link NumericRange numerics} and
+ * {@link StringRange String length}.
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface DecimalRange {
+    /**
+     * The minimal value allowed (inclusive.)
+     *
+     * @return the minimal value allowed (inclusive.)
+     * @since 4.2.0
+     */
+    double from();
+
+    /**
+     * The maximal value allowed (inclusive.)
+     *
+     * @return the maximal value allowed (inclusive.)
+     * @since 4.2.0
+     */
+    double to();
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/NumericRange.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/NumericRange.java
new file mode 100644
index 000000000..7595680ca
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/NumericRange.java
@@ -0,0 +1,51 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta.range;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation limits the values that a config node can have.
+ * Because of annotation limits, there is an annotation for:
+ * {@link DecimalRange decimals}, {@link NumericRange numerics} and
+ * {@link StringRange String length}.
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface NumericRange {
+    /**
+     * The minimal value allowed (inclusive.)
+     *
+     * @return the minimal value allowed (inclusive.)
+     * @since 4.2.0
+     */
+    long from();
+
+    /**
+     * The maximal value allowed (inclusive.)
+     *
+     * @return the maximal value allowed (inclusive.)
+     * @since 4.2.0
+     */
+    long to();
+
+}
diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/StringRange.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/StringRange.java
new file mode 100644
index 000000000..48ed5a74a
--- /dev/null
+++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/StringRange.java
@@ -0,0 +1,55 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces.meta.range;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation limits the values that a config node can have.
+ * Because of annotation limits, there is an annotation for:
+ * {@link DecimalRange decimals}, {@link NumericRange numerics} and
+ * {@link StringRange String length}.
+ *
+ * <p>When the String is null, the range validation is skipped. Use
+ * {@link org.spongepowered.configurate.objectmapping.meta.Required Required}
+ * if null shouldn't be allowed.</p>
+ *
+ * @since 4.2.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface StringRange {
+    /**
+     * The minimal String length allowed (inclusive.)
+     *
+     * @return the minimal value allowed (inclusive.)
+     * @since 4.2.0
+     */
+    int from();
+
+    /**
+     * The maximal String length allowed (inclusive.)
+     *
+     * @return the maximal value allowed (inclusive.)
+     * @since 4.2.0
+     */
+    int to();
+
+}
diff --git a/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/ConfigEmpty.java b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/ConfigEmpty.java
new file mode 100644
index 000000000..5cfc33b86
--- /dev/null
+++ b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/ConfigEmpty.java
@@ -0,0 +1,27 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces;
+
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+@ConfigSerializable
+interface ConfigEmpty {
+
+    @ConfigSerializable
+    interface ConfigEmptyInner {}
+
+}
diff --git a/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializerTest.java b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializerTest.java
new file mode 100644
index 000000000..23e826a1b
--- /dev/null
+++ b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializerTest.java
@@ -0,0 +1,54 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.spongepowered.configurate.interfaces.TypeUtils.configImplementationFor;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.spongepowered.configurate.BasicConfigurationNode;
+import org.spongepowered.configurate.ConfigurateException;
+
+class InterfaceTypeSerializerTest {
+
+    @Test
+    void testDeserialization() throws ConfigurateException {
+        final BasicConfigurationNode node = BasicConfigurationNode.root(InterfaceDefaultOptions.defaults());
+        // doesn't deserialize if value is NullValue
+        node.node("hello").set("world");
+
+        final @Nullable ConfigEmpty config = Assertions.assertDoesNotThrow(() -> node.get(ConfigEmpty.class));
+        assertNotNull(config);
+        assertInstanceOf(configImplementationFor(ConfigEmpty.class), config);
+    }
+
+    @Test
+    void testInnerDeserialization() throws ConfigurateException {
+        final BasicConfigurationNode node = BasicConfigurationNode.root(InterfaceDefaultOptions.defaults());
+        // doesn't deserialize if value is NullValue
+        node.node("hello").set("world");
+
+        final ConfigEmpty.@Nullable ConfigEmptyInner config =
+            Assertions.assertDoesNotThrow(() -> node.get(ConfigEmpty.ConfigEmptyInner.class));
+        assertNotNull(config);
+        assertInstanceOf(configImplementationFor(ConfigEmpty.ConfigEmptyInner.class), config);
+    }
+
+}
diff --git a/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/TypeUtils.java b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/TypeUtils.java
new file mode 100644
index 000000000..de682c970
--- /dev/null
+++ b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/TypeUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Configurate
+ * Copyright (C) zml and Configurate contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.spongepowered.configurate.interfaces;
+
+import java.util.Arrays;
+
+final class TypeUtils {
+
+    private TypeUtils() {}
+
+    static <T> Class<? extends T> configImplementationFor(final Class<T> interfaceClass) {
+        try {
+            final String implClassName = implClassNameFor(interfaceClass);
+
+            //noinspection unchecked
+            return (Class<? extends T>) Class.forName(implClassName);
+        } catch (final ClassNotFoundException notFound) {
+            throw new IllegalStateException("No implementation for " + interfaceClass.getCanonicalName(), notFound);
+        }
+    }
+
+    private static <T> String implClassNameFor(final Class<T> interfaceClass) {
+        final String packageName = interfaceClass.getPackage().getName();
+        // include the package name dot as well
+        final String classHierarchy = interfaceClass.getCanonicalName().substring(packageName.length() + 1);
+
+        // every subclass and the class itself has 'Impl' behind it
+        final String implClassName =
+                Arrays.stream(classHierarchy.split("\\."))
+                        .reduce("", (reduced, current) -> {
+                            if (!reduced.isEmpty()) {
+                                reduced += "$";
+                            }
+                            return reduced + current + "Impl";
+                        });
+        return packageName + "." + implClassName;
+    }
+
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0b93eb852..9e1a3f388 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,16 +1,19 @@
 [versions]
-assertj="3.24.2"
-autoValue="1.10.4"
-checkerQual="3.40.0"
-checkstyle="10.12.5"
+assertj = "3.24.2"
+autoValue = "1.10.4"
+checkerQual = "3.40.0"
+checkstyle = "10.12.5"
 geantyref = "1.3.14"
-errorprone="2.23.0"
+errorprone = "2.23.0"
 indra = "3.1.3"
-junit="5.10.1"
-ktlint="0.49.1"
-ktfmt="0.46"
+junit = "5.10.1"
+ktlint = "0.49.1"
+ktfmt = "0.46"
 pmd = "6.55.0"
 spotless = "6.23.2"
+javapoet = "1.10.0"
+auto-service = "1.1.1"
+compile-testing = "0.21.0"
 
 [libraries]
 # Shared
@@ -28,11 +31,16 @@ stylecheck = "ca.stellardrift:stylecheck:0.2.1"
 
 # Kotlin
 kotlin-coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
-kotlin-reflect = {module = "org.jetbrains.kotlin:kotlin-reflect"} # version from Kotlin BOM
+kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } # version from Kotlin BOM
 
 # Core
 checkerQual = { module = "org.checkerframework:checker-qual", version.ref = "checkerQual" }
-geantyref = {module = "io.leangen.geantyref:geantyref", version.ref = "geantyref" }
+geantyref = { module = "io.leangen.geantyref:geantyref", version.ref = "geantyref" }
+
+# Interface
+auto-service = { module = "com.google.auto.service:auto-service", version.ref = "auto-service" }
+javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" }
+compile-testing = { module = "com.google.testing.compile:compile-testing", version.ref = "compile-testing" }
 
 # DFU
 dfu-v2 = "com.mojang:datafixerupper:2.0.24"
diff --git a/settings.gradle b/settings.gradle
index b2dcf8727..bd00951f7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -56,10 +56,12 @@ rootProject.name = "$prefix-parent"
 }
 
 // extras
-["kotlin", "guice", "dfu2", "dfu3", "dfu4"].each {
+["kotlin", "guice", "dfu2", "dfu3", "dfu4", "interface"].each {
     include ":extra:$it"
     findProject(":extra:$it")?.name = "extra-$it"
 }
+include ":extra:extra-interface:ap"
+findProject(":extra:extra-interface:ap")?.name = "extra-interface-ap"
 
 includeBuild 'vendor', {
   name = "configurate-vendor"