You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2020/06/07 20:25:12 UTC

[logging-log4j2] 01/02: #335 Replace inline resolver DSL with JSON structure.

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

vy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 0dccbdbfbf5130a0616cf569c8a47c5d38b8bb89
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Sun Jun 7 21:10:16 2020 +0200

    #335 Replace inline resolver DSL with JSON structure.
    
    This major change replaces the custom ${json:*} resolver syntax
    with configuration passed using the JSON structure. This enables
    a way more versatile configuration syntax without parsing issues.
    
    The class documentation and the manual is improved extensively
    as well.
    
    The default template is switched from JsonLayout to EcsLayout.
---
 .../layout/json/template/JsonTemplateLayout.java   |  149 ++-
 .../json/template/JsonTemplateLayoutDefaults.java  |    2 +-
 .../resolver/ContextStackResolverFactory.java      |   39 -
 .../resolver/EndOfBatchResolverFactory.java        |    4 +-
 .../template/resolver/EventResolverContext.java    |   10 +-
 .../template/resolver/EventResolverFactories.java  |    4 +-
 .../resolver/ExceptionInternalResolverFactory.java |   49 +-
 .../json/template/resolver/ExceptionResolver.java  |   23 +-
 .../resolver/ExceptionResolverFactory.java         |   12 +-
 .../resolver/ExceptionRootCauseResolver.java       |   23 +-
 .../ExceptionRootCauseResolverFactory.java         |    6 +-
 .../json/template/resolver/LevelResolver.java      |   85 +-
 .../template/resolver/LevelResolverFactory.java    |    6 +-
 .../json/template/resolver/LoggerResolver.java     |   41 +-
 .../template/resolver/LoggerResolverFactory.java   |    6 +-
 .../json/template/resolver/MainMapResolver.java    |   49 +-
 .../template/resolver/MainMapResolverFactory.java  |    6 +-
 .../layout/json/template/resolver/MapResolver.java |   41 +-
 .../json/template/resolver/MapResolverFactory.java |    6 +-
 .../json/template/resolver/MarkerResolver.java     |   32 +-
 .../template/resolver/MarkerResolverFactory.java   |    6 +-
 .../json/template/resolver/MessageResolver.java    |   62 +-
 .../template/resolver/MessageResolverFactory.java  |    6 +-
 .../json/template/resolver/PatternResolver.java    |   30 +-
 .../template/resolver/PatternResolverFactory.java  |    6 +-
 .../json/template/resolver/SourceResolver.java     |   41 +-
 .../template/resolver/SourceResolverFactory.java   |    6 +-
 .../resolver/StackTraceElementObjectResolver.java  |   36 +-
 .../StackTraceElementObjectResolverFactory.java    |    4 +-
 ...Resolver.java => StackTraceStringResolver.java} |    4 +-
 ...verFactory.java => TemplateResolverConfig.java} |   10 +-
 .../template/resolver/TemplateResolverFactory.java |    2 +-
 .../json/template/resolver/TemplateResolvers.java  |  169 ++--
 ...esolver.java => ThreadContextDataResolver.java} |  223 +++--
 ....java => ThreadContextDataResolverFactory.java} |   18 +-
 ...solver.java => ThreadContextStackResolver.java} |   65 +-
 ...java => ThreadContextStackResolverFactory.java} |   18 +-
 .../json/template/resolver/ThreadResolver.java     |   34 +-
 .../template/resolver/ThreadResolverFactory.java   |    6 +-
 .../json/template/resolver/TimestampResolver.java  |  511 +++++-----
 .../resolver/TimestampResolverFactory.java         |    4 +-
 .../layout/json/template/util/MapAccessor.java     |  139 +++
 .../src/main/resources/EcsLayout.json              |   56 +-
 .../src/main/resources/GelfLayout.json             |   44 +-
 .../src/main/resources/JsonLayout.json             |   95 +-
 .../main/resources/LogstashJsonEventLayoutV1.json  |   65 +-
 .../main/resources/StackTraceElementLayout.json    |   20 +-
 .../log4j/layout/json/template/EcsLayoutTest.java  |   29 +-
 .../log4j/layout/json/template/GelfLayoutTest.java |   53 +-
 .../log4j/layout/json/template/JacksonFixture.java |    7 -
 .../log4j/layout/json/template/JsonLayoutTest.java |   14 +-
 .../json/template/JsonTemplateLayoutTest.java      | 1010 +++++++++++---------
 .../json/template/LayoutComparisonHelpers.java     |    6 +-
 .../log4j/layout/json/template/LogstashIT.java     |   24 +-
 .../src/test/resources/testJsonTemplateLayout.json |   79 +-
 .../template/JsonTemplateLayoutBenchmarkState.java |   31 +-
 src/site/asciidoc/manual/json-template-layout.adoc |  917 +++++++++++++-----
 src/site/asciidoc/manual/layouts.adoc              |   65 +-
 src/site/markdown/manual/cloud.md                  |   32 +-
 59 files changed, 3050 insertions(+), 1490 deletions(-)

diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
index 557741d..a5118c2 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
@@ -44,6 +44,7 @@ import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.util.Strings;
 
 import java.nio.charset.Charset;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
@@ -51,8 +52,7 @@ import java.util.function.Supplier;
 
 @Plugin(name = "JsonTemplateLayout",
         category = Node.CATEGORY,
-        elementType = Layout.ELEMENT_TYPE,
-        printObject = true)
+        elementType = Layout.ELEMENT_TYPE)
 public class JsonTemplateLayout implements StringLayout {
 
     private static final Map<String, String> CONTENT_FORMAT =
@@ -485,6 +485,11 @@ public class JsonTemplateLayout implements StringLayout {
                 throw new IllegalArgumentException(
                         "both stackTraceElementTemplate and stackTraceElementTemplateUri are blank");
             }
+            if (maxStringLength <= 0) {
+                throw new IllegalArgumentException(
+                        "was expecting a non-zero positive maxStringLength: " +
+                                maxStringLength);
+            }
             Objects.requireNonNull(truncatedStringSuffix, "truncatedStringSuffix");
             Objects.requireNonNull(recyclerFactory, "recyclerFactory");
         }
