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:11 UTC

[logging-log4j2] branch master updated (a529d8d -> 1d47321)

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

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


    from a529d8d  #335 Initial import of JsonTemplateLayout from LogstashLayout.
     new 0dccbdb  #335 Replace inline resolver DSL with JSON structure.
     new 1d47321  #335 Skip unresolvable entries.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../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  |  298 +++---
 ...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 |   57 +-
 .../json/template/JsonTemplateLayoutTest.java      | 1071 +++++++++++---------
 .../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, 3217 insertions(+), 1556 deletions(-)
 delete mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java
 rename log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/{StackTraceTextResolver.java => StackTraceStringResolver.java} (93%)
 copy log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/{TemplateResolverFactory.java => TemplateResolverConfig.java} (79%)
 rename log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/{ContextDataResolver.java => ThreadContextDataResolver.java} (63%)
 rename log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/{ContextDataResolverFactory.java => ThreadContextDataResolverFactory.java} (61%)
 rename log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/{ContextStackResolver.java => ThreadContextStackResolver.java} (64%)
 copy log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/{TimestampResolverFactory.java => ThreadContextStackResolverFactory.java} (64%)
 create mode 100644 log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/MapAccessor.java


[logging-log4j2] 02/02: #335 Skip unresolvable entries.

Posted by vy...@apache.org.
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 1d4732133eae1bb19c55e044fe6274fabf0fcafb
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Sun Jun 7 22:23:45 2020 +0200

    #335 Skip unresolvable entries.
