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/05/25 20:42:57 UTC

[logging-log4j2] branch master updated: #335 Initial import of JsonTemplateLayout from LogstashLayout.

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


The following commit(s) were added to refs/heads/master by this push:
     new a529d8d  #335 Initial import of JsonTemplateLayout from LogstashLayout.
a529d8d is described below

commit a529d8dcf6397c9faafb54b2f65ee8694b7454f8
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Mon May 25 22:42:48 2020 +0200

    #335 Initial import of JsonTemplateLayout from LogstashLayout.
---
 .../org/apache/logging/log4j/util/Strings.java     |   36 +-
 .../org/apache/logging/log4j/util/StringsTest.java |   19 +
 log4j-bom/pom.xml                                  |    6 +
 .../apache/logging/log4j/core/util/Throwables.java |   22 +-
 .../logging/log4j/core/GcFreeLoggingTestUtil.java  |   70 +-
 .../logging/log4j/core/util/ThrowablesTest.java    |   37 +-
 log4j-layout-json-template/pom.xml                 |  552 +++
 .../layout/json/template/JsonTemplateLayout.java   |  543 +++
 .../json/template/JsonTemplateLayoutDefaults.java  |  204 ++
 .../template/resolver/ContextDataResolver.java     |  303 ++
 .../resolver/ContextDataResolverFactory.java       |   39 +
 .../template/resolver/ContextStackResolver.java    |  102 +
 .../resolver/ContextStackResolverFactory.java      |   39 +
 .../json/template/resolver/EndOfBatchResolver.java |   44 +
 .../resolver/EndOfBatchResolverFactory.java        |   39 +
 .../json/template/resolver/EventResolver.java      |   21 +
 .../template/resolver/EventResolverContext.java    |  226 ++
 .../template/resolver/EventResolverFactories.java  |   65 +
 .../template/resolver/EventResolverFactory.java    |   21 +
 .../resolver/ExceptionInternalResolverFactory.java |   71 +
 .../json/template/resolver/ExceptionResolver.java  |  111 +
 .../resolver/ExceptionResolverFactory.java         |   39 +
 .../resolver/ExceptionRootCauseResolver.java       |  116 +
 .../ExceptionRootCauseResolverFactory.java         |   39 +
 .../json/template/resolver/LevelResolver.java      |  115 +
 .../template/resolver/LevelResolverFactory.java    |   39 +
 .../json/template/resolver/LoggerResolver.java     |   61 +
 .../template/resolver/LoggerResolverFactory.java   |   39 +
 .../json/template/resolver/MainMapResolver.java    |   45 +
 .../template/resolver/MainMapResolverFactory.java  |   39 +
 .../layout/json/template/resolver/MapResolver.java |   62 +
 .../json/template/resolver/MapResolverFactory.java |   39 +
 .../json/template/resolver/MarkerResolver.java     |   64 +
 .../template/resolver/MarkerResolverFactory.java   |   39 +
 .../json/template/resolver/MessageResolver.java    |  171 +
 .../template/resolver/MessageResolverFactory.java  |   39 +
 .../json/template/resolver/PatternResolver.java    |   50 +
 .../template/resolver/PatternResolverFactory.java  |   39 +
 .../json/template/resolver/SourceResolver.java     |  117 +
 .../template/resolver/SourceResolverFactory.java   |   39 +
 .../resolver/StackTraceElementObjectResolver.java  |   66 +
 .../StackTraceElementObjectResolverContext.java    |   93 +
 .../StackTraceElementObjectResolverFactories.java  |   39 +
 .../StackTraceElementObjectResolverFactory.java    |   43 +
 .../resolver/StackTraceObjectResolver.java         |   54 +
 .../json/template/resolver/StackTraceResolver.java |   19 +
 .../template/resolver/StackTraceTextResolver.java  |   51 +
 .../json/template/resolver/TemplateResolver.java   |   42 +
 .../template/resolver/TemplateResolverContext.java |   34 +
 .../template/resolver/TemplateResolverFactory.java |   25 +
 .../json/template/resolver/TemplateResolvers.java  |  355 ++
 .../json/template/resolver/ThreadResolver.java     |   68 +
 .../template/resolver/ThreadResolverFactory.java   |   39 +
 .../json/template/resolver/TimestampResolver.java  |  446 +++
 .../resolver/TimestampResolverFactory.java         |   41 +
 .../layout/json/template/util/DummyRecycler.java   |   37 +
 .../json/template/util/DummyRecyclerFactory.java   |   39 +
 .../layout/json/template/util/JsonReader.java      |  447 +++
 .../layout/json/template/util/JsonWriter.java      |  889 +++++
 .../json/template/util/QueueingRecycler.java       |   61 +
 .../template/util/QueueingRecyclerFactory.java     |   40 +
 .../log4j/layout/json/template/util/Recycler.java  |   25 +
 .../json/template/util/RecyclerFactories.java      |  205 ++
 .../layout/json/template/util/RecyclerFactory.java |   31 +
 .../json/template/util/StringParameterParser.java  |  292 ++
 .../json/template/util/ThreadLocalRecycler.java    |   45 +
 .../template/util/ThreadLocalRecyclerFactory.java  |   40 +
 .../util/TruncatingBufferedPrintWriter.java        |   60 +
 .../template/util/TruncatingBufferedWriter.java    |  208 ++
 .../log4j/layout/json/template/util/Uris.java      |  126 +
 .../src/main/resources/EcsLayout.json              |   12 +
 .../src/main/resources/GelfLayout.json             |   11 +
 .../src/main/resources/JsonLayout.json             |   26 +
 .../main/resources/LogstashJsonEventLayoutV1.json  |   19 +
 .../main/resources/StackTraceElementLayout.json    |    6 +
 .../src/site/manual/index.md                       |   32 +
 log4j-layout-json-template/src/site/site.xml       |   55 +
 .../template/BlackHoleByteBufferDestination.java   |   50 +
 .../log4j/layout/json/template/EcsLayoutTest.java  |   80 +
 .../log4j/layout/json/template/GelfLayoutTest.java |  102 +
 .../log4j/layout/json/template/JacksonFixture.java |   36 +
 .../log4j/layout/json/template/JsonLayoutTest.java |  102 +
 .../JsonTemplateLayoutConcurrentEncodeTest.java    |  192 ++
 .../template/JsonTemplateLayoutGcFreeTest.java     |   40 +
 .../json/template/JsonTemplateLayoutTest.java      | 1653 +++++++++
 .../json/template/LayoutComparisonHelpers.java     |   19 +
 .../layout/json/template/LogEventFixture.java      |  151 +
 .../log4j/layout/json/template/LogstashIT.java     |  485 +++
 .../layout/json/template/util/JsonReaderTest.java  |  380 +++
 .../layout/json/template/util/JsonWriterTest.java  |  729 ++++
 .../json/template/util/RecyclerFactoriesTest.java  |  120 +
 .../template/util/StringParameterParserTest.java   |  393 +++
 .../util/TruncatingBufferedWriterTest.java         |  228 ++
 .../log4j/layout/json/template/util/UrisTest.java  |   64 +
 .../resources/gcFreeJsonTemplateLayoutLogging.xml  |   22 +
 .../src/test/resources/testJsonTemplateLayout.json |   21 +
 log4j-perf/pom.xml                                 |   20 +
 .../json/template/JsonTemplateLayoutBenchmark.java |  185 ++
 .../JsonTemplateLayoutBenchmarkReport.java         |  359 ++
 .../template/JsonTemplateLayoutBenchmarkState.java |  201 ++
 .../log4j/perf/jmh/ThreadLocalVsPoolBenchmark.java |  252 +-
 pom.xml                                            | 3498 ++++++++++----------
 src/site/asciidoc/manual/garbagefree.adoc          |    5 +
 src/site/asciidoc/manual/json-template-layout.adoc |  659 ++++
 src/site/asciidoc/manual/layouts.adoc              |   68 +-
 src/site/markdown/manual/cloud.md                  |   97 +-
 src/site/site.xml                                  |    1 +
 107 files changed, 16135 insertions(+), 1959 deletions(-)

diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
index ea06dff..be50ab8 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
@@ -55,14 +55,23 @@ public final class Strings {
     }
 
     /**
-     * Checks if a String is blank. A blank string is one that is {@code null}, empty, or when trimmed using
-     * {@link String#trim()} is empty.
+     * Checks if a String is blank. A blank string is one that is either
+     * {@code null}, empty, or all characters are {@link Character#isWhitespace(char)}.
      *
      * @param s the String to check, may be {@code null}
-     * @return {@code true} if the String is {@code null}, empty, or trims to empty.
+     * @return {@code true} if the String is {@code null}, empty, or or all characters are {@link Character#isWhitespace(char)}
      */
     public static boolean isBlank(final String s) {
-        return s == null || s.trim().isEmpty();
+        if (s == null || s.isEmpty()) {
+            return true;
+        }
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (!Character.isWhitespace(c)) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
@@ -286,4 +295,23 @@ public final class Strings {
         return buf.toString();
     }
 
+    /**
+     * Creates a new string repeating given {@code str} {@code count} times.
+     * @param str input string
+     * @param count the repetition count
+     * @return the new string
+     * @throws IllegalArgumentException if either {@code str} is null or {@code count} is negative
+     */
+    public static String repeat(final String str, final int count) {
+        Objects.requireNonNull(str, "str");
+        if (count < 0) {
+            throw new IllegalArgumentException("count");
+        }
+        StringBuilder sb = new StringBuilder(str.length() * count);
+        for (int index = 0; index < count; index++) {
+            sb.append(str);
+        }
+        return sb.toString();
+    }
+
 }
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
index 162d162..e91e298 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
@@ -25,6 +25,25 @@ import java.util.Iterator;
 
 public class StringsTest {
 
+    @Test
+    public void testIsEmpty() {
+        Assert.assertTrue(Strings.isEmpty(null));
+        Assert.assertTrue(Strings.isEmpty(""));
+        Assert.assertFalse(Strings.isEmpty(" "));
+        Assert.assertFalse(Strings.isEmpty("a"));
+    }
+
+    @Test
+    public void testIsBlank() {
+        Assert.assertTrue(Strings.isBlank(null));
+        Assert.assertTrue(Strings.isBlank(""));
+        Assert.assertTrue(Strings.isBlank(" "));
+        Assert.assertTrue(Strings.isBlank("\n"));
+        Assert.assertTrue(Strings.isBlank("\r"));
+        Assert.assertTrue(Strings.isBlank("\t"));
+        Assert.assertFalse(Strings.isEmpty("a"));
+    }
+
     /**
      * A sanity test to make sure a typo does not mess up {@link Strings#EMPTY}.
      */
diff --git a/log4j-bom/pom.xml b/log4j-bom/pom.xml
index ea845e5..0f21c78 100644
--- a/log4j-bom/pom.xml
+++ b/log4j-bom/pom.xml
@@ -72,6 +72,12 @@
         <artifactId>log4j-layout-jackson-yaml</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <!-- JSON template layout -->
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-layout-json-template</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <!-- Legacy Log4j 1.2 API -->
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
index 0d56ef1..e6c758e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Throwables.java
@@ -40,12 +40,26 @@ public final class Throwables {
      * @return the deepest throwable or the given throwable
      */
     public static Throwable getRootCause(final Throwable throwable) {
+
+        // Keep a second pointer that slowly walks the causal chain. If the fast
+        // pointer ever catches the slower pointer, then there's a loop.
+        Throwable slowPointer = throwable;
+        boolean advanceSlowPointer = false;
+
+        Throwable parent = throwable;
         Throwable cause;
-        Throwable root = throwable;
-        while ((cause = root.getCause()) != null) {
-            root = cause;
+        while ((cause = parent.getCause()) != null) {
+            parent = cause;
+            if (parent == slowPointer) {
+                throw new IllegalArgumentException("loop in causal chain");
+            }
+            if (advanceSlowPointer) {
+                slowPointer = slowPointer.getCause();
+            }
+            advanceSlowPointer = !advanceSlowPointer; // only advance every other iteration
         }
-        return root;
+        return parent;
+
     }
 
     /**
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java
index 46efe64..938234f 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java
@@ -16,35 +16,33 @@
  */
 package org.apache.logging.log4j.core;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.net.URL;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
+import com.google.monitoring.runtime.instrumentation.AllocationRecorder;
+import com.google.monitoring.runtime.instrumentation.Sampler;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.MarkerManager;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.message.StringMapMessage;
-import org.apache.logging.log4j.util.Strings;
 
-import com.google.monitoring.runtime.instrumentation.AllocationRecorder;
-import com.google.monitoring.runtime.instrumentation.Sampler;
+import java.io.File;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 /**
- * Utily methods for the GC-free logging tests.s.
+ * Utility methods for the GC-free logging tests.
  */
-public class GcFreeLoggingTestUtil {
+public enum GcFreeLoggingTestUtil {;
 
     public static void executeLogging(final String configurationFile,
-            final Class<?> testClass) throws Exception {
+                                      final Class<?> testClass) throws Exception {
 
         System.setProperty("log4j2.enable.threadlocals", "true");
         System.setProperty("log4j2.enable.direct.encoders", "true");
@@ -148,16 +146,36 @@ public class GcFreeLoggingTestUtil {
         process.waitFor();
         process.exitValue();
 
-        final String text = new String(Files.readAllBytes(tempFile.toPath()));
-        final List<String> lines = Files.readAllLines(tempFile.toPath(), Charset.defaultCharset());
-        final String className = cls.getSimpleName();
-        assertEquals(text, "FATAL o.a.l.l.c." + className + " [main] value1 {aKey=value1, key2=value2, prop1=value1, prop2=value2} This message is logged to the console",
-                lines.get(0));
+        final AtomicInteger lineCounter = new AtomicInteger(0);
+        Files.lines(tempFile.toPath(), Charset.defaultCharset()).forEach(line -> {
+
+            // Trim the line.
+            line = line.trim();
+
+            // Check the first line.
+            final int lineNumber = lineCounter.incrementAndGet();
+            if (lineNumber == 1) {
+                final String className = cls.getSimpleName();
+                final String firstLinePattern = String.format(
+                        "^FATAL .*\\.%s %s",
+                        className,
+                        Pattern.quote("[main] value1 {aKey=value1, " +
+                                "key2=value2, prop1=value1, prop2=value2} " +
+                                "This message is logged to the console"));
+                assertTrue(
+                        "pattern mismatch at line 1: " + line,
+                        line.matches(firstLinePattern));
+            }
+
+            // Check the rest of the lines.
+            else {
+                assertFalse(
+                        "(allocated|array) pattern matches at line " + lineNumber + ": " + line,
+                        line.contains("allocated") || line.contains("array"));
+            }
+
+        });
 
-        for (int i = 1; i < lines.size(); i++) {
-            final String line = lines.get(i);
-            assertFalse(i + ": " + line + Strings.LINE_SEPARATOR + text, line.contains("allocated") || line.contains("array"));
-        }
     }
 
     private static File agentJar() throws Exception {
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
index e8f82f5..7e354bc 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ThrowablesTest.java
@@ -16,41 +16,54 @@
  */
 package org.apache.logging.log4j.core.util;
 
+import org.junit.Assert;
 import org.junit.Test;
 
 public class ThrowablesTest {
 
     @Test
-    public void testGetRootCauseNone() throws Exception {
+    public void testGetRootCauseNone() {
         final NullPointerException throwable = new NullPointerException();
-        org.junit.Assert.assertEquals(throwable, Throwables.getRootCause(throwable));
+        Assert.assertEquals(throwable, Throwables.getRootCause(throwable));
     }
 
     @Test
-    public void testGetRootCauseDepth1() throws Exception {
-        final NullPointerException throwable = new NullPointerException();
-        org.junit.Assert.assertEquals(throwable, Throwables.getRootCause(new UnsupportedOperationException(throwable)));
+    public void testGetRootCauseDepth1() {
+        final Throwable cause = new NullPointerException();
+        final Throwable error = new UnsupportedOperationException(cause);
+        Assert.assertEquals(cause, Throwables.getRootCause(error));
     }
 
     @Test
-    public void testGetRootCauseDepth2() throws Exception {
-        final NullPointerException throwable = new NullPointerException();
-        org.junit.Assert.assertEquals(throwable,
-                Throwables.getRootCause(new IllegalArgumentException(new UnsupportedOperationException(throwable))));
+    public void testGetRootCauseDepth2() {
+        final Throwable rootCause = new NullPointerException();
+        final Throwable cause = new UnsupportedOperationException(rootCause);
+        final Throwable error = new IllegalArgumentException(cause);
+        Assert.assertEquals(rootCause, Throwables.getRootCause(error));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testGetRootCauseLoop() {
+        final Throwable cause1 = new RuntimeException();
+        final Throwable cause2 = new RuntimeException(cause1);
+        final Throwable cause3 = new RuntimeException(cause2);
+        cause1.initCause(cause3);
+        // noinspection ThrowableNotThrown
+        Throwables.getRootCause(cause3);
     }
 
     @Test(expected = NullPointerException.class)
-    public void testRethrowRuntimeException() throws Exception {
+    public void testRethrowRuntimeException() {
         Throwables.rethrow(new NullPointerException());
     }
 
     @Test(expected = UnknownError.class)
-    public void testRethrowError() throws Exception {
+    public void testRethrowError() {
         Throwables.rethrow(new UnknownError());
     }
 
     @Test(expected = NoSuchMethodException.class)
-    public void testRethrowCheckedException() throws Exception {
+    public void testRethrowCheckedException() {
         Throwables.rethrow(new NoSuchMethodException());
     }
 }
diff --git a/log4j-layout-json-template/pom.xml b/log4j-layout-json-template/pom.xml
new file mode 100644
index 0000000..f9e74d6
--- /dev/null
+++ b/log4j-layout-json-template/pom.xml
@@ -0,0 +1,552 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j</artifactId>
+    <version>3.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>log4j-layout-json-template</artifactId>
+  <name>Apache Log4j Layout for JSON template</name>
+  <description>
+    Apache Log4j Layout for JSON template.
+  </description>
+
+  <properties>
+    <log4jParentDir>${basedir}/..</log4jParentDir>
+    <docLabel>Log4j Layout for JSON Template Documentation</docLabel>
+    <projectDir>/log4j-layout-json-template</projectDir>
+    <module.name>org.apache.logging.log4j.layout.json.template</module.name>
+  </properties>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-layout-jackson-json</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.jctools</groupId>
+      <artifactId>jctools-core</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>co.elastic.logging</groupId>
+      <artifactId>log4j2-ecs-layout</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.code.java-allocation-instrumenter</groupId>
+      <artifactId>java-allocation-instrumenter</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.elasticsearch.client</groupId>
+      <artifactId>elasticsearch-rest-high-level-client</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.awaitility</groupId>
+      <artifactId>awaitility</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+
+  <build>
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <configuration>
+          <instructions>
+            <Fragment-Host>org.apache.logging.log4j.layout.json.template</Fragment-Host>
+            <Export-Package>*</Export-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>default-jar</id>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+            <configuration combine.self="override">
+              <archive>
+                <manifestFile>${manifestfile}</manifestFile>
+                <manifestEntries>
+                  <Specification-Title>${project.name}</Specification-Title>
+                  <Specification-Version>${project.version}</Specification-Version>
+                  <Specification-Vendor>${project.organization.name}</Specification-Vendor>
+                  <Implementation-Title>${project.name}</Implementation-Title>
+                  <Implementation-Version>${project.version}</Implementation-Version>
+                  <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                  <Implementation-Vendor-Id>org.apache</Implementation-Vendor-Id>
+                  <X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
+                  <X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
+                  <Multi-Release>true</Multi-Release>
+                </manifestEntries>
+              </archive>
+            </configuration>
+          </execution>
+          <execution>
+            <id>default</id>
+            <goals>
+              <goal>test-jar</goal>
+            </goals>
+            <configuration>
+              <archive>
+                <manifestFile>${manifestfile}</manifestFile>
+                <manifestEntries>
+                  <Specification-Title>${project.name}</Specification-Title>
+                  <Specification-Version>${project.version}</Specification-Version>
+                  <Specification-Vendor>${project.organization.name}</Specification-Vendor>
+                  <Implementation-Title>${project.name}</Implementation-Title>
+                  <Implementation-Version>${project.version}</Implementation-Version>
+                  <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                  <Implementation-Vendor-Id>org.apache</Implementation-Vendor-Id>
+                  <X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
+                  <X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
+                </manifestEntries>
+              </archive>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <skip>${maven.test.skip}</skip>
+          <excludes>
+            <exclude>**/JsonTemplateLayoutConcurrentEncodeTest.java</exclude>
+            <exclude>**/JsonTemplateLayoutTest.java</exclude>
+          </excludes>
+          <!-- Enforcing a non-UTF-8 encoding to check that the layout
+               indeed handles everything in UTF-8 without implicitly
+               relying on the system defaults. -->
+          <argLine>-Dfile.encoding=US-ASCII</argLine>
+        </configuration>
+        <executions>
+          <!-- Dummy recycler execution -->
+          <execution>
+            <id>recycler-dummy</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <systemPropertyVariables>
+                <log4j2.layout.jsonTemplate.recyclerFactory>threadLocal</log4j2.layout.jsonTemplate.recyclerFactory>
+              </systemPropertyVariables>
+              <includes>
+                <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+                <include>**/JsonTemplateLayoutTest.java</include>
+              </includes>
+            </configuration>
+          </execution>
+          <!-- Thread-Local recycler execution -->
+          <execution>
+            <id>recycler-tl</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <systemPropertyVariables>
+                <log4j2.layout.jsonTemplate.recyclerFactory>threadLocal</log4j2.layout.jsonTemplate.recyclerFactory>
+              </systemPropertyVariables>
+              <includes>
+                <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+                <include>**/JsonTemplateLayoutTest.java</include>
+              </includes>
+            </configuration>
+          </execution>
+          <!-- ArrayBlockingQueue recycler execution -->
+          <execution>
+            <id>recycler-abq</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <systemPropertyVariables>
+                <log4j2.layout.jsonTemplate.recyclerFactory>queue:supplier=java.util.concurrent.ArrayBlockingQueue.new</log4j2.layout.jsonTemplate.recyclerFactory>
+              </systemPropertyVariables>
+              <includes>
+                <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+                <include>**/JsonTemplateLayoutTest.java</include>
+              </includes>
+            </configuration>
+          </execution>
+          <!-- MpmcArrayQueue recycler execution -->
+          <execution>
+            <id>recycler-mpmc</id>
+            <goals>
+              <goal>test</goal>
+            </goals>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <systemPropertyVariables>
+                <log4j2.layout.jsonTemplate.recyclerFactory>queue:supplier=org.jctools.queues.MpmcArrayQueue.new</log4j2.layout.jsonTemplate.recyclerFactory>
+              </systemPropertyVariables>
+              <includes>
+                <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+                <include>**/JsonTemplateLayoutTest.java</include>
+              </includes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <!-- Disable ITs, which are Docker-dependent, by default. -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>docker</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <build>
+        <plugins>
+
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+              </execution>
+            </executions>
+            <configuration>
+              <skip>${skipTests}</skip>
+              <includes>
+                <include>**/*IT.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
+            <groupId>io.fabric8</groupId>
+            <artifactId>docker-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>start</id>
+                <phase>pre-integration-test</phase>
+                <goals>
+                  <goal>start</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>stop</id>
+                <phase>post-integration-test</phase>
+                <goals>
+                  <goal>stop</goal>
+                </goals>
+              </execution>
+            </executions>
+            <configuration>
+              <verbose>all</verbose>
+              <startParallel>true</startParallel>
+              <autoCreateCustomNetworks>true</autoCreateCustomNetworks>
+              <images>
+                <image>
+                  <alias>elasticsearch</alias>
+                  <name>elasticsearch:${elastic.version}</name>
+                  <run>
+                    <env>
+                      <discovery.type>single-node</discovery.type>
+                    </env>
+                    <ports>
+                      <port>9200:9200</port>
+                    </ports>
+                    <network>
+                      <mode>custom</mode>
+                      <name>log4j-layout-json-template-network</name>
+                      <alias>elasticsearch</alias>
+                    </network>
+                    <log>
+                      <prefix>[ES]</prefix>
+                      <color>cyan</color>
+                    </log>
+                    <wait>
+                      <log>recovered \[0\] indices into cluster_state</log>
+                      <time>60000</time>
+                    </wait>
+                  </run>
+                </image>
+                <image>
+                  <alias>logstash</alias>
+                  <name>logstash:${elastic.version}</name>
+                  <run>
+                    <dependsOn>
+                      <container>elasticsearch</container>
+                    </dependsOn>
+                    <network>
+                      <mode>custom</mode>
+                      <name>log4j-layout-json-template-network</name>
+                      <alias>logstash</alias>
+                    </network>
+                    <ports>
+                      <port>12222:12222</port>
+                      <port>12345:12345</port>
+                    </ports>
+                    <log>
+                      <prefix>[LS]</prefix>
+                      <color>green</color>
+                    </log>
+                    <entrypoint>
+                      <exec>
+                        <arg>logstash</arg>
+                        <arg>--pipeline.batch.size</arg>
+                        <arg>1</arg>
+                        <arg>-e</arg>
+                        <arg>
+                          input {
+                            gelf {
+                              host => "logstash"
+                              use_tcp => true
+                              use_udp => false
+                              port => 12222
+                              type => "gelf"
+                            }
+                            tcp {
+                              port => 12345
+                              codec => json
+                              type => "tcp"
+                            }
+                          }
+
+                          filter {
+                            if [type] == "gelf" {
+                              # These are GELF/Syslog logging levels as defined in RFC 3164.
+                              # Map the integer level to its human readable format.
+                              translate {
+                                field => "[level]"
+                                destination => "[levelName]"
+                                dictionary => {
+                                  "0" => "EMERG"
+                                  "1" => "ALERT"
+                                  "2" => "CRITICAL"
+                                  "3" => "ERROR"
+                                  "4" => "WARN"
+                                  "5" => "NOTICE"
+                                  "6" => "INFO"
+                                  "7" => "DEBUG"
+                                }
+                              }
+                            }
+                          }
+
+                          output {
+                            # (Un)comment for debugging purposes
+                            # stdout { codec => rubydebug }
+                            elasticsearch {
+                              hosts => ["http://elasticsearch:9200"]
+                              index => "log4j"
+                            }
+                          }
+                        </arg>
+                      </exec>
+                    </entrypoint>
+                    <wait>
+                      <log>Successfully started Logstash API endpoint</log>
+                      <time>60000</time>
+                    </wait>
+                  </run>
+                </image>
+              </images>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+  <reporting>
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-changes-plugin</artifactId>
+        <version>${changes.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>changes-report</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+        <configuration>
+          <issueLinkTemplate>%URL%/show_bug.cgi?id=%ISSUE%</issueLinkTemplate>
+          <useJql>true</useJql>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <version>${checkstyle.plugin.version}</version>
+        <configuration>
+          <!--<propertiesLocation>${vfs.parent.dir}/checkstyle.properties</propertiesLocation> -->
+          <configLocation>${log4jParentDir}/checkstyle.xml</configLocation>
+          <suppressionsLocation>${log4jParentDir}/checkstyle-suppressions.xml</suppressionsLocation>
+          <enableRulesSummary>false</enableRulesSummary>
+          <propertyExpansion>basedir=${basedir}</propertyExpansion>
+          <propertyExpansion>licensedir=${log4jParentDir}/checkstyle-header.txt</propertyExpansion>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <version>${javadoc.plugin.version}</version>
+        <configuration>
+          <bottom><![CDATA[<p align="center">Copyright &#169; {inceptionYear}-{currentYear} {organizationName}. All Rights Reserved.<br />
+            Apache Logging, Apache Log4j, Log4j, Apache, the Apache feather logo, the Apache Logging project logo,
+            and the Apache Log4j logo are trademarks of The Apache Software Foundation.</p>]]></bottom>
+          <!-- module link generation is completely broken in the javadoc plugin for a multi-module non-aggregating project -->
+          <detectOfflineLinks>false</detectOfflineLinks>
+          <linksource>true</linksource>
+        </configuration>
+        <reportSets>
+          <reportSet>
+            <id>non-aggregate</id>
+            <reports>
+              <report>javadoc</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+
+      <plugin>
+        <groupId>com.github.spotbugs</groupId>
+        <artifactId>spotbugs-maven-plugin</artifactId>
+        <configuration>
+          <fork>true</fork>
+          <jvmArgs>-Duser.language=en</jvmArgs>
+          <threshold>Normal</threshold>
+          <effort>Default</effort>
+          <excludeFilterFile>${log4jParentDir}/spotbugs-exclude-filter.xml</excludeFilterFile>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jxr-plugin</artifactId>
+        <version>${jxr.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <id>non-aggregate</id>
+            <reports>
+              <report>jxr</report>
+            </reports>
+          </reportSet>
+          <reportSet>
+            <id>aggregate</id>
+            <reports>
+              <report>aggregate</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <version>${pmd.plugin.version}</version>
+        <configuration>
+          <targetJdk>${maven.compiler.target}</targetJdk>
+        </configuration>
+      </plugin>
+
+    </plugins>
+  </reporting>
+
+</project>
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
new file mode 100644
index 0000000..557741d
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
@@ -0,0 +1,543 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.StringLayout;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
+import org.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+import org.apache.logging.log4j.core.layout.Encoder;
+import org.apache.logging.log4j.core.layout.LockingStringBuilderEncoder;
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+import org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.core.util.StringEncoder;
+import org.apache.logging.log4j.layout.json.template.resolver.EventResolverContext;
+import org.apache.logging.log4j.layout.json.template.resolver.StackTraceElementObjectResolverContext;
+import org.apache.logging.log4j.layout.json.template.resolver.TemplateResolver;
+import org.apache.logging.log4j.layout.json.template.resolver.TemplateResolvers;
+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.RecyclerFactory;
+import org.apache.logging.log4j.layout.json.template.util.Uris;
+import org.apache.logging.log4j.plugins.Node;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.util.Strings;
+
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+@Plugin(name = "JsonTemplateLayout",
+        category = Node.CATEGORY,
+        elementType = Layout.ELEMENT_TYPE,
+        printObject = true)
+public class JsonTemplateLayout implements StringLayout {
+
+    private static final Map<String, String> CONTENT_FORMAT =
+            Collections.singletonMap("version", "1");
+
+    private final Charset charset;
+
+    private final String contentType;
+
+    private final TemplateResolver<LogEvent> eventResolver;
+
+    private final String eventDelimiter;
+
+    private final Recycler<Context> contextRecycler;
+
+    // The class and fields are visible for tests.
+    static final class Context implements AutoCloseable {
+
+        final JsonWriter jsonWriter;
+
+        final Encoder<StringBuilder> encoder;
+
+        private Context(
+                final JsonWriter jsonWriter,
+                final Encoder<StringBuilder> encoder) {
+            this.jsonWriter = jsonWriter;
+            this.encoder = encoder;
+        }
+
+        @Override
+        public void close() {
+            jsonWriter.close();
+        }
+
+    }
+
+    private JsonTemplateLayout(final Builder builder) {
+        this.charset = builder.charset;
+        this.contentType = "application/json; charset=" + charset;
+        this.eventDelimiter = builder.eventDelimiter;
+        final Configuration configuration = builder.configuration;
+        final StrSubstitutor substitutor = configuration.getStrSubstitutor();
+        final JsonWriter jsonWriter = JsonWriter
+                .newBuilder()
+                .setMaxStringLength(builder.maxStringLength)
+                .setTruncatedStringSuffix(builder.truncatedStringSuffix)
+                .build();
+        final TemplateResolver<StackTraceElement> stackTraceElementObjectResolver =
+                builder.stackTraceEnabled
+                        ? createStackTraceElementResolver(builder, substitutor, jsonWriter)
+                        : null;
+        this.eventResolver = createEventResolver(
+                builder,
+                configuration,
+                substitutor,
+                charset,
+                jsonWriter,
+                stackTraceElementObjectResolver);
+        this.contextRecycler = createContextRecycler(builder, jsonWriter);
+    }
+
+    private static TemplateResolver<StackTraceElement> createStackTraceElementResolver(
+            final Builder builder,
+            final StrSubstitutor substitutor,
+            final JsonWriter jsonWriter) {
+        final StackTraceElementObjectResolverContext stackTraceElementObjectResolverContext =
+                StackTraceElementObjectResolverContext
+                        .newBuilder()
+                        .setSubstitutor(substitutor)
+                        .setJsonWriter(jsonWriter)
+                        .build();
+        final String stackTraceElementTemplate = readStackTraceElementTemplate(builder);
+        return TemplateResolvers.ofTemplate(stackTraceElementObjectResolverContext, stackTraceElementTemplate);
+    }
+
+    private TemplateResolver<LogEvent> createEventResolver(
+            final Builder builder,
+            final Configuration configuration,
+            final StrSubstitutor substitutor,
+            final Charset charset,
+            final JsonWriter jsonWriter,
+            final TemplateResolver<StackTraceElement> stackTraceElementObjectResolver) {
+        final String eventTemplate = readEventTemplate(builder);
+        final float maxByteCountPerChar = builder.charset.newEncoder().maxBytesPerChar();
+        final int maxStringByteCount =
+                Math.toIntExact(Math.round(
+                        maxByteCountPerChar * builder.maxStringLength));
+        final EventResolverContext resolverContext = EventResolverContext
+                .newBuilder()
+                .setConfiguration(configuration)
+                .setSubstitutor(substitutor)
+                .setCharset(charset)
+                .setJsonWriter(jsonWriter)
+                .setRecyclerFactory(builder.recyclerFactory)
+                .setMaxStringByteCount(maxStringByteCount)
+                .setLocationInfoEnabled(builder.locationInfoEnabled)
+                .setStackTraceEnabled(builder.stackTraceEnabled)
+                .setStackTraceElementObjectResolver(stackTraceElementObjectResolver)
+                .setEventTemplateAdditionalFields(builder.eventTemplateAdditionalFields.additionalFields)
+                .build();
+        return TemplateResolvers.ofTemplate(resolverContext, eventTemplate);
+    }
+
+    private static String readEventTemplate(final Builder builder) {
+        return readTemplate(
+                builder.eventTemplate,
+                builder.eventTemplateUri,
+                builder.charset);
+    }
+
+    private static String readStackTraceElementTemplate(final Builder builder) {
+        return readTemplate(
+                builder.stackTraceElementTemplate,
+                builder.stackTraceElementTemplateUri,
+                builder.charset);
+    }
+
+    private static String readTemplate(
+            final String template,
+            final String templateUri,
+            final Charset charset) {
+        return Strings.isBlank(template)
+                ? Uris.readUri(templateUri, charset)
+                : template;
+    }
+
+    private static Recycler<Context> createContextRecycler(
+            final Builder builder,
+            final JsonWriter jsonWriter) {
+        final Supplier<Context> supplier =
+                createContextSupplier(builder.charset, jsonWriter);
+        return builder
+                .recyclerFactory
+                .create(supplier, Context::close);
+    }
+
+    private static Supplier<Context> createContextSupplier(
+            final Charset charset,
+            final JsonWriter jsonWriter) {
+        return () -> {
+            final JsonWriter clonedJsonWriter = jsonWriter.clone();
+            final Encoder<StringBuilder> encoder =
+                    Constants.ENABLE_DIRECT_ENCODERS
+                            ? new LockingStringBuilderEncoder(charset)
+                            : null;
+            return new Context(clonedJsonWriter, encoder);
+        };
+    }
+
+    @Override
+    public byte[] toByteArray(final LogEvent event) {
+        final String eventJson = toSerializable(event);
+        return StringEncoder.toBytes(eventJson, charset);
+    }
+
+    @Override
+    public String toSerializable(final LogEvent event) {
+        final Context context = acquireContext();
+        final JsonWriter jsonWriter = context.jsonWriter;
+        final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
+        try {
+            eventResolver.resolve(event, jsonWriter);
+            stringBuilder.append(eventDelimiter);
+            return stringBuilder.toString();
+        } finally {
+            contextRecycler.release(context);
+        }
+    }
+
+    @Override
+    public void encode(final LogEvent event, final ByteBufferDestination destination) {
+
+        // Acquire a context.
+        final Context context = acquireContext();
+        final JsonWriter jsonWriter = context.jsonWriter;
+        final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
+        final Encoder<StringBuilder> encoder = context.encoder;
+
+        try {
+
+            // Render the JSON.
+            eventResolver.resolve(event, jsonWriter);
+            stringBuilder.append(eventDelimiter);
+
+            // Write to the destination.
+            if (encoder == null) {
+                final String eventJson = stringBuilder.toString();
+                final byte[] eventJsonBytes = StringEncoder.toBytes(eventJson, charset);
+                destination.writeBytes(eventJsonBytes, 0, eventJsonBytes.length);
+            } else {
+                encoder.encode(stringBuilder, destination);
+            }
+
+        }
+
+        // Release the context.
+        finally {
+            contextRecycler.release(context);
+        }
+
+    }
+
+    // Visible for tests.
+    Context acquireContext() {
+        return contextRecycler.acquire();
+    }
+
+    @Override
+    public byte[] getFooter() {
+        return null;
+    }
+
+    @Override
+    public byte[] getHeader() {
+        return null;
+    }
+
+    @Override
+    public Charset getCharset() {
+        return charset;
+    }
+
+    @Override
+    public String getContentType() {
+        return contentType;
+    }
+
+    @Override
+    public Map<String, String> getContentFormat() {
+        return CONTENT_FORMAT;
+    }
+
+    @PluginBuilderFactory
+    @SuppressWarnings("WeakerAccess")
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    @SuppressWarnings({"unused", "WeakerAccess"})
+    public static final class Builder
+            implements org.apache.logging.log4j.core.util.Builder<JsonTemplateLayout> {
+
+        @PluginConfiguration
+        private Configuration configuration;
+
+        @PluginBuilderAttribute
+        private Charset charset = JsonTemplateLayoutDefaults.getCharset();
+
+        @PluginBuilderAttribute
+        private boolean locationInfoEnabled =
+                JsonTemplateLayoutDefaults.isLocationInfoEnabled();
+
+        @PluginBuilderAttribute
+        private boolean stackTraceEnabled =
+                JsonTemplateLayoutDefaults.isStackTraceEnabled();
+
+        @PluginBuilderAttribute
+        private String eventTemplate = JsonTemplateLayoutDefaults.getEventTemplate();
+
+        @PluginBuilderAttribute
+        private String eventTemplateUri =
+                JsonTemplateLayoutDefaults.getEventTemplateUri();
+
+        @PluginElement("EventTemplateAdditionalFields")
+        private EventTemplateAdditionalFields eventTemplateAdditionalFields
+                = EventTemplateAdditionalFields.EMPTY;
+
+        @PluginBuilderAttribute
+        private String stackTraceElementTemplate =
+                JsonTemplateLayoutDefaults.getStackTraceElementTemplate();
+
+        @PluginBuilderAttribute
+        private String stackTraceElementTemplateUri =
+                JsonTemplateLayoutDefaults.getStackTraceElementTemplateUri();
+
+        @PluginBuilderAttribute
+        private String eventDelimiter = JsonTemplateLayoutDefaults.getEventDelimiter();
+
+        @PluginBuilderAttribute
+        private int maxStringLength = JsonTemplateLayoutDefaults.getMaxStringLength();
+
+        @PluginBuilderAttribute
+        private String truncatedStringSuffix =
+                JsonTemplateLayoutDefaults.getTruncatedStringSuffix();
+
+        @PluginBuilderAttribute
+        private RecyclerFactory recyclerFactory =
+                JsonTemplateLayoutDefaults.getRecyclerFactory();
+
+        private Builder() {
+            // Do nothing.
+        }
+
+        public Configuration getConfiguration() {
+            return configuration;
+        }
+
+        public Builder setConfiguration(final Configuration configuration) {
+            this.configuration = configuration;
+            return this;
+        }
+
+        public Charset getCharset() {
+            return charset;
+        }
+
+        public Builder setCharset(final Charset charset) {
+            this.charset = charset;
+            return this;
+        }
+
+        public boolean isLocationInfoEnabled() {
+            return locationInfoEnabled;
+        }
+
+        public Builder setLocationInfoEnabled(final boolean locationInfoEnabled) {
+            this.locationInfoEnabled = locationInfoEnabled;
+            return this;
+        }
+
+        public boolean isStackTraceEnabled() {
+            return stackTraceEnabled;
+        }
+
+        public Builder setStackTraceEnabled(final boolean stackTraceEnabled) {
+            this.stackTraceEnabled = stackTraceEnabled;
+            return this;
+        }
+
+        public String getEventTemplate() {
+            return eventTemplate;
+        }
+
+        public Builder setEventTemplate(final String eventTemplate) {
+            this.eventTemplate = eventTemplate;
+            return this;
+        }
+
+        public String getEventTemplateUri() {
+            return eventTemplateUri;
+        }
+
+        public Builder setEventTemplateUri(final String eventTemplateUri) {
+            this.eventTemplateUri = eventTemplateUri;
+            return this;
+        }
+
+        public EventTemplateAdditionalFields getEventTemplateAdditionalFields() {
+            return eventTemplateAdditionalFields;
+        }
+
+        public Builder setEventTemplateAdditionalFields(
+                final EventTemplateAdditionalFields eventTemplateAdditionalFields) {
+            this.eventTemplateAdditionalFields = eventTemplateAdditionalFields;
+            return this;
+        }
+
+        public String getStackTraceElementTemplate() {
+            return stackTraceElementTemplate;
+        }
+
+        public Builder setStackTraceElementTemplate(
+                final String stackTraceElementTemplate) {
+            this.stackTraceElementTemplate = stackTraceElementTemplate;
+            return this;
+        }
+
+        public String getStackTraceElementTemplateUri() {
+            return stackTraceElementTemplateUri;
+        }
+
+        public Builder setStackTraceElementTemplateUri(
+                final String stackTraceElementTemplateUri) {
+            this.stackTraceElementTemplateUri = stackTraceElementTemplateUri;
+            return this;
+        }
+
+        public String getEventDelimiter() {
+            return eventDelimiter;
+        }
+
+        public Builder setEventDelimiter(final String eventDelimiter) {
+            this.eventDelimiter = eventDelimiter;
+            return this;
+        }
+
+        public int getMaxStringLength() {
+            return maxStringLength;
+        }
+
+        public Builder setMaxStringLength(final int maxStringLength) {
+            this.maxStringLength = maxStringLength;
+            return this;
+        }
+
+        public String getTruncatedStringSuffix() {
+            return truncatedStringSuffix;
+        }
+
+        public Builder setTruncatedStringSuffix(final String truncatedStringSuffix) {
+            this.truncatedStringSuffix = truncatedStringSuffix;
+            return this;
+        }
+
+        public RecyclerFactory getRecyclerFactory() {
+            return recyclerFactory;
+        }
+
+        public Builder setRecyclerFactory(final RecyclerFactory recyclerFactory) {
+            this.recyclerFactory = recyclerFactory;
+            return this;
+        }
+
+        @Override
+        public JsonTemplateLayout build() {
+            validate();
+            return new JsonTemplateLayout(this);
+        }
+
+        private void validate() {
+            Objects.requireNonNull(configuration, "config");
+            if (Strings.isBlank(eventTemplate) && Strings.isBlank(eventTemplateUri)) {
+                    throw new IllegalArgumentException(
+                            "both eventTemplate and eventTemplateUri are blank");
+            }
+            Objects.requireNonNull(eventTemplateAdditionalFields, "eventTemplateAdditionalFields");
+            if (stackTraceEnabled &&
+                    Strings.isBlank(stackTraceElementTemplate)
+                    && Strings.isBlank(stackTraceElementTemplateUri)) {
+                throw new IllegalArgumentException(
+                        "both stackTraceElementTemplate and stackTraceElementTemplateUri are blank");
+            }
+            Objects.requireNonNull(truncatedStringSuffix, "truncatedStringSuffix");
+            Objects.requireNonNull(recyclerFactory, "recyclerFactory");
+        }
+
+    }
+
+    // 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)
+    public static final class EventTemplateAdditionalFields {
+
+        private static final EventTemplateAdditionalFields EMPTY = newBuilder().build();
+
+        private final KeyValuePair[] additionalFields;
+
+        private EventTemplateAdditionalFields(final Builder builder) {
+            this.additionalFields = builder.additionalFields;
+        }
+
+        public KeyValuePair[] getAdditionalFields() {
+            return additionalFields;
+        }
+
+        @PluginBuilderFactory
+        public static Builder newBuilder() {
+            return new Builder();
+        }
+
+        public static class Builder
+                implements org.apache.logging.log4j.core.util.Builder<EventTemplateAdditionalFields> {
+
+            @PluginElement("AdditionalField")
+            private KeyValuePair[] additionalFields;
+
+            private Builder() {}
+
+            public KeyValuePair[] getAdditionalFields() {
+                return additionalFields;
+            }
+
+            public Builder setAdditionalFields(final KeyValuePair[] additionalFields) {
+                this.additionalFields = additionalFields;
+                return this;
+            }
+
+            @Override
+            public EventTemplateAdditionalFields build() {
+                return new EventTemplateAdditionalFields(this);
+            }
+
+        }
+
+    }
+
+}
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
new file mode 100644
index 0000000..c156e69
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
@@ -0,0 +1,204 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactories;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
+import org.apache.logging.log4j.util.PropertiesUtil;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public enum JsonTemplateLayoutDefaults {;
+
+    private static final PropertiesUtil PROPERTIES = PropertiesUtil.getProperties();
+
+    private static final Charset CHARSET = readCharset();
+
+    private static final boolean LOCATION_INFO_ENABLED =
+            PROPERTIES.getBooleanProperty(
+                    "log4j.layout.jsonTemplate.locationInfoEnabled",
+                    false);
+
+    private static final boolean STACK_TRACE_ENABLED =
+            PROPERTIES.getBooleanProperty(
+                    "log4j.layout.jsonTemplate.stackTraceEnabled",
+                    true);
+
+    private static final String TIMESTAMP_FORMAT_PATTERN =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.timestampFormatPattern",
+                    "yyyy-MM-dd'T'HH:mm:ss.SSSZZZ");
+
+    private static final TimeZone TIME_ZONE = readTimeZone();
+
+    private static final Locale LOCALE = readLocale();
+
+    private static final String EVENT_TEMPLATE =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.eventTemplate");
+
+    private static final String EVENT_TEMPLATE_URI =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.eventTemplateUri",
+                    "classpath:JsonLayout.json");
+
+    private static final String STACK_TRACE_ELEMENT_TEMPLATE =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.stackTraceElementTemplate");
+
+    private static final String STACK_TRACE_ELEMENT_TEMPLATE_URI =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.stackTraceElementTemplateUri",
+                    "classpath:StackTraceElementLayout.json");
+
+    private static final String MDC_KEY_PATTERN =
+            PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.mdcKeyPattern");
+
+    private static final String NDC_PATTERN =
+            PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.ndcPattern");
+
+    private static final String EVENT_DELIMITER =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.eventDelimiter",
+                    System.lineSeparator());
+
+    private static final int MAX_STRING_LENGTH = readMaxStringLength();
+
+    private static final String TRUNCATED_STRING_SUFFIX =
+            PROPERTIES.getStringProperty(
+                    "log4j.layout.jsonTemplate.truncatedStringSuffix",
+                    "…");
+
+    private static final RecyclerFactory RECYCLER_FACTORY = readRecyclerFactory();
+
+    private static Charset readCharset() {
+        final String charsetName =
+                PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.charset");
+        return charsetName != null
+                ? Charset.forName(charsetName)
+                : StandardCharsets.UTF_8;
+    }
+
+    private static TimeZone readTimeZone() {
+        final String timeZoneId =
+                PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.timeZone");
+        return timeZoneId != null
+                ? TimeZone.getTimeZone(timeZoneId)
+                : TimeZone.getDefault();
+    }
+
+    private static Locale readLocale() {
+        final String locale =
+                PROPERTIES.getStringProperty("log4j.layout.jsonTemplate.locale");
+        if (locale == null) {
+            return Locale.getDefault();
+        }
+        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: " + locale);
+        }
+    }
+
+    private static int readMaxStringLength() {
+        final int maxStringLength = PROPERTIES.getIntegerProperty(
+                "log4j.layout.jsonTemplate.maxStringLength",
+                16 * 1_024);
+        if (maxStringLength <= 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a non-zero positive maxStringLength: " +
+                            maxStringLength);
+        }
+        return maxStringLength;
+    }
+
+    private static RecyclerFactory readRecyclerFactory() {
+        final String recyclerFactorySpec = PROPERTIES.getStringProperty(
+                "log4j.layout.jsonTemplate.recyclerFactory");
+        return RecyclerFactories.ofSpec(recyclerFactorySpec);
+    }
+
+    public static Charset getCharset() {
+        return CHARSET;
+    }
+
+    public static boolean isLocationInfoEnabled() {
+        return LOCATION_INFO_ENABLED;
+    }
+
+    public static boolean isStackTraceEnabled() {
+        return STACK_TRACE_ENABLED;
+    }
+
+    public static String getTimestampFormatPattern() {
+        return TIMESTAMP_FORMAT_PATTERN;
+    }
+
+    public static TimeZone getTimeZone() {
+        return TIME_ZONE;
+    }
+
+    public static Locale getLocale() {
+        return LOCALE;
+    }
+
+    public static String getEventTemplate() {
+        return EVENT_TEMPLATE;
+    }
+
+    public static String getEventTemplateUri() {
+        return EVENT_TEMPLATE_URI;
+    }
+
+    public static String getStackTraceElementTemplate() {
+        return STACK_TRACE_ELEMENT_TEMPLATE;
+    }
+
+    public static String getStackTraceElementTemplateUri() {
+        return STACK_TRACE_ELEMENT_TEMPLATE_URI;
+    }
+
+    public static String getMdcKeyPattern() {
+        return MDC_KEY_PATTERN;
+    }
+
+    public static String getNdcPattern() {
+        return NDC_PATTERN;
+    }
+
+    public static String getEventDelimiter() {
+        return EVENT_DELIMITER;
+    }
+
+    public static int getMaxStringLength() {
+        return MAX_STRING_LENGTH;
+    }
+
+    public static String getTruncatedStringSuffix() {
+        return TRUNCATED_STRING_SUFFIX;
+    }
+
+    public static RecyclerFactory getRecyclerFactory() {
+        return RECYCLER_FACTORY;
+    }
+
+}
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/ContextDataResolver.java
new file mode 100644
index 0000000..71c973b
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+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.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).
+ */
+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));
+
+    private final EventResolver internalResolver;
+
+    ContextDataResolver(final EventResolverContext context, final String spec) {
+        this.internalResolver = createResolver(context, spec);
+    }
+
+    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) {
+                throw new IllegalArgumentException(
+                        "MDC key access doesn't take arguments: " + spec);
+            }
+            if (keyValue instanceof StringParameterParser.NullValue) {
+                throw new IllegalArgumentException("missing MDC key: " + spec);
+            }
+            final String key = keyValue.toString();
+            return createKeyResolver(key);
+        } else {
+            return createResolver(context, spec, params);
+        }
+    }
+
+    private static EventResolver createKeyResolver(final String key) {
+        return new EventResolver() {
+
+            @Override
+            public boolean isResolvable(final LogEvent logEvent) {
+                final ReadOnlyStringMap contextData = logEvent.getContextData();
+                return contextData != null && contextData.containsKey(key);
+            }
+
+            @Override
+            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);
+            }
+
+        };
+    }
+
+    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());
+        }
+
+        // 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;
+        }
+
+        // Create the recycler for the loop context.
+        final Recycler<LoopContext> loopContextRecycler =
+                context.getRecyclerFactory().create(() -> {
+                    final LoopContext loopContext = new LoopContext();
+                    if (prefix != null) {
+                        loopContext.prefix = prefix;
+                        loopContext.prefixedKey = new StringBuilder(prefix);
+                    }
+                    loopContext.pattern = pattern;
+                    loopContext.stringify = stringify;
+                    return loopContext;
+                });
+
+        // Create the resolver.
+        return createResolver(flatten, loopContextRecycler);
+
+    }
+
+    private static EventResolver createResolver(
+            final boolean flatten,
+            final Recycler<LoopContext> loopContextRecycler) {
+        return new EventResolver() {
+
+            @Override
+            public boolean isFlattening() {
+                return flatten;
+            }
+
+            @Override
+            public boolean isResolvable(final LogEvent logEvent) {
+                final ReadOnlyStringMap contextData = logEvent.getContextData();
+                return contextData != null && !contextData.isEmpty();
+            }
+
+            @Override
+            public void resolve(final LogEvent value, final JsonWriter jsonWriter) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void resolve(
+                    final LogEvent logEvent,
+                    final JsonWriter jsonWriter,
+                    final boolean succeedingEntry) {
+
+                // Retrieve the context data.
+                final ReadOnlyStringMap contextData = logEvent.getContextData();
+                if (contextData == null || contextData.isEmpty()) {
+                    if (!flatten) {
+                        jsonWriter.writeNull();
+                    }
+                    return;
+                }
+
+                // Resolve the context data.
+                if (!flatten) {
+                    jsonWriter.writeObjectStart();
+                }
+                final LoopContext loopContext = loopContextRecycler.acquire();
+                loopContext.jsonWriter = jsonWriter;
+                loopContext.initJsonWriterStringBuilderLength = jsonWriter.getStringBuilder().length();
+                loopContext.succeedingEntry = flatten && succeedingEntry;
+                try {
+                    contextData.forEach(LoopMethod.INSTANCE, loopContext);
+                } finally {
+                    loopContextRecycler.release(loopContext);
+                }
+                if (!flatten) {
+                    jsonWriter.writeObjectEnd();
+                }
+
+            }
+
+        };
+    }
+
+    private static final class LoopContext {
+
+        private String prefix;
+
+        private StringBuilder prefixedKey;
+
+        private Pattern pattern;
+
+        private boolean stringify;
+
+        private JsonWriter jsonWriter;
+
+        private int initJsonWriterStringBuilderLength;
+
+        private boolean succeedingEntry;
+
+    }
+
+    private static final class LoopMethod implements TriConsumer<String, Object, LoopContext> {
+
+        private static final LoopMethod INSTANCE = new LoopMethod();
+
+        @Override
+        public void accept(
+                final String key,
+                final Object value,
+                final LoopContext loopContext) {
+            final boolean keyMatched =
+                    loopContext.pattern == null ||
+                            loopContext.pattern.matcher(key).matches();
+            if (keyMatched) {
+                final boolean succeedingEntry =
+                        loopContext.succeedingEntry ||
+                                loopContext.initJsonWriterStringBuilderLength <
+                                        loopContext.jsonWriter.getStringBuilder().length();
+                if (succeedingEntry) {
+                    loopContext.jsonWriter.writeSeparator();
+                }
+                if (loopContext.prefix == null) {
+                    loopContext.jsonWriter.writeObjectKey(key);
+                } else {
+                    loopContext.prefixedKey.setLength(loopContext.prefix.length());
+                    loopContext.prefixedKey.append(key);
+                    loopContext.jsonWriter.writeObjectKey(loopContext.prefixedKey);
+                }
+                if (loopContext.stringify && !(value instanceof String)) {
+                    final String valueString = String.valueOf(value);
+                    loopContext.jsonWriter.writeString(valueString);
+                } else {
+                    loopContext.jsonWriter.writeValue(value);
+                }
+            }
+        }
+
+    }
+
+    static String getName() {
+        return "mdc";
+    }
+
+    @Override
+    public boolean isFlattening() {
+        return internalResolver.isFlattening();
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        final ReadOnlyStringMap contextData = logEvent.getContextData();
+        return contextData != null && !contextData.isEmpty();
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter,
+            final boolean succeedingEntry) {
+        internalResolver.resolve(logEvent, jsonWriter, succeedingEntry);
+    }
+
+}
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/ContextDataResolverFactory.java
new file mode 100644
index 0000000..c18c2a6
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 ContextDataResolverFactory implements EventResolverFactory<ContextDataResolver> {
+
+    private static final ContextDataResolverFactory INSTANCE = new ContextDataResolverFactory();
+
+    private ContextDataResolverFactory() {}
+
+    static ContextDataResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ContextDataResolver.getName();
+    }
+
+    @Override
+    public ContextDataResolver create(final EventResolverContext context, final String key) {
+        return new ContextDataResolver(context, key);
+    }
+
+}
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/ContextStackResolver.java
new file mode 100644
index 0000000..1b5fb9f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java
@@ -0,0 +1,102 @@
+/*
+ * 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;
+
+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.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Add Nested Diagnostic Context (NDC).
+ */
+final class ContextStackResolver 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);
+            }
+        }
+    }
+
+    static String getName() {
+        return "ndc";
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        final ThreadContext.ContextStack contextStack = logEvent.getContextStack();
+        return contextStack.getDepth() > 0;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final ThreadContext.ContextStack contextStack = logEvent.getContextStack();
+        if (contextStack.getDepth() == 0) {
+            jsonWriter.writeNull();
+            return;
+        }
+        boolean arrayStarted = false;
+        for (final String contextStackItem : contextStack.asList()) {
+            final boolean matched =
+                    itemPattern == null ||
+                            itemPattern.matcher(contextStackItem).matches();
+            if (matched) {
+                if (arrayStarted) {
+                    jsonWriter.writeSeparator();
+                } else {
+                    jsonWriter.writeArrayStart();
+                    arrayStarted = true;
+                }
+                jsonWriter.writeString(contextStackItem);
+            }
+        }
+        if (arrayStarted) {
+            jsonWriter.writeArrayEnd();
+        } else {
+            jsonWriter.writeNull();
+        }
+    }
+
+}
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
new file mode 100644
index 0000000..0db832a
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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/EndOfBatchResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolver.java
new file mode 100644
index 0000000..268df52
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolver.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class EndOfBatchResolver implements EventResolver {
+
+    private static final EndOfBatchResolver INSTANCE = new EndOfBatchResolver();
+
+    private EndOfBatchResolver() {}
+
+    static EndOfBatchResolver getInstance() {
+        return INSTANCE;
+    }
+
+    static String getName() {
+        return "endOfBatch";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final boolean endOfBatch = logEvent.isEndOfBatch();
+        jsonWriter.writeBoolean(endOfBatch);
+    }
+
+}
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
new file mode 100644
index 0000000..c55fc58
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 EndOfBatchResolverFactory implements EventResolverFactory<EndOfBatchResolver> {
+
+    private static final EndOfBatchResolverFactory INSTANCE = new EndOfBatchResolverFactory();
+
+    private EndOfBatchResolverFactory() {}
+
+    static EndOfBatchResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return EndOfBatchResolver.getName();
+    }
+
+    @Override
+    public EndOfBatchResolver create(final EventResolverContext context, String key) {
+        return EndOfBatchResolver.getInstance();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java
new file mode 100644
index 0000000..ce21181
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+
+interface EventResolver extends TemplateResolver<LogEvent> {}
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
new file mode 100644
index 0000000..e178a66
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
@@ -0,0 +1,226 @@
+/*
+ * 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;
+
+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.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
+
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.Objects;
+
+public final class EventResolverContext implements TemplateResolverContext<LogEvent, EventResolverContext> {
+
+    private final Configuration configuration;
+
+    private final StrSubstitutor substitutor;
+
+    private final Charset charset;
+
+    private final JsonWriter jsonWriter;
+
+    private final RecyclerFactory recyclerFactory;
+
+    private final int maxStringByteCount;
+
+    private final boolean locationInfoEnabled;
+
+    private final boolean stackTraceEnabled;
+
+    private final TemplateResolver<Throwable> stackTraceObjectResolver;
+
+    private final KeyValuePair[] additionalFields;
+
+    private EventResolverContext(final Builder builder) {
+        this.configuration = builder.configuration;
+        this.substitutor = builder.substitutor;
+        this.charset = builder.charset;
+        this.jsonWriter = builder.jsonWriter;
+        this.recyclerFactory = builder.recyclerFactory;
+        this.maxStringByteCount = builder.maxStringByteCount;
+        this.locationInfoEnabled = builder.locationInfoEnabled;
+        this.stackTraceEnabled = builder.stackTraceEnabled;
+        this.stackTraceObjectResolver = stackTraceEnabled
+                ? new StackTraceObjectResolver(builder.stackTraceElementObjectResolver)
+                : null;
+        this.additionalFields = builder.eventTemplateAdditionalFields;
+    }
+
+    @Override
+    public Class<EventResolverContext> getContextClass() {
+        return EventResolverContext.class;
+    }
+
+    @Override
+    public Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> getResolverFactoryByName() {
+        return EventResolverFactories.getResolverFactoryByName();
+    }
+
+    public Configuration getConfiguration() {
+        return configuration;
+    }
+
+    @Override
+    public StrSubstitutor getSubstitutor() {
+        return substitutor;
+    }
+
+    public Charset getCharset() {
+        return charset;
+    }
+
+    @Override
+    public JsonWriter getJsonWriter() {
+        return jsonWriter;
+    }
+
+    RecyclerFactory getRecyclerFactory() {
+        return recyclerFactory;
+    }
+
+    int getMaxStringByteCount() {
+        return maxStringByteCount;
+    }
+
+    boolean isLocationInfoEnabled() {
+        return locationInfoEnabled;
+    }
+
+    boolean isStackTraceEnabled() {
+        return stackTraceEnabled;
+    }
+
+    TemplateResolver<Throwable> getStackTraceObjectResolver() {
+        return stackTraceObjectResolver;
+    }
+
+    KeyValuePair[] getAdditionalFields() {
+        return additionalFields;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+
+        private Configuration configuration;
+
+        private StrSubstitutor substitutor;
+
+        private Charset charset;
+
+        private JsonWriter jsonWriter;
+
+        private RecyclerFactory recyclerFactory;
+
+        private int maxStringByteCount;
+
+        private boolean locationInfoEnabled;
+
+        private boolean stackTraceEnabled;
+
+        private TemplateResolver<StackTraceElement> stackTraceElementObjectResolver;
+
+        private KeyValuePair[] eventTemplateAdditionalFields;
+
+        private Builder() {
+            // Do nothing.
+        }
+
+        public Builder setConfiguration(final Configuration configuration) {
+            this.configuration = configuration;
+            return this;
+        }
+
+        public Builder setSubstitutor(final StrSubstitutor substitutor) {
+            this.substitutor = substitutor;
+            return this;
+        }
+
+        public Builder setCharset(final Charset charset) {
+            this.charset = charset;
+            return this;
+        }
+
+        public Builder setJsonWriter(final JsonWriter jsonWriter) {
+            this.jsonWriter = jsonWriter;
+            return this;
+        }
+
+        public Builder setRecyclerFactory(final RecyclerFactory recyclerFactory) {
+            this.recyclerFactory = recyclerFactory;
+            return this;
+        }
+
+        public Builder setMaxStringByteCount(final int maxStringByteCount) {
+            this.maxStringByteCount = maxStringByteCount;
+            return this;
+        }
+
+        public Builder setLocationInfoEnabled(final boolean locationInfoEnabled) {
+            this.locationInfoEnabled = locationInfoEnabled;
+            return this;
+        }
+
+        public Builder setStackTraceEnabled(final boolean stackTraceEnabled) {
+            this.stackTraceEnabled = stackTraceEnabled;
+            return this;
+        }
+
+        public Builder setStackTraceElementObjectResolver(
+                final TemplateResolver<StackTraceElement> stackTraceElementObjectResolver) {
+            this.stackTraceElementObjectResolver = stackTraceElementObjectResolver;
+            return this;
+        }
+
+        public Builder setEventTemplateAdditionalFields(
+                final KeyValuePair[] eventTemplateAdditionalFields) {
+            this.eventTemplateAdditionalFields = eventTemplateAdditionalFields;
+            return this;
+        }
+
+        public EventResolverContext build() {
+            validate();
+            return new EventResolverContext(this);
+        }
+
+        private void validate() {
+            Objects.requireNonNull(configuration, "configuration");
+            Objects.requireNonNull(substitutor, "substitutor");
+            Objects.requireNonNull(charset, "charset");
+            Objects.requireNonNull(jsonWriter, "jsonWriter");
+            Objects.requireNonNull(recyclerFactory, "recyclerFactory");
+            if (maxStringByteCount <= 0) {
+                throw new IllegalArgumentException(
+                        "was expecting maxStringByteCount > 0: " +
+                                maxStringByteCount);
+            }
+            if (stackTraceEnabled) {
+                Objects.requireNonNull(
+                        stackTraceElementObjectResolver,
+                        "stackTraceElementObjectResolver");
+            }
+        }
+
+    }
+
+}
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
new file mode 100644
index 0000000..86ea6bb
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactories.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+enum EventResolverFactories {;
+
+    private static final Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> RESOLVER_FACTORY_BY_NAME =
+            createResolverFactoryByName();
+
+    private static Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> createResolverFactoryByName() {
+
+        // Collect resolver factories.
+        final List<EventResolverFactory<? extends EventResolver>> resolverFactories = Arrays.asList(
+                ContextDataResolverFactory.getInstance(),
+                ContextStackResolverFactory.getInstance(),
+                EndOfBatchResolverFactory.getInstance(),
+                ExceptionResolverFactory.getInstance(),
+                ExceptionRootCauseResolverFactory.getInstance(),
+                LevelResolverFactory.getInstance(),
+                LoggerResolverFactory.getInstance(),
+                MainMapResolverFactory.getInstance(),
+                MapResolverFactory.getInstance(),
+                MarkerResolverFactory.getInstance(),
+                MessageResolverFactory.getInstance(),
+                PatternResolverFactory.getInstance(),
+                SourceResolverFactory.getInstance(),
+                ThreadResolverFactory.getInstance(),
+                TimestampResolverFactory.getInstance());
+
+        // Convert collection to map.
+        final Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> resolverFactoryByName = new LinkedHashMap<>();
+        for (final EventResolverFactory<? extends EventResolver> resolverFactory : resolverFactories) {
+            resolverFactoryByName.put(resolverFactory.getName(), resolverFactory);
+        }
+        return Collections.unmodifiableMap(resolverFactoryByName);
+
+    }
+
+    static Map<String, TemplateResolverFactory<LogEvent, EventResolverContext, ? extends TemplateResolver<LogEvent>>> getResolverFactoryByName() {
+        return RESOLVER_FACTORY_BY_NAME;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java
new file mode 100644
index 0000000..3c2f2db
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+
+interface EventResolverFactory<R extends TemplateResolver<LogEvent>> extends TemplateResolverFactory<LogEvent, EventResolverContext, R> {}
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
new file mode 100644
index 0000000..de1ec31
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+abstract class ExceptionInternalResolverFactory {
+
+    private static final EventResolver NULL_RESOLVER =
+            (ignored, jsonGenerator) -> jsonGenerator.writeNull();
+
+    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) {
+            case "className": return createClassNameResolver();
+            case "message": return createMessageResolver(context);
+            case "stackTrace": return createStackTraceResolver(context, minorKey);
+        }
+        throw new IllegalArgumentException("unknown key: " + key);
+
+    }
+
+    abstract EventResolver createClassNameResolver();
+
+    abstract EventResolver createMessageResolver(final EventResolverContext context);
+
+    private EventResolver createStackTraceResolver(
+            final EventResolverContext context,
+            final String minorKey) {
+        if (!context.isStackTraceEnabled()) {
+            return NULL_RESOLVER;
+        }
+        switch (minorKey) {
+            case "text": return createStackTraceTextResolver(context);
+            case "": return createStackTraceObjectResolver(context);
+        }
+        throw new IllegalArgumentException("unknown minor key: " + minorKey);
+    }
+
+    abstract EventResolver createStackTraceTextResolver(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
new file mode 100644
index 0000000..a5ab172
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+class ExceptionResolver implements EventResolver {
+
+    private static final ExceptionInternalResolverFactory INTERNAL_RESOLVER_FACTORY =
+            new ExceptionInternalResolverFactory() {
+
+                @Override
+                EventResolver createClassNameResolver() {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            String exceptionClassName = exception.getClass().getCanonicalName();
+                            jsonWriter.writeString(exceptionClassName);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createMessageResolver(final EventResolverContext context) {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            String exceptionMessage = exception.getMessage();
+                            jsonWriter.writeString(exceptionMessage);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createStackTraceTextResolver(final EventResolverContext context) {
+                    StackTraceTextResolver stackTraceTextResolver =
+                            new StackTraceTextResolver(context);
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            stackTraceTextResolver.resolve(exception, jsonWriter);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createStackTraceObjectResolver(final EventResolverContext context) {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            context.getStackTraceObjectResolver().resolve(exception, jsonWriter);
+                        }
+                    };
+                }
+
+            };
+
+    private final boolean stackTraceEnabled;
+
+    private final EventResolver internalResolver;
+
+    ExceptionResolver(final EventResolverContext context, final String key) {
+        this.stackTraceEnabled = context.isStackTraceEnabled();
+        this.internalResolver = INTERNAL_RESOLVER_FACTORY.createInternalResolver(context, key);
+    }
+
+    static String getName() {
+        return "exception";
+    }
+
+    @Override
+    public boolean isResolvable() {
+        return stackTraceEnabled;
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return stackTraceEnabled && logEvent.getThrown() != null;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
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
new file mode 100644
index 0000000..8a70bfd
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 ExceptionResolverFactory implements EventResolverFactory<ExceptionResolver> {
+
+    private static final ExceptionResolverFactory INSTANCE = new ExceptionResolverFactory();
+
+    private ExceptionResolverFactory() {}
+
+    static ExceptionResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ExceptionResolver.getName();
+    }
+
+    @Override
+    public ExceptionResolver create(final EventResolverContext context, final String key) {
+        return new ExceptionResolver(context, key);
+    }
+
+}
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
new file mode 100644
index 0000000..230ac59
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
@@ -0,0 +1,116 @@
+/*
+ * 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;
+
+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;
+
+final class ExceptionRootCauseResolver implements EventResolver {
+
+    private static final ExceptionInternalResolverFactory INTERNAL_RESOLVER_FACTORY =
+            new ExceptionInternalResolverFactory() {
+
+                @Override
+                EventResolver createClassNameResolver() {
+                    return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                        final Throwable exception = logEvent.getThrown();
+                        if (exception == null) {
+                            jsonWriter.writeNull();
+                        } else {
+                            final Throwable rootCause = Throwables.getRootCause(exception);
+                            final String rootCauseClassName = rootCause.getClass().getCanonicalName();
+                            jsonWriter.writeString(rootCauseClassName);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createMessageResolver(final EventResolverContext 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);
+                            final String rootCauseMessage = rootCause.getMessage();
+                            jsonWriter.writeString(rootCauseMessage);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createStackTraceTextResolver(final EventResolverContext context) {
+                    final StackTraceTextResolver stackTraceTextResolver =
+                            new StackTraceTextResolver(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);
+                        }
+                    };
+                }
+
+                @Override
+                EventResolver createStackTraceObjectResolver(EventResolverContext 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);
+                            context.getStackTraceObjectResolver().resolve(rootCause, jsonWriter);
+                        }
+                    };
+                }
+
+            };
+
+    private final boolean stackTraceEnabled;
+
+    private final EventResolver internalResolver;
+
+    ExceptionRootCauseResolver(final EventResolverContext context, final String key) {
+        this.stackTraceEnabled = context.isStackTraceEnabled();
+        this.internalResolver = INTERNAL_RESOLVER_FACTORY.createInternalResolver(context, key);
+    }
+
+    static String getName() {
+        return "exceptionRootCause";
+    }
+
+    @Override
+    public boolean isResolvable() {
+        return stackTraceEnabled;
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return stackTraceEnabled && logEvent.getThrown() != null;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
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
new file mode 100644
index 0000000..1f625b0
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 ExceptionRootCauseResolverFactory implements EventResolverFactory<ExceptionRootCauseResolver> {
+
+    private static final ExceptionRootCauseResolverFactory INSTANCE = new ExceptionRootCauseResolverFactory();
+
+    private ExceptionRootCauseResolverFactory() {}
+
+    static ExceptionRootCauseResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ExceptionRootCauseResolver.getName();
+    }
+
+    @Override
+    public ExceptionRootCauseResolver create(final EventResolverContext context, final String key) {
+        return new ExceptionRootCauseResolver(context, key);
+    }
+
+}
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
new file mode 100644
index 0000000..ad33a0a
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.net.Severity;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+final class LevelResolver implements EventResolver {
+
+    private static String[] SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL;
+
+    static {
+        final int levelCount = Level.values().length;
+        final String[] severityCodeResolutionByStandardLevelOrdinal =
+                new String[levelCount + 1];
+        for (final Level level : Level.values()) {
+            final int standardLevelOrdinal = level.getStandardLevel().ordinal();
+            final int severityCode = Severity.getSeverity(level).getCode();
+            severityCodeResolutionByStandardLevelOrdinal[standardLevelOrdinal] =
+                    String.valueOf(severityCode);
+        }
+        SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL =
+                severityCodeResolutionByStandardLevelOrdinal;
+    }
+
+    private static final EventResolver SEVERITY_CODE_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final int standardLevelOrdinal =
+                        logEvent.getLevel().getStandardLevel().ordinal();
+                final String severityCodeResolution =
+                        SEVERITY_CODE_RESOLUTION_BY_STANDARD_LEVEL_ORDINAL[
+                                standardLevelOrdinal];
+                jsonWriter.writeRawString(severityCodeResolution);
+            };
+
+    private final EventResolver internalResolver;
+
+    LevelResolver(final EventResolverContext context, final String key) {
+        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);
+        }
+    }
+
+    private static EventResolver createNameResolver(
+            final JsonWriter contextJsonWriter) {
+        final Map<Level, String> resolutionByLevel = Arrays
+                .stream(Level.values())
+                .collect(Collectors.toMap(
+                        Function.identity(),
+                        (final Level level) -> contextJsonWriter.use(() -> {
+                            final String name = level.name();
+                            contextJsonWriter.writeString(name);
+                        })));
+        return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+            final String resolution = resolutionByLevel.get(logEvent.getLevel());
+            jsonWriter.writeRawString(resolution);
+        };
+    }
+
+    private static EventResolver createSeverityNameResolver(
+            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);
+                        })));
+        return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+            final String resolution = resolutionByLevel.get(logEvent.getLevel());
+            jsonWriter.writeRawString(resolution);
+        };
+    }
+
+    static String getName() {
+        return "level";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
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
new file mode 100644
index 0000000..e28745b
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 LevelResolverFactory implements EventResolverFactory<LevelResolver> {
+
+    private static final LevelResolverFactory INSTANCE = new LevelResolverFactory();
+
+    private LevelResolverFactory() {}
+
+    static LevelResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return LevelResolver.getName();
+    }
+
+    @Override
+    public LevelResolver create(final EventResolverContext context, final String key) {
+        return new LevelResolver(context, key);
+    }
+
+}
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
new file mode 100644
index 0000000..b88bd0d
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class LoggerResolver implements EventResolver {
+
+    private static final EventResolver NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final String loggerName = logEvent.getLoggerName();
+                jsonWriter.writeString(loggerName);
+            };
+
+    private static final EventResolver FQCN_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final String loggerFqcn = logEvent.getLoggerFqcn();
+                jsonWriter.writeString(loggerFqcn);
+            };
+
+    private final EventResolver internalResolver;
+
+    LoggerResolver(final String key) {
+        this.internalResolver = createInternalResolver(key);
+    }
+
+    private static EventResolver createInternalResolver(final String key) {
+        switch (key) {
+            case "name": return NAME_RESOLVER;
+            case "fqcn": return FQCN_RESOLVER;
+        }
+        throw new IllegalArgumentException("unknown key: " + key);
+    }
+
+    static String getName() {
+        return "logger";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
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
new file mode 100644
index 0000000..10d4ba7
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 LoggerResolverFactory implements EventResolverFactory<LoggerResolver> {
+
+    private static final LoggerResolverFactory INSTANCE = new LoggerResolverFactory();
+
+    private LoggerResolverFactory() {}
+
+    static LoggerResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return LoggerResolver.getName();
+    }
+
+    @Override
+    public LoggerResolver create(final EventResolverContext context, final String key) {
+        return new LoggerResolver(key);
+    }
+
+}
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
new file mode 100644
index 0000000..29be9e8
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+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;
+
+final class MainMapResolver implements EventResolver {
+
+    private static final MainMapLookup MAIN_MAP_LOOKUP = new MainMapLookup();
+
+    private final String key;
+
+    static String getName() {
+        return "main";
+    }
+
+    MainMapResolver(final String key) {
+        this.key = key;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final String value = MAIN_MAP_LOOKUP.lookup(key);
+        jsonWriter.writeString(value);
+    }
+
+}
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
new file mode 100644
index 0000000..60262d4
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 MainMapResolverFactory implements EventResolverFactory<MainMapResolver> {
+
+    private static final MainMapResolverFactory INSTANCE = new MainMapResolverFactory();
+
+    private MainMapResolverFactory() {}
+
+    static MainMapResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return MainMapResolver.getName();
+    }
+
+    @Override
+    public MainMapResolver create(final EventResolverContext context, final String key) {
+        return new MainMapResolver(key);
+    }
+
+}
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
new file mode 100644
index 0000000..1c71619
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+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;
+
+final class MapResolver implements EventResolver {
+
+    private static final MapLookup MAP_LOOKUP = new MapLookup();
+
+    private final String key;
+
+    static String getName() {
+        return "map";
+    }
+
+    MapResolver(final String key) {
+        this.key = key;
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return logEvent.getMessage() instanceof MapMessage;
+    }
+
+    @Override
+    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);
+        }
+
+    }
+
+}
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
new file mode 100644
index 0000000..6e2e8d6
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 MapResolverFactory implements EventResolverFactory<MapResolver> {
+
+    private static final MapResolverFactory INSTANCE = new MapResolverFactory();
+
+    private MapResolverFactory() {}
+
+    static MapResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return MapResolver.getName();
+    }
+
+    @Override
+    public MapResolver create(final EventResolverContext context, final String key) {
+        return new MapResolver(key);
+    }
+
+}
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
new file mode 100644
index 0000000..86a352f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class MarkerResolver implements EventResolver {
+
+    private static final TemplateResolver<LogEvent> NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final Marker marker = logEvent.getMarker();
+                if (marker == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    jsonWriter.writeString(marker.getName());
+                }
+            };
+
+    private final TemplateResolver<LogEvent> internalResolver;
+
+    MarkerResolver(final String key) {
+        this.internalResolver = createInternalResolver(key);
+    }
+
+    private TemplateResolver<LogEvent> createInternalResolver(final String key) {
+        if ("name".equals(key)) {
+            return NAME_RESOLVER;
+        }
+        throw new IllegalArgumentException("unknown key: " + key);
+    }
+
+    static String getName() {
+        return "marker";
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return logEvent.getMarker() != null;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
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
new file mode 100644
index 0000000..42f20c5
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 MarkerResolverFactory implements EventResolverFactory<MarkerResolver> {
+
+    private static final MarkerResolverFactory INSTANCE = new MarkerResolverFactory();
+
+    static MarkerResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    private MarkerResolverFactory() {}
+
+    @Override
+    public String getName() {
+        return MarkerResolver.getName();
+    }
+
+    @Override
+    public MarkerResolver create(final EventResolverContext context, final String key) {
+        return new MarkerResolver(key);
+    }
+
+}
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
new file mode 100644
index 0000000..b2953df
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
@@ -0,0 +1,171 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.message.Message;
+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;
+
+final class MessageResolver implements EventResolver {
+
+    private static final String[] FORMATS = { "JSON" };
+
+    private final EventResolver internalResolver;
+
+    MessageResolver(final String key) {
+        this.internalResolver = createInternalResolver(key);
+    }
+
+    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);
+        }
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+    private static void resolveText(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        final Message message = logEvent.getMessage();
+        resolveText(message, jsonWriter);
+    }
+
+    private static void resolveText(
+            final Message message,
+            final JsonWriter jsonWriter) {
+        if (message instanceof StringBuilderFormattable) {
+            final StringBuilderFormattable formattable =
+                    (StringBuilderFormattable) message;
+            jsonWriter.writeString(formattable);
+        } else {
+            final String formattedMessage = message.getFormattedMessage();
+            jsonWriter.writeString(formattedMessage);
+        }
+    }
+
+    private static void resolveJson(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+
+        // Try SimpleMessage serializer.
+        final Message message = logEvent.getMessage();
+        if (writeSimpleMessage(jsonWriter, message)) {
+            return;
+        }
+
+        // Try MultiformatMessage serializer.
+        if (writeMultiformatMessage(jsonWriter, message)) {
+            return;
+        }
+
+        // Try ObjectMessage serializer.
+        if (writeObjectMessage(jsonWriter, message)) {
+            return;
+        }
+
+        // Fallback to plain Object write.
+        resolveText(logEvent, jsonWriter);
+
+    }
+
+    private static boolean writeSimpleMessage(
+            final JsonWriter jsonWriter,
+            final Message message) {
+
+        // Check type.
+        if (!(message instanceof SimpleMessage)) {
+            return false;
+        }
+        final SimpleMessage simpleMessage = (SimpleMessage) message;
+
+        // Write message.
+        final String formattedMessage = simpleMessage.getFormattedMessage();
+        jsonWriter.writeString(formattedMessage);
+        return true;
+
+    }
+
+    private static boolean writeMultiformatMessage(
+            final JsonWriter jsonWriter,
+            final Message message) {
+
+        // Check type.
+        if (!(message instanceof MultiformatMessage)) {
+            return false;
+        }
+        final MultiformatMessage multiformatMessage = (MultiformatMessage) message;
+
+        // Check formatter's JSON support.
+        boolean jsonSupported = false;
+        final String[] formats = multiformatMessage.getFormats();
+        for (final String format : formats) {
+            if (FORMATS[0].equalsIgnoreCase(format)) {
+                jsonSupported = true;
+                break;
+            }
+        }
+
+        // Write the formatted JSON, if supported.
+        if (jsonSupported) {
+            final String messageJson = multiformatMessage.getFormattedMessage(FORMATS);
+            jsonWriter.writeRawString(messageJson);
+            return true;
+        }
+
+        // Fallback to the default message formatter.
+        resolveText((LogEvent) message, jsonWriter);
+        return true;
+
+    }
+
+    private static boolean writeObjectMessage(
+            final JsonWriter jsonWriter,
+            final Message message) {
+
+        // Check type.
+        if (!(message instanceof ObjectMessage)) {
+            return false;
+        }
+
+        // Serialize object.
+        final ObjectMessage objectMessage = (ObjectMessage) message;
+        final Object object = objectMessage.getParameter();
+        jsonWriter.writeValue(object);
+        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
new file mode 100644
index 0000000..9032d9e
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 MessageResolverFactory implements EventResolverFactory<MessageResolver> {
+
+    private static final MessageResolverFactory INSTANCE = new MessageResolverFactory();
+
+    private MessageResolverFactory() {}
+
+    static MessageResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return MessageResolver.getName();
+    }
+
+    @Override
+    public MessageResolver create(final EventResolverContext context, final String key) {
+        return new MessageResolver(key);
+    }
+
+}
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
new file mode 100644
index 0000000..1b2fba0
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+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;
+
+final class PatternResolver implements EventResolver {
+
+    private final BiConsumer<StringBuilder, LogEvent> emitter;
+
+    PatternResolver(
+            final EventResolverContext context,
+            final String key) {
+        final PatternLayout patternLayout = PatternLayout
+                .newBuilder()
+                .setConfiguration(context.getConfiguration())
+                .setCharset(context.getCharset())
+                .setPattern(key)
+                .build();
+        this.emitter = (final StringBuilder stringBuilder, final LogEvent logEvent) ->
+                patternLayout.serialize(logEvent, stringBuilder);
+    }
+
+    static String getName() {
+        return "pattern";
+    }
+
+    @Override
+    public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
+        jsonWriter.writeString(emitter, logEvent);
+    }
+
+}
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
new file mode 100644
index 0000000..c47f850
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 PatternResolverFactory implements EventResolverFactory<PatternResolver> {
+
+    private static final PatternResolverFactory INSTANCE = new PatternResolverFactory();
+
+    private PatternResolverFactory() {}
+
+    static PatternResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return PatternResolver.getName();
+    }
+
+    @Override
+    public PatternResolver create(final EventResolverContext context, final String key) {
+        return new PatternResolver(context, key);
+    }
+
+}
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
new file mode 100644
index 0000000..7a029f1
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
@@ -0,0 +1,117 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class SourceResolver implements EventResolver {
+
+    private static final EventResolver NULL_RESOLVER =
+            (final LogEvent value, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeNull();
+
+    private static final EventResolver CLASS_NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final StackTraceElement logEventSource = logEvent.getSource();
+                if (logEventSource == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    final String sourceClassName = logEventSource.getClassName();
+                    jsonWriter.writeString(sourceClassName);
+                }
+            };
+
+    private static final EventResolver FILE_NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final StackTraceElement logEventSource = logEvent.getSource();
+                if (logEventSource == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    final String sourceFileName = logEventSource.getFileName();
+                    jsonWriter.writeString(sourceFileName);
+                }
+            };
+
+    private static final EventResolver LINE_NUMBER_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final StackTraceElement logEventSource = logEvent.getSource();
+                if (logEventSource == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    final int sourceLineNumber = logEventSource.getLineNumber();
+                    jsonWriter.writeNumber(sourceLineNumber);
+                }
+            };
+
+    private static final EventResolver METHOD_NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final StackTraceElement logEventSource = logEvent.getSource();
+                if (logEventSource == null) {
+                    jsonWriter.writeNull();
+                } else {
+                    final String sourceMethodName = logEventSource.getMethodName();
+                    jsonWriter.writeString(sourceMethodName);
+                }
+            };
+
+    private final boolean locationInfoEnabled;
+
+    private final EventResolver internalResolver;
+
+    SourceResolver(final EventResolverContext context, final String key) {
+        this.locationInfoEnabled = context.isLocationInfoEnabled();
+        this.internalResolver = createInternalResolver(context, key);
+    }
+
+    private static EventResolver createInternalResolver(
+            final EventResolverContext context,
+            final String key) {
+        if (!context.isLocationInfoEnabled()) {
+            return NULL_RESOLVER;
+        }
+        switch (key) {
+            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);
+    }
+
+    static String getName() {
+        return "source";
+    }
+
+    @Override
+    public boolean isResolvable() {
+        return locationInfoEnabled;
+    }
+
+    @Override
+    public boolean isResolvable(final LogEvent logEvent) {
+        return locationInfoEnabled && logEvent.getSource() != null;
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
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
new file mode 100644
index 0000000..0c5c2b5
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 SourceResolverFactory implements EventResolverFactory<SourceResolver> {
+
+    private static final SourceResolverFactory INSTANCE = new SourceResolverFactory();
+
+    private SourceResolverFactory() {}
+
+    static SourceResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return SourceResolver.getName();
+    }
+
+    @Override
+    public SourceResolver create(final EventResolverContext context, final String key) {
+        return new SourceResolver(context, key);
+    }
+
+}
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
new file mode 100644
index 0000000..1e30204
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class StackTraceElementObjectResolver implements TemplateResolver<StackTraceElement> {
+
+    private static final TemplateResolver<StackTraceElement> CLASS_NAME_RESOLVER =
+            (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeString(stackTraceElement.getClassName());
+
+    private static final TemplateResolver<StackTraceElement> METHOD_NAME_RESOLVER =
+            (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeString(stackTraceElement.getMethodName());
+
+    private static final TemplateResolver<StackTraceElement> FILE_NAME_RESOLVER =
+            (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeString(stackTraceElement.getFileName());
+
+    private static final TemplateResolver<StackTraceElement> LINE_NUMBER_RESOLVER =
+            (final StackTraceElement stackTraceElement, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeNumber(stackTraceElement.getLineNumber());
+
+    private final TemplateResolver<StackTraceElement> internalResolver;
+
+    StackTraceElementObjectResolver(final String key) {
+        this.internalResolver = createInternalResolver(key);
+    }
+
+    private TemplateResolver<StackTraceElement> createInternalResolver(final String key) {
+        switch (key) {
+            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);
+    }
+
+    static String getName() {
+        return "stackTraceElement";
+    }
+
+    @Override
+    public void resolve(
+            final StackTraceElement stackTraceElement,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(stackTraceElement, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverContext.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverContext.java
new file mode 100644
index 0000000..6e42237
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverContext.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.Map;
+import java.util.Objects;
+
+public final class StackTraceElementObjectResolverContext
+        implements TemplateResolverContext<StackTraceElement, StackTraceElementObjectResolverContext> {
+
+    private final StrSubstitutor substitutor;
+
+    private final JsonWriter jsonWriter;
+
+    private StackTraceElementObjectResolverContext(final Builder builder) {
+        this.substitutor = builder.substitutor;
+        this.jsonWriter = builder.jsonWriter;
+    }
+
+    @Override
+    public Class<StackTraceElementObjectResolverContext> getContextClass() {
+        return StackTraceElementObjectResolverContext.class;
+    }
+
+    @Override
+    public Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> getResolverFactoryByName() {
+        return StackTraceElementObjectResolverFactories.getResolverFactoryByName();
+    }
+
+    @Override
+    public StrSubstitutor getSubstitutor() {
+        return substitutor;
+    }
+
+    @Override
+    public JsonWriter getJsonWriter() {
+        return jsonWriter;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+
+        private StrSubstitutor substitutor;
+
+        private JsonWriter jsonWriter;
+
+        private Builder() {
+            // Do nothing.
+        }
+
+        public Builder setSubstitutor(final StrSubstitutor substitutor) {
+            this.substitutor = substitutor;
+            return this;
+        }
+
+        public Builder setJsonWriter(final JsonWriter jsonWriter) {
+            this.jsonWriter = jsonWriter;
+            return this;
+        }
+
+        public StackTraceElementObjectResolverContext build() {
+            validate();
+            return new StackTraceElementObjectResolverContext(this);
+        }
+
+        private void validate() {
+            Objects.requireNonNull(substitutor, "substitutor");
+            Objects.requireNonNull(jsonWriter, "jsonWriter");
+        }
+
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactories.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactories.java
new file mode 100644
index 0000000..90af9ab
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactories.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+enum StackTraceElementObjectResolverFactories {;
+
+    private static final Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> RESOLVER_FACTORY_BY_NAME =
+            createResolverFactoryByName();
+
+    private static Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> createResolverFactoryByName() {
+        final Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> resolverFactoryByName = new LinkedHashMap<>();
+        final StackTraceElementObjectResolverFactory stackTraceElementObjectResolverFactory = StackTraceElementObjectResolverFactory.getInstance();
+        resolverFactoryByName.put(stackTraceElementObjectResolverFactory.getName(), stackTraceElementObjectResolverFactory);
+        return Collections.unmodifiableMap(resolverFactoryByName);
+    }
+
+    static Map<String, TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, ? extends TemplateResolver<StackTraceElement>>> getResolverFactoryByName() {
+        return RESOLVER_FACTORY_BY_NAME;
+    }
+
+}
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
new file mode 100644
index 0000000..84456e7
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolverFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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 StackTraceElementObjectResolverFactory
+        implements TemplateResolverFactory<StackTraceElement, StackTraceElementObjectResolverContext, StackTraceElementObjectResolver> {
+
+    private static final StackTraceElementObjectResolverFactory INSTANCE =
+            new StackTraceElementObjectResolverFactory();
+
+    private StackTraceElementObjectResolverFactory() {}
+
+    public static StackTraceElementObjectResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return StackTraceElementObjectResolver.getName();
+    }
+
+    @Override
+    public StackTraceElementObjectResolver create(
+            final StackTraceElementObjectResolverContext context,
+            final String key) {
+        return new StackTraceElementObjectResolver(key);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceObjectResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceObjectResolver.java
new file mode 100644
index 0000000..53a9ce4
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceObjectResolver.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class StackTraceObjectResolver implements StackTraceResolver {
+
+    private final TemplateResolver<StackTraceElement> stackTraceElementResolver;
+
+    StackTraceObjectResolver(final TemplateResolver<StackTraceElement> stackTraceElementResolver) {
+        this.stackTraceElementResolver = stackTraceElementResolver;
+    }
+
+    @Override
+    public void resolve(
+            final Throwable throwable,
+            final JsonWriter jsonWriter) {
+        // Following check against the stacktrace element count is not
+        // implemented in isResolvable(), since Throwable#getStackTrace() incurs
+        // a significant cloning cost.
+        final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+        if (stackTraceElements.length  == 0) {
+            jsonWriter.writeNull();
+        } else {
+            jsonWriter.writeArrayStart();
+            for (int stackTraceElementIndex = 0;
+                 stackTraceElementIndex < stackTraceElements.length;
+                 stackTraceElementIndex++) {
+                if (stackTraceElementIndex > 0) {
+                    jsonWriter.writeSeparator();
+                }
+                final StackTraceElement stackTraceElement = stackTraceElements[stackTraceElementIndex];
+                stackTraceElementResolver.resolve(stackTraceElement, jsonWriter);
+            }
+            jsonWriter.writeArrayEnd();
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java
new file mode 100644
index 0000000..8275193
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+interface StackTraceResolver extends TemplateResolver<Throwable> {}
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/StackTraceTextResolver.java
new file mode 100644
index 0000000..17f9ac1
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.layout.json.template.util.TruncatingBufferedPrintWriter;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.Recycler;
+
+import java.util.function.Supplier;
+
+final class StackTraceTextResolver implements StackTraceResolver {
+
+    private final Recycler<TruncatingBufferedPrintWriter> writerRecycler;
+
+    StackTraceTextResolver(final EventResolverContext context) {
+        final Supplier<TruncatingBufferedPrintWriter> writerSupplier =
+                () -> TruncatingBufferedPrintWriter.ofCapacity(
+                        context.getMaxStringByteCount());
+        this.writerRecycler = context
+                .getRecyclerFactory()
+                .create(writerSupplier, TruncatingBufferedPrintWriter::close);
+    }
+
+    @Override
+    public void resolve(
+            final Throwable throwable,
+            final JsonWriter jsonWriter) {
+        final TruncatingBufferedPrintWriter writer = writerRecycler.acquire();
+        try {
+            throwable.printStackTrace(writer);
+            jsonWriter.writeString(writer.getBuffer(), 0, writer.getPosition());
+        } finally {
+            writerRecycler.release(writer);
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolver.java
new file mode 100644
index 0000000..a251075
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolver.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+@FunctionalInterface
+public interface TemplateResolver<V> {
+
+    default boolean isFlattening() {
+        return false;
+    }
+
+    default boolean isResolvable() {
+        return true;
+    }
+
+    default boolean isResolvable(V value) {
+        return true;
+    }
+
+    void resolve(V value, JsonWriter jsonWriter);
+
+    default void resolve(V value, JsonWriter jsonWriter, boolean succeedingEntry) {
+        resolve(value, jsonWriter);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java
new file mode 100644
index 0000000..74687d2
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.Map;
+
+interface TemplateResolverContext<V, C extends TemplateResolverContext<V, C>> {
+
+    Class<C> getContextClass();
+
+    Map<String, TemplateResolverFactory<V, C, ? extends TemplateResolver<V>>> getResolverFactoryByName();
+
+    StrSubstitutor getSubstitutor();
+
+    JsonWriter getJsonWriter();
+
+}
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
new file mode 100644
index 0000000..dacc07b
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>, R extends TemplateResolver<V>> {
+
+    String getName();
+
+    R create(C context, String key);
+
+}
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
new file mode 100644
index 0000000..0ba10ce
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
@@ -0,0 +1,355 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.layout.json.template.util.JsonReader;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public enum TemplateResolvers {;
+
+    private static final TemplateResolver<?> EMPTY_ARRAY_RESOLVER =
+            (final Object ignored, final JsonWriter jsonWriter) -> {
+                jsonWriter.writeArrayStart();
+                jsonWriter.writeArrayEnd();
+            };
+
+    private static final TemplateResolver<?> EMPTY_OBJECT_RESOLVER =
+            (final Object ignored, final JsonWriter jsonWriter) -> {
+                jsonWriter.writeObjectStart();
+                jsonWriter.writeObjectEnd();
+            };
+
+    private static final TemplateResolver<?> NULL_RESOLVER =
+            (final Object ignored, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeNull();
+
+    public static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofTemplate(
+            final C context,
+            final String template) {
+
+        // Read the template.
+        final Object node;
+        try {
+            node = JsonReader.read(template);
+        } catch (final Exception error) {
+            final String message = String.format("failed parsing template (template=%s)", template);
+            throw new RuntimeException(message, error);
+        }
+
+        // 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());
+                }
+
+            }
+        }
+
+        // Resolve the template.
+        return ofObject(context, node);
+
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofObject(
+            final C context,
+            final Object object) {
+        if (object == null) {
+            @SuppressWarnings("unchecked")
+            final TemplateResolver<V> nullResolver = (TemplateResolver<V>) NULL_RESOLVER;
+            return nullResolver;
+        } else if (object instanceof List) {
+            @SuppressWarnings("unchecked")
+            final List<Object> list = (List<Object>) object;
+            return ofList(context, list);
+        } else if (object instanceof Map) {
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> map = (Map<String, Object>) object;
+            return ofMap(context, map);
+        } else if (object instanceof String) {
+            final String string = (String) object;
+            return ofString(context, string);
+        } else if (object instanceof Number) {
+            final Number number = (Number) object;
+            return ofNumber(number);
+        } else if (object instanceof Boolean) {
+            final boolean value = (boolean) object;
+            return ofBoolean(value);
+        } else {
+            final String message = String.format(
+                    "invalid JSON node type (class=%s)",
+                    object.getClass().getName());
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofList(
+            final C context,
+            final List<Object> list) {
+
+        // Create resolver for each children.
+        final List<TemplateResolver<V>> itemResolvers = list
+                .stream()
+                .map(item -> {
+                    final TemplateResolver<V> itemResolver = ofObject(context, item);
+                    if (itemResolver.isFlattening()) {
+                        throw new IllegalArgumentException(
+                                "flattening resolvers are not allowed in lists");
+                    }
+                    return itemResolver;
+                })
+                .collect(Collectors.toList());
+
+        // Short-circuit if the array is empty.
+        if (itemResolvers.isEmpty()) {
+            @SuppressWarnings("unchecked")
+            final TemplateResolver<V> emptyArrayResolver =
+                    (TemplateResolver<V>) EMPTY_ARRAY_RESOLVER;
+            return emptyArrayResolver;
+        }
+
+        // Create a parent resolver collecting each child resolver execution.
+        return (final V value, final JsonWriter jsonWriter) -> {
+            jsonWriter.writeArrayStart();
+            for (int itemResolverIndex = 0;
+                 itemResolverIndex < itemResolvers.size();
+                 itemResolverIndex++) {
+                if (itemResolverIndex > 0) {
+                    jsonWriter.writeSeparator();
+                }
+                final TemplateResolver<V> itemResolver = itemResolvers.get(itemResolverIndex);
+                itemResolver.resolve(value, jsonWriter);
+            }
+            jsonWriter.writeArrayEnd();
+        };
+
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofMap(
+            final C context,
+            final Map<String, Object> map) {
+
+        // Create resolver for each object field.
+        final List<String> fieldNames = new ArrayList<>();
+        final List<TemplateResolver<V>> fieldResolvers = new ArrayList<>();
+        map.forEach((fieldName, fieldValue) -> {
+            final TemplateResolver<V> fieldResolver = ofObject(context, fieldValue);
+            final boolean resolvable = fieldResolver.isResolvable();
+            if (resolvable) {
+                fieldNames.add(fieldName);
+                fieldResolvers.add(fieldResolver);
+            }
+        });
+
+        // Short-circuit if the object is empty.
+        final int fieldCount = fieldNames.size();
+        if (fieldCount == 0) {
+            @SuppressWarnings("unchecked")
+            final TemplateResolver<V> emptyObjectResolver =
+                    (TemplateResolver<V>) EMPTY_OBJECT_RESOLVER;
+            return emptyObjectResolver;
+        }
+
+        // Prepare field names to avoid escape and truncation costs at runtime.
+        final List<String> fieldPrefixes = fieldNames
+                .stream()
+                .map(fieldName -> {
+                    try (JsonWriter jsonWriter = context.getJsonWriter()) {
+                        jsonWriter.writeString(fieldName);
+                        jsonWriter.getStringBuilder().append(':');
+                        return jsonWriter.getStringBuilder().toString();
+                    }
+                })
+                .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;
+                }
+                final boolean succeedingEntry = resolvedFieldCount > 0;
+                if (fieldResolver.isFlattening()) {
+                    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++;
+                }
+            }
+            jsonWriter.writeObjectEnd();
+        };
+
+    }
+
+    private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofString(
+            final C context,
+            final String fieldValue) {
+
+        // 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);
+            }
+        }
+
+        // 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.
+
+        // Check if substitution needed at all. (Copied logic from
+        // AbstractJacksonLayout.valueNeedsLookup() method.)
+        final boolean substitutionNeeded = fieldValue.contains("${");
+        final JsonWriter contextJsonWriter = context.getJsonWriter();
+        if (substitutionNeeded) {
+
+            // Use Log4j substitutor with LogEvent.
+            if (EventResolverContext.class.isAssignableFrom(context.getContextClass())) {
+                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);
+                    }
+                };
+            }
+
+            // Use standalone Log4j substitutor.
+            else {
+                final String replacedText = context.getSubstitutor().replace(null, fieldValue);
+                if (replacedText == null) {
+                    // noinspection unchecked
+                    return (TemplateResolver<V>) NULL_RESOLVER;
+                } else {
+                    // Prepare the escaped replacement first.
+                    final String escapedReplacedText =
+                            contextJsonWriter.use(() ->
+                                    contextJsonWriter.writeString(replacedText));
+                    // Create a resolver dedicated to the escaped replacement.
+                    return (final V value, final JsonWriter jsonWriter) ->
+                            jsonWriter.writeRawString(escapedReplacedText);
+                }
+            }
+
+        }
+
+        // Write the field value as is. (Blank value check has already been done at the top.)
+        else {
+            final String escapedFieldValue =
+                    contextJsonWriter.use(() ->
+                            contextJsonWriter.writeString(fieldValue));
+            return (final V value, final JsonWriter jsonWriter) ->
+                    jsonWriter.writeRawString(escapedFieldValue);
+        }
+
+    }
+
+    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) ->
+                jsonWriter.writeRawString(numberString);
+    }
+
+    private static <V> TemplateResolver<V> ofBoolean(final boolean value) {
+        return (final V ignored, final JsonWriter jsonWriter) ->
+                jsonWriter.writeBoolean(value);
+    }
+
+}
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
new file mode 100644
index 0000000..afe8ecb
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+final class ThreadResolver implements EventResolver {
+
+    private static final EventResolver NAME_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final String threadName = logEvent.getThreadName();
+                jsonWriter.writeString(threadName);
+            };
+
+    private static final EventResolver ID_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final long threadId = logEvent.getThreadId();
+                jsonWriter.writeNumber(threadId);
+            };
+
+    private static final EventResolver PRIORITY_RESOLVER =
+            (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+                final int threadPriority = logEvent.getThreadPriority();
+                jsonWriter.writeNumber(threadPriority);
+            };
+
+    private final EventResolver internalResolver;
+
+    ThreadResolver(final String key) {
+        this.internalResolver = createInternalResolver(key);
+    }
+
+    private static EventResolver createInternalResolver(final String key) {
+        switch (key) {
+            case "name": return NAME_RESOLVER;
+            case "id": return ID_RESOLVER;
+            case "priority": return PRIORITY_RESOLVER;
+        }
+        throw new IllegalArgumentException("unknown key: " + key);
+    }
+
+    static String getName() {
+        return "thread";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
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
new file mode 100644
index 0000000..266c982
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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 ThreadResolverFactory implements EventResolverFactory<ThreadResolver> {
+
+    private static final ThreadResolverFactory INSTANCE = new ThreadResolverFactory();
+
+    private ThreadResolverFactory() {}
+
+    static ThreadResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return ThreadResolver.getName();
+    }
+
+    @Override
+    public ThreadResolver create(final EventResolverContext context, final String key) {
+        return new ThreadResolver(key);
+    }
+
+}
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
new file mode 100644
index 0000000..c7c32b0
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
@@ -0,0 +1,446 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.LogEvent;
+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;
+
+final class TimestampResolver implements EventResolver {
+
+    private final EventResolver internalResolver;
+
+    TimestampResolver(final String key) {
+        this.internalResolver = (key != null && key.startsWith("epoch:"))
+                ? createEpochResolver(key)
+                : createFormatResolver(key);
+    }
+
+    /**
+     * Context for GC-free formatted timestamp resolver.
+     */
+    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;
+
+        private final StringBuilder formattedTimestampBuilder;
+
+        private FormatResolverContext(
+                final TimeZone timeZone,
+                final Locale locale,
+                final FastDateFormat timestampFormat) {
+            this.timestampFormat = timestampFormat;
+            this.formattedTimestampBuilder = new StringBuilder();
+            this.calendar = Calendar.getInstance(timeZone, locale);
+            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);
+            final FastDateFormat fastDateFormat =
+                    FastDateFormat.getInstance(pattern, 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) {
+                return JsonTemplateLayoutDefaults.getTimestampFormatPattern();
+            }
+            final String pattern = patternValue.toString();
+            try {
+                FastDateFormat.getInstance(pattern);
+            } catch (final IllegalArgumentException error) {
+                throw new IllegalArgumentException(
+                        "invalid pattern in timestamp key: " + pattern,
+                        error);
+            }
+            return pattern;
+        }
+
+        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) {
+                return JsonTemplateLayoutDefaults.getTimeZone();
+            }
+            final String timeZoneId = timeZoneValue.toString();
+            boolean found = false;
+            for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
+                if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                throw new IllegalArgumentException(
+                        "invalid time zone in timestamp key: " + timeZoneId);
+            }
+            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) {
+                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);
+            }
+        }
+
+    }
+
+    /**
+     * GC-free formatted timestamp resolver.
+     */
+    private static final class FormatResolver implements EventResolver {
+
+        private final FormatResolverContext formatResolverContext;
+
+        private FormatResolver(final FormatResolverContext formatResolverContext) {
+            this.formatResolverContext = formatResolverContext;
+        }
+
+        @Override
+        public synchronized void resolve(
+                final LogEvent logEvent,
+                final JsonWriter jsonWriter) {
+
+            // Format timestamp if it doesn't match the last cached one.
+            final long timestampMillis = logEvent.getTimeMillis();
+            if (formatResolverContext.calendar.getTimeInMillis() != timestampMillis) {
+
+                // Format the timestamp.
+                formatResolverContext.formattedTimestampBuilder.setLength(0);
+                formatResolverContext.calendar.setTimeInMillis(timestampMillis);
+                formatResolverContext.timestampFormat.format(
+                        formatResolverContext.calendar,
+                        formatResolverContext.formattedTimestampBuilder);
+
+                // Write the formatted timestamp.
+                final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+                final int startIndex = jsonWriterStringBuilder.length();
+                jsonWriter.writeString(formatResolverContext.formattedTimestampBuilder);
+
+                // Cache the written value.
+                formatResolverContext.formattedTimestampBuilder.setLength(0);
+                formatResolverContext.formattedTimestampBuilder.append(
+                        jsonWriterStringBuilder,
+                        startIndex,
+                        jsonWriterStringBuilder.length());
+
+            }
+
+            // Write the cached formatted timestamp.
+            else {
+                jsonWriter.writeRawString(
+                        formatResolverContext.formattedTimestampBuilder);
+            }
+
+        }
+
+    }
+
+    private static EventResolver createFormatResolver(final String key) {
+        final FormatResolverContext formatResolverContext =
+                FormatResolverContext.fromKey(key);
+        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 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 =
+                String.valueOf(Long.MAX_VALUE).length();
+
+        private Instant instant;
+
+        private char[] resolution = new char[/* integral: */MAX_LONG_LENGTH + /* dot: */1 + /* fractional: */MAX_LONG_LENGTH ];
+
+        private int resolutionLength;
+
+        private EpochResolutionRecord() {}
+
+    }
+
+    private static abstract class EpochResolver implements EventResolver {
+
+        private final EpochResolutionRecord resolutionRecord =
+                new EpochResolutionRecord();
+
+        @Override
+        public synchronized void resolve(
+                final LogEvent logEvent,
+                final JsonWriter jsonWriter) {
+            final Instant logEventInstant = logEvent.getInstant();
+            if (logEventInstant.equals(resolutionRecord.instant)) {
+                jsonWriter.writeRawString(
+                        resolutionRecord.resolution,
+                        0,
+                        resolutionRecord.resolutionLength);
+            } else {
+                resolutionRecord.instant = logEventInstant;
+                final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
+                final int startIndex = stringBuilder.length();
+                resolve(logEventInstant, jsonWriter);
+                resolutionRecord.resolutionLength = stringBuilder.length() - startIndex;
+                stringBuilder.getChars(
+                        startIndex,
+                        stringBuilder.length(),
+                        resolutionRecord.resolution,
+                        0);
+            }
+        }
+
+        abstract void resolve(Instant logEventInstant, JsonWriter jsonWriter);
+
+    }
+
+    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 EventResolver createEpochSecsNanosResolver() {
+        return new EpochResolver() {
+            @Override
+            void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+                jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
+            }
+        };
+    }
+
+    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 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 long epochNanos(Instant instant) {
+        return NANOS_PER_SEC * instant.getEpochSecond() + instant.getNanoOfSecond();
+    }
+
+    static String getName() {
+        return "timestamp";
+    }
+
+    @Override
+    public void resolve(
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        internalResolver.resolve(logEvent, jsonWriter);
+    }
+
+}
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
new file mode 100644
index 0000000..638df83
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolverFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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 TimestampResolverFactory implements EventResolverFactory<TimestampResolver> {
+
+    private static final TimestampResolverFactory INSTANCE = new TimestampResolverFactory();
+
+    private TimestampResolverFactory() {}
+
+    static TimestampResolverFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public String getName() {
+        return TimestampResolver.getName();
+    }
+
+    @Override
+    public TimestampResolver create(
+            final EventResolverContext context,
+            final String key) {
+        return new TimestampResolver(key);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java
new file mode 100644
index 0000000..2aae11f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java
@@ -0,0 +1,37 @@
+/*
+ * 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.function.Supplier;
+
+public class DummyRecycler<V> implements Recycler<V> {
+
+    private final Supplier<V> supplier;
+
+    public DummyRecycler(final Supplier<V> supplier) {
+        this.supplier = supplier;
+    }
+
+    @Override
+    public V acquire() {
+        return supplier.get();
+    }
+
+    @Override
+    public void release(final V value) {}
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecyclerFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecyclerFactory.java
new file mode 100644
index 0000000..dc3a8a1
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecyclerFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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.function.Consumer;
+import java.util.function.Supplier;
+
+public class DummyRecyclerFactory implements RecyclerFactory {
+
+    private static final DummyRecyclerFactory INSTANCE = new DummyRecyclerFactory();
+
+    private DummyRecyclerFactory() {}
+
+    public static DummyRecyclerFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public <V> Recycler<V> create(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner) {
+        return new DummyRecycler<V>(supplier);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonReader.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonReader.java
new file mode 100644
index 0000000..1a9f43e
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonReader.java
@@ -0,0 +1,447 @@
+/*
+ * 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.math.BigDecimal;
+import java.math.BigInteger;
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A simple JSON parser mapping tokens to basic Java types.
+ * <p>
+ * The type mapping is as follows:
+ * <p>
+ * <ul>
+ * <li><tt>object</tt>s are mapped to {@link LinkedHashMap LinkedHashMap&lt;String,Object&gt;}
+ * <li><tt>array</tt>s are mapped to {@link LinkedList}
+ * <li><tt>string</tt>s are mapped to {@link String} with proper Unicode and
+ * escape character conversion
+ * <li><tt>true</tt>, <tt>false</tt>, and <tt>null</tt> are mapped to their Java
+ * counterparts
+ * <li>floating point <tt>number</tt>s are mapped to {@link BigDecimal}
+ * <li>integral <tt>number</tt>s are mapped to either primitive types
+ * (<tt>int</tt>, <tt>long</tt>) or {@link BigInteger}
+ * </ul>
+ * <p>
+ * This code is heavily influenced by the reader of
+ * <a href="https://github.com/bolerio/mjson/blob/e7a4da2daa6e17a63ec057948bc30818e8f44686/src/java/mjson/Json.java#L2684">mjson</a>.
+ */
+public final class JsonReader {
+
+    private enum Delimiter {
+
+        OBJECT_START("{"),
+
+        OBJECT_END("}"),
+
+        ARRAY_START("["),
+
+        ARRAY_END("]"),
+
+        COLON(":"),
+
+        COMMA(",");
+
+        private final String string;
+
+        Delimiter(final String string) {
+            this.string = string;
+        }
+
+        private static boolean exists(final Object token) {
+            for (Delimiter delimiter : values()) {
+                if (delimiter.string.equals(token)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+    }
+
+    private CharacterIterator it;
+
+    private int readCharIndex = -1;
+
+    private char readChar;
+
+    private int readTokenStartIndex = -1;
+
+    private Object readToken;
+
+    private StringBuilder buffer = new StringBuilder();
+
+    private JsonReader() {}
+
+    public static Object read(final String string) {
+        final JsonReader reader = new JsonReader();
+        return reader.read(new StringCharacterIterator(string));
+    }
+
+    private Object read(final CharacterIterator ci) {
+        it = ci;
+        readCharIndex = 0;
+        readChar = it.first();
+        final Object token = readToken();
+        if (token instanceof Delimiter) {
+            final String message = String.format(
+                    "was not expecting %s at index %d",
+                    readToken, readTokenStartIndex);
+            throw new IllegalArgumentException(message);
+        }
+        skipWhiteSpace();
+        if (it.getIndex() != it.getEndIndex()) {
+            final String message = String.format(
+                    "was not expecting input at index %d: %c",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+        return token;
+    }
+
+    private Object readToken() {
+        skipWhiteSpace();
+        readTokenStartIndex = readCharIndex;
+        final char prevChar = readChar;
+        readChar();
+        switch (prevChar) {
+
+            case '"':
+                readToken = readString();
+                break;
+
+            case '[':
+                readToken = readArray();
+                break;
+
+            case ']':
+                readToken = Delimiter.ARRAY_END;
+                break;
+
+            case ',':
+                readToken = Delimiter.COMMA;
+                break;
+
+            case '{':
+                readToken = readObject();
+                break;
+
+            case '}':
+                readToken = Delimiter.OBJECT_END;
+                break;
+
+            case ':':
+                readToken = Delimiter.COLON;
+                break;
+
+            case 't':
+                readToken = readTrue();
+                break;
+
+            case 'f':
+                readToken = readFalse();
+                break;
+
+            case 'n':
+                readToken = readNull();
+                break;
+
+            default:
+                unreadChar();
+                if (Character.isDigit(readChar) || readChar == '-') {
+                    readToken = readNumber();
+                } else {
+                    String message = String.format(
+                            "invalid character at index %d: %c",
+                            readCharIndex, readChar);
+                    throw new IllegalArgumentException(message);
+                }
+
+        }
+        return readToken;
+    }
+
+    private void skipWhiteSpace() {
+        do {
+            if (!Character.isWhitespace(readChar)) {
+                break;
+            }
+        } while (readChar() != CharacterIterator.DONE);
+    }
+
+    private char readChar() {
+        if (it.getIndex() == it.getEndIndex()) {
+            throw new IllegalArgumentException("premature end of input");
+        }
+        readChar = it.next();
+        readCharIndex = it.getIndex();
+        return readChar;
+    }
+
+    private void unreadChar() {
+        readChar = it.previous();
+        readCharIndex = it.getIndex();
+    }
+
+    private String readString() {
+        buffer.setLength(0);
+        while (readChar != '"') {
+            if (readChar == '\\') {
+                readChar();
+                if (readChar == 'u') {
+                    final char unicodeChar = readUnicodeChar();
+                    bufferChar(unicodeChar);
+                } else {
+                    switch (readChar) {
+                        case '"':
+                        case '\\':
+                            bufferReadChar();
+                            break;
+                        case 'b':
+                            bufferChar('\b');
+                            break;
+                        case 'f':
+                            bufferChar('\f');
+                            break;
+                        case 'n':
+                            bufferChar('\n');
+                            break;
+                        case 'r':
+                            bufferChar('\r');
+                            break;
+                        case 't':
+                            bufferChar('\t');
+                            break;
+                        default: {
+                            final String message = String.format(
+                                    "was expecting an escape character at index %d: %c",
+                                    readCharIndex, readChar);
+                            throw new IllegalArgumentException(message);
+                        }
+                    }
+                }
+            } else {
+                bufferReadChar();
+            }
+        }
+        readChar();
+        return buffer.toString();
+    }
+
+    private void bufferReadChar() {
+        bufferChar(readChar);
+    }
+
+    private void bufferChar(final char c) {
+        buffer.append(c);
+        readChar();
+    }
+
+    private char readUnicodeChar() {
+        int value = 0;
+        for (int i = 0; i < 4; i++) {
+            readChar();
+            if (readChar >= '0' && readChar <= '9') {
+                value = (value << 4) + readChar - '0';
+            } else if (readChar >= 'a' && readChar <= 'f') {
+                value = (value << 4) + (readChar - 'a') + 10;
+            } else if (readChar >= 'A' && readChar <= 'F') {
+                value = (value << 4) + (readChar - 'A') + 10;
+            } else {
+                final String message = String.format(
+                        "was expecting a unicode character at index %d: %c",
+                        readCharIndex, readChar);
+                throw new IllegalArgumentException(message);
+            }
+        }
+        return (char) value;
+    }
+
+    private Map<String, Object> readObject() {
+        final Map<String, Object> object = new LinkedHashMap<>();
+        String key = readObjectKey();
+        while (readToken != Delimiter.OBJECT_END) {
+            expectDelimiter(Delimiter.COLON, readToken());
+            if (readToken != Delimiter.OBJECT_END) {
+                Object value = readToken();
+                object.put(key, value);
+                if (readToken() == Delimiter.COMMA) {
+                    key = readObjectKey();
+                    if (key == null || Delimiter.exists(key)) {
+                        String message = String.format(
+                                "was expecting an object key at index %d: %s",
+                                readTokenStartIndex, readToken);
+                        throw new IllegalArgumentException(message);
+                    }
+                } else {
+                    expectDelimiter(Delimiter.OBJECT_END, readToken);
+                }
+            }
+        }
+        return object;
+    }
+
+    private List<Object> readArray() {
+        @SuppressWarnings("JdkObsolete")
+        final List<Object> array = new LinkedList<>();
+        readToken();
+        while (readToken != Delimiter.ARRAY_END) {
+            if (readToken instanceof Delimiter) {
+                final String message = String.format(
+                        "was expecting an array element at index %d: %s",
+                        readTokenStartIndex, readToken);
+                throw new IllegalArgumentException(message);
+            }
+            array.add(readToken);
+            if (readToken() == Delimiter.COMMA) {
+                if (readToken() == Delimiter.ARRAY_END) {
+                    final String message = String.format(
+                            "was expecting an array element at index %d: %s",
+                            readTokenStartIndex, readToken);
+                    throw new IllegalArgumentException(message);
+                }
+            } else {
+                expectDelimiter(Delimiter.ARRAY_END, readToken);
+            }
+        }
+        return array;
+    }
+
+    private String readObjectKey() {
+        readToken();
+        if (readToken == Delimiter.OBJECT_END) {
+            return null;
+        } else if (readToken instanceof String) {
+            return (String) readToken;
+        } else {
+            final String message = String.format(
+                    "was expecting an object key at index %d: %s",
+                    readTokenStartIndex, readToken);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    private void expectDelimiter(
+            final Delimiter expectedDelimiter,
+            final Object actualToken) {
+        if (!expectedDelimiter.equals(actualToken)) {
+            String message = String.format(
+                    "was expecting %s at index %d: %s",
+                    expectedDelimiter, readTokenStartIndex, actualToken);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    private boolean readTrue() {
+        if (readChar != 'r' || readChar() != 'u' || readChar() != 'e') {
+            String message = String.format(
+                    "was expecting keyword 'true' at index %d: %s",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+        readChar();
+        return true;
+    }
+
+    private boolean readFalse() {
+        if (readChar != 'a' || readChar() != 'l' || readChar() != 's' || readChar() != 'e') {
+            String message = String.format(
+                    "was expecting keyword 'false' at index %d: %s",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+        readChar();
+        return false;
+    }
+
+    private Object readNull() {
+        if (readChar != 'u' || readChar() != 'l' || readChar() != 'l') {
+            String message = String.format(
+                    "was expecting keyword 'null' at index %d: %s",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+        readChar();
+        return null;
+    }
+
+    private Number readNumber() {
+
+        // Read sign.
+        buffer.setLength(0);
+        if (readChar == '-') {
+            bufferReadChar();
+        }
+
+        // Read fraction.
+        boolean floatingPoint = false;
+        bufferDigits();
+        if (readChar == '.') {
+            bufferReadChar();
+            bufferDigits();
+            floatingPoint = true;
+        }
+
+        // Read exponent.
+        if (readChar == 'e' || readChar == 'E') {
+            floatingPoint = true;
+            bufferReadChar();
+            if (readChar == '+' || readChar == '-') {
+                bufferReadChar();
+            }
+            bufferDigits();
+        }
+
+        // Convert the read number.
+        final String string = buffer.toString();
+        if (floatingPoint) {
+            return new BigDecimal(string);
+        } else {
+            final BigInteger bigInteger = new BigInteger(string);
+            try {
+                return bigInteger.intValueExact();
+            } catch (ArithmeticException ignoredIntOverflow) {
+                try {
+                    return bigInteger.longValueExact();
+                } catch (ArithmeticException ignoredLongOverflow) {
+                    return bigInteger;
+                }
+            }
+        }
+
+    }
+
+    private void bufferDigits() {
+        boolean found = false;
+        while (Character.isDigit(readChar)) {
+            found = true;
+            bufferReadChar();
+        }
+        if (!found) {
+            final String message = String.format(
+                    "was expecting a digit at index %d: %c",
+                    readCharIndex, readChar);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonWriter.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonWriter.java
new file mode 100644
index 0000000..a0dad93
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/JsonWriter.java
@@ -0,0 +1,889 @@
+/*
+ * 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 org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.apache.logging.log4j.util.StringMap;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A simple JSON writer with support for common Java data types.
+ * <p>
+ * The following types have specific handlers:
+ * <p>
+ * <ul>
+ *     <li> <tt>null</tt> input
+ *     <li>{@link Map}, {@link IndexedReadOnlyStringMap}, {@link StringMap}
+ *     <li>{@link Collection} and {@link List}
+ *     <li>{@link Number} ({@link BigDecimal}, {@link BigInteger}, {@link Float},
+ *     {@link Double}, {@link Byte}, {@link Short}, {@link Integer}, and
+ *     {@link Long})
+ *     <li>{@link Boolean}
+ *     <li>{@link StringBuilderFormattable}
+ *     <li>arrays of primitve types
+ *     <tt>char/boolean/byte/short/int/long/float/double</tt> and {@link Object}
+ *     <li>{@link CharSequence} and <tt>char[]</tt> with necessary escaping
+ * </ul>
+ * <p>
+ * JSON standard quoting routines are borrowed from
+ * <a href="https://github.com/FasterXML/jackson-core">Jackson</a>.
+ */
+public final class JsonWriter implements AutoCloseable, Cloneable {
+
+    private final static char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
+
+    /**
+     * Lookup table used for determining which output characters in 7-bit ASCII
+     * range (i.e., first 128 Unicode code points, single-byte UTF-8 characters)
+     * need to be quoted.
+     *<p>
+     * Value of 0 means "no escaping"; other positive values, that value is
+     * character to use after backslash; and negative values, that generic
+     * (backslash - u) escaping is to be used.
+     */
+    private final static int[] ESC_CODES;
+    static {
+        int[] table = new int[128];
+        // Control chars need generic escape sequence
+        for (int i = 0; i < 32; ++i) {
+            // 04-Mar-2011, tatu: Used to use "-(i + 1)", replaced with constant
+            table[i] = -1;
+        }
+        // Others (and some within that range too) have explicit shorter sequences
+        table['"'] = '"';
+        table['\\'] = '\\';
+        // Escaping of slash is optional, so let's not add it
+        table[0x08] = 'b';
+        table[0x09] = 't';
+        table[0x0C] = 'f';
+        table[0x0A] = 'n';
+        table[0x0D] = 'r';
+        ESC_CODES = table;
+    }
+
+    private final char[] quoteBuffer;
+
+    private final StringBuilder stringBuilder;
+
+    private final StringBuilder formattableBuffer;
+
+    private final int maxStringLength;
+
+    private final String truncatedStringSuffix;
+
+    private final String quotedTruncatedStringSuffix;
+
+    private JsonWriter(final Builder builder) {
+        this.quoteBuffer = new char[]{'\\', '-', '0', '0', '-', '-'};
+        this.stringBuilder = new StringBuilder();
+        this.formattableBuffer = new StringBuilder();
+        this.maxStringLength = builder.maxStringLength;
+        this.truncatedStringSuffix = builder.truncatedStringSuffix;
+        this.quotedTruncatedStringSuffix = quoteString(builder.truncatedStringSuffix);
+    }
+
+    private String quoteString(final String string) {
+        final int startIndex = stringBuilder.length();
+        quoteString(string, 0, string.length());
+        final StringBuilder quotedStringBuilder = new StringBuilder();
+        quotedStringBuilder.append(stringBuilder, startIndex, stringBuilder.length());
+        final String quotedString = quotedStringBuilder.toString();
+        stringBuilder.setLength(startIndex);
+        return quotedString;
+    }
+
+    public String use(Runnable runnable) {
+        final int startIndex = stringBuilder.length();
+        runnable.run();
+        final StringBuilder sliceStringBuilder = new StringBuilder();
+        sliceStringBuilder.append(stringBuilder, startIndex, stringBuilder.length());
+        stringBuilder.setLength(startIndex);
+        return sliceStringBuilder.toString();
+    }
+
+    public StringBuilder getStringBuilder() {
+        return stringBuilder;
+    }
+
+    public int getMaxStringLength() {
+        return maxStringLength;
+    }
+
+    public String getTruncatedStringSuffix() {
+        return truncatedStringSuffix;
+    }
+
+    public void writeValue(final Object value) {
+
+        // null
+        if (value == null) {
+            writeNull();
+        }
+
+        // map
+        else if (value instanceof IndexedReadOnlyStringMap) {
+            final IndexedReadOnlyStringMap map = (IndexedReadOnlyStringMap) value;
+            writeObject(map);
+        } else if (value instanceof StringMap) {
+            final StringMap map = (StringMap) value;
+            writeObject(map);
+        } else if (value instanceof Map) {
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> map = (Map<String, Object>) value;
+            writeObject(map);
+        }
+
+        // list & collection
+        else if (value instanceof List) {
+            @SuppressWarnings("unchecked")
+            final List<Object> list = (List<Object>) value;
+            writeArray(list);
+        } else if (value instanceof Collection) {
+            @SuppressWarnings("unchecked")
+            final Collection<Object> collection = (Collection<Object>) value;
+            writeArray(collection);
+        }
+
+        // number & boolean
+        else if (value instanceof Number) {
+            final Number number = (Number) value;
+            writeNumber(number);
+        } else if (value instanceof Boolean) {
+            final boolean booleanValue = (boolean) value;
+            writeBoolean(booleanValue);
+        }
+
+        // formattable
+        else if (value instanceof StringBuilderFormattable) {
+            final StringBuilderFormattable formattable = (StringBuilderFormattable) value;
+            writeString(formattable);
+        }
+
+        // arrays
+        else if (value instanceof char[]) {
+            final char[] charValues = (char[]) value;
+            writeArray(charValues);
+        } else if (value instanceof boolean[]) {
+            final boolean[] booleanValues = (boolean[]) value;
+            writeArray(booleanValues);
+        } else if (value instanceof byte[]) {
+            final byte[] byteValues = (byte[]) value;
+            writeArray(byteValues);
+        } else if (value instanceof short[]) {
+            final short[] shortValues = (short[]) value;
+            writeArray(shortValues);
+        } else if (value instanceof int[]) {
+            final int[] intValues = (int[]) value;
+            writeArray(intValues);
+        } else if (value instanceof long[]) {
+            final long[] longValues = (long[]) value;
+            writeArray(longValues);
+        } else if (value instanceof float[]) {
+            final float[] floatValues = (float[]) value;
+            writeArray(floatValues);
+        } else if (value instanceof double[]) {
+            final double[] doubleValues = (double[]) value;
+            writeArray(doubleValues);
+        } else if (value instanceof Object[]) {
+            final Object[] values = (Object[]) value;
+            writeArray(values);
+        }
+
+        // string
+        else {
+            final String stringValue = value instanceof String
+                    ? (String) value
+                    : String.valueOf(value);
+            writeString(stringValue);
+        }
+
+    }
+
+    public void writeObject(final StringMap map) {
+        if (map == null) {
+            writeNull();
+        } else {
+            writeObjectStart();
+            final boolean[] firstEntry = {true};
+            map.forEach((final String key, final Object value) -> {
+                if (key == null) {
+                    throw new IllegalArgumentException("null keys are not allowed");
+                }
+                if (firstEntry[0]) {
+                    firstEntry[0] = false;
+                } else {
+                    writeSeparator();
+                }
+                writeObjectKey(key);
+                writeValue(value);
+            });
+            writeObjectEnd();
+        }
+    }
+
+    public void writeObject(final IndexedReadOnlyStringMap map) {
+        if (map == null) {
+            writeNull();
+        } else {
+            writeObjectStart();
+            for (int entryIndex = 0; entryIndex < map.size(); entryIndex++) {
+                final String key = map.getKeyAt(entryIndex);
+                final Object value = map.getValueAt(entryIndex);
+                if (entryIndex > 0) {
+                    writeSeparator();
+                }
+                writeObjectKey(key);
+                writeValue(value);
+            }
+            writeObjectEnd();
+        }
+    }
+
+    public void writeObject(final Map<String, Object> map) {
+        if (map == null) {
+            writeNull();
+        } else {
+            writeObjectStart();
+            final boolean[] firstEntry = {true};
+            map.forEach((final String key, final Object value) -> {
+                if (key == null) {
+                    throw new IllegalArgumentException("null keys are not allowed");
+                }
+                if (firstEntry[0]) {
+                    firstEntry[0] = false;
+                } else {
+                    writeSeparator();
+                }
+                writeObjectKey(key);
+                writeValue(value);
+            });
+            writeObjectEnd();
+        }
+    }
+
+    public void writeObjectStart() {
+        stringBuilder.append('{');
+    }
+
+    public void writeObjectEnd() {
+        stringBuilder.append('}');
+    }
+
+    public void writeObjectKey(final CharSequence key) {
+        writeString(key);
+        stringBuilder.append(':');
+    }
+
+    public void writeArray(final List<Object> items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final Object item = items.get(itemIndex);
+                writeValue(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final Collection<Object> items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            final boolean[] firstItem = {true};
+            items.forEach((final Object item) -> {
+                if (firstItem[0]) {
+                    firstItem[0] = false;
+                } else {
+                    writeSeparator();
+                }
+                writeValue(item);
+            });
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final char[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                stringBuilder.append('"');
+                quoteString(items, itemIndex, 1);
+                stringBuilder.append('"');
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final boolean[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final boolean item = items[itemIndex];
+                writeBoolean(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final byte[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final byte item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final short[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final short item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final int[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final int item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final long[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final long item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final float[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final float item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final double[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final double item = items[itemIndex];
+                writeNumber(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArray(final Object[] items) {
+        if (items == null) {
+            writeNull();
+        } else {
+            writeArrayStart();
+            for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+                if (itemIndex > 0) {
+                    writeSeparator();
+                }
+                final Object item = items[itemIndex];
+                writeValue(item);
+            }
+            writeArrayEnd();
+        }
+    }
+
+    public void writeArrayStart() {
+        stringBuilder.append('[');
+    }
+
+    public void writeArrayEnd() {
+        stringBuilder.append(']');
+    }
+
+    public void writeSeparator() {
+        stringBuilder.append(',');
+    }
+
+    public <S> void writeString(
+            final BiConsumer<StringBuilder, S> emitter,
+            final S state) {
+        Objects.requireNonNull(emitter, "emitter");
+        stringBuilder.append('"');
+        formattableBuffer.setLength(0);
+        emitter.accept(formattableBuffer, state);
+        final int length = formattableBuffer.length();
+        // Handle max. string length complying input.
+        if (length <= maxStringLength) {
+            quoteString(formattableBuffer, 0, length);
+        }
+        // Handle max. string length violating input.
+        else {
+            quoteString(formattableBuffer, 0, maxStringLength);
+            stringBuilder.append(quotedTruncatedStringSuffix);
+        }
+        stringBuilder.append('"');
+    }
+
+    public void writeString(final StringBuilderFormattable formattable) {
+        if (formattable == null) {
+            writeNull();
+        } else {
+            stringBuilder.append('"');
+            formattableBuffer.setLength(0);
+            formattable.formatTo(formattableBuffer);
+            final int length = formattableBuffer.length();
+            // Handle max. string length complying input.
+            if (length <= maxStringLength) {
+                quoteString(formattableBuffer, 0, length);
+            }
+            // Handle max. string length violating input.
+            else {
+                quoteString(formattableBuffer, 0, maxStringLength);
+                stringBuilder.append(quotedTruncatedStringSuffix);
+            }
+            stringBuilder.append('"');
+        }
+    }
+
+    public void writeString(final CharSequence seq) {
+        if (seq == null) {
+            writeNull();
+        } else {
+            writeString(seq, 0, seq.length());
+        }
+    }
+
+    public void writeString(
+            final CharSequence seq,
+            final int offset,
+            final int length) {
+
+        // Handle null input.
+        if (seq == null) {
+            writeNull();
+            return;
+        }
+
+        // Check arguments.
+        if (offset < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive offset: " + offset);
+        }
+        if (length < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive length: " + length);
+        }
+
+        stringBuilder.append('"');
+        // Handle max. string length complying input.
+        if (length <= maxStringLength) {
+            quoteString(seq, offset, length);
+        }
+        // Handle max. string length violating input.
+        else {
+            quoteString(seq, offset, maxStringLength);
+            stringBuilder.append(quotedTruncatedStringSuffix);
+        }
+        stringBuilder.append('"');
+
+    }
+
+    /**
+     * Quote text contents using JSON standard quoting.
+     */
+    private void quoteString(
+            final CharSequence seq,
+            final int offset,
+            final int length) {
+        final int limit = offset + length;
+        int i = offset;
+        outer:
+        while (i < limit) {
+            while (true) {
+                final char c = seq.charAt(i);
+                if (c < ESC_CODES.length && ESC_CODES[c] != 0) {
+                    break;
+                }
+                stringBuilder.append(c);
+                if (++i >= limit) {
+                    break outer;
+                }
+            }
+            final char d = seq.charAt(i++);
+            final int escCode = ESC_CODES[d];
+            final int quoteBufferLength = escCode < 0
+                    ? quoteNumeric(d)
+                    : quoteNamed(escCode);
+            stringBuilder.append(quoteBuffer, 0, quoteBufferLength);
+        }
+    }
+
+    public void writeString(final char[] buffer) {
+        if (buffer == null) {
+            writeNull();
+        } else {
+            writeString(buffer, 0, buffer.length);
+        }
+    }
+
+    public void writeString(
+            final char[] buffer,
+            final int offset,
+            final int length) {
+
+        // Handle null input.
+        if (buffer == null) {
+            writeNull();
+            return;
+        }
+
+        // Check arguments.
+        if (offset < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive offset: " + offset);
+        }
+        if (length < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive length: " + length);
+        }
+
+        stringBuilder.append('"');
+        // Handle max. string length complying input.
+        if (length <= maxStringLength) {
+            quoteString(buffer, offset, length);
+        }
+        // Handle max. string length violating input.
+        else {
+            quoteString(buffer, offset, maxStringLength);
+            stringBuilder.append(quotedTruncatedStringSuffix);
+        }
+        stringBuilder.append('"');
+
+    }
+
+    /**
+     * Quote text contents using JSON standard quoting.
+     */
+    private void quoteString(
+            final char[] buffer,
+            final int offset,
+            final int length) {
+        final int limit = offset + length;
+        int i = offset;
+        outer:
+        while (i < limit) {
+            while (true) {
+                final char c = buffer[i];
+                if (c < ESC_CODES.length && ESC_CODES[c] != 0) {
+                    break;
+                }
+                stringBuilder.append(c);
+                if (++i >= limit) {
+                    break outer;
+                }
+            }
+            final char d = buffer[i++];
+            final int escCode = ESC_CODES[d];
+            final int quoteBufferLength = escCode < 0
+                    ? quoteNumeric(d)
+                    : quoteNamed(escCode);
+            stringBuilder.append(quoteBuffer, 0, quoteBufferLength);
+        }
+    }
+
+    private int quoteNumeric(final int value) {
+        quoteBuffer[1] = 'u';
+        // We know it's a control char, so only the last 2 chars are non-0
+        quoteBuffer[4] = HEX_CHARS[value >> 4];
+        quoteBuffer[5] = HEX_CHARS[value & 0xF];
+        return 6;
+    }
+
+    private int quoteNamed(final int esc) {
+        quoteBuffer[1] = (char) esc;
+        return 2;
+    }
+
+    private void writeNumber(final Number number) {
+        if (number instanceof BigDecimal) {
+            final BigDecimal decimalNumber = (BigDecimal) number;
+            writeNumber(decimalNumber);
+        } else if (number instanceof BigInteger) {
+            final BigInteger integerNumber = (BigInteger) number;
+            writeNumber(integerNumber);
+        } else if (number instanceof Double) {
+            final double doubleNumber = (Double) number;
+            writeNumber(doubleNumber);
+        } else if (number instanceof Float) {
+            final float floatNumber = (float) number;
+            writeNumber(floatNumber);
+        } else if (number instanceof Byte ||
+                number instanceof Short ||
+                number instanceof Integer ||
+                number instanceof Long) {
+            final long longNumber = number.longValue();
+            writeNumber(longNumber);
+        } else {
+            final long longNumber = number.longValue();
+            final double doubleValue = number.doubleValue();
+            if (Double.compare(longNumber, doubleValue) == 0) {
+                writeNumber(longNumber);
+            } else {
+                writeNumber(doubleValue);
+            }
+        }
+    }
+
+    public void writeNumber(final BigDecimal number) {
+        if (number == null) {
+            writeNull();
+        } else {
+            stringBuilder.append(number);
+        }
+    }
+
+    public void writeNumber(final BigInteger number) {
+        if (number == null) {
+            writeNull();
+        } else {
+            stringBuilder.append(number);
+        }
+    }
+
+    public void writeNumber(final float number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final double number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final short number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final int number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final long number) {
+        stringBuilder.append(number);
+    }
+
+    public void writeNumber(final long integralPart, final long fractionalPart) {
+        if (fractionalPart < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive fraction: " + fractionalPart);
+        }
+        stringBuilder.append(integralPart);
+        if (fractionalPart != 0) {
+            stringBuilder.append('.');
+            stringBuilder.append(fractionalPart);
+        }
+    }
+
+    public void writeBoolean(final boolean value) {
+        writeRawString(value ? "true" : "false");
+    }
+
+    public void writeNull() {
+        writeRawString("null");
+    }
+
+    public void writeRawString(final CharSequence seq) {
+        Objects.requireNonNull(seq, "seq");
+        writeRawString(seq, 0, seq.length());
+    }
+
+    public void writeRawString(
+            final CharSequence seq,
+            final int offset,
+            final int length) {
+
+        // Check arguments.
+        Objects.requireNonNull(seq, "seq");
+        if (offset < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive offset: " + offset);
+        }
+        if (length < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive length: " + length);
+        }
+
+        // Write characters.
+        final int limit = offset + length;
+        stringBuilder.append(seq, offset, limit);
+
+    }
+
+    public void writeRawString(final char[] buffer) {
+        Objects.requireNonNull(buffer, "buffer");
+        writeRawString(buffer, 0, buffer.length);
+    }
+
+    public void writeRawString(
+            final char[] buffer,
+            final int offset,
+            final int length) {
+
+        // Check arguments.
+        Objects.requireNonNull(buffer, "buffer");
+        if (offset < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive offset: " + offset);
+        }
+        if (length < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive length: " + length);
+        }
+
+        // Write characters.
+        stringBuilder.append(buffer, offset, length);
+
+    }
+
+    @Override
+    public void close() {
+        stringBuilder.setLength(0);
+    }
+
+    @Override
+    @SuppressWarnings("MethodDoesntCallSuperMethod")
+    public JsonWriter clone() {
+        final JsonWriter jsonWriter = newBuilder()
+                .setMaxStringLength(maxStringLength)
+                .setTruncatedStringSuffix(truncatedStringSuffix)
+                .build();
+        jsonWriter.stringBuilder.append(stringBuilder);
+        return jsonWriter;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static final class Builder {
+
+        private int maxStringLength;
+
+        private String truncatedStringSuffix;
+
+        public int getMaxStringLength() {
+            return maxStringLength;
+        }
+
+        public Builder setMaxStringLength(final int maxStringLength) {
+            this.maxStringLength = maxStringLength;
+            return this;
+        }
+
+        public String getTruncatedStringSuffix() {
+            return truncatedStringSuffix;
+        }
+
+        public Builder setTruncatedStringSuffix(final String truncatedStringSuffix) {
+            this.truncatedStringSuffix = truncatedStringSuffix;
+            return this;
+        }
+
+        public JsonWriter build() {
+            validate();
+            return new JsonWriter(this);
+        }
+
+        private void validate() {
+            if (maxStringLength <= 0) {
+                throw new IllegalArgumentException(
+                        "was expecting maxStringLength > 0: " +
+                                maxStringLength);
+            }
+            Objects.requireNonNull(truncatedStringSuffix, "truncatedStringSuffix");
+        }
+
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecycler.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecycler.java
new file mode 100644
index 0000000..5f091bd
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecycler.java
@@ -0,0 +1,61 @@
+/*
+ * 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.Queue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class QueueingRecycler<V> implements Recycler<V> {
+
+    private final Supplier<V> supplier;
+
+    private final Consumer<V> cleaner;
+
+    private final Queue<V> queue;
+
+    public QueueingRecycler(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner,
+            final Queue<V> queue) {
+        this.supplier = supplier;
+        this.cleaner = cleaner;
+        this.queue = queue;
+    }
+
+    // Visible for tests.
+    Queue<V> getQueue() {
+        return queue;
+    }
+
+    @Override
+    public V acquire() {
+        final V value = queue.poll();
+        if (value == null) {
+            return supplier.get();
+        } else {
+            cleaner.accept(value);
+            return value;
+        }
+    }
+
+    @Override
+    public void release(final V value) {
+        queue.offer(value);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecyclerFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecyclerFactory.java
new file mode 100644
index 0000000..c549522
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/QueueingRecyclerFactory.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template.util;
+
+import java.util.Queue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class QueueingRecyclerFactory implements RecyclerFactory {
+
+    private final Supplier<Queue<Object>> queueSupplier;
+
+    public QueueingRecyclerFactory(final Supplier<Queue<Object>> queueSupplier) {
+        this.queueSupplier = queueSupplier;
+    }
+
+    @Override
+    public <V> Recycler<V> create(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner) {
+        @SuppressWarnings("unchecked")
+        final Queue<V> queue = (Queue<V>) queueSupplier.get();
+        return new QueueingRecycler<V>(supplier, cleaner, queue);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java
new file mode 100644
index 0000000..b6a0c89
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+public interface Recycler<V> {
+
+    V acquire();
+
+    void release(V value);
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactories.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactories.java
new file mode 100644
index 0000000..a6937c6
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactories.java
@@ -0,0 +1,205 @@
+/*
+ * 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 org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.convert.TypeConverter;
+import org.apache.logging.log4j.plugins.convert.TypeConverters;
+import org.apache.logging.log4j.util.LoaderUtil;
+import org.jctools.queues.MpmcArrayQueue;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.function.Supplier;
+
+public enum RecyclerFactories {;
+
+    private static final String JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH =
+            "org.jctools.queues.MpmcArrayQueue.new";
+
+    private static final boolean JCTOOLS_QUEUE_CLASS_AVAILABLE =
+            isJctoolsQueueClassAvailable();
+
+    private static boolean isJctoolsQueueClassAvailable() {
+        try {
+            final String className = JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH
+                    .replaceAll("\\.new$", "");
+            LoaderUtil.loadClass(className);
+            return true;
+        } catch (final ClassNotFoundException ignored) {
+            return false;
+        }
+    }
+
+    @Plugin(name = "RecyclerFactory", category = TypeConverters.CATEGORY)
+    public static final class RecyclerFactoryConverter implements TypeConverter<RecyclerFactory> {
+        @Override
+        public RecyclerFactory convert(final String recyclerFactorySpec) {
+            return ofSpec(recyclerFactorySpec);
+        }
+    }
+
+    public static RecyclerFactory ofSpec(final String recyclerFactorySpec) {
+
+        // Determine the default capacity.
+        int defaultCapacity = Math.max(
+                2 * Runtime.getRuntime().availableProcessors() + 1,
+                8);
+
+        // TLA-, MPMC-, or ABQ-based queueing factory -- if nothing is specified.
+        if (recyclerFactorySpec == null) {
+            if (Constants.ENABLE_THREADLOCALS) {
+                return ThreadLocalRecyclerFactory.getInstance();
+            } else {
+                final Supplier<Queue<Object>> queueSupplier =
+                        JCTOOLS_QUEUE_CLASS_AVAILABLE
+                                ? () -> new MpmcArrayQueue<>(defaultCapacity)
+                                : () -> new ArrayBlockingQueue<>(defaultCapacity);
+                return new QueueingRecyclerFactory(queueSupplier);
+            }
+        }
+
+        // Is a dummy factory requested?
+        else if (recyclerFactorySpec.equals("dummy")) {
+            return DummyRecyclerFactory.getInstance();
+        }
+
+        // Is a TLA factory requested?
+        else if (recyclerFactorySpec.equals("threadLocal")) {
+            return ThreadLocalRecyclerFactory.getInstance();
+        }
+
+        // Is a queueing factory requested?
+        else if (recyclerFactorySpec.startsWith("queue")) {
+            return readQueueingRecyclerFactory(recyclerFactorySpec, defaultCapacity);
+        }
+
+        // Bogus input, bail out.
+        else {
+            throw new IllegalArgumentException(
+                    "invalid recycler factory: " + recyclerFactorySpec);
+        }
+
+    }
+
+    private static RecyclerFactory readQueueingRecyclerFactory(
+            final String recyclerFactorySpec,
+            final int defaultCapacity) {
+
+        // Parse the spec.
+        final String queueFactorySpec = recyclerFactorySpec.substring(
+                "queue".length() +
+                        (recyclerFactorySpec.startsWith("queue:")
+                                ? 1
+                                : 0));
+        final Map<String, StringParameterParser.Value> parsedValues =
+                StringParameterParser.parse(
+                        queueFactorySpec,
+                        new LinkedHashSet<>(Arrays.asList("supplier", "capacity")));
+
+        // Read the supplier path.
+        final StringParameterParser.Value supplierValue = parsedValues.get("supplier");
+        final String supplierPath;
+        if (supplierValue == null || supplierValue instanceof StringParameterParser.NullValue) {
+            supplierPath = JCTOOLS_QUEUE_CLASS_AVAILABLE
+                    ? JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH
+                    : "java.util.concurrent.ArrayBlockingQueue.new";
+        } else {
+            supplierPath = supplierValue.toString();
+        }
+
+        // Read the capacity.
+        final StringParameterParser.Value capacityValue = parsedValues.get("capacity");
+        final int capacity;
+        if (capacityValue == null || capacityValue instanceof StringParameterParser.NullValue) {
+            capacity = defaultCapacity;
+        } else {
+            try {
+                capacity = Integer.parseInt(capacityValue.toString());
+            } catch (final NumberFormatException error) {
+                throw new IllegalArgumentException(
+                        "failed reading capacity in queueing recycler " +
+                                "factory: " + queueFactorySpec, error);
+            }
+        }
+
+        // Execute the read spec.
+        return createRecyclerFactory(queueFactorySpec, supplierPath, capacity);
+
+    }
+
+    private static RecyclerFactory createRecyclerFactory(
+            final String queueFactorySpec,
+            final String supplierPath,
+            final int capacity) {
+        final int supplierPathSplitterIndex = supplierPath.lastIndexOf('.');
+        if (supplierPathSplitterIndex < 0) {
+            throw new IllegalArgumentException(
+                    "invalid supplier in queueing recycler factory: " +
+                            queueFactorySpec);
+        }
+        final String supplierClassName = supplierPath.substring(0, supplierPathSplitterIndex);
+        final String supplierMethodName = supplierPath.substring(supplierPathSplitterIndex + 1);
+        try {
+            final Class<?> supplierClass = LoaderUtil.loadClass(supplierClassName);
+            final Supplier<Queue<Object>> queueSupplier;
+            if ("new".equals(supplierMethodName)) {
+                final Constructor<?> supplierCtor =
+                        supplierClass.getDeclaredConstructor(int.class);
+                queueSupplier = () -> {
+                    try {
+                        @SuppressWarnings("unchecked")
+                        final Queue<Object> typedQueue =
+                                (Queue<Object>) supplierCtor.newInstance(capacity);
+                        return typedQueue;
+                    } catch (final Exception error) {
+                        throw new RuntimeException(
+                                "recycler queue construction failed for factory: " +
+                                        queueFactorySpec, error);
+                    }
+                };
+            } else {
+                final Method supplierMethod =
+                        supplierClass.getMethod(supplierMethodName, int.class);
+                queueSupplier = () -> {
+                    try {
+                        @SuppressWarnings("unchecked")
+                        final Queue<Object> typedQueue =
+                                (Queue<Object>) supplierMethod.invoke(null, capacity);
+                        return typedQueue;
+                    } catch (final Exception error) {
+                        throw new RuntimeException(
+                                "recycler queue construction failed for factory: " +
+                                        queueFactorySpec, error);
+                    }
+                };
+            }
+            return new QueueingRecyclerFactory(queueSupplier);
+        } catch (final Exception error) {
+            throw new RuntimeException(
+                    "failed executing queueing recycler factory: " +
+                            queueFactorySpec, error);
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java
new file mode 100644
index 0000000..3b7737c
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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.function.Consumer;
+import java.util.function.Supplier;
+
+@FunctionalInterface
+public interface RecyclerFactory {
+
+    default <V> Recycler<V> create(Supplier<V> supplier) {
+        return create(supplier, ignored -> {});
+    }
+
+    <V> Recycler<V> create(Supplier<V> supplier, Consumer<V> cleaner);
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParser.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParser.java
new file mode 100644
index 0000000..018f6b7
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParser.java
@@ -0,0 +1,292 @@
+package org.apache.logging.log4j.layout.json.template.util;
+
+import org.apache.logging.log4j.util.Strings;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+public enum StringParameterParser {;
+
+    public enum Values {;
+
+        static NullValue nullValue() {
+            return NullValue.INSTANCE;
+        }
+
+        static StringValue stringValue(final String string) {
+            return new StringValue(string);
+        }
+
+        static DoubleQuotedStringValue doubleQuotedStringValue(
+                final String doubleQuotedString) {
+            return new DoubleQuotedStringValue(doubleQuotedString);
+        }
+
+    }
+
+    public interface Value {}
+
+    public static final class NullValue implements Value {
+
+        private static final NullValue INSTANCE = new NullValue();
+
+        private NullValue() {}
+
+        @Override
+        public String toString() {
+            return null;
+        }
+
+    }
+
+    public static final class StringValue implements Value {
+
+        private final String string;
+
+        private StringValue(String string) {
+            this.string = string;
+        }
+
+        public String getString() {
+            return string;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) return true;
+            if (object == null || getClass() != object.getClass()) return false;
+            StringValue that = (StringValue) object;
+            return string.equals(that.string);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(string);
+        }
+
+        @Override
+        public String toString() {
+            return string;
+        }
+
+    }
+
+    public static final class DoubleQuotedStringValue implements Value {
+
+        private final String doubleQuotedString;
+
+        private DoubleQuotedStringValue(String doubleQuotedString) {
+            this.doubleQuotedString = doubleQuotedString;
+        }
+
+        public String getDoubleQuotedString() {
+            return doubleQuotedString;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) return true;
+            if (object == null || getClass() != object.getClass()) return false;
+            DoubleQuotedStringValue that = (DoubleQuotedStringValue) object;
+            return doubleQuotedString.equals(that.doubleQuotedString);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(doubleQuotedString);
+        }
+
+        @Override
+        public String toString() {
+            return doubleQuotedString.replaceAll("\\\\\"", "\"");
+        }
+
+    }
+
+    private enum State { READING_KEY, READING_VALUE }
+
+    private static final class Parser implements Callable<Map<String, Value>> {
+
+        private final String input;
+
+        private final Map<String, Value> map;
+
+        private State state;
+
+        private int i;
+
+        private String key;
+
+        private Parser(final String input) {
+            this.input = Objects.requireNonNull(input, "input");
+            this.map = new LinkedHashMap<>();
+            this.state = State.READING_KEY;
+            this.i = 0;
+            this.key = null;
+        }
+
+        @Override
+        public Map<String, Value> call() {
+            while (true) {
+                skipWhitespace();
+                if (i >= input.length()) {
+                    break;
+                }
+                switch (state) {
+                    case READING_KEY:
+                        readKey();
+                        break;
+                    case READING_VALUE:
+                        readValue();
+                        break;
+                    default:
+                        throw new IllegalStateException("unknown state: " + state);
+                }
+            }
+            if (state == State.READING_VALUE) {
+                map.put(key, Values.nullValue());
+            }
+            return map;
+        }
+
+        private void readKey() {
+            final int eq = input.indexOf('=', i);
+            final int co = input.indexOf(',', i);
+            final int j;
+            final int nextI;
+            if (eq < 0 && co < 0) {
+                // Neither '=', nor ',' was found.
+                j = nextI = input.length();
+            } else if (eq < 0) {
+                // Found ','.
+                j = nextI = co;
+            } else if (co < 0) {
+                // Found '='.
+                j = eq;
+                nextI = eq + 1;
+            } else if (eq < co) {
+                // Found '=...,'.
+                j = eq;
+                nextI = eq + 1;
+            } else {
+                // Found ',...='.
+                j = co;
+                nextI = co;
+            }
+            key = input.substring(i, j).trim();
+            if (Strings.isEmpty(key)) {
+                final String message = String.format(
+                        "failed to locate key at index %d: %s",
+                        i, input);
+                throw new IllegalArgumentException(message);
+            }
+            if (map.containsKey(key)) {
+                final String message = String.format(
+                        "conflicting key at index %d: %s",
+                        i, input);
+                throw new IllegalArgumentException(message);
+            }
+            state = State.READING_VALUE;
+            i = nextI;
+        }
+
+        private void readValue() {
+            final boolean doubleQuoted = input.charAt(i) == '"';
+            if (doubleQuoted) {
+                readDoubleQuotedStringValue();
+            } else {
+                readStringValue();
+            }
+            key = null;
+            state = State.READING_KEY;
+        }
+
+        private void readDoubleQuotedStringValue() {
+            int j = i + 1;
+            while (j < input.length()) {
+                if (input.charAt(j) == '"' && input.charAt(j - 1) != '\\') {
+                    break;
+                } else {
+                    j++;
+                }
+            }
+            if (j >= input.length()) {
+                final String message = String.format(
+                        "failed to locate the end of double-quoted content starting at index %d: %s",
+                        i, input);
+                throw new IllegalArgumentException(message);
+            }
+            final String content = input
+                    .substring(i + 1, j)
+                    .replaceAll("\\\\\"", "\"");
+            final Value value = Values.doubleQuotedStringValue(content);
+            map.put(key, value);
+            i = j + 1;
+            skipWhitespace();
+            if (i < input.length()) {
+                if (input.charAt(i) != ',') {
+                    final String message = String.format(
+                            "was expecting comma at index %d: %s",
+                            i, input);
+                    throw new IllegalArgumentException(message);
+                }
+                i++;
+            }
+        }
+
+        private void skipWhitespace() {
+            while (i < input.length()) {
+                final char c = input.charAt(i);
+                if (!Character.isWhitespace(c)) {
+                    break;
+                } else {
+                    i++;
+                }
+            }
+        }
+
+        private void readStringValue() {
+            int j = input.indexOf(',', i/* + 1*/);
+            if (j < 0) {
+                j = input.length();
+            }
+            final String content = input.substring(i, j);
+            final String trimmedContent = content.trim();
+            final Value value = trimmedContent.isEmpty()
+                    ? Values.nullValue()
+                    : Values.stringValue(trimmedContent);
+            map.put(key, value);
+            i += content.length() + 1;
+        }
+
+    }
+
+    public static Map<String, Value> parse(final String input) {
+        return parse(input, null);
+    }
+
+    public static Map<String, Value> parse(
+            final String input,
+            final Set<String> allowedKeys) {
+        if (Strings.isBlank(input)) {
+            return Collections.emptyMap();
+        }
+        final Map<String, Value> map = new Parser(input).call();
+        final Set<String> actualKeys = map.keySet();
+        for (final String actualKey : actualKeys) {
+            final boolean allowed = allowedKeys == null || allowedKeys.contains(actualKey);
+            if (!allowed) {
+                final String message = String.format(
+                        "unknown key \"%s\" is found in input: %s",
+                        actualKey, input);
+                throw new IllegalArgumentException(message);
+            }
+        }
+        return map;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecycler.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecycler.java
new file mode 100644
index 0000000..1c58d4f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecycler.java
@@ -0,0 +1,45 @@
+/*
+ * 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.function.Consumer;
+import java.util.function.Supplier;
+
+public class ThreadLocalRecycler<V> implements Recycler<V> {
+
+    private final Consumer<V> cleaner;
+
+    private final ThreadLocal<V> holder;
+
+    public ThreadLocalRecycler(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner) {
+        this.cleaner = cleaner;
+        this.holder = ThreadLocal.withInitial(supplier);
+    }
+
+    @Override
+    public V acquire() {
+        final V value = holder.get();
+        cleaner.accept(value);
+        return value;
+    }
+
+    @Override
+    public void release(final V value) {}
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecyclerFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecyclerFactory.java
new file mode 100644
index 0000000..8ea6c61
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/ThreadLocalRecyclerFactory.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template.util;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class ThreadLocalRecyclerFactory implements RecyclerFactory {
+
+    private static final ThreadLocalRecyclerFactory INSTANCE =
+            new ThreadLocalRecyclerFactory();
+
+    private ThreadLocalRecyclerFactory() {}
+
+    public static ThreadLocalRecyclerFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public <V> Recycler<V> create(
+            final Supplier<V> supplier,
+            final Consumer<V> cleaner) {
+        return new ThreadLocalRecycler<>(supplier, cleaner);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedPrintWriter.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedPrintWriter.java
new file mode 100644
index 0000000..37338e6
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedPrintWriter.java
@@ -0,0 +1,60 @@
+/*
+ * 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.io.PrintWriter;
+
+public final class TruncatingBufferedPrintWriter extends PrintWriter {
+
+    private final TruncatingBufferedWriter writer;
+
+    private TruncatingBufferedPrintWriter(final TruncatingBufferedWriter writer) {
+        super(writer, false);
+        this.writer = writer;
+    }
+
+    public static TruncatingBufferedPrintWriter ofCapacity(final int capacity) {
+        if (capacity < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a non-negative capacity: " + capacity);
+        }
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(capacity);
+        return new TruncatingBufferedPrintWriter(writer);
+    }
+
+    public char[] getBuffer() {
+        return writer.getBuffer();
+    }
+
+    public int getPosition() {
+        return writer.getPosition();
+    }
+
+    public int getCapacity() {
+        return writer.getCapacity();
+    }
+
+    public boolean isTruncated() {
+        return writer.isTruncated();
+    }
+
+    @Override
+    public void close() {
+        writer.close();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriter.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriter.java
new file mode 100644
index 0000000..e31f21c
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriter.java
@@ -0,0 +1,208 @@
+/*
+ * 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.io.Writer;
+import java.util.Objects;
+
+public final class TruncatingBufferedWriter extends Writer {
+
+    private final char[] buffer;
+
+    private int position;
+
+    private boolean truncated;
+
+    TruncatingBufferedWriter(final int capacity) {
+        this.buffer = new char[capacity];
+        this.position = 0;
+        this.truncated = false;
+    }
+
+    char[] getBuffer() {
+        return buffer;
+    }
+
+    int getPosition() {
+        return position;
+    }
+
+    int getCapacity() {
+        return buffer.length;
+    }
+
+    boolean isTruncated() {
+        return truncated;
+    }
+
+    @Override
+    public void write(final int c) {
+        if (position < buffer.length) {
+            buffer[position++] = (char) c;
+        } else {
+            truncated = true;
+        }
+    }
+
+    @Override
+    public void write(final char[] source) {
+        Objects.requireNonNull(source, "source");
+        write(source, 0, source.length);
+    }
+
+    @Override
+    public void write(final char[] source, final int offset, final int length) {
+
+        // Check arguments.
+        Objects.requireNonNull(source, "source");
+        if (offset < 0 || offset >= source.length) {
+            throw new IndexOutOfBoundsException("invalid offset: " + offset);
+        }
+        if (length < 0 || Math.addExact(offset, length) > source.length) {
+            throw new IndexOutOfBoundsException("invalid length: " + length);
+        }
+
+        // If input fits as is.
+        final int maxLength = buffer.length - position;
+        if (length < maxLength) {
+            System.arraycopy(source, offset, buffer, position, length);
+            position += length;
+        }
+
+        // If truncation is possible.
+        else if (maxLength > 0) {
+            System.arraycopy(source, offset, buffer, position, maxLength);
+            position += maxLength;
+            truncated = true;
+        }
+
+    }
+
+    @Override
+    public void write(final String string) {
+
+        // Check arguments.
+        Objects.requireNonNull(string, "string");
+        final int length = string.length();
+        final int maxLength = buffer.length - position;
+
+        // If input fits as is.
+        if (length < maxLength) {
+            string.getChars(0, length, buffer, position);
+            position += length;
+        }
+
+        // If truncation is possible.
+        else if (maxLength > 0) {
+            string.getChars(0, maxLength, buffer, position);
+            position += maxLength;
+            truncated = true;
+        }
+
+    }
+
+    @Override
+    public void write(final String string, final int offset, final int length) {
+
+        // Check arguments.
+        Objects.requireNonNull(string, "string");
+        if (offset < 0 || offset >= string.length()) {
+            throw new IndexOutOfBoundsException("invalid offset: " + offset);
+        }
+        if (length < 0 || Math.addExact(offset, length) > string.length()) {
+            throw new IndexOutOfBoundsException("invalid length: " + length);
+        }
+
+        // If input fits as is.
+        final int maxLength = buffer.length - position;
+        if (length < maxLength) {
+            string.getChars(offset, offset + length, buffer, position);
+            position += length;
+        }
+
+        // If truncation is possible.
+        else if (maxLength > 0) {
+            string.getChars(offset, offset + maxLength, buffer, position);
+            position += maxLength;
+            truncated = true;
+        }
+
+    }
+
+    @Override
+    public Writer append(final char c) {
+        write(c);
+        return this;
+    }
+
+    @Override
+    public Writer append(final CharSequence seq) {
+        return seq == null
+                ? append("null", 0, 4)
+                : append(seq, 0, seq.length());
+    }
+
+    @Override
+    public Writer append(final CharSequence seq, final int start, final int end) {
+
+        // Short-circuit on null sequence.
+        if (seq == null) {
+            write("null");
+            return this;
+        }
+
+        // Check arguments.
+        if (start < 0 || start >= seq.length()) {
+            throw new IndexOutOfBoundsException("invalid start: " + start);
+        }
+        if (end < start || end > seq.length()) {
+            throw new IndexOutOfBoundsException("invalid end: " + end);
+        }
+
+        // If input fits as is.
+        final int length = end - start;
+        final int maxLength = buffer.length - position;
+        if (length < maxLength) {
+            for (int i = start; i < end; i++) {
+                final char c = seq.charAt(i);
+                buffer[position++] = c;
+            }
+        }
+
+        // If truncation is possible.
+        else if (maxLength > 0) {
+            final int truncatedEnd = start + maxLength;
+            for (int i = start; i < truncatedEnd; i++) {
+                final char c = seq.charAt(i);
+                buffer[position++] = c;
+            }
+            truncated = true;
+        }
+        return this;
+
+    }
+
+    @Override
+    public void flush() {}
+
+    @Override
+    public void close() {
+        position = 0;
+        truncated = false;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Uris.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Uris.java
new file mode 100644
index 0000000..4c03843
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Uris.java
@@ -0,0 +1,126 @@
+/*
+ * 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 org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.LoaderUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public enum Uris {;
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    /**
+     * Reads {@link URI}s of scheme <tt>classpath</tt> and <tt>file</tt>.
+     *
+     * @param spec the {@link URI} spec, e.g., <tt>file:/holy/cow.txt</tt> or
+     *             <tt>classpath:/holy/cat.txt</tt>
+     * @param charset used {@link Charset} for decoding the file
+     */
+    public static String readUri(final String spec, final Charset charset) {
+        Objects.requireNonNull(spec, "spec");
+        Objects.requireNonNull(charset, "charset");
+        try {
+            return unsafeReadUri(spec, charset);
+        } catch (final Exception error) {
+            final String message = String.format(
+                    "failed reading URI (spec=%s, charset=%s)",
+                    spec, charset);
+            throw new RuntimeException(message, error);
+        }
+    }
+
+    private static String unsafeReadUri(
+            final String spec,
+            final Charset charset)
+            throws Exception {
+        final URI uri = new URI(spec);
+        final String uriScheme = uri.getScheme().toLowerCase();
+        switch (uriScheme) {
+            case "classpath":
+                return readClassPathUri(uri, charset);
+            case "file":
+                return readFileUri(uri, charset);
+            default: {
+                final String message = String.format("unknown URI scheme (spec=%s)", spec);
+                throw new IllegalArgumentException(message);
+            }
+        }
+
+    }
+
+    private static String readFileUri(
+            final URI uri,
+            final Charset charset)
+            throws IOException {
+        final Path path = Paths.get(uri);
+        try (final BufferedReader fileReader = Files.newBufferedReader(path, charset)) {
+            return consumeReader(fileReader);
+        }
+    }
+
+    private static String readClassPathUri(
+            final URI uri,
+            final Charset charset)
+            throws IOException {
+        final String spec = uri.toString();
+        final String path = spec.substring("classpath:".length());
+        final List<URL> resources = new ArrayList<>(LoaderUtil.findResources(path));
+        if (resources.isEmpty()) {
+            final String message = String.format(
+                    "could not locate classpath resource (path=%s)", path);
+            throw new RuntimeException(message);
+        }
+        final URL resource = resources.get(0);
+        if (resources.size() > 1) {
+            final String message = String.format(
+                    "for URI %s found %d resources, using the first one: %s",
+                    uri, resources.size(), resource);
+            LOGGER.warn(message);
+        }
+        try (final InputStream inputStream = resource.openStream()) {
+            try (final InputStreamReader reader = new InputStreamReader(inputStream, charset);
+                 final BufferedReader bufferedReader = new BufferedReader(reader)) {
+                return consumeReader(bufferedReader);
+            }
+        }
+    }
+
+    private static String consumeReader(final BufferedReader reader) throws IOException {
+        final StringBuilder builder = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            builder.append(line);
+        }
+        return builder.toString();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/main/resources/EcsLayout.json b/log4j-layout-json-template/src/main/resources/EcsLayout.json
new file mode 100644
index 0000000..7004e54
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/EcsLayout.json
@@ -0,0 +1,12 @@
+{
+  "@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}"
+}
diff --git a/log4j-layout-json-template/src/main/resources/GelfLayout.json b/log4j-layout-json-template/src/main/resources/GelfLayout.json
new file mode 100644
index 0000000..bcea9ee
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/GelfLayout.json
@@ -0,0 +1,11 @@
+{
+  "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}"
+}
diff --git a/log4j-layout-json-template/src/main/resources/JsonLayout.json b/log4j-layout-json-template/src/main/resources/JsonLayout.json
new file mode 100644
index 0000000..f31ec63
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/JsonLayout.json
@@ -0,0 +1,26 @@
+{
+  "instant": {
+    "epochSecond": "${json:timestamp:epoch:secs,integral}",
+    "nanoOfSecond": "${json:timestamp:epoch:secs.nanos}"
+  },
+  "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}",
+  "source": {
+    "class": "${json:source:className}",
+    "method": "${json:source:methodName}",
+    "file": "${json:source:fileName}",
+    "line": "${json:source:lineNumber}"
+  }
+}
diff --git a/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
new file mode 100644
index 0000000..62d4c20
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
@@ -0,0 +1,19 @@
+{
+  "mdc": "${json:mdc}",
+  "exception": {
+    "exception_class": "${json:exception:className}",
+    "exception_message": "${json:exception:message}",
+    "stacktrace": "${json:exception:stackTrace:text}"
+  },
+  "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}"
+}
diff --git a/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
new file mode 100644
index 0000000..3faacd4
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
@@ -0,0 +1,6 @@
+{
+  "class": "${json:stackTraceElement:className}",
+  "method": "${json:stackTraceElement:methodName}",
+  "file": "${json:stackTraceElement:fileName}",
+  "line": "${json:stackTraceElement:lineNumber}"
+}
diff --git a/log4j-layout-json-template/src/site/manual/index.md b/log4j-layout-json-template/src/site/manual/index.md
new file mode 100644
index 0000000..b8cb6e3
--- /dev/null
+++ b/log4j-layout-json-template/src/site/manual/index.md
@@ -0,0 +1,32 @@
+<!-- vim: set syn=markdown : -->
+<!--
+    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.
+-->
+
+# Apache Log4j JSON Template Layout module
+
+This module provides a customizable and efficient JSON layout.
+
+## Requirements
+
+This module was introduced in Log4j 3.0.0 and requires Jackson.
+
+Some features may require optional [dependencies](../runtime-dependencies.html).
+These dependencies are specified in the documentation for those features.
+
+Some Log4j features require external dependencies. See the
+[Dependency Tree](dependencies.html#Dependency_Tree) for the exact list of JAR
+files needed for these features.
diff --git a/log4j-layout-json-template/src/site/site.xml b/log4j-layout-json-template/src/site/site.xml
new file mode 100644
index 0000000..962392e
--- /dev/null
+++ b/log4j-layout-json-template/src/site/site.xml
@@ -0,0 +1,55 @@
+<!--
+ 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.
+-->
+<project name="Log4j Core"
+         xmlns="http://maven.apache.org/DECORATION/1.4.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/DECORATION/1.4.0 http://maven.apache.org/xsd/decoration-1.4.0.xsd">
+
+  <body>
+
+    <links>
+      <item name="Apache" href="http://www.apache.org/" />
+      <item name="Logging Services" href="http://logging.apache.org/"/>
+      <item name="Log4j" href="../index.html"/>
+    </links>
+
+    <!-- Component-specific reports -->
+    <menu ref="reports"/>
+
+	<!-- Overall Project Info -->
+    <menu name="Log4j Project Information" img="icon-info-sign">
+      <item name="Dependencies" href="../dependencies.html" />
+      <item name="Dependency Convergence" href="../dependency-convergence.html" />
+      <item name="Dependency Management" href="../dependency-management.html" />
+      <item name="Project Team" href="../team-list.html" />
+      <item name="Mailing Lists" href="../mail-lists.html" />
+      <item name="Issue Tracking" href="../issue-tracking.html" />
+      <item name="Project License" href="../license.html" />
+      <item name="Source Repository" href="../source-repository.html" />
+      <item name="Project Summary" href="../project-summary.html" />
+    </menu>
+
+    <menu name="Log4j Project Reports" img="icon-cog">
+      <item name="Changes Report" href="../changes-report.html" />
+      <item name="JIRA Report" href="../jira-report.html" />
+      <item name="Surefire Report" href="../surefire-report.html" />
+      <item name="RAT Report" href="../rat-report.html" />
+    </menu>
+
+  </body>
+
+</project>
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/BlackHoleByteBufferDestination.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/BlackHoleByteBufferDestination.java
new file mode 100644
index 0000000..01b3d4a
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/BlackHoleByteBufferDestination.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+
+import java.nio.ByteBuffer;
+
+class BlackHoleByteBufferDestination implements ByteBufferDestination {
+
+    private final ByteBuffer byteBuffer;
+
+    BlackHoleByteBufferDestination(final int maxByteCount) {
+        this.byteBuffer = ByteBuffer.allocate(maxByteCount);
+    }
+
+    @Override
+    public ByteBuffer getByteBuffer() {
+        return byteBuffer;
+    }
+
+    @Override
+    public ByteBuffer drain(final ByteBuffer byteBuffer) {
+        byteBuffer.clear();
+        return byteBuffer;
+    }
+
+    @Override
+    public void writeBytes(final ByteBuffer byteBuffer) {
+        byteBuffer.clear();
+    }
+
+    @Override
+    public void writeBytes(final byte[] buffer, final int offset, final int length) {}
+
+}
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
new file mode 100644
index 0000000..ec96407
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
@@ -0,0 +1,80 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import co.elastic.logging.log4j2.EcsLayout;
+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.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.logging.log4j.layout.json.template.LayoutComparisonHelpers.renderUsing;
+
+public class EcsLayoutTest {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setEventTemplateUri("classpath:EcsLayout.json")
+            .setEventTemplateAdditionalFields(
+                    JsonTemplateLayout
+                            .EventTemplateAdditionalFields
+                            .newBuilder()
+                            .setAdditionalFields(
+                                    new KeyValuePair[]{
+                                            new KeyValuePair(
+                                                    "service.name",
+                                                    "test")
+                                    })
+                            .build())
+            .build();
+
+    private static final EcsLayout ECS_LAYOUT = EcsLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setServiceName("test")
+            .build();
+
+    @Test
+    public void test_lite_log_events() throws Exception {
+        final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
+        test(logEvents);
+    }
+
+    @Test
+    public void test_full_log_events() throws Exception {
+        final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
+        test(logEvents);
+    }
+
+    private static void test(final Collection<LogEvent> logEvents) throws Exception {
+        for (final LogEvent logEvent : logEvents) {
+            test(logEvent);
+        }
+    }
+
+    private static void test(final LogEvent logEvent) throws Exception {
+        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 {
+        return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+    }
+
+    private static Map<String, Object> renderUsingEcsLayout(
+            final LogEvent logEvent)
+            throws Exception {
+        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
new file mode 100644
index 0000000..b561ea6
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
@@ -0,0 +1,102 @@
+package org.apache.logging.log4j.layout.json.template;
+
+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.assertj.core.api.Assertions;
+import org.assertj.core.data.Percentage;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.logging.log4j.layout.json.template.LayoutComparisonHelpers.renderUsing;
+
+public class GelfLayoutTest {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final String HOST_NAME = "localhost";
+
+    private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setEventTemplateUri("classpath:GelfLayout.json")
+            .setEventTemplateAdditionalFields(
+                    JsonTemplateLayout
+                            .EventTemplateAdditionalFields
+                            .newBuilder()
+                            .setAdditionalFields(
+                                    new KeyValuePair[]{
+                                            new KeyValuePair(
+                                                    "host",
+                                                    HOST_NAME)
+                                    })
+                            .build())
+            .build();
+
+    private static final GelfLayout GELF_LAYOUT = GelfLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setHost(HOST_NAME)
+            .setCompressionType(GelfLayout.CompressionType.OFF)
+            .build();
+
+    @Test
+    public void test_lite_log_events() throws Exception {
+        final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
+        test(logEvents);
+    }
+
+    @Test
+    public void test_full_log_events() throws Exception {
+        final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
+        test(logEvents);
+    }
+
+    private static void test(final Collection<LogEvent> logEvents) throws Exception {
+        for (final LogEvent logEvent : logEvents) {
+            test(logEvent);
+        }
+    }
+
+    private static void test(final LogEvent logEvent) throws Exception {
+        final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
+        final Map<String, Object> gelfLayoutMap = renderUsingGelfLayout(logEvent);
+        verifyTimestamp(jsonTemplateLayoutMap, gelfLayoutMap);
+        Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(gelfLayoutMap);
+    }
+
+    private static Map<String, Object> renderUsingJsonTemplateLayout(
+            final LogEvent logEvent)
+            throws Exception {
+        return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+    }
+
+    private static Map<String, Object> renderUsingGelfLayout(
+            final LogEvent logEvent)
+            throws Exception {
+        return renderUsing(logEvent, GELF_LAYOUT);
+    }
+
+    /**
+     * Handle timestamps individually to avoid floating-point comparison hiccups.
+     */
+    private static void verifyTimestamp(
+            final Map<String, Object> jsonTemplateLayoutMap,
+            final Map<String, Object> gelfLayoutMap) {
+        final Number jsonTemplateLayoutTimestamp =
+                (Number) jsonTemplateLayoutMap.remove("timestamp");
+        final Number gelfLayoutTimestamp =
+                (Number) gelfLayoutMap.remove("timestamp");
+        Assertions
+                .assertThat(jsonTemplateLayoutTimestamp.doubleValue())
+                .isCloseTo(
+                        gelfLayoutTimestamp.doubleValue(),
+                        Percentage.withPercentage(0.01));
+    }
+
+}
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
new file mode 100644
index 0000000..0eb077f
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+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
new file mode 100644
index 0000000..7bbd9a9
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
@@ -0,0 +1,102 @@
+package org.apache.logging.log4j.layout.json.template;
+
+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.jackson.json.layout.JsonLayout;
+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;
+
+public class JsonLayoutTest {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setEventTemplateUri("classpath:JsonLayout.json")
+            .build();
+
+    private static final JsonLayout JSON_LAYOUT = JsonLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .build();
+
+    @Test
+    public void test_lite_log_events() throws Exception {
+        final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
+        test(logEvents);
+    }
+
+    @Test
+    public void test_full_log_events() throws Exception {
+        final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
+        test(logEvents);
+    }
+
+    private static void test(final Collection<LogEvent> logEvents) throws Exception {
+        for (final LogEvent logEvent : logEvents) {
+            test(logEvent);
+        }
+    }
+
+    private static void test(final LogEvent logEvent) throws Exception {
+        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 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;
+    }
+
+    private static Map<String, Object> renderUsingJsonLayout(
+            final LogEvent logEvent)
+            throws Exception {
+        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;
+    }
+
+    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;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java
new file mode 100644
index 0000000..d17a8be
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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;
+
+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.ByteBufferDestination;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class JsonTemplateLayoutConcurrentEncodeTest {
+
+    private static class ConcurrentAccessError extends RuntimeException {
+
+        public static final long serialVersionUID = 0;
+
+        private ConcurrentAccessError(final int concurrentAccessCount) {
+            super("concurrentAccessCount=" + concurrentAccessCount);
+        }
+
+    }
+
+    private static class ConcurrentAccessDetectingByteBufferDestination
+            extends BlackHoleByteBufferDestination {
+
+        private final AtomicInteger concurrentAccessCounter = new AtomicInteger(0);
+
+        ConcurrentAccessDetectingByteBufferDestination() {
+            super(2_000);
+        }
+
+        @Override
+        public ByteBuffer getByteBuffer() {
+            final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+            if (concurrentAccessCount > 1) {
+                throw new ConcurrentAccessError(concurrentAccessCount);
+            }
+            try {
+                return super.getByteBuffer();
+            } finally {
+                concurrentAccessCounter.decrementAndGet();
+            }
+        }
+
+        @Override
+        public ByteBuffer drain(final ByteBuffer byteBuffer) {
+            final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+            if (concurrentAccessCount > 1) {
+                throw new ConcurrentAccessError(concurrentAccessCount);
+            }
+            try {
+                return super.drain(byteBuffer);
+            } finally {
+                concurrentAccessCounter.decrementAndGet();
+            }
+        }
+
+        @Override
+        public void writeBytes(final ByteBuffer byteBuffer) {
+            final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+            if (concurrentAccessCount > 1) {
+                throw new ConcurrentAccessError(concurrentAccessCount);
+            }
+            try {
+                super.writeBytes(byteBuffer);
+            } finally {
+                concurrentAccessCounter.decrementAndGet();
+            }
+        }
+
+        @Override
+        public void writeBytes(final byte[] buffer, final int offset, final int length) {
+            int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+            if (concurrentAccessCount > 1) {
+                throw new ConcurrentAccessError(concurrentAccessCount);
+            }
+            try {
+                super.writeBytes(buffer, offset, length);
+            } finally {
+                concurrentAccessCounter.decrementAndGet();
+            }
+        }
+
+    }
+
+    private static final LogEvent[] LOG_EVENTS = createMessages();
+
+    private static LogEvent[] createMessages() {
+        final int messageCount = 1_000;
+        final LogEvent[] logEvents = new LogEvent[messageCount];
+        LogEventFixture
+                .createLiteLogEvents(messageCount)
+                .toArray(logEvents);
+        return logEvents;
+    }
+
+    @Test
+    public void test_concurrent_encode() {
+        final AtomicReference<Exception> encodeFailureRef = new AtomicReference<>(null);
+        produce(encodeFailureRef);
+        Assertions.assertThat(encodeFailureRef.get()).isNull();
+    }
+
+    private void produce(final AtomicReference<Exception> encodeFailureRef) {
+        final int threadCount = 10;
+        final JsonTemplateLayout layout = createLayout();
+        final ByteBufferDestination destination =
+                new ConcurrentAccessDetectingByteBufferDestination();
+        final AtomicLong encodeCounter = new AtomicLong(0);
+        final List<Thread> workers = IntStream
+                .range(0, threadCount)
+                .mapToObj((final int threadIndex) ->
+                        createWorker(
+                                layout,
+                                destination,
+                                encodeFailureRef,
+                                encodeCounter,
+                                threadIndex))
+                .collect(Collectors.toList());
+        workers.forEach(Thread::start);
+        workers.forEach((final Thread worker) -> {
+            try {
+                worker.join();
+            } catch (final InterruptedException ignored) {
+                System.err.format("join to %s interrupted%n", worker.getName());
+            }
+        });
+    }
+
+    private static JsonTemplateLayout createLayout() {
+        final Configuration config = new DefaultConfiguration();
+        return JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(config)
+                .setEventTemplate("{\"message\": \"${json:message}\"}")
+                .setStackTraceEnabled(false)
+                .setLocationInfoEnabled(false)
+                .build();
+    }
+
+    private Thread createWorker(
+            final JsonTemplateLayout layout,
+            final ByteBufferDestination destination,
+            final AtomicReference<Exception> encodeFailureRef,
+            final AtomicLong encodeCounter,
+            final int threadIndex) {
+        final int maxEncodeCount = 1_000;
+        final String threadName = String.format("Worker-%d", threadIndex);
+        return new Thread(
+                () -> {
+                    try {
+                        for (int logEventIndex = threadIndex % LOG_EVENTS.length;
+                             encodeFailureRef.get() == null && encodeCounter.incrementAndGet() < maxEncodeCount;
+                             logEventIndex = (logEventIndex + 1) % LOG_EVENTS.length) {
+                            final LogEvent logEvent = LOG_EVENTS[logEventIndex];
+                            layout.encode(logEvent, destination);
+                        }
+                    } catch (final Exception error) {
+                        final boolean succeeded = encodeFailureRef.compareAndSet(null, error);
+                        if (succeeded) {
+                            System.err.format("%s failed%n", threadName);
+                            error.printStackTrace(System.err);
+                        }
+                    }
+                },
+                threadName);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java
new file mode 100644
index 0000000..dfe097a
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.core.GcFreeLoggingTestUtil;
+import org.junit.Test;
+
+public class JsonTemplateLayoutGcFreeTest {
+
+    @Test
+    public void test_no_allocation_during_steady_state_logging() throws Exception {
+        GcFreeLoggingTestUtil.runTest(getClass());
+    }
+
+    /**
+     * This code runs in a separate process, instrumented with the Google Allocation Instrumenter.
+     */
+    public static void main(final String[] args) throws Exception {
+        System.setProperty("log4j.layout.jsonTemplate.recyclerFactory", "threadLocal");
+        System.setProperty("log4j2.garbagefree.threadContextMap", "true");
+        GcFreeLoggingTestUtil.executeLogging(
+                "gcFreeJsonTemplateLayoutLogging.xml",
+                JsonTemplateLayoutGcFreeTest.class);
+    }
+
+}
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
new file mode 100644
index 0000000..4e330f9
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
@@ -0,0 +1,1653 @@
+/*
+ * 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;
+
+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.LogEvent;
+import org.apache.logging.log4j.core.appender.SocketAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+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.message.ObjectMessage;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.message.StringMapMessage;
+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.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SuppressWarnings("DoubleBraceInitialization")
+public class JsonTemplateLayoutTest {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final List<LogEvent> LOG_EVENTS = LogEventFixture.createFullLogEvents(5);
+
+    private static final JsonNodeFactory JSON_NODE_FACTORY = JsonNodeFactory.instance;
+
+    private static final ObjectMapper OBJECT_MAPPER = JacksonFixture.getObjectMapper();
+
+    private static final String LOGGER_NAME = JsonTemplateLayoutTest.class.getSimpleName();
+
+    @Test
+    public void test_serialized_event() throws IOException {
+        final String lookupTestKey = "lookup_test_key";
+        final String lookupTestVal =
+                String.format("lookup_test_value_%d", (int) (1000 * Math.random()));
+        System.setProperty(lookupTestKey, lookupTestVal);
+        for (final LogEvent logEvent : LOG_EVENTS) {
+            checkLogEvent(logEvent, lookupTestKey, lookupTestVal);
+        }
+    }
+
+    private void checkLogEvent(
+            final LogEvent logEvent,
+            @SuppressWarnings("SameParameterValue")
+            final String lookupTestKey,
+            final String lookupTestVal) throws IOException {
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplateUri("classpath:testJsonTemplateLayout.json")
+                .setStackTraceEnabled(true)
+                .setLocationInfoEnabled(true)
+                .build();
+        final String serializedLogEvent = layout.toSerializable(logEvent);
+        final JsonNode rootNode = OBJECT_MAPPER.readValue(serializedLogEvent, JsonNode.class);
+        checkConstants(rootNode);
+        checkBasicFields(logEvent, rootNode);
+        checkSource(logEvent, rootNode);
+        checkException(layout.getCharset(), logEvent, rootNode);
+        checkLookupTest(lookupTestKey, lookupTestVal, rootNode);
+    }
+
+    private static void checkConstants(final JsonNode rootNode) {
+        assertThat(point(rootNode, "@version").asInt()).isEqualTo(1);
+    }
+
+    private static void checkBasicFields(final LogEvent logEvent, final JsonNode rootNode) {
+        assertThat(point(rootNode, "message").asText())
+                .isEqualTo(logEvent.getMessage().getFormattedMessage());
+        assertThat(point(rootNode, "level").asText())
+                .isEqualTo(logEvent.getLevel().name());
+        assertThat(point(rootNode, "logger_fqcn").asText())
+                .isEqualTo(logEvent.getLoggerFqcn());
+        assertThat(point(rootNode, "logger_name").asText())
+                .isEqualTo(logEvent.getLoggerName());
+        assertThat(point(rootNode, "thread_id").asLong())
+                .isEqualTo(logEvent.getThreadId());
+        assertThat(point(rootNode, "thread_name").asText())
+                .isEqualTo(logEvent.getThreadName());
+        assertThat(point(rootNode, "thread_priority").asInt())
+                .isEqualTo(logEvent.getThreadPriority());
+        assertThat(point(rootNode, "end_of_batch").asBoolean())
+                .isEqualTo(logEvent.isEndOfBatch());
+    }
+
+    private static void checkSource(final LogEvent logEvent, final JsonNode rootNode) {
+        assertThat(point(rootNode, "class").asText()).isEqualTo(logEvent.getSource().getClassName());
+        assertThat(point(rootNode, "file").asText()).isEqualTo(logEvent.getSource().getFileName());
+        assertThat(point(rootNode, "line_number").asInt()).isEqualTo(logEvent.getSource().getLineNumber());
+    }
+
+    private static void checkException(
+            final Charset charset,
+            final LogEvent logEvent,
+            final JsonNode rootNode) {
+        final Throwable thrown = logEvent.getThrown();
+        if (thrown != null) {
+            assertThat(point(rootNode, "exception_class").asText()).isEqualTo(thrown.getClass().getCanonicalName());
+            assertThat(point(rootNode, "exception_message").asText()).isEqualTo(thrown.getMessage());
+            final String stackTrace = serializeStackTrace(charset, thrown);
+            assertThat(point(rootNode, "stacktrace").asText()).isEqualTo(stackTrace);
+        }
+    }
+
+    private static String serializeStackTrace(
+            final Charset charset,
+            final Throwable exception) {
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        final String charsetName = charset.name();
+        try (final PrintStream printStream =
+                     new PrintStream(outputStream, false, charsetName)) {
+            exception.printStackTrace(printStream);
+            return outputStream.toString(charsetName);
+        }  catch (final UnsupportedEncodingException error) {
+            throw new RuntimeException("failed converting the stack trace to string", error);
+        }
+    }
+
+    private static void checkLookupTest(
+            final String lookupTestKey,
+            final String lookupTestVal,
+            final JsonNode rootNode) {
+        assertThat(point(rootNode, lookupTestKey).asText()).isEqualTo(lookupTestVal);
+    }
+
+    private static JsonNode point(final JsonNode node, final Object... fields) {
+        final String pointer = createJsonPointer(fields);
+        return node.at(pointer);
+    }
+
+    private static String createJsonPointer(final Object... fields) {
+        final StringBuilder jsonPathBuilder = new StringBuilder();
+        for (final Object field : fields) {
+            jsonPathBuilder.append("/").append(field);
+        }
+        return jsonPathBuilder.toString();
+    }
+
+    @Test
+    public void test_inline_template() throws Exception {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World");
+        final String timestamp = "2017-09-28T17:13:29.098+02:00";
+        final long timeMillis = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
+                .parse(timestamp)
+                .getTime();
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setTimeMillis(timeMillis)
+                .build();
+
+        // Create the event template.
+        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+        eventTemplateRootNode.put("@timestamp", "${json:timestamp:timeZone=Europe/Amsterdam}");
+        final String staticFieldName = "staticFieldName";
+        final String staticFieldValue = "staticFieldValue";
+        eventTemplateRootNode.put(staticFieldName, staticFieldValue);
+        final String eventTemplate = eventTemplateRootNode.toString();
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .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);
+
+    }
+
+    @Test
+    public void test_log4j_deferred_runtime_resolver_for_MapMessage() throws Exception {
+
+        // 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();
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event with a MapMessage.
+        final StringMapMessage mapMessage = new StringMapMessage()
+                .with("key1", "val1")
+                .with("key2", "val2")
+                .with("key3", Collections.singletonMap("foo", "bar"));
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(mapMessage)
+                .setTimeMillis(System.currentTimeMillis())
+                .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");
+
+    }
+
+    @Test
+    public void test_MapMessage_serialization() throws Exception {
+
+        // 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)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event with a MapMessage.
+        final StringMapMessage mapMessage = new StringMapMessage()
+                .with("key1", "val1")
+                .with("key2", 0xDEADBEEF)
+                .with("key3", Collections.singletonMap("key3.1", "val3.1"));
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(mapMessage)
+                .setTimeMillis(System.currentTimeMillis())
+                .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");
+
+    }
+
+    @Test
+    public void test_property_injection() throws Exception {
+
+        // 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 with property.
+        final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+        final String propertyName = "propertyName";
+        eventTemplateRootNode.put(propertyName, "${" + propertyName + "}");
+        final String eventTemplate = eventTemplateRootNode.toString();
+
+        // Create the layout with property.
+        final String propertyValue = "propertyValue";
+        final Configuration config = ConfigurationBuilderFactory
+                .newConfigurationBuilder()
+                .addProperty(propertyName, propertyValue)
+                .build();
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(config)
+                .setEventTemplate(eventTemplate)
+                .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);
+
+    }
+
+    @Test
+    public void test_empty_root_cause() throws Exception {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final RuntimeException exception = new RuntimeException("failure for test purposes");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.ERROR)
+                .setMessage(message)
+                .setThrown(exception)
+                .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();
+
+        // 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, "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());
+
+    }
+
+    @Test
+    public void test_root_cause() throws Exception {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final RuntimeException exceptionCause = new RuntimeException("failure cause for test purposes");
+        final RuntimeException exception = new RuntimeException("failure for test purposes", exceptionCause);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.ERROR)
+                .setMessage(message)
+                .setThrown(exception)
+                .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();
+
+        // 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, "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());
+
+    }
+
+    @Test
+    public void test_marker_name() throws IOException {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final String markerName = "test";
+        final Marker marker = MarkerManager.getMarker(markerName);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.ERROR)
+                .setMessage(message)
+                .setMarker(marker)
+                .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();
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .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);
+
+    }
+
+    @Test
+    public void test_lineSeparator_suffix() {
+
+        // 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();
+
+        // Check line separators.
+        test_lineSeparator_suffix(logEvent, true);
+        test_lineSeparator_suffix(logEvent, false);
+
+    }
+
+    private void test_lineSeparator_suffix(
+            final LogEvent logEvent,
+            final boolean prettyPrintEnabled) {
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplateUri("classpath:LogstashJsonEventLayoutV1.json")
+                .build();
+
+        // Check the serialized event.
+        final String serializedLogEvent = layout.toSerializable(logEvent);
+        final String assertionCaption = String.format("testing lineSeperator (prettyPrintEnabled=%s)", prettyPrintEnabled);
+        assertThat(serializedLogEvent).as(assertionCaption).endsWith("}" + System.lineSeparator());
+
+    }
+
+    @Test
+    public void test_main_key_access() throws IOException {
+
+        // Set main() arguments.
+        final String kwKey = "--name";
+        final String kwVal = "aNameValue";
+        final String positionArg = "position2Value";
+        final String missingKwKey = "--missing";
+        final String[] mainArgs = {kwKey, kwVal, positionArg};
+        MainMapLookup.setMainArguments(mainArgs);
+
+        // 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 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();
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(template)
+                .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);
+
+    }
+
+    @Test
+    public void test_mdc_key_access() throws IOException {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final StringMap contextData = new SortedArrayStringMap();
+        final String mdcDirectlyAccessedKey = "mdcKey1";
+        final String mdcDirectlyAccessedValue = "mdcValue1";
+        contextData.putValue(mdcDirectlyAccessedKey, mdcDirectlyAccessedValue);
+        final String mdcDirectlyAccessedNullPropertyKey = "mdcKey2";
+        final String mdcDirectlyAccessedNullPropertyValue = null;
+        // noinspection ConstantConditions
+        contextData.putValue(mdcDirectlyAccessedNullPropertyKey, mdcDirectlyAccessedNullPropertyValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setContextData(contextData)
+                .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();
+
+        // 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, mdcDirectlyAccessedKey).asText()).isEqualTo(mdcDirectlyAccessedValue);
+        assertThat(point(rootNode, mdcDirectlyAccessedNullPropertyKey)).isInstanceOf(NullNode.class);
+
+    }
+
+    @Test
+    public void test_mdc_pattern() throws IOException {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final StringMap contextData = new SortedArrayStringMap();
+        final String mdcPatternMatchedKey = "mdcKey1";
+        final String mdcPatternMatchedValue = "mdcValue1";
+        contextData.putValue(mdcPatternMatchedKey, mdcPatternMatchedValue);
+        final String mdcPatternMismatchedKey = "mdcKey2";
+        final String mdcPatternMismatchedValue = "mdcValue2";
+        contextData.putValue(mdcPatternMismatchedKey, mdcPatternMismatchedValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setContextData(contextData)
+                .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();
+
+        // 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, mdcFieldName, mdcPatternMatchedKey).asText()).isEqualTo(mdcPatternMatchedValue);
+        assertThat(point(rootNode, mdcFieldName, mdcPatternMismatchedKey)).isInstanceOf(MissingNode.class);
+
+    }
+
+    @Test
+    public void test_mdc_flatten() throws IOException {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final StringMap contextData = new SortedArrayStringMap();
+        final String mdcPatternMatchedKey = "mdcKey1";
+        final String mdcPatternMatchedValue = "mdcValue1";
+        contextData.putValue(mdcPatternMatchedKey, mdcPatternMatchedValue);
+        final String mdcPatternMismatchedKey = "mdcKey2";
+        final String mdcPatternMismatchedValue = "mdcValue2";
+        contextData.putValue(mdcPatternMismatchedKey, mdcPatternMismatchedValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setContextData(contextData)
+                .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();
+
+        // 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, mdcPrefix + mdcPatternMatchedKey).asText()).isEqualTo(mdcPatternMatchedValue);
+        assertThat(point(rootNode, mdcPrefix + mdcPatternMismatchedKey)).isInstanceOf(MissingNode.class);
+
+    }
+
+    @Test
+    public void test_MapResolver() throws IOException {
+
+        // Create the log event.
+        final StringMapMessage message = new StringMapMessage().with("key1", "val1");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .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();
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .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);
+
+    }
+
+    @Test
+    public void test_message_json() throws IOException {
+
+        // Create the log event.
+        final StringMapMessage message = new StringMapMessage();
+        message.put("message", "Hello, World!");
+        message.put("bottle", "Kickapoo Joy Juice");
+        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();
+
+        // 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();
+
+        // 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").asText()).isEqualTo("Hello, World!");
+
... 9055 lines suppressed ...