@@ -494,21 +499,43 @@ public class JsonTemplateLayout implements StringLayout {
     // We need this ugly model and its builder just to be able to allow
     // key-value pairs in a dedicated element.
     @SuppressWarnings({"unused", "WeakerAccess"})
-    @Plugin(name = "EventTemplateAdditionalFields", category = Node.CATEGORY, printObject = true)
+    @Plugin(name = "EventTemplateAdditionalFields",
+            category = Node.CATEGORY,
+            printObject = true)
     public static final class EventTemplateAdditionalFields {
 
         private static final EventTemplateAdditionalFields EMPTY = newBuilder().build();
 
-        private final KeyValuePair[] additionalFields;
+        private final EventTemplateAdditionalField[] additionalFields;
 
         private EventTemplateAdditionalFields(final Builder builder) {
-            this.additionalFields = builder.additionalFields;
+            this.additionalFields = builder.additionalFields != null
+                    ? builder.additionalFields
+                    : new EventTemplateAdditionalField[0];
         }
 
-        public KeyValuePair[] getAdditionalFields() {
+        public EventTemplateAdditionalField[] getAdditionalFields() {
             return additionalFields;
         }
 
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) return true;
+            if (object == null || getClass() != object.getClass()) return false;
+            EventTemplateAdditionalFields that = (EventTemplateAdditionalFields) object;
+            return Arrays.equals(additionalFields, that.additionalFields);
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(additionalFields);
+        }
+
+        @Override
+        public String toString() {
+            return Arrays.toString(additionalFields);
+        }
+
         @PluginBuilderFactory
         public static Builder newBuilder() {
             return new Builder();
@@ -518,15 +545,16 @@ public class JsonTemplateLayout implements StringLayout {
                 implements org.apache.logging.log4j.core.util.Builder<EventTemplateAdditionalFields> {
 
             @PluginElement("AdditionalField")
-            private KeyValuePair[] additionalFields;
+            private EventTemplateAdditionalField[] additionalFields;
 
             private Builder() {}
 
-            public KeyValuePair[] getAdditionalFields() {
+            public EventTemplateAdditionalField[] getAdditionalFields() {
                 return additionalFields;
             }
 
-            public Builder setAdditionalFields(final KeyValuePair[] additionalFields) {
+            public Builder setAdditionalFields(
+                    final EventTemplateAdditionalField[] additionalFields) {
                 this.additionalFields = additionalFields;
                 return this;
             }
@@ -540,4 +568,107 @@ public class JsonTemplateLayout implements StringLayout {
 
     }
 
+    @Plugin(name = "EventTemplateAdditionalField",
+            category = Node.CATEGORY,
+            printObject = true)
+    public static final class EventTemplateAdditionalField {
+
+        public enum Type { STRING, JSON }
+
+        private final String key;
+
+        private final String value;
+
+        private final Type type;
+
+        private EventTemplateAdditionalField(final Builder builder) {
+            this.key = builder.key;
+            this.value = builder.value;
+            this.type = builder.type;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public Type getType() {
+            return type;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) return true;
+            if (object == null || getClass() != object.getClass()) return false;
+            EventTemplateAdditionalField that = (EventTemplateAdditionalField) object;
+            return key.equals(that.key) &&
+                    value.equals(that.value) &&
+                    type == that.type;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(key, value, type);
+        }
+
+        @Override
+        public String toString() {
+            final String formattedValue = Type.STRING.equals(type)
+                    ? String.format("\"%s\"", value)
+                    : value;
+            return String.format("%s=%s", key, formattedValue);
+        }
+
+        @PluginBuilderFactory
+        public static EventTemplateAdditionalField.Builder newBuilder() {
+            return new EventTemplateAdditionalField.Builder();
+        }
+
+        public static class Builder
+                implements org.apache.logging.log4j.core.util.Builder<EventTemplateAdditionalField> {
+
+            private String key;
+
+            private String value;
+
+            private Type type = Type.STRING;
+
+            public Builder setKey(final String key) {
+                this.key = key;
+                return this;
+            }
+
+            public Builder setValue(final String value) {
+                this.value = value;
+                return this;
+            }
+
+            public Builder setType(final Type type) {
+                this.type = type;
+                return this;
+            }
+
+            @Override
+            public EventTemplateAdditionalField build() {
+                validate();
+                return new EventTemplateAdditionalField(this);
+            }
+
+            private void validate() {
+                if (Strings.isBlank(key)) {
+                    throw new IllegalArgumentException("blank key");
+                }
+                if (Strings.isBlank(value)) {
+                    throw new IllegalArgumentException("blank value");
+                }
+                Objects.requireNonNull(type, "type");
+            }
+
+        }
+
+    }
+
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
index c156e69..adfe760 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
@@ -57,7 +57,7 @@ public enum JsonTemplateLayoutDefaults {;
     private static final String EVENT_TEMPLATE_URI =
             PROPERTIES.getStringProperty(
                     "log4j.layout.jsonTemplate.eventTemplateUri",
-                    "classpath:JsonLayout.json");
+                    "classpath:EcsLayout.json");
 
     private static final String STACK_TRACE_ELEMENT_TEMPLATE =
             PROPERTIES.getStringProperty(
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java
deleted file mode 100644
index 0db832a..0000000
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.layout.json.template.resolver;
-
-final class ContextStackResolverFactory implements EventResolverFactory<ContextStackResolver> {
-
-    private static final ContextStackResolverFactory INSTANCE = new ContextStackResolverFactory();
-
-    private ContextStackResolverFactory() {}
-
-    static ContextStackResolverFactory getInstance() {
-        return INSTANCE;
-    }
-
-    @Override
-    public String getName() {
-        return ContextStackResolver.getName();
-    }
-
-    @Override
-    public ContextStackResolver create(final EventResolverContext context, final String key) {
-        return new ContextStackResolver(key);
-    }
-
-}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
index c55fc58..0f013a4 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
@@ -32,7 +32,9 @@ final class EndOfBatchResolverFactory implements EventResolverFactory<EndOfBatch
     }
 
     @Override
-    public EndOfBatchResolver create(final EventResolverContext context, String key) {
+    public EndOfBatchResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
         return EndOfBatchResolver.getInstance();
     }
 
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
index e178a66..a83b2c9 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
@@ -20,6 +20,8 @@ import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.lookup.StrSubstitutor;
 import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
 
@@ -47,7 +49,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
 
     private final TemplateResolver<Throwable> stackTraceObjectResolver;
 
-    private final KeyValuePair[] additionalFields;
+    private final EventTemplateAdditionalField[] additionalFields;
 
     private EventResolverContext(final Builder builder) {
         this.configuration = builder.configuration;
@@ -112,7 +114,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
         return stackTraceObjectResolver;
     }
 
-    KeyValuePair[] getAdditionalFields() {
+    EventTemplateAdditionalField[] getAdditionalFields() {
         return additionalFields;
     }
 
@@ -140,7 +142,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
 
         private TemplateResolver<StackTraceElement> stackTraceElementObjectResolver;
 
-        private KeyValuePair[] eventTemplateAdditionalFields;
+        private EventTemplateAdditionalField[] eventTemplateAdditionalFields;
 
         private Builder() {
             // Do nothing.
@@ -193,7 +195,7 @@ public final class EventResolverContext implements TemplateResolverContext<LogEv
         }
 
         public Builder setEventTemplateAdditionalFields(
-                final KeyValuePair[] eventTemplateAdditionalFields) {
+                final EventTemplateAdditionalField[] eventTemplateAdditionalFields) {
             this.eventTemplateAdditionalFields = eventTemplateAdditionalFields;
             return this;
         }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
index 86ea6bb..fc8c6e9 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
@@ -33,8 +33,8 @@ enum EventResolverFactories {;
 
         // Collect resolver factories.
         final List<EventResolverFactory<? extends EventResolver>> resolverFactories = Arrays.asList(
-                ContextDataResolverFactory.getInstance(),
-                ContextStackResolverFactory.getInstance(),
+                ThreadContextDataResolverFactory.getInstance(),
+                ThreadContextStackResolverFactory.getInstance(),
                 EndOfBatchResolverFactory.getInstance(),
                 ExceptionResolverFactory.getInstance(),
                 ExceptionRootCauseResolverFactory.getInstance(),
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
index de1ec31..b6e5ff8 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
@@ -16,6 +16,17 @@
  */
 package org.apache.logging.log4j.layout.json.template.resolver;
 
+/**
+ * Exception resolver factory.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config      = field , [ stringified ]
+ * field       = "field" -> ( "className" | "message" | "stackTrace" )
+ * stringified = "stringified" -> boolean
+ * </pre>
+ */
 abstract class ExceptionInternalResolverFactory {
 
     private static final EventResolver NULL_RESOLVER =
@@ -23,48 +34,34 @@ abstract class ExceptionInternalResolverFactory {
 
     EventResolver createInternalResolver(
             final EventResolverContext context,
-            final String key) {
-
-        // Split the key into its major and minor components.
-        String majorKey;
-        String minorKey;
-        final int colonIndex = key.indexOf(':');
-        if (colonIndex >= 0) {
-            majorKey = key.substring(0, colonIndex);
-            minorKey = key.substring(colonIndex + 1);
-        } else {
-            majorKey = key;
-            minorKey = "";
-        }
-
-        // Create the resolver.
-        switch (majorKey) {
+            final TemplateResolverConfig config) {
+        final String fieldName = config.getString("field");
+        switch (fieldName) {
             case "className": return createClassNameResolver();
             case "message": return createMessageResolver(context);
-            case "stackTrace": return createStackTraceResolver(context, minorKey);
+            case "stackTrace": return createStackTraceResolver(context, config);
         }
-        throw new IllegalArgumentException("unknown key: " + key);
+        throw new IllegalArgumentException("unknown field: " + config);
 
     }
 
     abstract EventResolver createClassNameResolver();
 
-    abstract EventResolver createMessageResolver(final EventResolverContext context);
+    abstract EventResolver createMessageResolver(EventResolverContext context);
 
     private EventResolver createStackTraceResolver(
             final EventResolverContext context,
-            final String minorKey) {
+            final TemplateResolverConfig config) {
         if (!context.isStackTraceEnabled()) {
             return NULL_RESOLVER;
         }
-        switch (minorKey) {
-            case "text": return createStackTraceTextResolver(context);
-            case "": return createStackTraceObjectResolver(context);
-        }
-        throw new IllegalArgumentException("unknown minor key: " + minorKey);
+        final boolean stringified = config.getBoolean("stringified", false);
+        return stringified
+                ? createStackTraceStringResolver(context)
+                : createStackTraceObjectResolver(context);
     }
 
-    abstract EventResolver createStackTraceTextResolver(EventResolverContext context);
+    abstract EventResolver createStackTraceStringResolver(EventResolverContext context);
 
     abstract EventResolver createStackTraceObjectResolver(EventResolverContext context);
 
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
index a5ab172..140cc42 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
@@ -19,6 +19,14 @@ package org.apache.logging.log4j.layout.json.template.resolver;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
+/**
+ * Exception resolver.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setStackTraceEnabled(boolean)}.
+ *
+ * @see ExceptionInternalResolverFactory
+ */
 class ExceptionResolver implements EventResolver {
 
     private static final ExceptionInternalResolverFactory INTERNAL_RESOLVER_FACTORY =
@@ -51,15 +59,15 @@ class ExceptionResolver implements EventResolver {
                 }
 
                 @Override
-                EventResolver createStackTraceTextResolver(final EventResolverContext context) {
-                    StackTraceTextResolver stackTraceTextResolver =
-                            new StackTraceTextResolver(context);
+                EventResolver createStackTraceStringResolver(final EventResolverContext context) {
+                    StackTraceStringResolver stackTraceStringResolver =
+                            new StackTraceStringResolver(context);
                     return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
                         final Throwable exception = logEvent.getThrown();
                         if (exception == null) {
                             jsonWriter.writeNull();
                         } else {
-                            stackTraceTextResolver.resolve(exception, jsonWriter);
+                            stackTraceStringResolver.resolve(exception, jsonWriter);
                         }
                     };
                 }
@@ -82,9 +90,12 @@ class ExceptionResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    ExceptionResolver(final EventResolverContext context, final String key) {
+    ExceptionResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
         this.stackTraceEnabled = context.isStackTraceEnabled();
-        this.internalResolver = INTERNAL_RESOLVER_FACTORY.createInternalResolver(context, key);
+        this.internalResolver = INTERNAL_RESOLVER_FACTORY
+                .createInternalResolver(context, config);
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
index 8a70bfd..7ca79b0 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
@@ -16,9 +16,11 @@
  */
 package org.apache.logging.log4j.layout.json.template.resolver;
 
-final class ExceptionResolverFactory implements EventResolverFactory<ExceptionResolver> {
+final class ExceptionResolverFactory
+        implements EventResolverFactory<ExceptionResolver> {
 
-    private static final ExceptionResolverFactory INSTANCE = new ExceptionResolverFactory();
+    private static final ExceptionResolverFactory INSTANCE =
+            new ExceptionResolverFactory();
 
     private ExceptionResolverFactory() {}
 
@@ -32,8 +34,10 @@ final class ExceptionResolverFactory implements EventResolverFactory<ExceptionRe
     }
 
     @Override
-    public ExceptionResolver create(final EventResolverContext context, final String key) {
-        return new ExceptionResolver(context, key);
+    public ExceptionResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ExceptionResolver(context, config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
index 230ac59..f3d4705 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
@@ -20,6 +20,14 @@ import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.util.Throwables;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
+/**
+ * Exception root cause resolver.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setStackTraceEnabled(boolean)}.
+ *
+ * @see ExceptionInternalResolverFactory
+ */
 final class ExceptionRootCauseResolver implements EventResolver {
 
     private static final ExceptionInternalResolverFactory INTERNAL_RESOLVER_FACTORY =
@@ -54,16 +62,16 @@ final class ExceptionRootCauseResolver implements EventResolver {
                 }
 
                 @Override
-                EventResolver createStackTraceTextResolver(final EventResolverContext context) {
-                    final StackTraceTextResolver stackTraceTextResolver =
-                            new StackTraceTextResolver(context);
+                EventResolver createStackTraceStringResolver(final EventResolverContext context) {
+                    final StackTraceStringResolver stackTraceStringResolver =
+                            new StackTraceStringResolver(context);
                     return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
                         final Throwable exception = logEvent.getThrown();
                         if (exception == null) {
                             jsonWriter.writeNull();
                         } else {
                             final Throwable rootCause = Throwables.getRootCause(exception);
-                            stackTraceTextResolver.resolve(rootCause, jsonWriter);
+                            stackTraceStringResolver.resolve(rootCause, jsonWriter);
                         }
                     };
                 }
@@ -87,9 +95,12 @@ final class ExceptionRootCauseResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    ExceptionRootCauseResolver(final EventResolverContext context, final String key) {
+    ExceptionRootCauseResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
         this.stackTraceEnabled = context.isStackTraceEnabled();
-        this.internalResolver = INTERNAL_RESOLVER_FACTORY.createInternalResolver(context, key);
+        this.internalResolver = INTERNAL_RESOLVER_FACTORY
+                .createInternalResolver(context, config);
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
index 1f625b0..e511f0d 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
@@ -32,8 +32,10 @@ final class ExceptionRootCauseResolverFactory implements EventResolverFactory<Ex
     }
 
     @Override
-    public ExceptionRootCauseResolver create(final EventResolverContext context, final String key) {
-        return new ExceptionRootCauseResolver(context, key);
+    public ExceptionRootCauseResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ExceptionRootCauseResolver(context, config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
index ad33a0a..422e445 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
@@ -26,6 +26,52 @@ import java.util.Map;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+/**
+ * Level resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config         = field , [ severity ]
+ * field          = "field" -> ( "name" | "severity" )
+ * severity       = severity-field
+ * severity-field = "field" -> ( "keyword" | "code" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the level name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "level",
+ *   "field": "name"
+ * }
+ * </pre>
+ *
+ * Resolve the severity keyword:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "level",
+ *   "field": "severity",
+ *   "severity": {
+ *     "field": "keyword"
+ *   }
+ * }
+ *
+ * Resolve the severity code:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "level",
+ *   "field": "severity",
+ *   "severity": {
+ *     "field": "code"
+ *   }
+ * }
+ * </pre>
+ */
 final class LevelResolver implements EventResolver {
 
     private static String[] SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL;
@@ -56,16 +102,31 @@ final class LevelResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    LevelResolver(final EventResolverContext context, final String key) {
+    LevelResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        this.internalResolver = createResolver(context, config);
+    }
+
+    private static EventResolver createResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
         final JsonWriter jsonWriter = context.getJsonWriter();
-        if (key == null) {
-            internalResolver = createNameResolver(jsonWriter);
-        } else if ("severity".equals(key)) {
-            internalResolver = createSeverityNameResolver(jsonWriter);
-        } else if ("severity:code".equals(key)) {
-            internalResolver = SEVERITY_CODE_RESOLVER;
-        } else {
-            throw new IllegalArgumentException("unknown key: " + key);
+        final String fieldName = config.getString("field");
+        switch (fieldName) {
+            case "name": return createNameResolver(jsonWriter);
+            case "severity": {
+                final String severityFieldName =
+                        config.getString(new String[]{"severity", "field"});
+                switch (severityFieldName) {
+                    case "keyword": return createSeverityKeywordResolver(jsonWriter);
+                    case "code": return SEVERITY_CODE_RESOLVER;
+                    default:
+                        throw new IllegalArgumentException(
+                                "unknown severity field: " + config);
+                }
+            }
+            default: throw new IllegalArgumentException("unknown field: " + config);
         }
     }
 
@@ -85,15 +146,15 @@ final class LevelResolver implements EventResolver {
         };
     }
 
-    private static EventResolver createSeverityNameResolver(
+    private static EventResolver createSeverityKeywordResolver(
             final JsonWriter contextJsonWriter) {
         final Map<Level, String> resolutionByLevel = Arrays
                 .stream(Level.values())
                 .collect(Collectors.toMap(
                         Function.identity(),
                         (final Level level) -> contextJsonWriter.use(() -> {
-                            final String severityName = Severity.getSeverity(level).name();
-                            contextJsonWriter.writeString(severityName);
+                            final String severityKeyword = Severity.getSeverity(level).name();
+                            contextJsonWriter.writeString(severityKeyword);
                         })));
         return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
             final String resolution = resolutionByLevel.get(logEvent.getLevel());
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
index e28745b..f5ee519 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
@@ -32,8 +32,10 @@ final class LevelResolverFactory implements EventResolverFactory<LevelResolver>
     }
 
     @Override
-    public LevelResolver create(final EventResolverContext context, final String key) {
-        return new LevelResolver(context, key);
+    public LevelResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new LevelResolver(context, config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
index b88bd0d..66f1f87 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
@@ -19,6 +19,35 @@ package org.apache.logging.log4j.layout.json.template.resolver;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
+/**
+ * Logger resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> ( "name" | "fqcn" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the logger name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "logger",
+ *   "field": "name"
+ * }
+ * </pre>
+ *
+ * Resolve the logger's fully qualified class name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "logger",
+ *   "field": "fqcn"
+ * }
+ * </pre>
+ */
 final class LoggerResolver implements EventResolver {
 
     private static final EventResolver NAME_RESOLVER =
@@ -35,16 +64,18 @@ final class LoggerResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    LoggerResolver(final String key) {
-        this.internalResolver = createInternalResolver(key);
+    LoggerResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
     }
 
-    private static EventResolver createInternalResolver(final String key) {
-        switch (key) {
+    private static EventResolver createInternalResolver(
+            final TemplateResolverConfig config) {
+        final String fieldName = config.getString("field");
+        switch (fieldName) {
             case "name": return NAME_RESOLVER;
             case "fqcn": return FQCN_RESOLVER;
         }
-        throw new IllegalArgumentException("unknown key: " + key);
+        throw new IllegalArgumentException("unknown field: " + config);
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
index 10d4ba7..5539f6e 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
@@ -32,8 +32,10 @@ final class LoggerResolverFactory implements EventResolverFactory<LoggerResolver
     }
 
     @Override
-    public LoggerResolver create(final EventResolverContext context, final String key) {
-        return new LoggerResolver(key);
+    public LoggerResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new LoggerResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
index 29be9e8..b12821c 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
@@ -20,6 +20,39 @@ import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.lookup.MainMapLookup;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
+/**
+ * An index-based resolver for the <tt>main()</tt> method arguments.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = index | key
+ * index  = "index" -> number
+ * key    = "key" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the 1st <tt>main()</tt> method argument:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "main",
+ *   "index": 0
+ * }
+ * </pre>
+ *
+ * Resolve the argument coming right after <tt>--userId</tt>:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "main",
+ *   "key": "--userId"
+ * }
+ * </pre>
+ *
+ * @see MainMapResolver
+ */
 final class MainMapResolver implements EventResolver {
 
     private static final MainMapLookup MAIN_MAP_LOOKUP = new MainMapLookup();
@@ -30,8 +63,20 @@ final class MainMapResolver implements EventResolver {
         return "main";
     }
 
-    MainMapResolver(final String key) {
-        this.key = key;
+    MainMapResolver(final TemplateResolverConfig config) {
+        final String key = config.getString("key");
+        final Integer index = config.getInteger("index");
+        if (key != null && index != null) {
+            throw new IllegalArgumentException(
+                    "provided both key and index: " + config);
+        }
+        if (key == null && index == null) {
+            throw new IllegalArgumentException(
+                    "either key or index must be provided: " + config);
+        }
+        this.key = index != null
+                ? String.valueOf(index)
+                : key;
     }
 
     @Override
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
index 60262d4..83b93a1 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
@@ -32,8 +32,10 @@ final class MainMapResolverFactory implements EventResolverFactory<MainMapResolv
     }
 
     @Override
-    public MainMapResolver create(final EventResolverContext context, final String key) {
-        return new MainMapResolver(key);
+    public MainMapResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new MainMapResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
index 1c71619..622d8f3 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
@@ -21,6 +21,26 @@ import org.apache.logging.log4j.core.lookup.MapLookup;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 import org.apache.logging.log4j.message.MapMessage;
 
+/**
+ * {@link MapMessage} field resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "key" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the <tt>userRole</tt> field of the message:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "map",
+ *   "key": "userRole"
+ * }
+ * </pre>
+ */
 final class MapResolver implements EventResolver {
 
     private static final MapLookup MAP_LOOKUP = new MapLookup();
@@ -31,7 +51,11 @@ final class MapResolver implements EventResolver {
         return "map";
     }
 
-    MapResolver(final String key) {
+    MapResolver(final TemplateResolverConfig config) {
+        final String key = config.getString("key");
+        if (key == null) {
+            throw new IllegalArgumentException("missing key: " + config);
+        }
         this.key = key;
     }
 
@@ -44,19 +68,8 @@ final class MapResolver implements EventResolver {
     public void resolve(
             final LogEvent logEvent,
             final JsonWriter jsonWriter) {
-
-        // If the event message is not of type MapMessage, then do not even try
-        // to perform the map lookup.
-        if (!(logEvent.getMessage() instanceof MapMessage)) {
-            jsonWriter.writeNull();
-        }
-
-        // Perform the map lookup against Log4j.
-        else {
-            final String resolvedValue = MAP_LOOKUP.lookup(logEvent, key);
-            jsonWriter.writeString(resolvedValue);
-        }
-
+        final String resolvedValue = MAP_LOOKUP.lookup(logEvent, key);
+        jsonWriter.writeString(resolvedValue);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
index 6e2e8d6..df57601 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
@@ -32,8 +32,10 @@ final class MapResolverFactory implements EventResolverFactory<MapResolver> {
     }
 
     @Override
-    public MapResolver create(final EventResolverContext context, final String key) {
-        return new MapResolver(key);
+    public MapResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new MapResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
index 86a352f..0bef3ff 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
@@ -20,6 +20,26 @@ import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
+/**
+ * A {@link Marker} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> "name"
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the marker name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "marker",
+ *   "field": "name"
+ * }
+ * </pre>
+ */
 final class MarkerResolver implements EventResolver {
 
     private static final TemplateResolver<LogEvent> NAME_RESOLVER =
@@ -34,15 +54,17 @@ final class MarkerResolver implements EventResolver {
 
     private final TemplateResolver<LogEvent> internalResolver;
 
-    MarkerResolver(final String key) {
-        this.internalResolver = createInternalResolver(key);
+    MarkerResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
     }
 
-    private TemplateResolver<LogEvent> createInternalResolver(final String key) {
-        if ("name".equals(key)) {
+    private TemplateResolver<LogEvent> createInternalResolver(
+            final TemplateResolverConfig config) {
+        final String fieldName = config.getString("field");
+        if ("name".equals(fieldName)) {
             return NAME_RESOLVER;
         }
-        throw new IllegalArgumentException("unknown key: " + key);
+        throw new IllegalArgumentException("unknown field: " + config);
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
index 42f20c5..2d4a2cb 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
@@ -32,8 +32,10 @@ final class MarkerResolverFactory implements EventResolverFactory<MarkerResolver
     }
 
     @Override
-    public MarkerResolver create(final EventResolverContext context, final String key) {
-        return new MarkerResolver(key);
+    public MarkerResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new MarkerResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
index b2953df..54daefe 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
@@ -23,30 +23,58 @@ import org.apache.logging.log4j.message.MultiformatMessage;
 import org.apache.logging.log4j.message.ObjectMessage;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.util.StringBuilderFormattable;
-import org.apache.logging.log4j.util.Strings;
 
+/**
+ * {@link Message} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config      = [ stringified ]
+ * stringified = "stringified" -> boolean
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the message into a string:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "message",
+ *   "stringified": true
+ * }
+ * </pre>
+ *
+ * Resolve the message such that if it is a {@link ObjectMessage} or {@link
+ * MultiformatMessage} with JSON support, its emitted JSON type (string, list,
+ * object, etc.) will be retained:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "message"
+ * }
+ * </pre>
+ */
 final class MessageResolver implements EventResolver {
 
     private static final String[] FORMATS = { "JSON" };
 
     private final EventResolver internalResolver;
 
-    MessageResolver(final String key) {
-        this.internalResolver = createInternalResolver(key);
+    MessageResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
     }
 
     static String getName() {
         return "message";
     }
 
-    private static EventResolver createInternalResolver(final String key) {
-        if (Strings.isBlank(key)) {
-            return MessageResolver::resolveText;
-        } else if (FORMATS[0].equalsIgnoreCase(key)) {
-            return MessageResolver::resolveJson;
-        } else {
-            throw new IllegalArgumentException("unknown key: " + key);
-        }
+    private static EventResolver createInternalResolver(
+            final TemplateResolverConfig config) {
+        final boolean stringified = config.getBoolean("stringified", false);
+        return stringified
+                ? MessageResolver::resolveString
+                : MessageResolver::resolveObject;
     }
 
     @Override
@@ -56,14 +84,14 @@ final class MessageResolver implements EventResolver {
         internalResolver.resolve(logEvent, jsonWriter);
     }
 
-    private static void resolveText(
+    private static void resolveString(
             final LogEvent logEvent,
             final JsonWriter jsonWriter) {
         final Message message = logEvent.getMessage();
-        resolveText(message, jsonWriter);
+        resolveString(message, jsonWriter);
     }
 
-    private static void resolveText(
+    private static void resolveString(
             final Message message,
             final JsonWriter jsonWriter) {
         if (message instanceof StringBuilderFormattable) {
@@ -76,7 +104,7 @@ final class MessageResolver implements EventResolver {
         }
     }
 
-    private static void resolveJson(
+    private static void resolveObject(
             final LogEvent logEvent,
             final JsonWriter jsonWriter) {
 
@@ -97,7 +125,7 @@ final class MessageResolver implements EventResolver {
         }
 
         // Fallback to plain Object write.
-        resolveText(logEvent, jsonWriter);
+        resolveString(logEvent, jsonWriter);
 
     }
 
@@ -146,7 +174,7 @@ final class MessageResolver implements EventResolver {
         }
 
         // Fallback to the default message formatter.
-        resolveText((LogEvent) message, jsonWriter);
+        resolveString((LogEvent) message, jsonWriter);
         return true;
 
     }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
index 9032d9e..4d46bb5 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
@@ -32,8 +32,10 @@ final class MessageResolverFactory implements EventResolverFactory<MessageResolv
     }
 
     @Override
-    public MessageResolver create(final EventResolverContext context, final String key) {
-        return new MessageResolver(key);
+    public MessageResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new MessageResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
index 1b2fba0..727a8e8 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
@@ -20,19 +20,45 @@ import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.layout.PatternLayout;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.Strings;
 
+/**
+ * Resolver delegating to {@link PatternLayout}.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config  = pattern
+ * pattern = "pattern" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Inject string produced by <tt>%p %c{1.} [%t] %X{userId} %X %m%ex</tt> pattern:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "pattern",
+ *   "pattern": "%p %c{1.} [%t] %X{userId} %X %m%ex"
+ * }
+ * </pre>
+ */
 final class PatternResolver implements EventResolver {
 
     private final BiConsumer<StringBuilder, LogEvent> emitter;
 
     PatternResolver(
             final EventResolverContext context,
-            final String key) {
+            final TemplateResolverConfig config) {
+        final String pattern = config.getString("pattern");
+        if (Strings.isBlank(pattern)) {
+            throw new IllegalArgumentException("blank pattern: " + config);
+        }
         final PatternLayout patternLayout = PatternLayout
                 .newBuilder()
                 .setConfiguration(context.getConfiguration())
                 .setCharset(context.getCharset())
-                .setPattern(key)
+                .setPattern(pattern)
                 .build();
         this.emitter = (final StringBuilder stringBuilder, final LogEvent logEvent) ->
                 patternLayout.serialize(logEvent, stringBuilder);
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
index c47f850..e3ddaf9 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
@@ -32,8 +32,10 @@ final class PatternResolverFactory implements EventResolverFactory<PatternResolv
     }
 
     @Override
-    public PatternResolver create(final EventResolverContext context, final String key) {
-        return new PatternResolver(context, key);
+    public PatternResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new PatternResolver(context, config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
index 7a029f1..8f857d0 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
@@ -19,6 +19,34 @@ package org.apache.logging.log4j.layout.json.template.resolver;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
+/**
+ * Resolver for the {@link StackTraceElement} returned by {@link LogEvent#getSource()}.
+ *
+ * Note that this resolver is toggled by {@link
+ * org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.Builder#setLocationInfoEnabled(boolean)}
+ * method.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> (
+ *            "className"  |
+ *            "fileName"   |
+ *            "methodName" |
+ *            "lineNumber" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the line number:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "source",
+ *   "field": "lineNumber"
+ * }
+ * </pre>
+ */
 final class SourceResolver implements EventResolver {
 
     private static final EventResolver NULL_RESOLVER =
@@ -73,24 +101,27 @@ final class SourceResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    SourceResolver(final EventResolverContext context, final String key) {
+    SourceResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
         this.locationInfoEnabled = context.isLocationInfoEnabled();
-        this.internalResolver = createInternalResolver(context, key);
+        this.internalResolver = createInternalResolver(context, config);
     }
 
     private static EventResolver createInternalResolver(
             final EventResolverContext context,
-            final String key) {
+            final TemplateResolverConfig config) {
         if (!context.isLocationInfoEnabled()) {
             return NULL_RESOLVER;
         }
-        switch (key) {
+        final String fieldName = config.getString("field");
+        switch (fieldName) {
             case "className": return CLASS_NAME_RESOLVER;
             case "fileName": return FILE_NAME_RESOLVER;
             case "lineNumber": return LINE_NUMBER_RESOLVER;
             case "methodName": return METHOD_NAME_RESOLVER;
         }
-        throw new IllegalArgumentException("unknown key: " + key);
+        throw new IllegalArgumentException("unknown field: " + config);
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
index 0c5c2b5..3f1e957 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
@@ -32,8 +32,10 @@ final class SourceResolverFactory implements EventResolverFactory<SourceResolver
     }
 
     @Override
-    public SourceResolver create(final EventResolverContext context, final String key) {
-        return new SourceResolver(context, key);
+    public SourceResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new SourceResolver(context, config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
index 1e30204..1c8a483 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
@@ -18,6 +18,30 @@ package org.apache.logging.log4j.layout.json.template.resolver;
 
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
+/**
+ * {@link StackTraceElement} resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> (
+ *            "className"  |
+ *            "fileName"   |
+ *            "methodName" |
+ *            "lineNumber" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the line number:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "source",
+ *   "field": "lineNumber"
+ * }
+ * </pre>
+ */
 final class StackTraceElementObjectResolver implements TemplateResolver<StackTraceElement> {
 
     private static final TemplateResolver<StackTraceElement> CLASS_NAME_RESOLVER =
@@ -38,18 +62,20 @@ final class StackTraceElementObjectResolver implements TemplateResolver<StackTra
 
     private final TemplateResolver<StackTraceElement> internalResolver;
 
-    StackTraceElementObjectResolver(final String key) {
-        this.internalResolver = createInternalResolver(key);
+    StackTraceElementObjectResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
     }
 
-    private TemplateResolver<StackTraceElement> createInternalResolver(final String key) {
-        switch (key) {
+    private TemplateResolver<StackTraceElement> createInternalResolver(
+            final TemplateResolverConfig config) {
+        final String fieldName = config.getString("field");
+        switch (fieldName) {
             case "className": return CLASS_NAME_RESOLVER;
             case "methodName": return METHOD_NAME_RESOLVER;
             case "fileName": return FILE_NAME_RESOLVER;
             case "lineNumber": return LINE_NUMBER_RESOLVER;
         }
-        throw new IllegalArgumentException("unknown key: " + key);
+        throw new IllegalArgumentException("unknown field: " + config);
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
index 84456e7..a07694c 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
@@ -36,8 +36,8 @@ final class StackTraceElementObjectResolverFactory
     @Override
     public StackTraceElementObjectResolver create(
             final StackTraceElementObjectResolverContext context,
-            final String key) {
-        return new StackTraceElementObjectResolver(key);
+            final TemplateResolverConfig config) {
+        return new StackTraceElementObjectResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
similarity index 93%
rename from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.java
rename to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
index 17f9ac1..d744070 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceStringResolver.java
@@ -22,11 +22,11 @@ import org.apache.logging.log4j.layout.json.template.util.Recycler;
 
 import java.util.function.Supplier;
 
-final class StackTraceTextResolver implements StackTraceResolver {
+final class StackTraceStringResolver implements StackTraceResolver {
 
     private final Recycler<TruncatingBufferedPrintWriter> writerRecycler;
 
-    StackTraceTextResolver(final EventResolverContext context) {
+    StackTraceStringResolver(final EventResolverContext context) {
         final Supplier<TruncatingBufferedPrintWriter> writerSupplier =
                 () -> TruncatingBufferedPrintWriter.ofCapacity(
                         context.getMaxStringByteCount());
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
similarity index 79%
copy from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
index dacc07b..a83fffa 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
@@ -16,10 +16,14 @@
  */
 package org.apache.logging.log4j.layout.json.template.resolver;
 
-interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>, R extends TemplateResolver<V>> {
+import org.apache.logging.log4j.layout.json.template.util.MapAccessor;
 
-    String getName();
+import java.util.Map;
 
-    R create(C context, String key);
+class TemplateResolverConfig extends MapAccessor {
+
+    TemplateResolverConfig(final Map<String, Object> map) {
+        super(map);
+    }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
index dacc07b..3e3c8ef 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
@@ -20,6 +20,6 @@ interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>, R
 
     String getName();
 
-    R create(C context, String key);
+    R create(C context, TemplateResolverConfig config);
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
index 0ba10ce..ca304b7 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
@@ -17,7 +17,7 @@
 package org.apache.logging.log4j.layout.json.template.resolver;
 
 import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
 import org.apache.logging.log4j.layout.json.template.util.JsonReader;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
@@ -28,6 +28,8 @@ import java.util.stream.Collectors;
 
 public enum TemplateResolvers {;
 
+    private static final String RESOLVER_FIELD_NAME = "$resolver";
+
     private static final TemplateResolver<?> EMPTY_ARRAY_RESOLVER =
             (final Object ignored, final JsonWriter jsonWriter) -> {
                 jsonWriter.writeArrayStart();
@@ -60,28 +62,8 @@ public enum TemplateResolvers {;
         // Append the additional fields.
         if (context instanceof EventResolverContext) {
             final EventResolverContext eventResolverContext = (EventResolverContext) context;
-            final KeyValuePair[] additionalFields = eventResolverContext.getAdditionalFields();
-            if (additionalFields != null) {
-
-                // Check that the root is an object node.
-                final Map<String, Object> objectNode;
-                try {
-                    @SuppressWarnings("unchecked")
-                    final Map<String, Object> map = (Map<String, Object>) node;
-                    objectNode = map;
-                } catch (final ClassCastException error) {
-                    final String message = String.format(
-                            "was expecting an object to merge additional fields (class=%s)",
-                            node.getClass().getName());
-                    throw new IllegalArgumentException(message);
-                }
-
-                // Merge additional fields.
-                for (final KeyValuePair additionalField : additionalFields) {
-                    objectNode.put(additionalField.getKey(), additionalField.getValue());
-                }
-
-            }
+            final EventTemplateAdditionalField[] additionalFields = eventResolverContext.getAdditionalFields();
+            appendAdditionalFields(node, additionalFields);
         }
 
         // Resolve the template.
@@ -89,6 +71,55 @@ public enum TemplateResolvers {;
 
     }
 
+    private static void appendAdditionalFields(
+            final Object node,
+            EventTemplateAdditionalField[] additionalFields) {
+        if (additionalFields.length > 0) {
+
+            // Check that the root is an object node.
+            final Map<String, Object> objectNode;
+            try {
+                @SuppressWarnings("unchecked")
+                final Map<String, Object> map = (Map<String, Object>) node;
+                objectNode = map;
+            } catch (final ClassCastException error) {
+                final String message = String.format(
+                        "was expecting an object to merge additional fields: %s",
+                        node.getClass().getName());
+                throw new IllegalArgumentException(message);
+            }
+
+            // Merge additional fields.
+            for (final EventTemplateAdditionalField additionalField : additionalFields) {
+                final String additionalFieldKey = additionalField.getKey();
+                final Object additionalFieldValue;
+                switch (additionalField.getType()) {
+                    case STRING:
+                        additionalFieldValue = additionalField.getValue();
+                        break;
+                    case JSON:
+                        try {
+                            additionalFieldValue =  JsonReader.read(additionalField.getValue());
+                        } catch (final Exception error) {
+                            final String message = String.format(
+                                    "failed reading JSON provided by additional field: %s",
+                                    additionalFieldKey);
+                            throw new IllegalArgumentException(message, error);
+                        }
+                        break;
+                    default: {
+                        final String message = String.format(
+                                "unknown type %s for additional field: %s",
+                                additionalFieldKey, additionalField.getType());
+                        throw new IllegalArgumentException(message);
+                    }
+                }
+                objectNode.put(additionalFieldKey, additionalFieldValue);
+            }
+
+        }
+    }
+
     private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofObject(
             final C context,
             final Object object) {
@@ -167,6 +198,11 @@ public enum TemplateResolvers {;
             final C context,
             final Map<String, Object> map) {
 
+        // Check if this is a resolver request.
+        if (map.containsKey(RESOLVER_FIELD_NAME)) {
+            return ofResolver(context, map);
+        }
+
         // Create resolver for each object field.
         final List<String> fieldNames = new ArrayList<>();
         final List<TemplateResolver<V>> fieldResolvers = new ArrayList<>();
@@ -211,7 +247,8 @@ public enum TemplateResolvers {;
                     continue;
                 }
                 final boolean succeedingEntry = resolvedFieldCount > 0;
-                if (fieldResolver.isFlattening()) {
+                final boolean flattening = fieldResolver.isFlattening();
+                if (flattening) {
                     final int initLength = jsonWriterStringBuilder.length();
                     fieldResolver.resolve(value, jsonWriter, succeedingEntry);
                     final boolean resolved = jsonWriterStringBuilder.length() > initLength;
@@ -233,25 +270,32 @@ public enum TemplateResolvers {;
 
     }
 
-    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofString(
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofResolver(
             final C context,
-            final String fieldValue) {
+            final Map<String, Object> map) {
 
-        // Try to resolve the directive as a ${json:xxx} parameter.
-        final TemplateResolverRequest resolverRequest = readResolverRequest(fieldValue);
-        if (resolverRequest != null) {
-            final TemplateResolverFactory<V, C, ? extends TemplateResolver<V>> resolverFactory =
-                    context.getResolverFactoryByName().get(resolverRequest.resolverName);
-            if (resolverFactory != null) {
-                return resolverFactory.create(context, resolverRequest.resolverKey);
-            }
+        // Extract the resolver name.
+        final Object resolverNameObject = map.get(RESOLVER_FIELD_NAME);
+        if (!(resolverNameObject instanceof String)) {
+            throw new IllegalArgumentException(
+                    "invalid resolver name: " + resolverNameObject);
+        }
+        final String resolverName = (String) resolverNameObject;
+
+        // Retrieve the resolver.
+        final TemplateResolverFactory<V, C, ? extends TemplateResolver<V>> resolverFactory =
+                context.getResolverFactoryByName().get(resolverName);
+        if (resolverFactory == null) {
+            throw new IllegalArgumentException("unknown resolver: " + resolverName);
         }
+        final TemplateResolverConfig resolverConfig = new TemplateResolverConfig(map);
+        return resolverFactory.create(context, resolverConfig);
+
+    }
 
-        // The rest is the fallback template resolver that delegates every other
-        // substitution to Log4j. This will be the case for every template value
-        // that does not use directives of pattern ${json:xxx}. This
-        // additionally serves as a mechanism to resolve values at runtime when
-        // this layout misses certain resolvers.
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofString(
+            final C context,
+            final String fieldValue) {
 
         // Check if substitution needed at all. (Copied logic from
         // AbstractJacksonLayout.valueNeedsLookup() method.)
@@ -264,11 +308,7 @@ public enum TemplateResolvers {;
                 return (final V value, final JsonWriter jsonWriter) -> {
                     final LogEvent logEvent = (LogEvent) value;
                     final String replacedText = context.getSubstitutor().replace(logEvent, fieldValue);
-                    if (replacedText == null) {
-                        jsonWriter.writeNull();
-                    } else {
-                        jsonWriter.writeString(replacedText);
-                    }
+                    jsonWriter.writeString(replacedText);
                 };
             }
 
@@ -291,7 +331,7 @@ public enum TemplateResolvers {;
 
         }
 
-        // Write the field value as is. (Blank value check has already been done at the top.)
+        // Write the field value as is.
         else {
             final String escapedFieldValue =
                     contextJsonWriter.use(() ->
@@ -302,45 +342,6 @@ public enum TemplateResolvers {;
 
     }
 
-    private static TemplateResolverRequest readResolverRequest(final String fieldValue) {
-
-        // Bail-out if cannot spot the template signature.
-        if (!fieldValue.startsWith("${json:") || !fieldValue.endsWith("}")) {
-            return null;
-        }
-
-        // Try to read both resolver name and key.
-        final int resolverNameStartIndex = 7;
-        final int fieldNameSeparatorIndex = fieldValue.indexOf(':', resolverNameStartIndex);
-        if (fieldNameSeparatorIndex < 0) {
-            final int resolverNameEndIndex = fieldValue.length() - 1;
-            final String resolverName = fieldValue.substring(resolverNameStartIndex, resolverNameEndIndex);
-            return new TemplateResolverRequest(resolverName, null);
-        } else {
-            @SuppressWarnings("UnnecessaryLocalVariable")
-            final int resolverNameEndIndex = fieldNameSeparatorIndex;
-            final int resolverKeyStartIndex = fieldNameSeparatorIndex + 1;
-            final int resolverKeyEndIndex = fieldValue.length() - 1;
-            final String resolverName = fieldValue.substring(resolverNameStartIndex, resolverNameEndIndex);
-            final String resolverKey = fieldValue.substring(resolverKeyStartIndex, resolverKeyEndIndex);
-            return new TemplateResolverRequest(resolverName, resolverKey);
-        }
-
-    }
-
-    private static final class TemplateResolverRequest {
-
-        private final String resolverName;
-
-        private final String resolverKey;
-
-        private TemplateResolverRequest(final String resolverName, final String resolverKey) {
-            this.resolverName = resolverName;
-            this.resolverKey = resolverKey;
-        }
-
-    }
-
     private static <V> TemplateResolver<V> ofNumber(final Number number) {
         final String numberString = String.valueOf(number);
         return (final V ignored, final JsonWriter jsonWriter) ->
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
similarity index 63%
rename from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java
rename to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
index 71c973b..a2671fb 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolver.java
@@ -6,7 +6,7 @@
  * (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
+ *    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,
@@ -19,67 +19,143 @@ package org.apache.logging.log4j.layout.json.template.resolver;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 import org.apache.logging.log4j.layout.json.template.util.Recycler;
-import org.apache.logging.log4j.layout.json.template.util.StringParameterParser;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.TriConsumer;
 
-import java.util.Arrays;
-import java.util.LinkedHashSet;
 import java.util.Map;
-import java.util.Set;
 import java.util.regex.Pattern;
 
 /**
- * Add Mapped Diagnostic Context (MDC).
+ * Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config        = singleAccess | multiAccess
+ *
+ * singleAccess  = key , [ stringified ]
+ * key           = "key" -> string
+ * stringified   = "stringified" -> boolean
+ *
+ * multiAccess   = [ pattern ] , [ flatten ] , [ stringified ]
+ * pattern       = "pattern" -> string
+ * flatten       = "flatten" -> ( boolean | flattenConfig )
+ * flattenConfig = [ flattenPrefix ]
+ * flattenPrefix = "prefix" -> string
+ * </pre>
+ *
+ * Note that <tt>singleAccess</tt> resolves the MDC value as is, whilst
+ * <tt>multiAccess</tt> resolves a multitude of MDC values. If <tt>flatten</tt>
+ * is provided, <tt>multiAccess</tt> merges the values with the parent,
+ * otherwise creates a new JSON object containing the values.
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the <tt>userRole</tt> MDC value:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "key": "userRole"
+ * }
+ * </pre>
+ *
+ * Resolve the string representation of the <tt>userRank</tt> MDC value:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "key": "userRank",
+ *   "stringified": true
+ * }
+ * </pre>
+ *
+ * Resolve all MDC entries into an object:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc"
+ * }
+ * </pre>
+ *
+ * Resolve all MDC entries into an object such that values are converted to
+ * string:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "stringified": true
+ * }
+ * </pre>
+ *
+ * Merge all MDC entries whose keys are matching with the
+ * <tt>user(Role|Rank)</tt> regex into the parent:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "flatten": true,
+ *   "pattern": "user(Role|Rank)"
+ * }
+ * </pre>
+ *
+ * After converting the corresponding entries to string, merge all MDC entries
+ * to parent such that keys are prefixed with <tt>_</tt>:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "mdc",
+ *   "stringified": true,
+ *   "flatten": {
+ *     "prefix": "_"
+ *   }
+ * }
+ * </pre>
  */
-final class ContextDataResolver implements EventResolver {
-
-    private enum Param {;
-
-        private static final String FLATTEN = "flatten";
-
-        private static final String PATTERN = "pattern";
-
-        private static final String KEY = "key";
-
-        private static final String STRINGIFY = "stringify";
-
-    }
-
-    private static final Set<String> PARAMS =
-            new LinkedHashSet<>(Arrays.asList(
-                    Param.FLATTEN,
-                    Param.PATTERN,
-                    Param.KEY,
-                    Param.STRINGIFY));
+final class ThreadContextDataResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    ContextDataResolver(final EventResolverContext context, final String spec) {
-        this.internalResolver = createResolver(context, spec);
+    ThreadContextDataResolver(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        this.internalResolver = createResolver(context, config);
     }
 
     private static EventResolver createResolver(
             final EventResolverContext context,
-            final String spec) {
-        final Map<String, StringParameterParser.Value> params = StringParameterParser.parse(spec, PARAMS);
-        final StringParameterParser.Value keyValue = params.get(Param.KEY);
-        if (keyValue != null) {
-            if (params.size() != 1) {
+            final TemplateResolverConfig config) {
+        final Object flattenObject = config.getObject("flatten");
+        final boolean flatten;
+        if (flattenObject == null) {
+            flatten = false;
+        } else if (flattenObject instanceof Boolean) {
+            flatten = (boolean) flattenObject;
+        } else if (flattenObject instanceof Map) {
+            flatten = true;
+        } else {
+            throw new IllegalArgumentException("invalid flatten option: " + config);
+        }
+        final String key = config.getString("key");
+        final String prefix = config.getString(new String[] {"flatten", "prefix"});
+        final String pattern = config.getString("pattern");
+        final boolean stringified = config.getBoolean("stringified", false);
+        if (key != null) {
+            if (flatten) {
                 throw new IllegalArgumentException(
-                        "MDC key access doesn't take arguments: " + spec);
-            }
-            if (keyValue instanceof StringParameterParser.NullValue) {
-                throw new IllegalArgumentException("missing MDC key: " + spec);
+                        "both key and flatten options cannot be supplied: " + config);
             }
-            final String key = keyValue.toString();
-            return createKeyResolver(key);
+            return createKeyResolver(key, stringified);
         } else {
-            return createResolver(context, spec, params);
+            final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
+            return createResolver(recyclerFactory, flatten, prefix, pattern, stringified);
         }
     }
 
-    private static EventResolver createKeyResolver(final String key) {
+    private static EventResolver createKeyResolver(
+            final String key,
+            final boolean stringified) {
         return new EventResolver() {
 
             @Override
@@ -92,62 +168,41 @@ final class ContextDataResolver implements EventResolver {
             public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
                 final ReadOnlyStringMap contextData = logEvent.getContextData();
                 final Object value = contextData == null ? null : contextData.getValue(key);
-                jsonWriter.writeValue(value);
+                if (stringified) {
+                    // TODO Write the value as string without extra allocations.
+                    final String valueString = String.valueOf(value);
+                    jsonWriter.writeString(valueString);
+                } else {
+                    jsonWriter.writeValue(value);
+                }
             }
 
         };
     }
 
     private static EventResolver createResolver(
-            final EventResolverContext context,
-            final String spec,
-            final Map<String, StringParameterParser.Value> params) {
-
-        // Read the flatten prefix.
-        final StringParameterParser.Value flattenValue = params.get(Param.FLATTEN);
-        final boolean flatten;
-        final String prefix;
-        if (flattenValue != null) {
-            flatten = true;
-            prefix = flattenValue.toString();
-        } else {
-            flatten = false;
-            prefix = null;
-        }
-
-        // Read the pattern.
-        final StringParameterParser.Value patternValue = params.get(Param.PATTERN);
-        final Pattern pattern;
-        if (patternValue == null) {
-            pattern = null;
-        } else if (patternValue instanceof StringParameterParser.NullValue) {
-            throw new IllegalArgumentException("missing MDC pattern: " + spec);
-        } else {
-            pattern = Pattern.compile(patternValue.toString());
-        }
+            final RecyclerFactory recyclerFactory,
+            final boolean flatten,
+            final String prefix,
+            final String pattern,
+            final boolean stringified) {
 
-        // Read the stringify flag.
-        final StringParameterParser.Value stringifyValue = params.get(Param.STRINGIFY);
-        final boolean stringify;
-        if (stringifyValue == null) {
-            stringify = false;
-        } else if (!(stringifyValue instanceof StringParameterParser.NullValue)) {
-            throw new IllegalArgumentException(
-                    "MDC stringify directive doesn't take parameters: " + spec);
-        } else {
-            stringify = true;
-        }
+        // Compile the pattern.
+        final Pattern compiledPattern =
+                pattern == null
+                        ? null
+                        : Pattern.compile(pattern);
 
         // Create the recycler for the loop context.
         final Recycler<LoopContext> loopContextRecycler =
-                context.getRecyclerFactory().create(() -> {
+                recyclerFactory.create(() -> {
                     final LoopContext loopContext = new LoopContext();
                     if (prefix != null) {
                         loopContext.prefix = prefix;
                         loopContext.prefixedKey = new StringBuilder(prefix);
                     }
-                    loopContext.pattern = pattern;
-                    loopContext.stringify = stringify;
+                    loopContext.pattern = compiledPattern;
+                    loopContext.stringified = stringified;
                     return loopContext;
                 });
 
@@ -222,7 +277,7 @@ final class ContextDataResolver implements EventResolver {
 
         private Pattern pattern;
 
-        private boolean stringify;
+        private boolean stringified;
 
         private JsonWriter jsonWriter;
 
@@ -259,7 +314,7 @@ final class ContextDataResolver implements EventResolver {
                     loopContext.prefixedKey.append(key);
                     loopContext.jsonWriter.writeObjectKey(loopContext.prefixedKey);
                 }
-                if (loopContext.stringify && !(value instanceof String)) {
+                if (loopContext.stringified && !(value instanceof String)) {
                     final String valueString = String.valueOf(value);
                     loopContext.jsonWriter.writeString(valueString);
                 } else {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
similarity index 61%
rename from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.java
rename to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
index c18c2a6..3ef164d 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextDataResolverFactory.java
@@ -16,24 +16,28 @@
  */
 package org.apache.logging.log4j.layout.json.template.resolver;
 
-final class ContextDataResolverFactory implements EventResolverFactory<ContextDataResolver> {
+final class ThreadContextDataResolverFactory
+        implements EventResolverFactory<ThreadContextDataResolver> {
 
-    private static final ContextDataResolverFactory INSTANCE = new ContextDataResolverFactory();
+    private static final ThreadContextDataResolverFactory INSTANCE =
+            new ThreadContextDataResolverFactory();
 
-    private ContextDataResolverFactory() {}
+    private ThreadContextDataResolverFactory() {}
 
-    static ContextDataResolverFactory getInstance() {
+    static ThreadContextDataResolverFactory getInstance() {
         return INSTANCE;
     }
 
     @Override
     public String getName() {
-        return ContextDataResolver.getName();
+        return ThreadContextDataResolver.getName();
     }
 
     @Override
-    public ContextDataResolver create(final EventResolverContext context, final String key) {
-        return new ContextDataResolver(context, key);
+    public ThreadContextDataResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ThreadContextDataResolver(context, config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
similarity index 64%
rename from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java
rename to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
index 1b5fb9f..6a9af12 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolver.java
@@ -6,7 +6,7 @@
  * (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
+ *    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,
@@ -19,43 +19,48 @@ package org.apache.logging.log4j.layout.json.template.resolver;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
-import org.apache.logging.log4j.layout.json.template.util.StringParameterParser;
 
-import java.util.Collections;
-import java.util.Map;
+import java.util.Optional;
 import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
 
 /**
- * Add Nested Diagnostic Context (NDC).
+ * Nested Diagnostic Context (NDC), aka. Thread Context Stack, resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config  = [ pattern ]
+ * pattern = "pattern" -> string
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve all NDC values into a list:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "ndc"
+ * }
+ * </pre>
+ *
+ * Resolve all NDC values matching with the <tt>pattern</tt> regex:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "ndc",
+ *   "pattern": "user(Role|Rank):\\w+"
+ * }
+ * </pre>
  */
-final class ContextStackResolver implements EventResolver {
+final class ThreadContextStackResolver implements EventResolver {
 
     private final Pattern itemPattern;
 
-    private enum Param {;
-
-        private static final String PATTERN = "pattern";
-
-    }
-
-    ContextStackResolver(final String spec) {
-        final Map<String, StringParameterParser.Value> params =
-                StringParameterParser.parse(spec, Collections.singleton(Param.PATTERN));
-        final StringParameterParser.Value patternValue = params.get(Param.PATTERN);
-        if (patternValue == null) {
-            this.itemPattern = null;
-        } else if (patternValue instanceof StringParameterParser.NullValue) {
-            throw new IllegalArgumentException("missing NDC pattern: " + spec);
-        } else {
-            final String pattern = patternValue.toString();
-            try {
-                this.itemPattern = Pattern.compile(pattern);
-            } catch (final PatternSyntaxException error) {
-                throw new IllegalArgumentException(
-                        "invalid NDC pattern: " + spec, error);
-            }
-        }
+    ThreadContextStackResolver(final TemplateResolverConfig config) {
+        this.itemPattern = Optional
+                .ofNullable(config.getString("pattern"))
+                .map(Pattern::compile)
+                .orElse(null);
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
similarity index 64%
copy from log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
index 638df83..82a5c23 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadContextStackResolverFactory.java
@@ -16,26 +16,28 @@
  */
 package org.apache.logging.log4j.layout.json.template.resolver;
 
-final class TimestampResolverFactory implements EventResolverFactory<TimestampResolver> {
+final class ThreadContextStackResolverFactory
+        implements EventResolverFactory<ThreadContextStackResolver> {
 
-    private static final TimestampResolverFactory INSTANCE = new TimestampResolverFactory();
+    private static final ThreadContextStackResolverFactory INSTANCE
+            = new ThreadContextStackResolverFactory();
 
-    private TimestampResolverFactory() {}
+    private ThreadContextStackResolverFactory() {}
 
-    static TimestampResolverFactory getInstance() {
+    static ThreadContextStackResolverFactory getInstance() {
         return INSTANCE;
     }
 
     @Override
     public String getName() {
-        return TimestampResolver.getName();
+        return ThreadContextStackResolver.getName();
     }
 
     @Override
-    public TimestampResolver create(
+    public ThreadContextStackResolver create(
             final EventResolverContext context,
-            final String key) {
-        return new TimestampResolver(key);
+            final TemplateResolverConfig config) {
+        return new ThreadContextStackResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
index afe8ecb..a316afe 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
@@ -6,7 +6,7 @@
  * (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
+ *    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,
@@ -19,6 +19,26 @@ package org.apache.logging.log4j.layout.json.template.resolver;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
+/**
+ * Thread resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = "field" -> ( "name" | "id" | "priority" )
+ * </pre>
+ *
+ * <h3>Examples</h3>
+ *
+ * Resolve the thread name:
+ *
+ * <pre>
+ * {
+ *   "$resolver": "thread",
+ *   "field": "name"
+ * }
+ * </pre>
+ */
 final class ThreadResolver implements EventResolver {
 
     private static final EventResolver NAME_RESOLVER =
@@ -41,17 +61,19 @@ final class ThreadResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    ThreadResolver(final String key) {
-        this.internalResolver = createInternalResolver(key);
+    ThreadResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(config);
     }
 
-    private static EventResolver createInternalResolver(final String key) {
-        switch (key) {
+    private static EventResolver createInternalResolver(
+            final TemplateResolverConfig config) {
+        final String fieldName = config.getString("field");
+        switch (fieldName) {
             case "name": return NAME_RESOLVER;
             case "id": return ID_RESOLVER;
             case "priority": return PRIORITY_RESOLVER;
         }
-        throw new IllegalArgumentException("unknown key: " + key);
+        throw new IllegalArgumentException("unknown field: " + config);
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
index 266c982..75df1e3 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
@@ -32,8 +32,10 @@ final class ThreadResolverFactory implements EventResolverFactory<ThreadResolver
     }
 
     @Override
-    public ThreadResolver create(final EventResolverContext context, final String key) {
-        return new ThreadResolver(key);
+    public ThreadResolver create(
+            final EventResolverContext context,
+            final TemplateResolverConfig config) {
+        return new ThreadResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
index c7c32b0..3d023b3 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
@@ -21,24 +21,195 @@ import org.apache.logging.log4j.core.time.Instant;
 import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
 import org.apache.logging.log4j.layout.json.template.JsonTemplateLayoutDefaults;
 import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
-import org.apache.logging.log4j.layout.json.template.util.StringParameterParser;
 
-import java.util.Arrays;
 import java.util.Calendar;
-import java.util.LinkedHashSet;
 import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
 import java.util.TimeZone;
 
+/**
+ * Timestamp resolver.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config        = [ patternConfig | epochConfig ]
+ *
+ * patternConfig = "pattern" -> ( [ format ] , [ timeZone ] , [ locale ] )
+ * format        = "format" -> string
+ * timeZone      = "timeZone" -> string
+ * locale        = "locale" -> (
+ *                     language                                   |
+ *                   ( language , "_" , country )                 |
+ *                   ( language , "_" , country , "_" , variant )
+ *                 )
+ *
+ * epochConfig   = "epoch" -> ( unit , [ rounded ] )
+ * unit          = "unit" -> (
+ *                     "nanos"         |
+ *                     "millis"        |
+ *                     "secs"          |
+ *                     "millis.nanos"  |
+ *                     "secs.nanos"    |
+ *                  )
+ * rounded       = "rounded" -> boolean
+ * </pre>
+ *
+ * If no configuration options are provided, <tt>pattern-config</tt> is
+ * employed. There {@link
+ * JsonTemplateLayoutDefaults#getTimestampFormatPattern()}, {@link
+ * JsonTemplateLayoutDefaults#getTimeZone()}, {@link
+ * JsonTemplateLayoutDefaults#getLocale()} are used as defaults for
+ * <tt>pattern</tt>, <tt>timeZone</tt>, and <tt>locale</tt>, respectively.
+ *
+ * In <tt>epoch-config</tt>, <tt>millis.nanos</tt>, <tt>secs.nanos</tt> stand
+ * for the fractional component in nanoseconds.
+ *
+ * <h3>Examples</h3>
+ *
+ * <table>
+ * <tr>
+ *     <td>Configuration</td>
+ *     <td>Output</td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp"
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 2020-02-07T13:38:47.098+02:00
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "pattern": {
+ *     "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ *     "timeZone": "UTC",
+ *     "locale": "en_US"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 2020-02-07T13:38:47.098Z
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "secs"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727.982123456
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "secs",
+ *     "rounded": true
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "secs.nanos"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ *            982123456
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "millis"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727982.123456
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "millis",
+ *     "rounded": true
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727982
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "millis.nanos"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ *              123456
+ *     </pre></td>
+ * </tr>
+ * <tr>
+ *     <td><pre>
+ * {
+ *   "$resolver": "timestamp",
+ *   "epoch": {
+ *     "unit": "nanos"
+ *   }
+ * }
+ *     </pre></td>
+ *     <td><pre>
+ * 1581082727982123456
+ *     </pre></td>
+ * </tr>
+ * </table>
+ */
 final class TimestampResolver implements EventResolver {
 
     private final EventResolver internalResolver;
 
-    TimestampResolver(final String key) {
-        this.internalResolver = (key != null && key.startsWith("epoch:"))
-                ? createEpochResolver(key)
-                : createFormatResolver(key);
+    TimestampResolver(final TemplateResolverConfig config) {
+        this.internalResolver = createResolver(config);
+    }
+
+    private static EventResolver createResolver(
+            final TemplateResolverConfig config) {
+        final boolean patternProvided = config.exists("pattern");
+        final boolean epochProvided = config.exists("epoch");
+        if (patternProvided && epochProvided) {
+            throw new IllegalArgumentException(
+                    "conflicting configuration options are provided: " + config);
+        }
+        return epochProvided
+                ? createEpochResolver(config)
+                : createFormatResolver(config);
     }
 
     /**
@@ -46,20 +217,6 @@ final class TimestampResolver implements EventResolver {
      */
     private static final class FormatResolverContext {
 
-        private enum Key {;
-
-            private static final String PATTERN = "pattern";
-
-            private static final String TIME_ZONE = "timeZone";
-
-            private static final String LOCALE = "locale";
-
-        }
-
-        private static final Set<String> KEYS =
-                new LinkedHashSet<>(Arrays.asList(
-                        Key.PATTERN, Key.TIME_ZONE, Key.LOCALE));
-
         private final FastDateFormat timestampFormat;
 
         private final Calendar calendar;
@@ -76,41 +233,36 @@ final class TimestampResolver implements EventResolver {
             timestampFormat.format(calendar, formattedTimestampBuilder);
         }
 
-        private static FormatResolverContext fromKey(final String key) {
-            final Map<String, StringParameterParser.Value> keys =
-                    StringParameterParser.parse(key, KEYS);
-            final String pattern = readPattern(keys);
-            final TimeZone timeZone = readTimeZone(keys);
-            final Locale locale = readLocale(keys);
+        private static FormatResolverContext fromConfig(
+                final TemplateResolverConfig config) {
+            final String format = readFormat(config);
+            final TimeZone timeZone = readTimeZone(config);
+            final Locale locale = readLocale(config);
             final FastDateFormat fastDateFormat =
-                    FastDateFormat.getInstance(pattern, timeZone, locale);
+                    FastDateFormat.getInstance(format, timeZone, locale);
             return new FormatResolverContext(timeZone, locale, fastDateFormat);
         }
 
-        private static String readPattern(
-                final Map<String, StringParameterParser.Value> keys) {
-            final StringParameterParser.Value patternValue = keys.get(Key.PATTERN);
-            if (patternValue == null || patternValue instanceof StringParameterParser.NullValue) {
+        private static String readFormat(final TemplateResolverConfig config) {
+            final String format = config.getString(new String[]{"pattern", "format"});
+            if (format == null) {
                 return JsonTemplateLayoutDefaults.getTimestampFormatPattern();
             }
-            final String pattern = patternValue.toString();
             try {
-                FastDateFormat.getInstance(pattern);
+                FastDateFormat.getInstance(format);
             } catch (final IllegalArgumentException error) {
                 throw new IllegalArgumentException(
-                        "invalid pattern in timestamp key: " + pattern,
+                        "invalid timestamp format: " + config,
                         error);
             }
-            return pattern;
+            return format;
         }
 
-        private static TimeZone readTimeZone(
-                final Map<String, StringParameterParser.Value> keys) {
-            final StringParameterParser.Value timeZoneValue = keys.get(Key.TIME_ZONE);
-            if (timeZoneValue == null || timeZoneValue instanceof StringParameterParser.NullValue) {
+        private static TimeZone readTimeZone(final TemplateResolverConfig config) {
+            final String timeZoneId = config.getString(new String[]{"pattern", "timeZone"});
+            if (timeZoneId == null) {
                 return JsonTemplateLayoutDefaults.getTimeZone();
             }
-            final String timeZoneId = timeZoneValue.toString();
             boolean found = false;
             for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
                 if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
@@ -120,27 +272,23 @@ final class TimestampResolver implements EventResolver {
             }
             if (!found) {
                 throw new IllegalArgumentException(
-                        "invalid time zone in timestamp key: " + timeZoneId);
+                        "invalid timestamp time zone: " + config);
             }
             return TimeZone.getTimeZone(timeZoneId);
         }
 
-        private static Locale readLocale(
-                final Map<String, StringParameterParser.Value> keys) {
-            final StringParameterParser.Value localeValue = keys.get(Key.LOCALE);
-            if (localeValue == null || localeValue instanceof StringParameterParser.NullValue) {
+        private static Locale readLocale(final TemplateResolverConfig config) {
+            final String locale = config.getString(new String[]{"pattern", "locale"});
+            if (locale == null) {
                 return JsonTemplateLayoutDefaults.getLocale();
             }
-            final String locale = localeValue.toString();
             final String[] localeFields = locale.split("_", 3);
             switch (localeFields.length) {
                 case 1: return new Locale(localeFields[0]);
                 case 2: return new Locale(localeFields[0], localeFields[1]);
                 case 3: return new Locale(localeFields[0], localeFields[1], localeFields[2]);
-                default:
-                    throw new IllegalArgumentException(
-                            "invalid locale in timestamp key: " + locale);
             }
+            throw new IllegalArgumentException("invalid timestamp locale: " + config);
         }
 
     }
@@ -196,54 +344,36 @@ final class TimestampResolver implements EventResolver {
 
     }
 
-    private static EventResolver createFormatResolver(final String key) {
+    private static EventResolver createFormatResolver(
+            final TemplateResolverConfig config) {
         final FormatResolverContext formatResolverContext =
-                FormatResolverContext.fromKey(key);
+                FormatResolverContext.fromConfig(config);
         return new FormatResolver(formatResolverContext);
     }
 
-    private static EventResolver createEpochResolver(final String key) {
-        switch (key) {
-            case "epoch:nanos":
-                return createEpochNanosResolver();
-            case "epoch:micros":
-                return createEpochMicrosDoubleResolver();
-            case "epoch:micros,integral":
-                return createEpochMicrosLongResolver();
-            case "epoch:millis":
-                return createEpochMillisDoubleResolver();
-            case "epoch:millis,integral":
-                return createEpochMillisLongResolver();
-            case "epoch:secs":
-                return createEpochSecsDoubleResolver();
-            case "epoch:secs,integral":
-                return createEpochSecsLongResolver();
-            case "epoch:micros.nanos":
-                return createEpochMicrosNanosResolver();
-            case "epoch:millis.nanos":
-                return createEpochMillisNanosResolver();
-            case "epoch:millis.micros":
-                return createEpochMillisMicrosResolver();
-            case "epoch:secs.nanos":
-                return createEpochSecsNanosResolver();
-            case "epoch:secs.micros":
-                return createEpochSecsMicrosResolver();
-            case "epoch:secs.millis":
-                return createEpochSecsMillisResolver();
-            default:
-                throw new IllegalArgumentException(
-                        "was expecting an epoch key: " + key);
+    private static EventResolver createEpochResolver(
+            final TemplateResolverConfig config) {
+        final String unit = config.getString(new String[]{"epoch", "unit"});
+        final Boolean rounded = config.getBoolean(new String[]{"epoch", "rounded"});
+        if ("nanos".equals(unit) && !Boolean.FALSE.equals(rounded)) {
+            return EPOCH_NANOS_RESOLVER;
+        } else if ("millis".equals(unit)) {
+            return !Boolean.TRUE.equals(rounded)
+                    ? EPOCH_MILLIS_RESOLVER
+                    : EPOCH_MILLIS_ROUNDED_RESOLVER;
+        } else if ("millis.nanos".equals(unit) && rounded == null) {
+                return EPOCH_MILLIS_NANOS_RESOLVER;
+        } else if ("secs".equals(unit)) {
+            return !Boolean.TRUE.equals(rounded)
+                    ? EPOCH_SECS_RESOLVER
+                    : EPOCH_SECS_ROUNDED_RESOLVER;
+        } else if ("secs.nanos".equals(unit) && rounded == null) {
+            return EPOCH_SECS_NANOS_RESOLVER;
         }
+        throw new IllegalArgumentException(
+                "invalid epoch configuration: " + config);
     }
 
-    private static final int MICROS_PER_SEC = 1_000_000;
-
-    private static final int NANOS_PER_SEC = 1_000_000_000;
-
-    private static final int NANOS_PER_MILLI = 1_000_000;
-
-    private static final int NANOS_PER_MICRO = 1_000;
-
     private static final class EpochResolutionRecord {
 
         private static final int MAX_LONG_LENGTH =
@@ -292,144 +422,73 @@ final class TimestampResolver implements EventResolver {
 
     }
 
-    private static EventResolver createEpochNanosResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                final long nanos = epochNanos(logEventInstant);
-                jsonWriter.writeNumber(nanos);
-            }
-        };
-    }
-
-    private static EventResolver createEpochMicrosDoubleResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                final long secs = logEventInstant.getEpochSecond();
-                final int nanosOfSecs = logEventInstant.getNanoOfSecond();
-                final long micros = MICROS_PER_SEC * secs + nanosOfSecs / NANOS_PER_MICRO;
-                final int nanosOfMicros = nanosOfSecs - nanosOfSecs % NANOS_PER_MICRO;
-                jsonWriter.writeNumber(micros, nanosOfMicros);
-            }
-        };
-    }
-
-    private static EventResolver createEpochMicrosLongResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                final long nanos = epochNanos(logEventInstant);
-                final long micros = nanos / NANOS_PER_MICRO;
-                jsonWriter.writeNumber(micros);
-            }
-        };
-    }
-
-    private static EventResolver createEpochMillisDoubleResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                jsonWriter.writeNumber(
-                        logEventInstant.getEpochMillisecond(),
-                        logEventInstant.getNanoOfMillisecond());
-            }
-        };
-    }
-
-    private static EventResolver createEpochMillisLongResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
-            }
-        };
-    }
-
-    private static EventResolver createEpochSecsDoubleResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                jsonWriter.writeNumber(
-                        logEventInstant.getEpochSecond(),
-                        logEventInstant.getNanoOfSecond());
-            }
-        };
-    }
-
-    private static EventResolver createEpochSecsLongResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                jsonWriter.writeNumber(logEventInstant.getEpochSecond());
-            }
-        };
-    }
-
-    private static EventResolver createEpochMicrosNanosResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                final int nanosOfSecs = logEventInstant.getNanoOfSecond();
-                final int nanosOfMicros = nanosOfSecs % NANOS_PER_MICRO;
-                jsonWriter.writeNumber(nanosOfMicros);
-            }
-        };
-    }
-
-    private static EventResolver createEpochMillisNanosResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                jsonWriter.writeNumber(logEventInstant.getNanoOfMillisecond());
-            }
-        };
-    }
-
-    private static EventResolver createEpochMillisMicrosResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                final int nanosOfMillis = logEventInstant.getNanoOfMillisecond();
-                final int microsOfMillis = nanosOfMillis / NANOS_PER_MICRO;
-                jsonWriter.writeNumber(microsOfMillis);
-            }
-        };
-    }
+    private static final EventResolver EPOCH_NANOS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    final long nanos = epochNanos(logEventInstant);
+                    jsonWriter.writeNumber(nanos);
+                }
+            };
+
+    private static final EventResolver EPOCH_MILLIS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+                    final long nanos = epochNanos(logEventInstant);
+                    jsonWriterStringBuilder.append(nanos);
+                    jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 6, '.');
+                }
+            };
 
-    private static EventResolver createEpochSecsNanosResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
-            }
-        };
-    }
+    private static final EventResolver EPOCH_MILLIS_ROUNDED_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
+                }
+            };
+
+    private static final EventResolver EPOCH_MILLIS_NANOS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    final long nanos = epochNanos(logEventInstant);
+                    final long fraction = nanos % 1_000_000L;
+                    jsonWriter.writeNumber(fraction);
+                }
+            };
+
+    private static final EventResolver EPOCH_SECS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+                    final long nanos = epochNanos(logEventInstant);
+                    jsonWriterStringBuilder.append(nanos);
+                    jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 9, '.');
+                }
+            };
 
-    private static EventResolver createEpochSecsMicrosResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                final int nanosOfSecs = logEventInstant.getNanoOfSecond();
-                final int microsOfSecs = nanosOfSecs / NANOS_PER_MICRO;
-                jsonWriter.writeNumber(microsOfSecs);
-            }
-        };
-    }
+    private static final EventResolver EPOCH_SECS_ROUNDED_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    jsonWriter.writeNumber(logEventInstant.getEpochSecond());
+                }
+            };
 
-    private static EventResolver createEpochSecsMillisResolver() {
-        return new EpochResolver() {
-            @Override
-            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
-                final int nanosOfSecs = logEventInstant.getNanoOfSecond();
-                final int millisOfSecs = nanosOfSecs / NANOS_PER_MILLI;
-                jsonWriter.writeNumber(millisOfSecs);
-            }
-        };
-    }
+    private static final EventResolver EPOCH_SECS_NANOS_RESOLVER =
+            new EpochResolver() {
+                @Override
+                void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                    jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
+                }
+            };
 
     private static long epochNanos(Instant instant) {
-        return NANOS_PER_SEC * instant.getEpochSecond() + instant.getNanoOfSecond();
+        return 1_000_000_000L * instant.getEpochSecond() + instant.getNanoOfSecond();
     }
 
     static String getName() {
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
index 638df83..f1547f2 100644
--- a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
@@ -34,8 +34,8 @@ final class TimestampResolverFactory implements EventResolverFactory<TimestampRe
     @Override
     public TimestampResolver create(
             final EventResolverContext context,
-            final String key) {
-        return new TimestampResolver(key);
+            final TemplateResolverConfig config) {
+        return new TimestampResolver(config);
     }
 
 }
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java
new file mode 100644
index 0000000..a4c140f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template.util;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+
+public class MapAccessor {
+
+    private final Map<String, Object> map;
+
+    public MapAccessor(final Map<String, Object> map) {
+        this.map = Objects.requireNonNull(map, "map");
+    }
+
+    public String getString(final String key) {
+        final String[] path = {key};
+        return getObject(path, String.class);
+    }
+
+    public String getString(final String[] path) {
+        return getObject(path, String.class);
+    }
+
+    public boolean getBoolean(final String key, final boolean defaultValue) {
+        final String[] path = {key};
+        return getBoolean(path, defaultValue);
+    }
+
+    public boolean getBoolean(final String[] path, final boolean defaultValue) {
+        final Boolean value = getObject(path, Boolean.class);
+        return value == null ? defaultValue : value;
+    }
+
+    public Boolean getBoolean(final String key) {
+        final String[] path = {key};
+        return getObject(path, Boolean.class);
+    }
+
+    public Boolean getBoolean(final String[] path) {
+        return getObject(path, Boolean.class);
+    }
+
+    public Integer getInteger(final String key) {
+        final String[] path = {key};
+        return getInteger(path);
+    }
+
+    public Integer getInteger(final String[] path) {
+        return getObject(path, Integer.class);
+    }
+
+    public boolean exists(final String key) {
+        final String[] path = {key};
+        return exists(path);
+    }
+
+    public boolean exists(final String[] path) {
+        final Object value = getObject(path, Object.class);
+        return value != null;
+    }
+
+    public Object getObject(final String key) {
+        final String[] path = {key};
+        return getObject(path, Object.class);
+    }
+
+    public <T> T getObject(final String key, final Class<T> clazz) {
+        final String[] path = {key};
+        return getObject(path, clazz);
+    }
+
+    public Object getObject(final String[] path) {
+        return getObject(path, Object.class);
+    }
+
+    public <T> T getObject(final String[] path, final Class<T> clazz) {
+        Objects.requireNonNull(path, "path");
+        Objects.requireNonNull(clazz, "clazz");
+        if (path.length == 0) {
+            throw new IllegalArgumentException("empty path");
+        }
+        Object parent = map;
+        for (final String key : path) {
+            if (!(parent instanceof Map)) {
+                return null;
+            }
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> parentMap = (Map<String, Object>) parent;
+            parent = parentMap.get(key);
+        }
+        if (parent != null && !clazz.isInstance(parent)) {
+            final String message = String.format(
+                    "was expecting %s at path %s: %s (of type %s)",
+                    clazz.getSimpleName(),
+                    Arrays.asList(path),
+                    parent,
+                    parent.getClass().getCanonicalName());
+            throw new IllegalArgumentException(message);
+        }
+        @SuppressWarnings("unchecked")
+        final T typedValue = (T) parent;
+        return typedValue;
+    }
+
+    @Override
+    public boolean equals(final Object instance) {
+        if (this == instance) return true;
+        if (instance == null || getClass() != instance.getClass()) return false;
+        final MapAccessor that = (MapAccessor) instance;
+        return map.equals(that.map);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(map);
+    }
+
+    @Override
+    public String toString() {
+        return map.toString();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/resources/EcsLayout.json b/log4j-layout-json-template/src/main/resources/EcsLayout.json
index 7004e54..f5f957b 100644
--- a/log4j-layout-json-template/src/main/resources/EcsLayout.json
+++ b/log4j-layout-json-template/src/main/resources/EcsLayout.json
@@ -1,12 +1,48 @@
 {
-  "@timestamp": "${json:timestamp:pattern=yyyy-MM-dd'T'HH:mm:ss.SSS'Z',timeZone=UTC}",
-  "log.level": "${json:level}",
-  "message": "${json:message}",
-  "process.thread.name": "${json:thread:name}",
-  "log.logger": "${json:logger:name}",
-  "labels": "${json:mdc:flatten=labels.,stringify}",
-  "tags": "${json:ndc}",
-  "error.type": "${json:exception:className}",
-  "error.message": "${json:exception:message}",
-  "error.stack_trace": "${json:exception:stackTrace:text}"
+  "@timestamp": {
+    "$resolver": "timestamp",
+    "pattern": {
+      "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+      "timeZone": "UTC"
+    }
+  },
+  "log.level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "process.thread.name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "log.logger": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "labels": {
+    "$resolver": "mdc",
+    "flatten": {
+      "prefix": "labels."
+    },
+    "stringified": true
+  },
+  "tags": {
+    "$resolver": "ndc"
+  },
+  "error.type": {
+    "$resolver": "exception",
+    "field": "className"
+  },
+  "error.message": {
+    "$resolver": "exception",
+    "field": "message"
+  },
+  "error.stack_trace": {
+    "$resolver": "exception",
+    "field": "stackTrace",
+    "stringified": true
+  }
 }
diff --git a/log4j-layout-json-template/src/main/resources/GelfLayout.json b/log4j-layout-json-template/src/main/resources/GelfLayout.json
index bcea9ee..dd43cc8 100644
--- a/log4j-layout-json-template/src/main/resources/GelfLayout.json
+++ b/log4j-layout-json-template/src/main/resources/GelfLayout.json
@@ -1,11 +1,41 @@
 {
   "version": "1.1",
   "host": "${hostName}",
-  "short_message": "${json:message}",
-  "full_message": "${json:exception:stackTrace:text}",
-  "timestamp": "${json:timestamp:epoch:secs}",
-  "level": "${json:level:severity:code}",
-  "_logger": "${json:logger:name}",
-  "_thread": "${json:thread:name}",
-  "_mdc": "${json:mdc:flatten=_,stringify}"
+  "short_message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "full_message": {
+    "$resolver": "exception",
+    "field": "stackTrace",
+    "stringified": true
+  },
+  "timestamp": {
+    "$resolver": "timestamp",
+    "epoch": {
+      "unit": "secs"
+    }
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "severity",
+    "severity": {
+      "field": "code"
+    }
+  },
+  "_logger": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "_thread": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "_mdc": {
+    "$resolver": "mdc",
+    "flatten": {
+      "prefix": "_"
+    },
+    "stringified": true
+  }
 }
diff --git a/log4j-layout-json-template/src/main/resources/JsonLayout.json b/log4j-layout-json-template/src/main/resources/JsonLayout.json
index f31ec63..503e2cd 100644
--- a/log4j-layout-json-template/src/main/resources/JsonLayout.json
+++ b/log4j-layout-json-template/src/main/resources/JsonLayout.json
@@ -1,26 +1,83 @@
 {
   "instant": {
-    "epochSecond": "${json:timestamp:epoch:secs,integral}",
-    "nanoOfSecond": "${json:timestamp:epoch:secs.nanos}"
+    "epochSecond": {
+      "$resolver": "timestamp",
+      "epoch": {
+        "unit": "secs",
+        "rounded": true
+      }
+    },
+    "nanoOfSecond": {
+      "$resolver": "timestamp",
+      "epoch": {
+        "unit": "secs.nanos"
+      }
+    }
+  },
+  "thread": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "loggerName": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "message": {
+    "$resolver": "message",
+    "stringified": true
   },
-  "thread": "${json:thread:name}",
-  "level": "${json:level}",
-  "loggerName": "${json:logger:name}",
-  "message": "${json:message}",
   "thrown": {
-    "message": "${json:exception:message}",
-    "name": "${json:exception:className}",
-    "extendedStackTrace": "${json:exception:stackTrace}"
-  },
-  "contextStack": "${json:ndc}",
-  "endOfBatch": "${json:endOfBatch}",
-  "loggerFqcn": "${json:logger:fqcn}",
-  "threadId": "${json:thread:id}",
-  "threadPriority": "${json:thread:priority}",
+    "message": {
+      "$resolver": "exception",
+      "field": "message"
+    },
+    "name": {
+      "$resolver": "exception",
+      "field": "className"
+    },
+    "extendedStackTrace": {
+      "$resolver": "exception",
+      "field": "stackTrace"
+    }
+  },
+  "contextStack": {
+    "$resolver": "ndc"
+  },
+  "endOfBatch": {
+    "$resolver": "endOfBatch"
+  },
+  "loggerFqcn": {
+    "$resolver": "logger",
+    "field": "fqcn"
+  },
+  "threadId": {
+    "$resolver": "thread",
+    "field": "id"
+  },
+  "threadPriority": {
+    "$resolver": "thread",
+    "field": "priority"
+  },
   "source": {
-    "class": "${json:source:className}",
-    "method": "${json:source:methodName}",
-    "file": "${json:source:fileName}",
-    "line": "${json:source:lineNumber}"
+    "class": {
+      "$resolver": "source",
+      "field": "className"
+    },
+    "method": {
+      "$resolver": "source",
+      "field": "methodName"
+    },
+    "file": {
+      "$resolver": "source",
+      "field": "fileName"
+    },
+    "line": {
+      "$resolver": "source",
+      "field": "lineNumber"
+    }
   }
 }
diff --git a/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
index 62d4c20..3225930 100644
--- a/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
+++ b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
@@ -1,19 +1,58 @@
 {
-  "mdc": "${json:mdc}",
+  "mdc": {
+    "$resolver": "mdc"
+  },
   "exception": {
-    "exception_class": "${json:exception:className}",
-    "exception_message": "${json:exception:message}",
-    "stacktrace": "${json:exception:stackTrace:text}"
+    "exception_class": {
+      "$resolver": "exception",
+      "field": "className"
+    },
+    "exception_message": {
+      "$resolver": "exception",
+      "field": "message",
+      "stringified": true
+    },
+    "stacktrace": {
+      "$resolver": "exception",
+      "field": "stackTrace",
+      "stringified": true
+    }
+  },
+  "line_number": {
+    "$resolver": "source",
+    "field": "lineNumber"
+  },
+  "class": {
+    "$resolver": "source",
+    "field": "className"
   },
-  "line_number": "${json:source:lineNumber}",
-  "class": "${json:source:className}",
   "@version": 1,
   "source_host": "${hostName}",
-  "message": "${json:message}",
-  "thread_name": "${json:thread:name}",
-  "@timestamp": "${json:timestamp}",
-  "level": "${json:level}",
-  "file": "${json:source:fileName}",
-  "method": "${json:source:methodName}",
-  "logger_name": "${json:logger:name}"
+  "message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "thread_name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "@timestamp": {
+    "$resolver": "timestamp"
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "file": {
+    "$resolver": "source",
+    "field": "fileName"
+  },
+  "method": {
+    "$resolver": "source",
+    "field": "methodName"
+  },
+  "logger_name": {
+    "$resolver": "logger",
+    "field": "name"
+  }
 }
diff --git a/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
index 3faacd4..218a01a 100644
--- a/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
+++ b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
@@ -1,6 +1,18 @@
 {
-  "class": "${json:stackTraceElement:className}",
-  "method": "${json:stackTraceElement:methodName}",
-  "file": "${json:stackTraceElement:fileName}",
-  "line": "${json:stackTraceElement:lineNumber}"
+  "class": {
+    "$resolver": "stackTraceElement",
+    "field": "className"
+  },
+  "method": {
+    "$resolver": "stackTraceElement",
+    "field": "methodName"
+  },
+  "file": {
+    "$resolver": "stackTraceElement",
+    "field": "fileName"
+  },
+  "line": {
+    "$resolver": "stackTraceElement",
+    "field": "lineNumber"
+  }
 }
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
index ec96407..262ddb1 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
@@ -5,6 +5,7 @@ import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.DefaultConfiguration;
 import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
 import org.assertj.core.api.Assertions;
 import org.junit.Test;
 
@@ -18,6 +19,8 @@ public class EcsLayoutTest {
 
     private static final Configuration CONFIGURATION = new DefaultConfiguration();
 
+    private static final String SERVICE_NAME = "test";
+
     private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
             .newBuilder()
             .setConfiguration(CONFIGURATION)
@@ -27,10 +30,12 @@ public class EcsLayoutTest {
                             .EventTemplateAdditionalFields
                             .newBuilder()
                             .setAdditionalFields(
-                                    new KeyValuePair[]{
-                                            new KeyValuePair(
-                                                    "service.name",
-                                                    "test")
+                                    new EventTemplateAdditionalField[]{
+                                            EventTemplateAdditionalField
+                                                    .newBuilder()
+                                                    .setKey("service.name")
+                                                    .setValue(SERVICE_NAME)
+                                                    .build()
                                     })
                             .build())
             .build();
@@ -38,42 +43,40 @@ public class EcsLayoutTest {
     private static final EcsLayout ECS_LAYOUT = EcsLayout
             .newBuilder()
             .setConfiguration(CONFIGURATION)
-            .setServiceName("test")
+            .setServiceName(SERVICE_NAME)
             .build();
 
     @Test
-    public void test_lite_log_events() throws Exception {
+    public void test_lite_log_events() {
         final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
         test(logEvents);
     }
 
     @Test
-    public void test_full_log_events() throws Exception {
+    public void test_full_log_events() {
         final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
         test(logEvents);
     }
 
-    private static void test(final Collection<LogEvent> logEvents) throws Exception {
+    private static void test(final Collection<LogEvent> logEvents) {
         for (final LogEvent logEvent : logEvents) {
             test(logEvent);
         }
     }
 
-    private static void test(final LogEvent logEvent) throws Exception {
+    private static void test(final LogEvent logEvent) {
         final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
         final Map<String, Object> ecsLayoutMap = renderUsingEcsLayout(logEvent);
         Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(ecsLayoutMap);
     }
 
     private static Map<String, Object> renderUsingJsonTemplateLayout(
-            final LogEvent logEvent)
-            throws Exception {
+            final LogEvent logEvent) {
         return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
     }
 
     private static Map<String, Object> renderUsingEcsLayout(
-            final LogEvent logEvent)
-            throws Exception {
+            final LogEvent logEvent) {
         return renderUsing(logEvent, ECS_LAYOUT);
     }
 
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
index b561ea6..795546b 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
@@ -4,11 +4,12 @@ import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.DefaultConfiguration;
 import org.apache.logging.log4j.core.layout.GelfLayout;
-import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
 import org.assertj.core.api.Assertions;
-import org.assertj.core.data.Percentage;
 import org.junit.Test;
 
+import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -30,10 +31,12 @@ public class GelfLayoutTest {
                             .EventTemplateAdditionalFields
                             .newBuilder()
                             .setAdditionalFields(
-                                    new KeyValuePair[]{
-                                            new KeyValuePair(
-                                                    "host",
-                                                    HOST_NAME)
+                                    new EventTemplateAdditionalField[]{
+                                            EventTemplateAdditionalField
+                                                    .newBuilder()
+                                                    .setKey("host")
+                                                    .setValue(HOST_NAME)
+                                                    .build()
                                     })
                             .build())
             .build();
@@ -46,39 +49,37 @@ public class GelfLayoutTest {
             .build();
 
     @Test
-    public void test_lite_log_events() throws Exception {
+    public void test_lite_log_events() {
         final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
         test(logEvents);
     }
 
     @Test
-    public void test_full_log_events() throws Exception {
+    public void test_full_log_events() {
         final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
         test(logEvents);
     }
 
-    private static void test(final Collection<LogEvent> logEvents) throws Exception {
+    private static void test(final Collection<LogEvent> logEvents) {
         for (final LogEvent logEvent : logEvents) {
             test(logEvent);
         }
     }
 
-    private static void test(final LogEvent logEvent) throws Exception {
+    private static void test(final LogEvent logEvent) {
         final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
         final Map<String, Object> gelfLayoutMap = renderUsingGelfLayout(logEvent);
-        verifyTimestamp(jsonTemplateLayoutMap, gelfLayoutMap);
+        verifyTimestamp(logEvent.getInstant(), jsonTemplateLayoutMap, gelfLayoutMap);
         Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(gelfLayoutMap);
     }
 
     private static Map<String, Object> renderUsingJsonTemplateLayout(
-            final LogEvent logEvent)
-            throws Exception {
+            final LogEvent logEvent) {
         return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
     }
 
     private static Map<String, Object> renderUsingGelfLayout(
-            final LogEvent logEvent)
-            throws Exception {
+            final LogEvent logEvent) {
         return renderUsing(logEvent, GELF_LAYOUT);
     }
 
@@ -86,17 +87,23 @@ public class GelfLayoutTest {
      * Handle timestamps individually to avoid floating-point comparison hiccups.
      */
     private static void verifyTimestamp(
+            final Instant logEventInstant,
             final Map<String, Object> jsonTemplateLayoutMap,
             final Map<String, Object> gelfLayoutMap) {
-        final Number jsonTemplateLayoutTimestamp =
-                (Number) jsonTemplateLayoutMap.remove("timestamp");
-        final Number gelfLayoutTimestamp =
-                (Number) gelfLayoutMap.remove("timestamp");
+        final BigDecimal jsonTemplateLayoutTimestamp =
+                (BigDecimal) jsonTemplateLayoutMap.remove("timestamp");
+        final BigDecimal gelfLayoutTimestamp =
+                (BigDecimal) gelfLayoutMap.remove("timestamp");
+        final String description = String.format(
+                "instantEpochSecs=%d.%d, jsonTemplateLayoutTimestamp=%s, gelfLayoutTimestamp=%s",
+                logEventInstant.getEpochSecond(),
+                logEventInstant.getNanoOfSecond(),
+                jsonTemplateLayoutTimestamp,
+                gelfLayoutTimestamp);
         Assertions
-                .assertThat(jsonTemplateLayoutTimestamp.doubleValue())
-                .isCloseTo(
-                        gelfLayoutTimestamp.doubleValue(),
-                        Percentage.withPercentage(0.01));
+                .assertThat(jsonTemplateLayoutTimestamp.compareTo(gelfLayoutTimestamp))
+                .as(description)
+                .isEqualTo(0);
     }
 
 }
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
index 0eb077f..a2ebe6f 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
@@ -16,21 +16,14 @@
  */
 package org.apache.logging.log4j.layout.json.template;
 
-import com.fasterxml.jackson.core.JsonFactory;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 public enum JacksonFixture {;
 
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
-    private static final JsonFactory JSON_FACTORY = OBJECT_MAPPER.getFactory();
-
     public static ObjectMapper getObjectMapper() {
         return OBJECT_MAPPER;
     }
 
-    public static JsonFactory getJsonFactory() {
-        return JSON_FACTORY;
-    }
-
 }
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
index 7bbd9a9..ec8458e 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
@@ -32,32 +32,31 @@ public class JsonLayoutTest {
             .build();
 
     @Test
-    public void test_lite_log_events() throws Exception {
+    public void test_lite_log_events() {
         final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
         test(logEvents);
     }
 
     @Test
-    public void test_full_log_events() throws Exception {
+    public void test_full_log_events() {
         final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
         test(logEvents);
     }
 
-    private static void test(final Collection<LogEvent> logEvents) throws Exception {
+    private static void test(final Collection<LogEvent> logEvents) {
         for (final LogEvent logEvent : logEvents) {
             test(logEvent);
         }
     }
 
-    private static void test(final LogEvent logEvent) throws Exception {
+    private static void test(final LogEvent logEvent) {
         final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
         final Map<String, Object> jsonLayoutMap = renderUsingJsonLayout(logEvent);
         Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(jsonLayoutMap);
     }
 
     private static Map<String, Object> renderUsingJsonTemplateLayout(
-            final LogEvent logEvent)
-            throws Exception {
+            final LogEvent logEvent) {
         final Map<String, Object> map = renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
         final Map<String, Object> emptySourceExcludedMap = removeEmptyObject(map, "source");
         // JsonLayout blindly serializes the Throwable as a POJO, this is,
@@ -69,8 +68,7 @@ public class JsonLayoutTest {
     }
 
     private static Map<String, Object> renderUsingJsonLayout(
-            final LogEvent logEvent)
-            throws Exception {
+            final LogEvent logEvent) {
         final Map<String, Object> map = renderUsing(logEvent, JSON_LAYOUT);
         // JsonLayout blindly serializes the Throwable as a POJO, this is,
         // to say the least, quite wrong, and I ain't gonna try to emulate
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
index 4e330f9..315e50f 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
@@ -19,13 +19,10 @@ package org.apache.logging.log4j.layout.json.template;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.MappingIterator;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-import com.fasterxml.jackson.databind.node.MissingNode;
-import com.fasterxml.jackson.databind.node.NullNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.MarkerManager;
+import org.apache.logging.log4j.core.Layout;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.appender.SocketAppender;
 import org.apache.logging.log4j.core.config.Configuration;
@@ -36,7 +33,11 @@ import org.apache.logging.log4j.core.layout.ByteBufferDestination;
 import org.apache.logging.log4j.core.lookup.MainMapLookup;
 import org.apache.logging.log4j.core.net.Severity;
 import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalFields;
+import org.apache.logging.log4j.layout.json.template.util.JsonReader;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.MapAccessor;
 import org.apache.logging.log4j.message.ObjectMessage;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.message.StringMapMessage;
@@ -44,7 +45,7 @@ import org.apache.logging.log4j.test.AvailablePortFinder;
 import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.StringMap;
 import org.apache.logging.log4j.util.Strings;
-import org.assertj.core.data.Percentage;
+import org.assertj.core.api.Assertions;
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -53,6 +54,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
 import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.nio.ByteBuffer;
@@ -60,12 +62,15 @@ import java.nio.charset.Charset;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
@@ -78,7 +83,11 @@ public class JsonTemplateLayoutTest {
 
     private static final List<LogEvent> LOG_EVENTS = LogEventFixture.createFullLogEvents(5);
 
-    private static final JsonNodeFactory JSON_NODE_FACTORY = JsonNodeFactory.instance;
+    private static final JsonWriter JSON_WRITER = JsonWriter
+            .newBuilder()
+            .setMaxStringLength(10_000)
+            .setTruncatedStringSuffix("…")
+            .build();
 
     private static final ObjectMapper OBJECT_MAPPER = JacksonFixture.getObjectMapper();
 
@@ -210,12 +219,14 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("@timestamp", "${json:timestamp:timeZone=Europe/Amsterdam}");
+        final String timestampFieldName = "@timestamp";
         final String staticFieldName = "staticFieldName";
         final String staticFieldValue = "staticFieldValue";
-        eventTemplateRootNode.put(staticFieldName, staticFieldValue);
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                timestampFieldName, Map(
+                        "$resolver", "timestamp",
+                        "pattern", Map("timeZone", "Europe/Amsterdam")),
+                staticFieldName, staticFieldValue));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -225,28 +236,23 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "@timestamp").asText()).isEqualTo(timestamp);
-        assertThat(point(rootNode, staticFieldName).asText()).isEqualTo(staticFieldValue);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(timestampFieldName)).isEqualTo(timestamp);
+            assertThat(accessor.getString(staticFieldName)).isEqualTo(staticFieldValue);
+        });
 
     }
 
     @Test
-    public void test_log4j_deferred_runtime_resolver_for_MapMessage() throws Exception {
+    public void test_log4j_deferred_runtime_resolver_for_MapMessage() {
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("mapValue3", "${json:message:json}");
-        eventTemplateRootNode.put("mapValue1", "${map:key1}");
-        eventTemplateRootNode.put("mapValue2", "${map:key2}");
-        eventTemplateRootNode.put(
-                "nestedLookupEmptyValue",
-                "${map:noExist:-${map:noExist2:-${map:noExist3:-}}}");
-        eventTemplateRootNode.put(
-                "nestedLookupStaticValue",
-                "${map:noExist:-${map:noExist2:-${map:noExist3:-Static Value}}}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "mapValue3", Map("$resolver", "message"),
+                "mapValue1", "${map:key1}",
+                "mapValue2", "${map:key2}",
+                "nestedLookupEmptyValue", "${map:noExist:-${map:noExist2:-${map:noExist3:-}}}",
+                "nestedLookupStaticValue", "${map:noExist:-${map:noExist2:-${map:noExist3:-Static Value}}}"));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -269,22 +275,21 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "mapValue1").asText()).isEqualTo("val1");
-        assertThat(point(rootNode, "mapValue2").asText()).isEqualTo("val2");
-        assertThat(point(rootNode, "nestedLookupEmptyValue").asText()).isEmpty();
-        assertThat(point(rootNode, "nestedLookupStaticValue").asText()).isEqualTo("Static Value");
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("mapValue1")).isEqualTo("val1");
+            assertThat(accessor.getString("mapValue2")).isEqualTo("val2");
+            assertThat(accessor.getString("nestedLookupEmptyValue")).isEmpty();
+            assertThat(accessor.getString("nestedLookupStaticValue")).isEqualTo("Static Value");
+        });
 
     }
 
     @Test
-    public void test_MapMessage_serialization() throws Exception {
+    public void test_MapMessage_serialization() {
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("message", "${json:message:json}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message")));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -307,16 +312,16 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "message", "key1").asText()).isEqualTo("val1");
-        assertThat(point(rootNode, "message", "key2").asLong()).isEqualTo(0xDEADBEEF);
-        assertThat(point(rootNode, "message", "key3", "key3.1").asText()).isEqualTo("val3.1");
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(new String[]{"message", "key1"})).isEqualTo("val1");
+            assertThat(accessor.getInteger(new String[]{"message", "key2"})).isEqualTo(0xDEADBEEF);
+            assertThat(accessor.getString(new String[]{"message", "key3", "key3.1"})).isEqualTo("val3.1");
+        });
 
     }
 
     @Test
-    public void test_property_injection() throws Exception {
+    public void test_property_injection() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World");
@@ -328,10 +333,9 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template with property.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
         final String propertyName = "propertyName";
-        eventTemplateRootNode.put(propertyName, "${" + propertyName + "}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                propertyName, "${" + propertyName + "}"));
 
         // Create the layout with property.
         final String propertyValue = "propertyValue";
@@ -346,14 +350,13 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, propertyName).asText()).isEqualTo(propertyValue);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                assertThat(accessor.getString(propertyName)).isEqualTo(propertyValue));
 
     }
 
     @Test
-    public void test_empty_root_cause() throws Exception {
+    public void test_empty_root_cause() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -367,14 +370,27 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("ex_class", "${json:exception:className}");
-        eventTemplateRootNode.put("ex_message", "${json:exception:message}");
-        eventTemplateRootNode.put("ex_stacktrace", "${json:exception:stackTrace:text}");
-        eventTemplateRootNode.put("root_ex_class", "${json:exceptionRootCause:className}");
-        eventTemplateRootNode.put("root_ex_message", "${json:exceptionRootCause:message}");
-        eventTemplateRootNode.put("root_ex_stacktrace", "${json:exceptionRootCause:stackTrace:text}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "ex_class", Map(
+                        "$resolver", "exception",
+                        "field", "className"),
+                "ex_message", Map(
+                        "$resolver", "exception",
+                        "field", "message"),
+                "ex_stacktrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true),
+                "root_ex_class", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "className"),
+                "root_ex_message", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "message"),
+                "root_ex_stacktrace", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "stackTrace",
+                        "stringified", true)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -385,25 +401,25 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "ex_class").asText())
-                .isEqualTo(exception.getClass().getCanonicalName());
-        assertThat(point(rootNode, "ex_message").asText())
-                .isEqualTo(exception.getMessage());
-        assertThat(point(rootNode, "ex_stacktrace").asText())
-                .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
-        assertThat(point(rootNode, "root_ex_class").asText())
-                .isEqualTo(point(rootNode, "ex_class").asText());
-        assertThat(point(rootNode, "root_ex_message").asText())
-                .isEqualTo(point(rootNode, "ex_message").asText());
-        assertThat(point(rootNode, "root_ex_stacktrace").asText())
-                .isEqualTo(point(rootNode, "ex_stacktrace").asText());
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("ex_class"))
+                    .isEqualTo(exception.getClass().getCanonicalName());
+            assertThat(accessor.getString("ex_message"))
+                    .isEqualTo(exception.getMessage());
+            assertThat(accessor.getString("ex_stacktrace"))
+                    .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
+            assertThat(accessor.getString("root_ex_class"))
+                    .isEqualTo(accessor.getString("ex_class"));
+            assertThat(accessor.getString("root_ex_message"))
+                    .isEqualTo(accessor.getString("ex_message"));
+            assertThat(accessor.getString("root_ex_stacktrace"))
+                    .isEqualTo(accessor.getString("ex_stacktrace"));
+        });
 
     }
 
     @Test
-    public void test_root_cause() throws Exception {
+    public void test_root_cause() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -418,14 +434,27 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("ex_class", "${json:exception:className}");
-        eventTemplateRootNode.put("ex_message", "${json:exception:message}");
-        eventTemplateRootNode.put("ex_stacktrace", "${json:exception:stackTrace:text}");
-        eventTemplateRootNode.put("root_ex_class", "${json:exceptionRootCause:className}");
-        eventTemplateRootNode.put("root_ex_message", "${json:exceptionRootCause:message}");
-        eventTemplateRootNode.put("root_ex_stacktrace", "${json:exceptionRootCause:stackTrace:text}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "ex_class", Map(
+                        "$resolver", "exception",
+                        "field", "className"),
+                "ex_message", Map(
+                        "$resolver", "exception",
+                        "field", "message"),
+                "ex_stacktrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true),
+                "root_ex_class", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "className"),
+                "root_ex_message", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "message"),
+                "root_ex_stacktrace", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "stackTrace",
+                        "stringified", true)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -436,25 +465,25 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "ex_class").asText())
-                .isEqualTo(exception.getClass().getCanonicalName());
-        assertThat(point(rootNode, "ex_message").asText())
-                .isEqualTo(exception.getMessage());
-        assertThat(point(rootNode, "ex_stacktrace").asText())
-                .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
-        assertThat(point(rootNode, "root_ex_class").asText())
-                .isEqualTo(exceptionCause.getClass().getCanonicalName());
-        assertThat(point(rootNode, "root_ex_message").asText())
-                .isEqualTo(exceptionCause.getMessage());
-        assertThat(point(rootNode, "root_ex_stacktrace").asText())
-                .startsWith(exceptionCause.getClass().getCanonicalName() + ": " + exceptionCause.getMessage());
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("ex_class"))
+                    .isEqualTo(exception.getClass().getCanonicalName());
+            assertThat(accessor.getString("ex_message"))
+                    .isEqualTo(exception.getMessage());
+            assertThat(accessor.getString("ex_stacktrace"))
+                    .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
+            assertThat(accessor.getString("root_ex_class"))
+                    .isEqualTo(exceptionCause.getClass().getCanonicalName());
+            assertThat(accessor.getString("root_ex_message"))
+                    .isEqualTo(exceptionCause.getMessage());
+            assertThat(accessor.getString("root_ex_stacktrace"))
+                    .startsWith(exceptionCause.getClass().getCanonicalName() + ": " + exceptionCause.getMessage());
+        });
 
     }
 
     @Test
-    public void test_marker_name() throws IOException {
+    public void test_marker_name() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -469,12 +498,13 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
         final String messageKey = "message";
-        eventTemplateRootNode.put(messageKey, "${json:message}");
         final String markerNameKey = "marker";
-        eventTemplateRootNode.put(markerNameKey, "${json:marker:name}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message"),
+                "marker", Map(
+                        "$resolver", "marker",
+                        "field", "name")));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -484,10 +514,10 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, messageKey).asText()).isEqualTo(message.getFormattedMessage());
-        assertThat(point(rootNode, markerNameKey).asText()).isEqualTo(markerName);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(messageKey)).isEqualTo(message.getFormattedMessage());
+            assertThat(accessor.getString(markerNameKey)).isEqualTo(markerName);
+        });
 
     }
 
@@ -528,7 +558,7 @@ public class JsonTemplateLayoutTest {
     }
 
     @Test
-    public void test_main_key_access() throws IOException {
+    public void test_main_key_access() {
 
         // Set main() arguments.
         final String kwKey = "--name";
@@ -548,11 +578,16 @@ public class JsonTemplateLayoutTest {
             .build();
 
         // Create the template.
-        final ObjectNode templateRootNode = JSON_NODE_FACTORY.objectNode();
-        templateRootNode.put("name", String.format("${json:main:%s}", kwKey));
-        templateRootNode.put("positionArg", "${json:main:2}");
-        templateRootNode.put("notFoundArg", String.format("${json:main:%s}", missingKwKey));
-        final String template = templateRootNode.toString();
+        final String template = writeJson(Map(
+                "name", Map(
+                        "$resolver", "main",
+                        "key", kwKey),
+                "positionArg", Map(
+                        "$resolver", "main",
+                        "index", 2),
+                "notFoundArg", Map(
+                        "$resolver", "main",
+                        "key", missingKwKey)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -562,16 +597,16 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "name").asText()).isEqualTo(kwVal);
-        assertThat(point(rootNode, "positionArg").asText()).isEqualTo(positionArg);
-        assertThat(point(rootNode, "notFoundArg")).isInstanceOf(NullNode.class);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("name")).isEqualTo(kwVal);
+            assertThat(accessor.getString("positionArg")).isEqualTo(positionArg);
+            assertThat(accessor.exists("notFoundArg")).isFalse();
+        });
 
     }
 
     @Test
-    public void test_mdc_key_access() throws IOException {
+    public void test_mdc_key_access() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -592,14 +627,13 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put(
-                mdcDirectlyAccessedKey,
-                String.format("${json:mdc:key=%s}", mdcDirectlyAccessedKey));
-        eventTemplateRootNode.put(
-                mdcDirectlyAccessedNullPropertyKey,
-                String.format("${json:mdc:key=%s}", mdcDirectlyAccessedNullPropertyKey));
-        String eventTemplate = eventTemplateRootNode.toString();
+        String eventTemplate = writeJson(Map(
+                mdcDirectlyAccessedKey, Map(
+                        "$resolver", "mdc",
+                        "key", mdcDirectlyAccessedKey),
+                mdcDirectlyAccessedNullPropertyKey, Map(
+                        "$resolver", "mdc",
+                        "key", mdcDirectlyAccessedNullPropertyKey)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -610,15 +644,15 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, mdcDirectlyAccessedKey).asText()).isEqualTo(mdcDirectlyAccessedValue);
-        assertThat(point(rootNode, mdcDirectlyAccessedNullPropertyKey)).isInstanceOf(NullNode.class);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(mdcDirectlyAccessedKey)).isEqualTo(mdcDirectlyAccessedValue);
+            assertThat(accessor.getString(mdcDirectlyAccessedNullPropertyKey)).isNull();
+        });
 
     }
 
     @Test
-    public void test_mdc_pattern() throws IOException {
+    public void test_mdc_pattern() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -638,10 +672,11 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
         final String mdcFieldName = "mdc";
-        eventTemplateRootNode.put(mdcFieldName, "${json:mdc:pattern=" + mdcPatternMatchedKey + "}");
-        String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                mdcFieldName, Map(
+                        "$resolver", "mdc",
+                        "pattern", mdcPatternMatchedKey)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -652,15 +687,15 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, mdcFieldName, mdcPatternMatchedKey).asText()).isEqualTo(mdcPatternMatchedValue);
-        assertThat(point(rootNode, mdcFieldName, mdcPatternMismatchedKey)).isInstanceOf(MissingNode.class);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(new String[]{mdcFieldName, mdcPatternMatchedKey})).isEqualTo(mdcPatternMatchedValue);
+            assertThat(accessor.exists(new String[]{mdcFieldName, mdcPatternMismatchedKey})).isFalse();
+        });
 
     }
 
     @Test
-    public void test_mdc_flatten() throws IOException {
+    public void test_mdc_flatten() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -680,12 +715,12 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
         final String mdcPrefix = "_mdc.";
-        eventTemplateRootNode.put(
-                mdcPrefix,
-                "${json:mdc:flatten=" + mdcPrefix + ",pattern=" + mdcPatternMatchedKey + "}");
-        String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "ignoredFieldName", Map(
+                        "$resolver", "mdc",
+                        "pattern", mdcPatternMatchedKey,
+                        "flatten", Map("prefix", mdcPrefix))));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -696,15 +731,15 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, mdcPrefix + mdcPatternMatchedKey).asText()).isEqualTo(mdcPatternMatchedValue);
-        assertThat(point(rootNode, mdcPrefix + mdcPatternMismatchedKey)).isInstanceOf(MissingNode.class);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(mdcPrefix + mdcPatternMatchedKey)).isEqualTo(mdcPatternMatchedValue);
+            assertThat(accessor.exists(mdcPrefix + mdcPatternMismatchedKey)).isFalse();
+        });
 
     }
 
     @Test
-    public void test_MapResolver() throws IOException {
+    public void test_MapResolver() {
 
         // Create the log event.
         final StringMapMessage message = new StringMapMessage().with("key1", "val1");
@@ -716,10 +751,13 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template node with map values.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("mapValue1", "${json:map:key1}");
-        eventTemplateRootNode.put("mapValue2", "${json:map:noExist}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "mapValue1", Map(
+                        "$resolver", "map",
+                        "key", "key1"),
+                "mapValue2", Map(
+                        "$resolver", "map",
+                        "key", "key?")));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -729,15 +767,15 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "mapValue1").asText()).isEqualTo("val1");
-        assertThat(point(rootNode, "mapValue2")).isInstanceOf(NullNode.class);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("mapValue1")).isEqualTo("val1");
+            assertThat(accessor.getString("mapValue2")).isNull();
+        });
 
     }
 
     @Test
-    public void test_message_json() throws IOException {
+    public void test_StringMapMessage() {
 
         // Create the log event.
         final StringMapMessage message = new StringMapMessage();
@@ -751,42 +789,8 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("message", "${json:message:json}");
-        final String eventTemplate = eventTemplateRootNode.toString();
-
-        // Create the layout.
-        final JsonTemplateLayout layout = JsonTemplateLayout
-                .newBuilder()
-                .setConfiguration(CONFIGURATION)
-                .setStackTraceEnabled(true)
-                .setEventTemplate(eventTemplate)
-                .build();
-
-        // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "message", "message").asText()).isEqualTo("Hello, World!");
-        assertThat(point(rootNode, "message", "bottle").asText()).isEqualTo("Kickapoo Joy Juice");
-
-    }
-
-    @Test
-    public void test_message_json_fallback() throws IOException {
-
-        // Create the log event.
-        final SimpleMessage message = new SimpleMessage("Hello, World!");
-        final LogEvent logEvent = Log4jLogEvent
-                .newBuilder()
-                .setLoggerName(LOGGER_NAME)
-                .setLevel(Level.INFO)
-                .setMessage(message)
-                .build();
-
-        // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("message", "${json:message:json}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message")));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -797,14 +801,15 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "message").asText()).isEqualTo("Hello, World!");
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(new String[]{"message", "message"})).isEqualTo("Hello, World!");
+            assertThat(accessor.getString(new String[]{"message", "bottle"})).isEqualTo("Kickapoo Joy Juice");
+        });
 
     }
 
     @Test
-    public void test_message_object() throws IOException {
+    public void test_ObjectMessage() {
 
         // Create the log event.
         final int id = 0xDEADBEEF;
@@ -822,9 +827,8 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("message", "${json:message:json}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message")));
 
         // Create the layout.
         JsonTemplateLayout layout = JsonTemplateLayout
@@ -835,41 +839,41 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "message", "id").asInt()).isEqualTo(id);
-        assertThat(point(rootNode, "message", "name").asText()).isEqualTo(name);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getInteger(new String[]{"message", "id"})).isEqualTo(id);
+            assertThat(accessor.getString(new String[]{"message", "name"})).isEqualTo(name);
+        });
 
     }
 
     @Test
-    public void test_StackTraceElement_template() throws IOException {
+    public void test_StackTraceElement_template() {
 
         // Create the stack trace element template.
-        final ObjectNode stackTraceElementTemplateRootNode = JSON_NODE_FACTORY.objectNode();
         final String classNameFieldName = "className";
-        stackTraceElementTemplateRootNode.put(
-                classNameFieldName,
-                "${json:stackTraceElement:className}");
         final String methodNameFieldName = "methodName";
-        stackTraceElementTemplateRootNode.put(
-                methodNameFieldName,
-                "${json:stackTraceElement:methodName}");
         final String fileNameFieldName = "fileName";
-        stackTraceElementTemplateRootNode.put(
-                fileNameFieldName,
-                "${json:stackTraceElement:fileName}");
         final String lineNumberFieldName = "lineNumber";
-        stackTraceElementTemplateRootNode.put(
-                lineNumberFieldName,
-                "${json:stackTraceElement:lineNumber}");
-        final String stackTraceElementTemplate = stackTraceElementTemplateRootNode.toString();
+        final String stackTraceElementTemplate = writeJson(Map(
+                classNameFieldName, Map(
+                        "$resolver", "stackTraceElement",
+                        "field", "className"),
+                methodNameFieldName, Map(
+                        "$resolver", "stackTraceElement",
+                        "field", "methodName"),
+                fileNameFieldName, Map(
+                        "$resolver", "stackTraceElement",
+                        "field", "fileName"),
+                lineNumberFieldName, Map(
+                        "$resolver", "stackTraceElement",
+                        "field", "lineNumber")));
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
         final String stackTraceFieldName = "stackTrace";
-        eventTemplateRootNode.put(stackTraceFieldName, "${json:exception:stackTrace}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                stackTraceFieldName, Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace")));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -893,27 +897,29 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        final JsonNode stackTraceNode = point(rootNode, stackTraceFieldName);
-        assertThat(stackTraceNode.isArray()).isTrue();
-        final StackTraceElement[] stackTraceElements = exception.getStackTrace();
-        assertThat(stackTraceNode.size()).isEqualTo(stackTraceElements.length);
-        for (int stackTraceElementIndex = 0;
-             stackTraceElementIndex < stackTraceElements.length;
-             stackTraceElementIndex++) {
-            final StackTraceElement stackTraceElement = stackTraceElements[stackTraceElementIndex];
-            final JsonNode stackTraceElementNode = stackTraceNode.get(stackTraceElementIndex);
-            assertThat(stackTraceElementNode.size()).isEqualTo(4);
-            assertThat(point(stackTraceElementNode, classNameFieldName).asText())
-                    .isEqualTo(stackTraceElement.getClassName());
-            assertThat(point(stackTraceElementNode, methodNameFieldName).asText())
-                    .isEqualTo(stackTraceElement.getMethodName());
-            assertThat(point(stackTraceElementNode, fileNameFieldName).asText())
-                    .isEqualTo(stackTraceElement.getFileName());
-            assertThat(point(stackTraceElementNode, lineNumberFieldName).asInt())
-                    .isEqualTo(stackTraceElement.getLineNumber());
-        }
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.exists(stackTraceFieldName)).isTrue();
+            @SuppressWarnings("unchecked")
+            final List<Map<String, Object>> deserializedStackTraceElements =
+                    accessor.getObject(stackTraceFieldName, List.class);
+            final StackTraceElement[] stackTraceElements = exception.getStackTrace();
+            assertThat(deserializedStackTraceElements.size()).isEqualTo(stackTraceElements.length);
+            for (int stackTraceElementIndex = 0;
+                 stackTraceElementIndex < stackTraceElements.length;
+                 stackTraceElementIndex++) {
+                final StackTraceElement stackTraceElement = stackTraceElements[stackTraceElementIndex];
+                final Map<String, Object> deserializedStackTraceElement = deserializedStackTraceElements.get(stackTraceElementIndex);
+                assertThat(deserializedStackTraceElement.size()).isEqualTo(4);
+                assertThat(deserializedStackTraceElement.get(classNameFieldName))
+                        .isEqualTo(stackTraceElement.getClassName());
+                assertThat(deserializedStackTraceElement.get(methodNameFieldName))
+                        .isEqualTo(stackTraceElement.getMethodName());
+                assertThat(deserializedStackTraceElement.get(fileNameFieldName))
+                        .isEqualTo(stackTraceElement.getFileName());
+                assertThat(deserializedStackTraceElement.get(lineNumberFieldName))
+                        .isEqualTo(stackTraceElement.getLineNumber());
+            }
+        });
 
     }
 
@@ -981,7 +987,7 @@ public class JsonTemplateLayoutTest {
     }
 
     @Test
-    public void test_maxStringLength() throws IOException {
+    public void test_maxStringLength() {
 
         // Create the log event.
         final int maxStringLength = 30;
@@ -997,15 +1003,16 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template node with map values.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
         final String messageKey = "message";
-        eventTemplateRootNode.put(messageKey, "${json:message}");
         final String excessiveKey = Strings.repeat("k", maxStringLength) + 'K';
         final String excessiveValue = Strings.repeat("v", maxStringLength) + 'V';
-        eventTemplateRootNode.put(excessiveKey, excessiveValue);
         final String nullValueKey = "nullValueKey";
-        eventTemplateRootNode.put(nullValueKey, "${json:exception:message}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                messageKey, Map("$resolver", "message"),
+                excessiveKey, excessiveValue,
+                nullValueKey, Map(
+                        "$resolver", "exception",
+                        "field", "message")));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1016,22 +1023,22 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        final String truncatedStringSuffix =
-                JsonTemplateLayoutDefaults.getTruncatedStringSuffix();
-        final String truncatedMessageString =
-                excessiveMessageString.substring(0, maxStringLength) +
-                        truncatedStringSuffix;
-        assertThat(point(rootNode, messageKey).asText()).isEqualTo(truncatedMessageString);
-        final String truncatedKey =
-                excessiveKey.substring(0, maxStringLength) +
-                        truncatedStringSuffix;
-        final String truncatedValue =
-                excessiveValue.substring(0, maxStringLength) +
-                        truncatedStringSuffix;
-        assertThat(point(rootNode, truncatedKey).asText()).isEqualTo(truncatedValue);
-        assertThat(point(rootNode, nullValueKey).isNull()).isTrue();
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            final String truncatedStringSuffix =
+                    JsonTemplateLayoutDefaults.getTruncatedStringSuffix();
+            final String truncatedMessageString =
+                    excessiveMessageString.substring(0, maxStringLength) +
+                            truncatedStringSuffix;
+            assertThat(accessor.getString(messageKey)).isEqualTo(truncatedMessageString);
+            final String truncatedKey =
+                    excessiveKey.substring(0, maxStringLength) +
+                            truncatedStringSuffix;
+            final String truncatedValue =
+                    excessiveValue.substring(0, maxStringLength) +
+                            truncatedStringSuffix;
+            assertThat(accessor.getString(truncatedKey)).isEqualTo(truncatedValue);
+            assertThat(accessor.getString(nullValueKey)).isNull();
+        });
 
     }
 
@@ -1066,7 +1073,7 @@ public class JsonTemplateLayoutTest {
     }
 
     @Test
-    public void test_exception_with_nonAscii_utf8_method_name() throws IOException {
+    public void test_exception_with_nonAscii_utf8_method_name() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -1080,9 +1087,11 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("ex_stacktrace", "${json:exception:stackTrace:text}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "ex_stacktrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1093,15 +1102,14 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "ex_stacktrace").asText())
-                .contains(NonAsciiUtf8MethodNameContainingException.NON_ASCII_UTF8_TEXT);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                assertThat(accessor.getString("ex_stacktrace"))
+                        .contains(NonAsciiUtf8MethodNameContainingException.NON_ASCII_UTF8_TEXT));
 
     }
 
     @Test
-    public void test_event_template_additional_fields() throws IOException {
+    public void test_event_template_additional_fields() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
@@ -1116,16 +1124,29 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("level", "${json:level}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = "{}";
 
         // Create the layout.
-        final KeyValuePair additionalField1 = new KeyValuePair("message", "${json:message}");
-        final KeyValuePair additionalField2 = new KeyValuePair("@version", "1");
-        final KeyValuePair[] additionalFieldPairs = {additionalField1, additionalField2};
-        final JsonTemplateLayout.EventTemplateAdditionalFields additionalFields = JsonTemplateLayout
-                .EventTemplateAdditionalFields
+        final EventTemplateAdditionalField[] additionalFieldPairs = {
+                EventTemplateAdditionalField
+                        .newBuilder()
+                        .setKey("number")
+                        .setValue("1")
+                        .setType(EventTemplateAdditionalField.Type.JSON)
+                        .build(),
+                EventTemplateAdditionalField
+                        .newBuilder()
+                        .setKey("string")
+                        .setValue("foo")
+                        .build(),
+                EventTemplateAdditionalField
+                        .newBuilder()
+                        .setKey("level")
+                        .setValue("{\"$resolver\": \"level\", \"field\": \"name\"}")
+                        .setType(EventTemplateAdditionalField.Type.JSON)
+                        .build()
+        };
+        final EventTemplateAdditionalFields additionalFields = EventTemplateAdditionalFields
                 .newBuilder()
                 .setAdditionalFields(additionalFieldPairs)
                 .build();
@@ -1138,53 +1159,63 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "level").asText()).isEqualTo(level.name());
-        assertThat(point(rootNode, additionalField1.getKey()).asText()).isEqualTo(message.getFormattedMessage());
-        assertThat(point(rootNode, additionalField2.getKey()).asText()).isEqualTo(additionalField2.getValue());
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getInteger("number")).isEqualTo(1);
+            assertThat(accessor.getString("string")).isEqualTo("foo");
+            assertThat(accessor.getString("level")).isEqualTo(level.name());
+        });
 
     }
 
     @Test
     @SuppressWarnings("FloatingPointLiteralPrecision")
-    public void test_timestamp_epoch_accessor() throws IOException {
-
-        // Create the log event.
-        final SimpleMessage message = new SimpleMessage("Hello, World!");
-        final Level level = Level.ERROR;
-        final MutableInstant instant = new MutableInstant();
-        final long instantEpochSecond = 1581082727L;
-        final int instantEpochSecondNano = 982123456;
-        instant.initFromEpochSecond(instantEpochSecond, instantEpochSecondNano);
-        final LogEvent logEvent = Log4jLogEvent
-                .newBuilder()
-                .setLoggerName(LOGGER_NAME)
-                .setLevel(level)
-                .setMessage(message)
-                .setInstant(instant)
-                .build();
+    public void test_timestamp_epoch_resolvers() {
+
+        final List<Map<String, Object>> testCases = Arrays.asList(
+                Map(
+                        "epochSecs", new BigDecimal("1581082727.982123456"),
+                        "epochSecsRounded", 1581082727,
+                        "epochSecsNanos", 982123456,
+                        "epochMillis", new BigDecimal("1581082727982.123456"),
+                        "epochMillisRounded", 1581082727982L,
+                        "epochMillisNanos", 123456,
+                        "epochNanos", 1581082727982123456L),
+                Map(
+                        "epochSecs", new BigDecimal("1591177590.005000001"),
+                        "epochSecsRounded", 1591177590,
+                        "epochSecsNanos", 5000001,
+                        "epochMillis", new BigDecimal("1591177590005.000001"),
+                        "epochMillisRounded", 1591177590005L,
+                        "epochMillisNanos", 1,
+                        "epochNanos", 1591177590005000001L));
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        final ObjectNode epochSecsNode = eventTemplateRootNode.putObject("epochSecs");
-        epochSecsNode.put("double", "${json:timestamp:epoch:secs}");
-        epochSecsNode.put("long", "${json:timestamp:epoch:secs,integral}");
-        epochSecsNode.put("nanos", "${json:timestamp:epoch:secs.nanos}");
-        epochSecsNode.put("micros", "${json:timestamp:epoch:secs.micros}");
-        epochSecsNode.put("millis", "${json:timestamp:epoch:secs.millis}");
-        final ObjectNode epochMillisNode = eventTemplateRootNode.putObject("epochMillis");
-        epochMillisNode.put("double", "${json:timestamp:epoch:millis}");
-        epochMillisNode.put("long", "${json:timestamp:epoch:millis,integral}");
-        epochMillisNode.put("nanos", "${json:timestamp:epoch:millis.nanos}");
-        epochMillisNode.put("micros", "${json:timestamp:epoch:millis.micros}");
-        final ObjectNode epochMicrosNode = eventTemplateRootNode.putObject("epochMicros");
-        epochMicrosNode.put("double", "${json:timestamp:epoch:micros}");
-        epochMicrosNode.put("long", "${json:timestamp:epoch:micros,integral}");
-        epochMicrosNode.put("nanos", "${json:timestamp:epoch:micros.nanos}");
-        final ObjectNode epochNanosNode = eventTemplateRootNode.putObject("epochNanos");
-        epochNanosNode.put("long", "${json:timestamp:epoch:nanos}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "epochSecs", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "secs")),
+                "epochSecsRounded", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map(
+                                "unit", "secs",
+                                "rounded", true)),
+                "epochSecsNanos", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "secs.nanos")),
+                "epochMillis", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "millis")),
+                "epochMillisRounded", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map(
+                                "unit", "millis",
+                                "rounded", true)),
+                "epochMillisNanos", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "millis.nanos")),
+                "epochNanos", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "nanos"))));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1193,120 +1224,40 @@ public class JsonTemplateLayoutTest {
                 .setEventTemplate(eventTemplate)
                 .build();
 
-        // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        final Percentage errorMargin = Percentage.withPercentage(0.001D);
-        assertThat(point(rootNode, "epochSecs", "double").asDouble())
-                .isCloseTo(1581082727.982123456D, errorMargin);
-        assertThat(point(rootNode, "epochSecs", "long").asLong())
-                .isEqualTo(1581082727L);
-        assertThat(point(rootNode, "epochSecs", "nanos").asInt())
-                .isEqualTo(982123456L);
-        assertThat(point(rootNode, "epochSecs", "micros").asInt())
-                .isEqualTo(982123L);
-        assertThat(point(rootNode, "epochSecs", "millis").asInt())
-                .isEqualTo(982L);
-        assertThat(point(rootNode, "epochMillis", "double").asDouble())
-                .isCloseTo(1581082727982.123456D, errorMargin);
-        assertThat(point(rootNode, "epochMillis", "long").asLong())
-                .isEqualTo(1581082727982L);
-        assertThat(point(rootNode, "epochMillis", "nanos").asInt())
-                .isEqualTo(123456);
-        assertThat(point(rootNode, "epochMillis", "micros").asInt())
-                .isEqualTo(123);
-        assertThat(point(rootNode, "epochMicros", "double").asDouble())
-                .isCloseTo(1581082727982123.456D, errorMargin);
-        assertThat(point(rootNode, "epochMicros", "long").asLong())
-                .isEqualTo(1581082727982123L);
-        assertThat(point(rootNode, "epochMicros", "nanos").asInt())
-                .isEqualTo(456);
-        assertThat(point(rootNode, "epochNanos", "long").asLong())
-                .isEqualTo(1581082727982123456L);
-
-    }
-
-    @Test
-    public void test_level_severity() throws IOException {
-
-        // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("severity", "${json:level:severity}");
-        eventTemplateRootNode.put("severityCode", "${json:level:severity:code}");
-        final String eventTemplate = eventTemplateRootNode.toString();
-
-        // Create the layout.
-        final JsonTemplateLayout layout = JsonTemplateLayout
-                .newBuilder()
-                .setConfiguration(CONFIGURATION)
-                .setEventTemplate(eventTemplate)
-                .build();
-
-        for (final Level level : Level.values()) {
+        testCases.forEach(testCase -> {
 
             // Create the log event.
             final SimpleMessage message = new SimpleMessage("Hello, World!");
+            final Level level = Level.ERROR;
+            final MutableInstant instant = new MutableInstant();
+            final Object instantSecsObject = testCase.get("epochSecsRounded");
+            final long instantSecs = instantSecsObject instanceof Long
+                    ? (long) instantSecsObject
+                    : (int) instantSecsObject;
+            final int instantSecsNanos = (int) testCase.get("epochSecsNanos");
+            instant.initFromEpochSecond(instantSecs, instantSecsNanos);
             final LogEvent logEvent = Log4jLogEvent
                     .newBuilder()
                     .setLoggerName(LOGGER_NAME)
                     .setLevel(level)
                     .setMessage(message)
+                    .setInstant(instant)
                     .build();
 
-            // Check the serialized event.
-            final String serializedLogEvent = layout.toSerializable(logEvent);
-            final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-            final Severity expectedSeverity = Severity.getSeverity(level);
-            final String expectedSeverityName = expectedSeverity.name();
-            final int expectedSeverityCode = expectedSeverity.getCode();
-            assertThat(point(rootNode, "severity").asText()).isEqualTo(expectedSeverityName);
-            assertThat(point(rootNode, "severityCode").asInt()).isEqualTo(expectedSeverityCode);
-
-        }
-
-    }
-
-    @Test
-    public void test_exception_resolvers_against_no_exceptions() throws IOException {
-
-        // Create the log event.
-        final SimpleMessage message = new SimpleMessage("Hello, World!");
-        final LogEvent logEvent = Log4jLogEvent
-                .newBuilder()
-                .setLoggerName(LOGGER_NAME)
-                .setMessage(message)
-                .build();
-
-        // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("exceptionStackTrace", "${json:exception:stackTrace}");
-        eventTemplateRootNode.put("exceptionStackTraceText", "${json:exception:stackTrace:text}");
-        eventTemplateRootNode.put("exceptionRootCauseStackTrace", "${json:exceptionRootCause:stackTrace}");
-        eventTemplateRootNode.put("exceptionRootCauseStackTraceText", "${json:exceptionRootCause:stackTrace:text}");
-        eventTemplateRootNode.put("requiredFieldTriggeringError", true);
-        final String eventTemplate = eventTemplateRootNode.toString();
+            // Verify the test case.
+            usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                    testCase.forEach((key, expectedValue) ->
+                            Assertions
+                                    .assertThat(accessor.getObject(key))
+                                    .describedAs("key=%s", key)
+                                    .isEqualTo(expectedValue)));
 
-        // Create the layout.
-        final JsonTemplateLayout layout = JsonTemplateLayout
-                .newBuilder()
-                .setConfiguration(CONFIGURATION)
-                .setEventTemplate(eventTemplate)
-                .setStackTraceEnabled(true)
-                .build();
-
-        // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "exceptionStackTrace")).isInstanceOf(MissingNode.class);
-        assertThat(point(rootNode, "exceptionStackTraceText")).isInstanceOf(MissingNode.class);
-        assertThat(point(rootNode, "exceptionRootCauseStackTrace")).isInstanceOf(MissingNode.class);
-        assertThat(point(rootNode, "exceptionRootCauseStackTraceText")).isInstanceOf(MissingNode.class);
-        assertThat(point(rootNode, "requiredFieldTriggeringError").asBoolean()).isTrue();
+        });
 
     }
 
     @Test