---
 .../json/template/resolver/TemplateResolvers.java  | 133 ++++++++++++++++-----
 .../log4j/layout/json/template/JsonLayoutTest.java |  45 ++-----
 .../json/template/JsonTemplateLayoutTest.java      |  61 ++++++++++
 3 files changed, 170 insertions(+), 69 deletions(-)

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 ca304b7..8bb11d6 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
@@ -30,21 +30,46 @@ public enum TemplateResolvers {;
 
     private static final String RESOLVER_FIELD_NAME = "$resolver";
 
+    private static abstract class UnresolvableTemplateResolver
+            implements TemplateResolver<Object> {
+
+        @Override
+        public final boolean isResolvable() {
+            return false;
+        }
+
+        @Override
+        public final boolean isResolvable(Object value) {
+            return false;
+        }
+
+    }
+
     private static final TemplateResolver<?> EMPTY_ARRAY_RESOLVER =
-            (final Object ignored, final JsonWriter jsonWriter) -> {
-                jsonWriter.writeArrayStart();
-                jsonWriter.writeArrayEnd();
+            new UnresolvableTemplateResolver() {
+                @Override
+                public void resolve(final Object value, final JsonWriter jsonWriter) {
+                    jsonWriter.writeArrayStart();
+                    jsonWriter.writeArrayEnd();
+                }
             };
 
     private static final TemplateResolver<?> EMPTY_OBJECT_RESOLVER =
-            (final Object ignored, final JsonWriter jsonWriter) -> {
-                jsonWriter.writeObjectStart();
-                jsonWriter.writeObjectEnd();
+            new UnresolvableTemplateResolver() {
+                @Override
+                public void resolve(final Object value, final JsonWriter jsonWriter) {
+                    jsonWriter.writeObjectStart();
+                    jsonWriter.writeObjectEnd();
+                }
             };
 
     private static final TemplateResolver<?> NULL_RESOLVER =
-            (final Object ignored, final JsonWriter jsonWriter) ->
+            new UnresolvableTemplateResolver() {
+                @Override
+                public void resolve(final Object value, final JsonWriter jsonWriter) {
                     jsonWriter.writeNull();
+                }
+            };
 
     public static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofTemplate(
             final C context,
@@ -236,36 +261,80 @@ public enum TemplateResolvers {;
                 })
                 .collect(Collectors.toList());
 
-        // Create a parent resolver collecting each object field resolver execution.
-        return (value, jsonWriter) -> {
-            final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
-            jsonWriter.writeObjectStart();
-            for (int resolvedFieldCount = 0, fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {
-                final TemplateResolver<V> fieldResolver = fieldResolvers.get(fieldIndex);
-                final boolean resolvable = fieldResolver.isResolvable(value);
-                if (!resolvable) {
-                    continue;
+        return new TemplateResolver<V>() {
+
+            /**
+             * The parent resolver checking if each child is resolvable.
+             *
+             * This is an optimization to skip the rendering of a parent if all
+             * its children are not resolvable.
+             */
+            @Override
+            public boolean isResolvable() {
+                for (int fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {
+                    final TemplateResolver<V> fieldResolver = fieldResolvers.get(fieldIndex);
+                    final boolean resolvable = fieldResolver.isResolvable();
+                    if (resolvable) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            /**
+             * The parent resolver checking if each child is resolvable given
+             * the passed {@code value}.
+             *
+             * This is an optimization to skip the rendering of a parent if all
+             * its children are not resolvable given the passed {@code value}.
+             */
+            @Override
+            public boolean isResolvable(final V value) {
+                for (int fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {
+                    final TemplateResolver<V> fieldResolver = fieldResolvers.get(fieldIndex);
+                    final boolean resolvable = fieldResolver.isResolvable(value);
+                    if (resolvable) {
+                        return true;
+                    }
                 }
-                final boolean succeedingEntry = resolvedFieldCount > 0;
-                final boolean flattening = fieldResolver.isFlattening();
-                if (flattening) {
-                    final int initLength = jsonWriterStringBuilder.length();
-                    fieldResolver.resolve(value, jsonWriter, succeedingEntry);
-                    final boolean resolved = jsonWriterStringBuilder.length() > initLength;
-                    if (resolved) {
-                        resolvedFieldCount++;
+                return false;
+            }
+
+            /**
+             * The parent resolver combining all child resolver executions.
+              */
+            @Override
+            public void resolve(final V value, final JsonWriter jsonWriter) {
+                final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+                jsonWriter.writeObjectStart();
+                for (int resolvedFieldCount = 0, fieldIndex = 0; fieldIndex < fieldCount; fieldIndex++) {
+                    final TemplateResolver<V> fieldResolver = fieldResolvers.get(fieldIndex);
+                    final boolean resolvable = fieldResolver.isResolvable(value);
+                    if (!resolvable) {
+                        continue;
                     }
-                } else {
-                    if (succeedingEntry) {
-                        jsonWriter.writeSeparator();
+                    final boolean succeedingEntry = resolvedFieldCount > 0;
+                    final boolean flattening = fieldResolver.isFlattening();
+                    if (flattening) {
+                        final int initLength = jsonWriterStringBuilder.length();
+                        fieldResolver.resolve(value, jsonWriter, succeedingEntry);
+                        final boolean resolved = jsonWriterStringBuilder.length() > initLength;
+                        if (resolved) {
+                            resolvedFieldCount++;
+                        }
+                    } else {
+                        if (succeedingEntry) {
+                            jsonWriter.writeSeparator();
+                        }
+                        final String fieldPrefix = fieldPrefixes.get(fieldIndex);
+                        jsonWriter.writeRawString(fieldPrefix);
+                        fieldResolver.resolve(value, jsonWriter, succeedingEntry);
+                        resolvedFieldCount++;
                     }
-                    final String fieldPrefix = fieldPrefixes.get(fieldIndex);
-                    jsonWriter.writeRawString(fieldPrefix);
-                    fieldResolver.resolve(value, jsonWriter, succeedingEntry);
-                    resolvedFieldCount++;
                 }
+                jsonWriter.writeObjectEnd();
             }
-            jsonWriter.writeObjectEnd();
+
         };
 
     }
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 ec8458e..6996ea5 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
@@ -8,11 +8,8 @@ import org.assertj.core.api.Assertions;
 import org.junit.Test;
 
 import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 
 import static org.apache.logging.log4j.layout.json.template.LayoutComparisonHelpers.renderUsing;
 
@@ -52,49 +49,23 @@ public class JsonLayoutTest {
     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) {
-        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,
         // to say the least, quite wrong, and I ain't gonna try to emulate
         // this behaviour in JsonTemplateLayout. Hence, discarding the "thrown"
         // field.
-        emptySourceExcludedMap.remove("thrown");
-        return emptySourceExcludedMap;
+        jsonTemplateLayoutMap.remove("thrown");
+        jsonLayoutMap.remove("thrown");
+        Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(jsonLayoutMap);
     }
 
-    private static Map<String, Object> renderUsingJsonLayout(
+    private static Map<String, Object> renderUsingJsonTemplateLayout(
             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
-        // this behaviour in JsonTemplateLayout. Hence, discarding the "thrown"
-        // field.
-        map.remove("thrown");
-        return map;
+        return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
     }
 
-    private static Map<String, Object> removeEmptyObject(
-            final Map<String, Object> root,
-            final String key) {
-        @SuppressWarnings("unchecked")
-        final Map<String, Object> source =
-                (Map<String, Object>) root.getOrDefault(
-                        key, Collections.emptyMap());
-        boolean emptySource = source
-                .values()
-                .stream()
-                .allMatch(Objects::isNull);
-        if (!emptySource) {
-            return root;
-        }
-        final Map<String, Object> trimmedRoot = new LinkedHashMap<>(root);
-        trimmedRoot.remove(key);
-        return trimmedRoot;
+    private static Map<String, Object> renderUsingJsonLayout(
+            final LogEvent logEvent) {
+        return renderUsing(logEvent, JSON_LAYOUT);
     }
 
 }
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 315e50f..38ef58f 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
@@ -1694,6 +1694,67 @@ public class JsonTemplateLayoutTest {
 
     }
 
+    @Test
+    public void test_unresolvable_nested_fields_are_skipped() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "exception", Map(
+                        "message", Map(
+                                "$resolver", "exception",
+                                "field", "message"),
+                        "className", Map(
+                                "$resolver", "exception",
+                                "field", "className")),
+                "exceptionRootCause", Map(
+                        "message", Map(
+                                "$resolver", "exceptionRootCause",
+                                "field", "message"),
+                        "className", Map(
+                                "$resolver", "exceptionRootCause",
+                                "field", "className")),
+                "source", Map(
+                        "lineNumber", Map(
+                                "$resolver", "source",
+                                "field", "lineNumber"),
+                        "fileName", Map(
+                                "$resolver", "source",
+                                "field", "fileName")),
+                "emptyMap", Collections.emptyMap(),
+                "emptyList", Collections.emptyList(),
+                "null", null));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .setStackTraceEnabled(false)        // Disable "exception" and "exceptionRootCause" resolvers.
+                .setLocationInfoEnabled(false)      // Disable the "source" resolver.
+                .build();
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("foo");
+        final Level level = Level.FATAL;
+        final Exception thrown = new RuntimeException("bar");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .setLevel(level)
+                .setThrown(thrown)
+                .build();
+
+        // Check the serialized event.
+        final String expectedSerializedLogEventJson =
+                "{}" + JsonTemplateLayoutDefaults.getEventDelimiter();
+        final String actualSerializedLogEventJson = layout.toSerializable(logEvent);
+        Assertions
+                .assertThat(actualSerializedLogEventJson)
+                .isEqualTo(expectedSerializedLogEventJson);
+
+    }
+
     private static String writeJson(final Object value) {
         final StringBuilder stringBuilder = JSON_WRITER.getStringBuilder();
         stringBuilder.setLength(0);


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

Posted by vy...@apache.org.
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>