-    public void test_timestamp_resolver() throws IOException {
+    public void test_timestamp_pattern_resolver() {
 
         // Create log events.
         final String logEvent1FormattedInstant = "2019-01-02T09:34:11Z";
@@ -1320,14 +1271,12 @@ public class JsonTemplateLayoutTest {
         final LogEvent logEvent4 = createLogEventAtInstant(logEvent4FormattedInstant);
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put(
-                "timestamp",
-                "${json:timestamp:" +
-                        "pattern=yyyy-MM-dd'T'HH:mm:ss'Z'," +
-                        "timeZone=UTC" +
-                        "}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "timestamp", Map(
+                        "$resolver", "timestamp",
+                        "pattern", Map(
+                                "format", "yyyy-MM-dd'T'HH:mm:ss'Z'",
+                                "timeZone", "UTC"))));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1337,24 +1286,24 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized 1st event.
-        final String serializedLogEvent1 = layout.toSerializable(logEvent1);
-        final JsonNode rootNode1 = OBJECT_MAPPER.readTree(serializedLogEvent1);
-        assertThat(point(rootNode1, "timestamp").asText()).isEqualTo(logEvent1FormattedInstant);
+        usingSerializedLogEventAccessor(layout, logEvent1, accessor ->
+                assertThat(accessor.getString("timestamp"))
+                        .isEqualTo(logEvent1FormattedInstant));
 
         // Check the serialized 2nd event.
-        final String serializedLogEvent2 = layout.toSerializable(logEvent2);
-        final JsonNode rootNode2 = OBJECT_MAPPER.readTree(serializedLogEvent2);
-        assertThat(point(rootNode2, "timestamp").asText()).isEqualTo(logEvent2FormattedInstant);
+        usingSerializedLogEventAccessor(layout, logEvent2, accessor ->
+                assertThat(accessor.getString("timestamp"))
+                        .isEqualTo(logEvent2FormattedInstant));
 
         // Check the serialized 3rd event.
-        final String serializedLogEvent3 = layout.toSerializable(logEvent3);
-        final JsonNode rootNode3 = OBJECT_MAPPER.readTree(serializedLogEvent3);
-        assertThat(point(rootNode3, "timestamp").asText()).isEqualTo(logEvent3FormattedInstant);
+        usingSerializedLogEventAccessor(layout, logEvent3, accessor ->
+                assertThat(accessor.getString("timestamp"))
+                        .isEqualTo(logEvent3FormattedInstant));
 
         // Check the serialized 4th event.
-        final String serializedLogEvent4 = layout.toSerializable(logEvent4);
-        final JsonNode rootNode4 = OBJECT_MAPPER.readTree(serializedLogEvent4);
-        assertThat(point(rootNode4, "timestamp").asText()).isEqualTo(logEvent4FormattedInstant);
+        usingSerializedLogEventAccessor(layout, logEvent4, accessor ->
+                assertThat(accessor.getString("timestamp"))
+                        .isEqualTo(logEvent4FormattedInstant));
 
     }
 
@@ -1372,12 +1321,107 @@ public class JsonTemplateLayoutTest {
     }
 
     @Test
-    public void test_StackTraceTextResolver_with_maxStringLength() throws Exception {
+    public void test_level_severity() {
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("stackTrace", "${json:exception:stackTrace:text}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "severityKeyword", Map(
+                        "$resolver", "level",
+                        "field", "severity",
+                        "severity", Map("field", "keyword")),
+                "severityCode", Map(
+                        "$resolver", "level",
+                        "field", "severity",
+                        "severity", Map("field", "code"))));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        for (final Level level : Level.values()) {
+
+            // Create the log event.
+            final SimpleMessage message = new SimpleMessage("Hello, World!");
+            final LogEvent logEvent = Log4jLogEvent
+                    .newBuilder()
+                    .setLoggerName(LOGGER_NAME)
+                    .setLevel(level)
+                    .setMessage(message)
+                    .build();
+
+            // Check the serialized event.
+            usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+                final Severity expectedSeverity = Severity.getSeverity(level);
+                final String expectedSeverityKeyword = expectedSeverity.name();
+                final int expectedSeverityCode = expectedSeverity.getCode();
+                assertThat(accessor.getString("severityKeyword")).isEqualTo(expectedSeverityKeyword);
+                assertThat(accessor.getInteger("severityCode")).isEqualTo(expectedSeverityCode);
+            });
+
+        }
+
+    }
+
+    @Test
+    public void test_exception_resolvers_against_no_exceptions() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .build();
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "exStackTrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace"),
+                "exStackTraceString", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true),
+                "exRootCauseStackTrace", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "stackTrace"),
+                "exRootCauseStackTraceString", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "stackTrace",
+                        "stringified", true),
+                "requiredFieldTriggeringError", true));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .setStackTraceEnabled(true)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getObject("exStackTrace")).isNull();
+            assertThat(accessor.getObject("exStackTraceString")).isNull();
+            assertThat(accessor.getObject("exRootCauseStackTrace")).isNull();
+            assertThat(accessor.getObject("exRootCauseStackTraceString")).isNull();
+            assertThat(accessor.getBoolean("requiredFieldTriggeringError")).isTrue();
+        });
+
+    }
+
+    @Test
+    public void test_StackTraceTextResolver_with_maxStringLength() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "stackTrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true)));
 
         // Create the layout.
         final int maxStringLength = eventTemplate.length();
@@ -1399,9 +1443,11 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        assertThat(point(rootNode, "stackTrace").asText()).isNotBlank();
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            final int expectedLength = maxStringLength +
+                    JsonTemplateLayoutDefaults.getTruncatedStringSuffix().length();
+            assertThat(accessor.getString("stackTrace").length()).isEqualTo(expectedLength);
+        });
 
     }
 
@@ -1409,9 +1455,7 @@ public class JsonTemplateLayoutTest {
     public void test_null_eventDelimiter() {
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("key", "val");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map("key", "val"));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1443,9 +1487,8 @@ public class JsonTemplateLayoutTest {
         final List<LogEvent> logEvents = createNastyLogEvents();
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("message", "${json:message}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message")));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1616,12 +1659,13 @@ public class JsonTemplateLayoutTest {
     }
 
     @Test
-    public void test_PatternResolver() throws IOException {
+    public void test_PatternResolver() {
 
         // Create the event template.
-        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
-        eventTemplateRootNode.put("message", "${json:pattern:%p:%m}");
-        final String eventTemplate = eventTemplateRootNode.toString();
+        final String eventTemplate = writeJson(Map(
+                "message", Map(
+                        "$resolver", "pattern",
+                        "pattern", "%p:%m")));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
@@ -1641,13 +1685,53 @@ public class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        final String serializedLogEvent = layout.toSerializable(logEvent);
-        final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
-        final String expectedMessage = String.format(
-                "%s:%s",
-                level, message.getFormattedMessage());
-        assertThat(point(rootNode, "message").asText()).isEqualTo(expectedMessage);
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            final String expectedMessage = String.format(
+                    "%s:%s",
+                    level, message.getFormattedMessage());
+            assertThat(accessor.getString("message")).isEqualTo(expectedMessage);
+        });
+
+    }
 
+    private static String writeJson(final Object value) {
+        final StringBuilder stringBuilder = JSON_WRITER.getStringBuilder();
+        stringBuilder.setLength(0);
+        try {
+            JSON_WRITER.writeValue(value);
+            return stringBuilder.toString();
+        } finally {
+            stringBuilder.setLength(0);
+        }
+    }
+
+    private static void usingSerializedLogEventAccessor(
+            final Layout<String> layout,
+            final LogEvent logEvent,
+            final Consumer<MapAccessor> accessorConsumer) {
+        final String serializedLogEventJson = layout.toSerializable(logEvent);
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> serializedLogEvent =
+                (Map<String, Object>) readJson(serializedLogEventJson);
+        final MapAccessor serializedLogEventAccessor = new MapAccessor(serializedLogEvent);
+        accessorConsumer.accept(serializedLogEventAccessor);
+    }
+
+    private static Object readJson(final String json) {
+        return JsonReader.read(json);
+    }
+
+    private static Map<String, Object> Map(final Object... pairs) {
+        final Map<String, Object> map = new LinkedHashMap<>();
+        if (pairs.length % 2 != 0) {
+            throw new IllegalArgumentException("odd number of arguments");
+        }
+        for (int i = 0; i < pairs.length; i += 2) {
+            final String key = (String) pairs[i];
+            final Object value = pairs[i + 1];
+            map.put(key, value);
+        }
+        return map;
     }
 
 }
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
index 62ad35d..6f0a6c0 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
@@ -2,6 +2,7 @@ package org.apache.logging.log4j.layout.json.template;
 
 import org.apache.logging.log4j.core.Layout;
 import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonReader;
 
 import java.util.Map;
 
@@ -10,10 +11,9 @@ enum LayoutComparisonHelpers {;
     @SuppressWarnings("unchecked")
     static Map<String, Object> renderUsing(
             final LogEvent logEvent,
-            final Layout<String> layout)
-            throws Exception {
+            final Layout<String> layout) {
         final String json = layout.toSerializable(logEvent);
-        return JacksonFixture.getObjectMapper().readValue(json, Map.class);
+        return (Map<String, Object>) JsonReader.read(json);
     }
 
 }
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
index 00571b7..9c30be8 100644
--- a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
@@ -10,8 +10,8 @@ import org.apache.logging.log4j.core.appender.SocketAppender;
 import org.apache.logging.log4j.core.config.DefaultConfiguration;
 import org.apache.logging.log4j.core.impl.Log4jLogEvent;
 import org.apache.logging.log4j.core.layout.GelfLayout;
-import org.apache.logging.log4j.core.util.KeyValuePair;
 import org.apache.logging.log4j.core.util.NetUtils;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
 import org.apache.logging.log4j.layout.json.template.util.ThreadLocalRecyclerFactory;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.status.StatusLogger;
@@ -81,9 +81,14 @@ public class LogstashIT {
             .setEventTemplateAdditionalFields(JsonTemplateLayout
                     .EventTemplateAdditionalFields
                     .newBuilder()
-                    .setAdditionalFields(new KeyValuePair[]{
-                            new KeyValuePair("host", HOST_NAME)
-                    })
+                    .setAdditionalFields(
+                            new EventTemplateAdditionalField[]{
+                                    EventTemplateAdditionalField
+                                            .newBuilder()
+                                            .setKey("host")
+                                            .setValue(HOST_NAME)
+                                            .build()
+                            })
                     .build())
             .build();
 
@@ -103,9 +108,14 @@ public class LogstashIT {
             .setEventTemplateAdditionalFields(JsonTemplateLayout
                     .EventTemplateAdditionalFields
                     .newBuilder()
-                    .setAdditionalFields(new KeyValuePair[]{
-                            new KeyValuePair("service.name", SERVICE_NAME)
-                    })
+                    .setAdditionalFields(
+                            new EventTemplateAdditionalField[]{
+                                    EventTemplateAdditionalField
+                                            .newBuilder()
+                                            .setKey("service.name")
+                                            .setValue(SERVICE_NAME)
+                                            .build()
+                            })
                     .build())
             .build();
 
diff --git a/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json b/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
index c1cc187..daf455e 100644
--- a/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
+++ b/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
@@ -1,21 +1,68 @@
 {
-  "exception_class": "${json:exception:className}",
-  "exception_message": "${json:exception:message}",
-  "stacktrace": "${json:exception:stackTrace:text}",
-  "line_number": "${json:source:lineNumber}",
-  "class": "${json:source:className}",
+  "exception_class": {
+    "$resolver": "exception",
+    "field": "className"
+  },
+  "exception_message": {
+    "$resolver": "exception",
+    "field": "message"
+  },
+  "stacktrace": {
+    "$resolver": "exception",
+    "field": "stackTrace",
+    "stringified": true
+  },
+  "line_number": {
+    "$resolver": "source",
+    "field": "lineNumber"
+  },
+  "class": {
+    "$resolver": "source",
+    "field": "className"
+  },
   "@version": 1,
   "source_host": "${hostName}",
-  "message": "${json:message}",
-  "thread_id": "${json:thread:id}",
-  "thread_name": "${json:thread:name}",
-  "thread_priority": "${json:thread:priority}",
-  "@timestamp": "${json:timestamp}",
-  "level": "${json:level}",
-  "file": "${json:source:fileName}",
-  "method": "${json:source:methodName}",
-  "logger_fqcn": "${json:logger:fqcn}",
-  "logger_name": "${json:logger:name}",
-  "end_of_batch": "${json:endOfBatch}",
+  "message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "thread_id": {
+    "$resolver": "thread",
+    "field": "id"
+  },
+  "thread_name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "thread_priority": {
+    "$resolver": "thread",
+    "field": "priority"
+  },
+  "@timestamp": {
+    "$resolver": "timestamp"
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "file": {
+    "$resolver": "source",
+    "field": "fileName"
+  },
+  "method": {
+    "$resolver": "source",
+    "field": "methodName"
+  },
+  "logger_fqcn": {
+    "$resolver": "logger",
+    "field": "fqcn"
+  },
+  "logger_name": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "end_of_batch": {
+    "$resolver": "endOfBatch"
+  },
   "lookup_test_key": "${sys:lookup_test_key}"
 }
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
index c175f1f..dc8dcd1 100644
--- a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
@@ -25,6 +25,7 @@ import org.apache.logging.log4j.core.layout.GelfLayout;
 import org.apache.logging.log4j.core.util.KeyValuePair;
 import org.apache.logging.log4j.core.util.NetUtils;
 import org.apache.logging.log4j.jackson.json.layout.JsonLayout;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
 import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalFields;
 import org.apache.logging.log4j.layout.json.template.util.ThreadLocalRecyclerFactory;
 import org.openjdk.jmh.annotations.Scope;
@@ -88,9 +89,14 @@ public class JsonTemplateLayoutBenchmarkState {
     private static JsonTemplateLayout createJsonTemplateLayout4EcsLayout() {
         final EventTemplateAdditionalFields additionalFields = EventTemplateAdditionalFields
                 .newBuilder()
-                .setAdditionalFields(new KeyValuePair[]{
-                        new KeyValuePair("service.name", "benchmark")
-                })
+                .setAdditionalFields(
+                        new EventTemplateAdditionalField[]{
+                                EventTemplateAdditionalField
+                                        .newBuilder()
+                                        .setKey("service.name")
+                                        .setValue("benchmark")
+                                        .build()
+                        })
                 .build();
         return JsonTemplateLayout
                 .newBuilder()
@@ -111,12 +117,17 @@ public class JsonTemplateLayoutBenchmarkState {
                 .setRecyclerFactory(ThreadLocalRecyclerFactory.getInstance())
                 .setEventTemplateAdditionalFields(EventTemplateAdditionalFields
                         .newBuilder()
-                        .setAdditionalFields(new KeyValuePair[]{
-                                // Adding "host" as a constant rather than using
-                                // the "hostName" property lookup at runtime, which
-                                // is what GelfLayout does as well.
-                                new KeyValuePair("host", NetUtils.getLocalHostname())
-                        })
+                        .setAdditionalFields(
+                                new EventTemplateAdditionalField[]{
+                                        // Adding "host" as a constant rather than using
+                                        // the "hostName" property lookup at runtime, which
+                                        // is what GelfLayout does as well.
+                                        EventTemplateAdditionalField
+                                                .newBuilder()
+                                                .setKey("host")
+                                                .setValue(NetUtils.getLocalHostname())
+                                                .build()
+                                })
                         .build())
                 .build();
     }
@@ -135,7 +146,7 @@ public class JsonTemplateLayoutBenchmarkState {
                 .setConfiguration(CONFIGURATION)
                 .setCharset(CHARSET)
                 .setAdditionalFields(new KeyValuePair[]{
-                        new KeyValuePair("@version", "1")
+                        new KeyValuePair("@version", "\"1\"")
                 })
                 .build();
     }
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc b/src/site/asciidoc/manual/json-template-layout.adoc
index 29db221..d541dc2 100644
--- a/src/site/asciidoc/manual/json-template-layout.adoc
+++ b/src/site/asciidoc/manual/json-template-layout.adoc
@@ -29,8 +29,8 @@ by the JSON template provided. In a nutshell, it shines with its
 [#usage]
 == Usage
 
-Adding `log4j-layout-json-template` artifact to your dependencies is enough to
-enable access to `JsonTemplateLayout` in your Log4j configuration:
+Adding `log4j-layout-json-template` artifact to your list of dependencies is
+enough to enable access to `JsonTemplateLayout` in your Log4j configuration:
 
 [source,xml]
 ----
@@ -43,28 +43,67 @@ enable access to `JsonTemplateLayout` in your Log4j configuration:
 
 For instance, given the following JSON template modelling the
 https://github.com/logstash/log4j-jsonevent-layout[the official Logstash
-`JSONEventLayoutV1`]
+`JSONEventLayoutV1`] (accessible via `classpath:LogstashJsonEventLayoutV1.json`)
 
 [source,json]
 ----
 {
-  "mdc": "${json:mdc}",
+  "mdc": {
+    "$resolver": "mdc"
+  },
   "exception": {
-    "exception_class": "${json:exception:className}",
-    "exception_message": "${json:exception:message}",
-    "stacktrace": "${json:exception:stackTrace:text}"
+    "exception_class": {
+      "$resolver": "exception",
+      "field": "className"
+    },
+    "exception_message": {
+      "$resolver": "exception",
+      "field": "message",
+      "stringified": true
+    },
+    "stacktrace": {
+      "$resolver": "exception",
+      "field": "stackTrace",
+      "stringified": true
+    }
+  },
+  "line_number": {
+    "$resolver": "source",
+    "field": "lineNumber"
+  },
+  "class": {
+    "$resolver": "source",
+    "field": "className"
   },
-  "line_number": "${json:source:lineNumber}",
-  "class": "${json:source:className}",
   "@version": 1,
   "source_host": "${hostName}",
-  "message": "${json:message}",
-  "thread_name": "${json:thread:name}",
-  "@timestamp": "${json:timestamp}",
-  "level": "${json:level}",
-  "file": "${json:source:fileName}",
-  "method": "${json:source:methodName}",
-  "logger_name": "${json:logger:name}"
+  "message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "thread_name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "@timestamp": {
+    "$resolver": "timestamp"
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "file": {
+    "$resolver": "source",
+    "field": "fileName"
+  },
+  "method": {
+    "$resolver": "source",
+    "field": "methodName"
+  },
+  "logger_name": {
+    "$resolver": "logger",
+    "field": "name"
+  }
 }
 ----
 
@@ -125,12 +164,13 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
 
 | locationInfoEnabled
 | boolean
-| includes the filename and line number in the output (defaults to `false` set
-  by `log4j.layout.jsonTemplate.locationInfoEnabled` property)
+| toggles access to the `LogEvent` source; file name, line number, etc.
+  (defaults to `false` set by `log4j.layout.jsonTemplate.locationInfoEnabled`
+  property)
 
 | stackTraceEnabled
 | boolean
-| includes stack traces (defaults to `true` set by
+| toggles access to the stack traces (defaults to `true` set by
   `log4j.layout.jsonTemplate.stackTraceEnabled` property)
 
 | eventTemplate
@@ -142,7 +182,7 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
 | eventTemplateUri
 | String
 | URI pointing to the JSON template for rendering ``LogEvent``s (defaults to
-  `classpath:JsonLayout.json` set by `log4j.layout.jsonTemplate.eventTemplateUri`
+  `classpath:EcsLayout.json` set by `log4j.layout.jsonTemplate.eventTemplateUri`
   property)
 
 | eventTemplateAdditionalFields
@@ -174,8 +214,9 @@ appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.jso
 
 | truncatedStringSuffix
 | String
-| suffix to append to the truncated strings (defaults to `…` set by
-  `log4j.layout.jsonTemplate.truncatedStringSuffix` property)
+| suffix to append to strings truncated due to exceeding `maxStringLength`
+  (defaults to `…` set by `log4j.layout.jsonTemplate.truncatedStringSuffix`
+  property)
 
 | recyclerFactory
 | RecyclerFactory
@@ -193,12 +234,30 @@ One can configure additional event template fields via
 ----
 <JsonTemplateLayout ...>
     <EventTemplateAdditionalFields>
-        <KeyValuePair key="serviceName" value="auth-service"/>
-        <KeyValuePair key="containerId" value="6ede3f0ca7d9"/>
+        <EventTemplateAdditionalField key="serviceName" value="auth-service"/>
+        <EventTemplateAdditionalField key="containerId" value="6ede3f0ca7d9"/>
     </EventTemplateAdditionalFields>
 </JsonTemplateLayout>
 ----
 
+One can also pass JSON literals into additional fields:
+
+[source,xml]
+----
+<EventTemplateAdditionalField
+     key="marker"
+     type="JSON"
+     value='{"$resolver": "marker", "field": "name"}'/>
+<EventTemplateAdditionalField
+     key="aNumber"
+     type="JSON"
+     value="1"/>
+<EventTemplateAdditionalField
+     key="aList"
+     type="JSON"
+     value='[1,2,"string"]'/>
+----
+
 [#recycling-strategy]
 === Recycling strategy
 
@@ -245,7 +304,7 @@ parameters:
 
 - `eventTemplate[Uri]` (for serializing ``LogEvent``s)
 - `stackTraceElementTemplate[Uri]` (for serializing ``StackStraceElement``s)
-- `eventTemplateAdditionalFields` (for extending the used `LogEvent` template)
+- `eventTemplateAdditionalFields` (for extending the used event template)
 
 [#event-templates]
 === Event Templates
@@ -253,38 +312,60 @@ parameters:
 `eventTemplate[Uri]` describes the JSON structure `JsonTemplateLayout` uses to
 serialize ``LogEvent``s. The default configuration (accessible by
 `log4j.layout.jsonTemplate.eventTemplate[Uri]` property) is set to
-`classpath:JsonLayout.json` provided by the `log4j-layout-json-template`
+`classpath:EcsLayout.json` provided by the `log4j-layout-json-template`
 artifact:
 
 [source,json]
 ----
 {
-  "instant": {
-    "epochSecond": "${json:timestamp:epoch:secs,integral}",
-    "nanoOfSecond": "${json:timestamp:epoch:secs.nanos}"
+  "@timestamp": {
+    "$resolver": "timestamp",
+    "pattern": {
+      "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+      "timeZone": "UTC"
+    }
+  },
+  "log.level": {
+    "$resolver": "level",
+    "field": "name"
   },
-  "thread": "${json:thread:name}",
-  "level": "${json:level}",
-  "loggerName": "${json:logger:name}",
-  "message": "${json:message}",
-  "thrown": {
-    "message": "${json:exception:message}",
-    "name": "${json:exception:className}",
-    "extendedStackTrace": "${json:exception:stackTrace}"
+  "message": {
+    "$resolver": "message",
+    "stringified": true
   },
-  "contextStack": "${json:ndc}",
-  "endOfBatch": "${json:endOfBatch}",
-  "loggerFqcn": "${json:logger:fqcn}",
-  "contextMap": "${json:mdc}",
-  "threadId": "${json:thread:id}",
-  "threadPriority": "${json:thread:priority}",
-  "source": {
-    "class": "${json:source:className}",
-    "method": "${json:source:methodName}",
-    "file": "${json:source:fileName}",
-    "line": "${json:source:lineNumber}"
+  "process.thread.name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "log.logger": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "labels": {
+    "$resolver": "mdc",
+    "flatten": {
+      "prefix": "labels."
+    },
+    "stringified": true
+  },
+  "tags": {
+    "$resolver": "ndc"
+  },
+  "error.type": {
+    "$resolver": "exception",
+    "field": "className"
+  },
+  "error.message": {
+    "$resolver": "exception",
+    "field": "message"
+  },
+  "error.stack_trace": {
+    "$resolver": "exception",
+    "field": "stackTrace",
+    "stringified": true
   }
 }
+
 ----
 
 `log4j-layout-json-template` artifact contains the following predefined event
@@ -294,7 +375,8 @@ templates:
   described by https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[the Elastic Common Schema (ECS) specification]
 
 - https://github.com/apache/logging-log4j2/tree/master/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json[`LogstashJsonEventLayoutV1.json`]
-  described in https://github.com/logstash/log4j-jsonevent-layout[log4j-jsonevent-layout]
+  described in https://github.com/logstash/log4j-jsonevent-layout[Logstash
+  `json_event` pattern for log4j]
 
 - https://github.com/apache/logging-log4j2/tree/master/log4j-layout-json-template/src/main/resources/GelfLayout.json[`GelfLayout.json`]
   described by https://docs.graylog.org/en/3.1/pages/gelf.html#gelf-payload-specification[the
@@ -308,202 +390,600 @@ templates:
   with the exception of `thrown` field. (`JsonLayout` serializes the `Throwable`
   as is via Jackson `ObjectMapper`, whereas `JsonLayout.json` template of
   `JsonTemplateLayout` employs the `StackTraceElementLayout.json` template
-  for stack traces to generate an always document-store-friendly flat structure.)
+  for stack traces to generate a document-store-friendly flat structure.)
 
-Below is the list of supported event template variables:
+Below is the list of supported event template resolvers:
 
-.`LogEvent` template variables
-[cols="1m,4"]
+[#event-template-resolvers]
+.`LogEvent` template resolvers
+[cols="1m,3,2,2,4"]
 |===
-| Variable Name
+| Resolver Name
+| Syntax
 | Description
+| Garbage Footprint
+| Examples
 
 | endOfBatch
+|
 | `logEvent.isEndOfBatch()`
+| none
+a|
+[source,json]
+----
+{
+  "$resolver": "endOfBatch"
+}
+----
+
+| exception
+a|
+[source]
+----
+config      = field , [ stringified ]
+field       = "field" -> (
+                "className"  \|
+                "message"    \|
+                "stackTrace" )
+stringified = "stringified" -> boolean
+----
+a|
+Resolves fields of the `Throwable` returned by `logEvent.getThrown()`.
+
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.stackTraceEnabled` property.
+| Since `Throwable#getStackTrace()` clones the original `StackTraceElement[]`,
+  access to (and hence rendering of) stack traces are not garbage-free.
+a|
+Resolve `logEvent.getThrown().getClass().getCanonicalName()`:
 
-| exception:className
-| `logEvent.getThrown().getClass().getCanonicalName()`
+[source,json]
+----
+{
+  "$resolver": "exception",
+  "field": "className"
+}
+----
 
-| exception:message
-| `logEvent.getThrown().getMessage()`
+Resolve the stack trace into a list of `StackTraceElement` objects:
 
-| exception:stackTrace
-| `logEvent.getThrown().getStackTrace()` (inactive when `stackTraceEnabled=false`)
+[source,json]
+----
+{
+  "$resolver": "exception",
+  "field": "stackTrace"
+}
+----
 
-| exception:stackTrace:text
-| `logEvent.getThrown().printStackTrace()` (inactive when `stackTraceEnabled=false`)
+Resolve the stack trace into a string field:
 
-| exceptionRootCause:className
-| the innermost `exception:className` in causal chain
+[source,json]
+----
+{
+  "$resolver": "exception",
+  "field": "stackTrace",
+  "stringified": true
+}
+----
 
-| exceptionRootCause:message
-| the innermost `exception:message` in causal chain
+| exceptionRootCause
+| identical to `exception` resolver
+a|
+Resolves the fields of the innermost `Throwable` returned by
+`logEvent.getThrown()`.
 
-| exceptionRootCause:stackTrace[:text]
-| the innermost `exception:stackTrace[:text]` in causal chain
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.stackTraceEnabled` property.
+| identical to `exception` resolver
+| identical to `exception` resolver
 
 | level
-| `logEvent.getLevel()`
+a|
+[source]
+----
+config         = field , [ severity ]
+field          = "field" -> ( "name" \| "severity" )
+severity       = severity-field
+severity-field = "field" -> ( "keyword" \| "code" )
+----
+| resolves the fields of the `logEvent.getLevel()`
+| none
+a|
+Resolve the level name:
+
+[source,json]
+----
+{
+  "$resolver": "level",
+  "field": "name"
+}
+----
+
+Resolve the https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity]
+keyword:
+
+[source,json]
+----
+{
+  "$resolver": "level",
+  "field": "severity",
+  "severity": {
+    "field": "keyword"
+  }
+}
+----
+
+Resolve the https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity]
+code:
+
+[source,json]
+----
+{
+  "$resolver": "level",
+  "field": "severity",
+  "severity": {
+    "field": "code"
+  }
+}
+----
 
-| level:severity
-| https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity] keyword
-  of `logEvent.getLevel()`
+| logger
+a|
+[source]
+----
+config = "field" -> ( "name" \| "fqcn" )
+----
+| resolves `logEvent.getLoggerFqcn()` and `logEvent.getLoggerName()`
+| none
+a|
+Resolve the logger name:
 
-| level:severity:code
-| https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity] code of
-  `logEvent.getLevel()`
+[source,json]
+----
+{
+  "$resolver": "logger",
+  "field": "name"
+}
+----
 
-| logger:fqcn
-| `logEvent.getLoggerFqcn()`
+Resolve the logger's fully qualified class name:
 
-| logger:name
-| `logEvent.getLoggerName()`
+[source,json]
+----
+{
+  "$resolver": "logger",
+  "field": "fqcn"
+}
+----
 
 | main:<key>
+a|
+[source]
+----
+config = ( index \| key )
+index  = "index" -> number
+key    = "key" -> string
+----
 | performs link:lookups.html#AppMainArgsLookup[Main Argument Lookup] for the
-  given `key`
+  given `index` or `key`
+| none
+a|
+Resolve the 1st `main()` method argument:
+
+[source,json]
+----
+{
+  "$resolver": "main",
+  "index": 0
+}
+----
+
+Resolve the argument coming right after `--userId`:
+
+[source,json]
+----
+{
+  "$resolver": "main",
+  "key": "--userId"
+}
+----
 
-| map:<key>
+| map
+a|
+[source]
+----
+config = "key" -> string
+----
 | performs link:lookups.html#MapLookup[Map Lookup] for the given `key`
+| none
+a|
+Resolve the `userRole` field of the message:
+
+[source,json]
+----
+{
+  "$resolver": "map",
+  "key": "userRole"
+}
+----
 
-| marker:name
-| `logEvent.getMarker.getName()`
+| marker
+a|
+[source]
+----
+config = "field" -> "name"
+----
+| `logEvent.getMarker().getName()`
+| none
+a|
+Resolve the marker name:
+
+[source,json]
+----
+{
+  "$resolver": "marker",
+  "field": "name"
+}
+----
 
 | mdc
-.4+| Mapped Diagnostic Context `Map<String, String>` returned by
-  `logEvent.getContextData()`, where one can merge it with the parent JSON
-  object via `flatten[=<prefix>]`, filter keys matching a regex `pattern`, just
-  extract a certain `key`, or `stringify` values
+a|
+[source]
+----
+config        = singleAccess \| multiAccess
 
-| mdc:flatten[=<prefix>][,stringify]
+singleAccess  = key , [ stringified ]
+key           = "key" -> string
+stringified   = "stringified" -> boolean
 
-| mdc:pattern=<pattern>[,flatten=<prefix>][,stringify]
+multi-access  = [ pattern ] , [ flatten ] , [ stringified ]
+pattern       = "pattern" -> string
+flatten       = "flatten" -> ( boolean \| flattenConfig )
+flattenConfig = [ flattenPrefix ]
+flattenPrefix = "prefix" -> string
+----
+a| Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
 
-| mdc:key=<key>[,stringify]
+`singleAccess` resolves the MDC value as is, whilst `multiAccess` resolves a
+multitude of MDC values. If `flatten` is provided, `multiAccess` merges the
+values with the parent, otherwise creates a new JSON object containing the
+values.
 
-| message
-| `logEvent.getFormattedMessage()`
+Enabling `stringified` flag converts each value to its string representation.
 
-| message:json
-| if `logEvent.getMessage()` is of type `MultiformatMessage` and supports JSON,
-  its read value; if is of type `ObjectMessage`, its serialized output via
-  Jackson `ObjectMapper`; otherwise, `{"message": <formattedMessage>}` object
+Regex provided in the `pattern` is used to match against the keys.
+a|
+`log4j2.garbagefreeThreadContextMap` flag needs to be turned on to iterate
+the map without allocations.
 
-| ndc[:pattern=<pattern>]
-| Nested Diagnostic Context `String[]` returned by `logEvent.getContextStack()`,
-  where filtering is supported via the supplied regex `pattern`
+`stringify` allocates a new `String` for values that are not of type `String`.
 
-| pattern:<pattern>
-| injects link:layouts.html#PatternLayout[`PatternLayout`] serialized string
-  described by the `pattern` parameter
+Writing certain non-primitive values (e.g., `BigDecimal`, `Set`, etc.) to JSON
+generates garbage, though most (e.g., `int`, `long`, `String`, `List`,
+`boolean[]`, etc.) don't.
+a|
+Resolve the `userRole` MDC value:
 
-| source:className
-| `logEvent.getSource().getClassName()`
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "key": "userRole"
+}
+----
 
-| source:fileName
-| `logEvent.getSource().getFileName()` (inactive when `locationInfoEnabled=false`)
+Resolve the string representation of the `userRank` MDC value:
 
-| source:lineNumber
-| `logEvent.getSource().getLineNumber()` (inactive when `locationInfoEnabled=false`)
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "key": "userRank",
+  "stringified": true
+}
+----
 
-| source:methodName
-| `logEvent.getSource().getMethodName()`
+Resolve all MDC entries into an object:
 
-| thread:id
-| `logEvent.getThreadId()`
+[source,json]
+----
+{
+  "$resolver": "mdc"
+}
+----
 
-| thread:name
-| `logEvent.getThreadName()`
+Resolve all MDC entries into an object such that values are converted to string:
 
-| thread:priority
-| `logEvent.getThreadPriority()`
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "stringified": true
+}
+----
 
-| timestamp
-.4+| `logEvent.getInstant()` formatted using optional
-  `pattern` (defaults to `yyyy-MM-dd'T'HH:mm:ss.SSSZZZ` set by
-  `log4j.layout.jsonTemplate.timestampFormatPattern` property), `timeZone`
-  (defaults to `TimeZone.getDefault()` set by
-  `log4j.layout.jsonTemplate.timeZone` property), and `locale` (represented by
-  `language[_country[_variant]]` pattern, defaults to `Locale.getDefault()` set
-  by `log4j.layout.jsonTemplate.locale` property) parameters
+Merge all MDC entries whose keys are matching with the `user(Role\|Rank)` regex
+into the parent:
 
-| timestamp:pattern=<pattern>
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "flatten": true,
+  "pattern": "user(Role\|Rank)"
+}
+----
 
-| timestamp:timeZone=<timeZone>
+After converting the corresponding entries to string, merge all MDC entries to
+parent such that keys are prefixed with `_`:
 
-| timestamp:locale=<locale>
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "stringified": true,
+  "flatten": {
+    "prefix": "_"
+  }
+}
+----
 
-| timestamp:epoch:nanos
-| UTC epoch nanoseconds (of type `long`) derived from `logEvent.getInstant()`
+| message
+a|
+[source]
+----
+config      = [ stringified ]
+stringified = "stringified" -> boolean
+----
+a| `logEvent.getMessage()`
+| For simple string messages, the resolution is performed without allocations.
+  For `ObjectMessage`s and `MultiformatMessage`s, it depends.
+a|
+Resolve the message into a string:
 
-| timestamp:epoch:<secs\|micros\|millis>[,integral]
-| UTC epoch seconds, microseconds, or milliseconds (of type `double`) derived from
-  `logEvent.getInstant()` and, if `integral` is provided, cast to `long`
+[source,json]
+----
+{
+  "$resolver": "message",
+  "stringified": true
+}
+----
 
-| timestamp:epoch:secs.<micros\|millis\|nanos>
-.3+| UTC epoch fractions (of type `long`) derived from `logEvent.getInstant()`;
-  `secs.micros` denotes the "fractional part of epoch seconds, in microseconds",
-  `micros.millis` denotes the "fractional part of epoch microseconds, in
-milliseconds", etc.
+Resolve the message such that if it is an `ObjectMessage` or a
+`MultiformatMessage` with JSON support, its type (string, list, object, etc.)
+will be retained:
 
-| timestamp:epoch:micros.<millis\|nanos>
+[source,json]
+----
+{
+  "$resolver": "message"
+}
+----
 
-| timestamp:epoch:millis.nanos
-|===
+| ndc
+a|
+[source]
+----
+config  = [ pattern ]
+pattern = "pattern" -> string
+----
+| Resolves the Nested Diagnostic Context (NDC), aka. Thread Context Stack,
+  `String[]` returned by `logEvent.getContextStack()`
+| none
+a|
+Resolve all NDC values into a list:
 
-In the following table, timestamp template variables are illustrated by
-examples:
+[source,json]
+----
+{
+  "$resolver": "ndc"
+}
+----
 
-.`timestamp` template variable examples
-[cols="1m,4m"]
-|===
-| Variable Name
-| Output
+Resolve all NDC values matching with the `pattern` regex:
 
-|timestamp
-|2020-02-07T13:38:47.098+02:00
+[source,json]
+----
+{
+  "$resolver": "ndc",
+  "pattern": "user(Role\|Rank):\\w+"
+}
+----
 
-|timestamp:pattern=yyyy-MM-dd'T'HH:mm:ss.SSS'Z',timeZone=UTC,locale=en_US
-|2020-02-07T13:38:47.098Z
+| source
+a|
+[source]
+----
+config = "field" -> (
+           "className"  \|
+           "fileName"   \|
+           "methodName" \|
+           "lineNumber" )
+----
+a|
+Resolves the fields of the `StackTraceElement` returned by
+`logEvent.getSource()`.
 
-|timestamp:epoch:secs
-|1581082727.982123456
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.locationInfoEnabled` property.
+| none
+a|
+Resolve the line number:
 
-|timestamp:epoch:secs,integral
-|1581082727
+[source,json]
+----
+{
+  "$resolver": "source",
+  "field": "lineNumber"
+}
+----
 
-|timestamp:epoch:millis
-|1581082727982.123456
+| thread
+a|
+[source]
+----
+config = "field" -> ( "name" \| "id" \| "priority" )
+----
+| resolves `logEvent.getThreadId()`, `logEvent.getThreadName()`,
+  `logEvent.getThreadPriority()`
+| none
+a|
+Resolve the thread name:
 
-|timestamp:epoch:millis,integral
-|1581082727982
+[source,json]
+----
+{
+  "$resolver": "thread",
+  "field": "name"
+}
+----
 
-|timestamp:epoch:micros
-|1581082727982123.456
+| timestamp
+a|
+[source]
+----
+config        = [ patternConfig \| epochConfig ]
+
+patternConfig = "pattern" -> (
+                  [ format ]   ,
+                  [ timeZone ] ,
+                  [ locale ]   )
+format        = "format" -> string
+timeZone      = "timeZone" -> string
+locale        = "locale" -> (
+                   language                                   \|
+                 ( language , "_" , country )                 \|
+                 ( language , "_" , country , "_" , variant )
+               )
+
+epochConfig   = "epoch" -> ( unit , [ rounded ] )
+unit          = "unit" -> (
+                   "nanos"         \|
+                   "millis"        \|
+                   "secs"          \|
+                   "millis.nanos"  \|
+                   "secs.nanos"    \|
+                )
+rounded       = "rounded" -> boolean
+----
+| resolves `logEvent.getInstant()` in various forms
+| none
+a|
+.`timestamp` template resolver examples
+[cols="5,2m"]
+!===
+! Configuration
+! Output
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp"
+}
+----
+! 2020-02-07T13:38:47.098+02:00
 
-|timestamp:epoch:millis,integral
-|1581082727982123
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "pattern": {
+    "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+    "timeZone": "UTC",
+    "locale": "en_US"
+  }
+}
+----
+! 2020-02-07T13:38:47.098Z
 
-|timestamp:epoch:nanos
-|1581082727982123456
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "secs"
+  }
+}
+----
+! 1581082727.982123456
 
-|timestamp:epoch:secs.millis
-|0000000000982
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "secs",
+    "rounded": true
+  }
+}
+----
+! 1581082727
 
-|timestamp:epoch:secs.micros
-|0000000000982123
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "secs.nanos"
+  }
+}
+----
+! 982123456
 
-|timestamp:epoch:secs.nanos
-|0000000000982123456
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "millis"
+  }
+}
+----
+! 1581082727982.123456
 
-|timestamp:epoch:millis.micros
-|0000000000000123
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "millis",
+    "rounded": true
+  }
+}
+----
+! 1581082727982
 
-|timestamp:epoch:millis.nanos
-|0000000000000123456
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "millis.nanos"
+  }
+}
+----
+! 123456
 
-|timestamp:epoch:micros.nanos
-|0000000000000000456
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "nanos"
+  }
+}
+----
+! 1581082727982123456
+!===
 |===
 
 [#stack-trace-element-templates]
@@ -518,46 +998,37 @@ to format ``StackTraceElement``s. The default configuration (accessible by
 [source,json]
 ----
 {
-  "class": "${json:stackTraceElement:className}",
-  "method": "${json:stackTraceElement:methodName}",
-  "file": "${json:stackTraceElement:fileName}",
-  "line": "${json:stackTraceElement:lineNumber}"
+  "class": {
+    "$resolver": "stackTraceElement",
+    "field": "className"
+  },
+  "method": {
+    "$resolver": "stackTraceElement",
+    "field": "methodName"
+  },
+  "file": {
+    "$resolver": "stackTraceElement",
+    "field": "fileName"
+  },
+  "line": {
+    "$resolver": "stackTraceElement",
+    "field": "lineNumber"
+  }
 }
 ----
 
-Below is the list of supported stack trace element template variables:
-
-.`StackTraceElement` template variables
-[cols="1m,4m"]
-|===
-| Variable Name
-| Description
+The allowed template configuration syntax is as follows:
 
-| stackTraceElement:className
-| stackTraceElement.getClassName()
-
-| stackTraceElement:methodName
-| stackTraceElement.getMethodName()
-
-| stackTraceElement:fileName
-| stackTraceElement.getFileName()
-
-| stackTraceElement:lineNumber
-| stackTraceElement.getLineNumber()
-|===
-
-[#template-variables]
-=== Template Variables
-
-JSON field lookups are performed using the `${json:<variable-name>}` scheme
-where `<variable-name>` is defined as `<resolver-name>[:<resolver-key>]`.
-Characters following colon (`:`) are treated as the `resolver-key`.
+[source]
+----
+config = "field" -> (
+           "className"  |
+           "fileName"   |
+           "methodName" |
+           "lineNumber" )
+----
 
-link:lookups.html[Lookups] (e.g., `${java:version}`, `${env:USER}`,
-`${date:MM-dd-yyyy}`) are supported in templates too. Though note that while
-`${json:...}` template variables are expected to occupy an entire field, that
-is, `"level": "${json:level}"`, a lookup can be mixed within a regular string as
-in `"greeting": "Hello, ${env:USER}!"`.
+All above accesses to `StackTraceElement` is garbage-free.
 
 [#features]
 == Features
@@ -638,22 +1109,32 @@ alternatives.
 [#faq]
 == F.A.Q.
 
+[#faq-lookups]
+=== Are lookups supported in templates?
+
+Yes, link:lookups.html[lookups] (e.g., `${java:version}`, `${env:USER}`,
+`${date:MM-dd-yyyy}`) are supported in string literals of templates. Though note
+that they are not garbage-free.
+
 [#faq-garbage-free]
 === Is `JsonTemplateLayout` garbage-free?
 
-Given the garbage-free layout behaviour enabler properties
-`log4j2.enableDirectEncoders` and `log4j2.garbagefreeThreadContextMap` are set
-to `true`, `JsonTemplateLayout` is garbage-free with the following exceptions:
+Yes, if the garbage-free layout behaviour toggling properties
+`log4j2.enableDirectEncoders` and `log4j2.garbagefreeThreadContextMap` are
+enabled. Take into account the following caveats:
 
-* When recycling strategy is either `dummy` or `queue` but access concurrency
-  exceeds the configured `capacity`, then the recycler is not garbage-free.
+* The configured link:#recycling-strategy[recycling strategy] might not be
+  garbage-free.
 
 * Since `Throwable#getStackTrace()` clones the original `StackTraceElement[]`,
   access to (and hence rendering of) stack traces are not garbage-free.
 
-* Serialization of ``ObjectMessage``s via `${json:message:json}` is mostly
+* Serialization of ``MapMessage``s and ``ObjectMessage``s are mostly
   garbage-free except for certain types (e.g., `BigDecimal`, `BigInteger`,
   ``Collection``s with the exception of `List`).
 
 * link:lookups.html[Lookups] (that is, `${...}` variables, excluding
   `${json:...}` ones) are not garbage-free.
+
+Don't forget to checkout link:#event-template-resolvers[the notes on garbage footprint of resolvers]
+you employ in templates.
diff --git a/src/site/asciidoc/manual/layouts.adoc b/src/site/asciidoc/manual/layouts.adoc
index fc842bf..a1d540f 100644
--- a/src/site/asciidoc/manual/layouts.adoc
+++ b/src/site/asciidoc/manual/layouts.adoc
@@ -502,23 +502,62 @@ Logstash `JSONEventLayoutV1`]
 [source,json]
 ----
 {
-  "mdc": "${json:mdc}",
+  "mdc": {
+    "$resolver": "mdc"
+  },
   "exception": {
-    "exception_class": "${json:exception:className}",
-    "exception_message": "${json:exception:message}",
-    "stacktrace": "${json:exception:stackTrace:text}"
+    "exception_class": {
+      "$resolver": "exception",
+      "field": "className"
+    },
+    "exception_message": {
+      "$resolver": "exception",
+      "field": "message",
+      "stringified": true
+    },
+    "stacktrace": {
+      "$resolver": "exception",
+      "field": "stackTrace",
+      "stringified": true
+    }
+  },
+  "line_number": {
+    "$resolver": "source",
+    "field": "lineNumber"
+  },
+  "class": {
+    "$resolver": "source",
+    "field": "className"
   },
-  "line_number": "${json:source:lineNumber}",
-  "class": "${json:source:className}",
   "@version": 1,
   "source_host": "${hostName}",
-  "message": "${json:message}",
-  "thread_name": "${json:thread:name}",
-  "@timestamp": "${json:timestamp}",
-  "level": "${json:level}",
-  "file": "${json:source:fileName}",
-  "method": "${json:source:methodName}",
-  "logger_name": "${json:logger:name}"
+  "message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "thread_name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "@timestamp": {
+    "$resolver": "timestamp"
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "file": {
+    "$resolver": "source",
+    "field": "fileName"
+  },
+  "method": {
+    "$resolver": "source",
+    "field": "methodName"
+  },
+  "logger_name": {
+    "$resolver": "logger",
+    "field": "name"
+  }
 }
 ----
 
diff --git a/src/site/markdown/manual/cloud.md b/src/site/markdown/manual/cloud.md
index bbca8db..3df1d33 100644
--- a/src/site/markdown/manual/cloud.md
+++ b/src/site/markdown/manual/cloud.md
@@ -138,22 +138,22 @@ great fit for the bill.
             bufferedIo="true">
         <JsonTemplateLayout eventTemplateUri="classpath:EcsLayout.json">
             <EventTemplateAdditionalFields>
-                <KeyValuePair key="containerId" value="${docker:containerId:-}"/>
-                <KeyValuePair key="application" value="$${lower:${spring:spring.application.name:-spring}}"/>
-                <KeyValuePair key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
-                <KeyValuePair key="kubernetes.containerId" value="${k8s:containerId:-}"/>
-                <KeyValuePair key="kubernetes.containerName" value="${k8s:containerName:-}"/>
-                <KeyValuePair key="kubernetes.host" value="${k8s:host:-}"/>
-                <KeyValuePair key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
-                <KeyValuePair key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
-                <KeyValuePair key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
-                <KeyValuePair key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
-                <KeyValuePair key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
-                <KeyValuePair key="kubernetes.podID" value="${k8s:podId:-}"/>
-                <KeyValuePair key="kubernetes.podIP" value="${k8s:podIp:-}"/>
-                <KeyValuePair key="kubernetes.podName" value="${k8s:podName:-}"/>
-                <KeyValuePair key="kubernetes.imageId" value="${k8s:imageId:-}"/>
-                <KeyValuePair key="kubernetes.imageName" value="${k8s:imageName:-}"/>
+                <EventTemplateAdditionalField key="containerId" value="${docker:containerId:-}"/>
+                <EventTemplateAdditionalField key="application" value="$${lower:${spring:spring.application.name:-spring}}"/>
+                <EventTemplateAdditionalField key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.containerId" value="${k8s:containerId:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.containerName" value="${k8s:containerName:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.host" value="${k8s:host:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.podID" value="${k8s:podId:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.podIP" value="${k8s:podIp:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.podName" value="${k8s:podName:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.imageId" value="${k8s:imageId:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.imageName" value="${k8s:imageName:-}"/>
             </EventTemplateAdditionalFields>
         </JsonTemplateLayout>
     </Socket>