diff --git a/extensions/jackson3/pom.xml b/extensions/jackson3/pom.xml
new file mode 100644
index 000000000..b73485d63
--- /dev/null
+++ b/extensions/jackson3/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+
+ io.jsonwebtoken
+ jjwt-root
+ 0.14.0-SNAPSHOT
+ ../../pom.xml
+
+
+ jjwt-jackson3
+ JJWT :: Extensions :: Jackson3
+ jar
+
+
+ ${basedir}/../..
+
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+
+
+ tools.jackson.core
+ jackson-databind
+
+
+
+
+
+
+
+
+
diff --git a/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3Deserializer.java b/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3Deserializer.java
new file mode 100644
index 000000000..b3b4f96ef
--- /dev/null
+++ b/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3Deserializer.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2014 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.jackson.io;
+
+import io.jsonwebtoken.io.AbstractDeserializer;
+import io.jsonwebtoken.lang.Assert;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonParser;
+import tools.jackson.databind.DeserializationContext;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.deser.jdk.UntypedObjectDeserializer;
+import tools.jackson.databind.module.SimpleModule;
+
+import java.io.Reader;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Deserializer using a Jackson {@link ObjectMapper}.
+ *
+ * @since 0.14.0
+ */
+public class Jackson3Deserializer extends AbstractDeserializer {
+
+ private final Class returnType;
+
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Constructor using JJWT's default {@link ObjectMapper} singleton for deserialization.
+ */
+ public Jackson3Deserializer() {
+ this(Jackson3Serializer.DEFAULT_OBJECT_MAPPER);
+ }
+
+ /**
+ * Creates a new JacksonDeserializer where the values of the claims can be parsed into given types. A common usage
+ * example is to parse custom User object out of a claim, for example the claims:
+ *
+ * Passing a map of {@code ["user": User.class]} to this constructor would result in the {@code user} claim being
+ * transformed to an instance of your custom {@code User} class, instead of the default of {@code Map}.
+ *
+ * Because custom type parsing requires modifying the state of a Jackson {@code ObjectMapper}, this
+ * constructor creates a new internal {@code ObjectMapper} instance and customizes it to support the
+ * specified {@code claimTypeMap}. This ensures that the JJWT parsing behavior does not unexpectedly
+ * modify the state of another application-specific {@code ObjectMapper}.
+ *
+ * If you would like to use your own {@code ObjectMapper} instance that also supports custom types for
+ * JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering
+ * your custom types and then use the {@link #Jackson3Deserializer(ObjectMapper)} constructor instead.
+ *
+ * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type
+ */
+ public Jackson3Deserializer(Map> claimTypeMap) {
+ // DO NOT specify JacksonSerializer.DEFAULT_OBJECT_MAPPER here as that would modify the shared instance
+ this(Jackson3Serializer.newObjectMapper(), claimTypeMap);
+ }
+
+ /**
+ * Deserializer using a Jackson {@link ObjectMapper}.
+ *
+ * @since 0.14.0
+ * @param objectMapper
+ */
+ @SuppressWarnings("unchecked")
+ public Jackson3Deserializer(ObjectMapper objectMapper) {
+ this(objectMapper, (Class) Object.class);
+ }
+
+ /**
+ * Creates a new JacksonDeserializer where the values of the claims can be parsed into given types by registering
+ * a type-converting {@link tools.jackson.databind.JacksonModule Module} on the specified {@link ObjectMapper}.
+ * A common usage example is to parse custom User object out of a claim, for example the claims:
+ *
+ * Passing a map of {@code ["user": User.class]} to this constructor would result in the {@code user} claim being
+ * transformed to an instance of your custom {@code User} class, instead of the default of {@code Map}.
+ *
+ * Because custom type parsing requires modifying the state of a Jackson {@code ObjectMapper}, this
+ * constructor modifies the specified {@code objectMapper} argument and customizes it to support the
+ * specified {@code claimTypeMap}.
+ *
+ * If you do not want your {@code ObjectMapper} instance modified, but also want to support custom types for
+ * JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering
+ * your custom types separately and then use the {@link #Jackson3Deserializer(ObjectMapper)} constructor instead
+ * (which does not modify the {@code objectMapper} argument).
+ *
+ * @param objectMapper the objectMapper to modify by registering a custom type-converting
+ * {@link tools.jackson.databind.JacksonModule Module}
+ * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type
+ */
+ public Jackson3Deserializer(ObjectMapper objectMapper, Map> claimTypeMap) {
+ this(objectMapper, (Class) Object.class, claimTypeMap);
+ }
+
+ private Jackson3Deserializer(ObjectMapper objectMapper, Class returnType) {
+ Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
+ Assert.notNull(returnType, "Return type cannot be null.");
+ this.objectMapper = objectMapper;
+ this.returnType = returnType;
+ }
+
+ /**
+ *
+ * @param objectMapper
+ * @param returnType
+ * @param claimTypeMap
+ */
+ private Jackson3Deserializer(ObjectMapper objectMapper, Class returnType, Map> claimTypeMap) {
+ Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
+ Assert.notNull(returnType, "Return type cannot be null.");
+ Assert.notNull(claimTypeMap, "Claim type map cannot be null.");
+ // register a new Deserializer on the ObjectMapper instance:
+ SimpleModule module = new SimpleModule();
+ module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap)));
+ this.objectMapper = objectMapper.rebuild().addModule(module).build();
+ this.returnType = returnType;
+ }
+
+ @Override
+ protected T doDeserialize(Reader reader) throws Exception {
+ return objectMapper.readValue(reader, returnType);
+ }
+
+ /**
+ * A Jackson {@link tools.jackson.databind.deser.std.StdDeserializer JsonDeserializer}, that will convert claim
+ * values to types based on {@code claimTypeMap}.
+ */
+ private static class MappedTypeDeserializer extends UntypedObjectDeserializer {
+
+ private final Map> claimTypeMap;
+
+ private MappedTypeDeserializer(Map> claimTypeMap) {
+ super((JavaType) null, null);
+ this.claimTypeMap = claimTypeMap;
+ }
+
+ @Override
+ public Object deserialize(JsonParser parser, DeserializationContext context) throws JacksonException {
+ // check if the current claim key is mapped, if so traverse it's value
+ String name = parser.currentName();
+ if (claimTypeMap != null && name != null && claimTypeMap.containsKey(name)) {
+ Class> type = claimTypeMap.get(name);
+ //noinspection resource
+ JsonNode node = parser.readValueAsTree();
+ return context.readTreeAsValue(node, type);
+ }
+ // otherwise default to super
+ return super.deserialize(parser, context);
+ }
+ }
+}
diff --git a/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3Serializer.java b/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3Serializer.java
new file mode 100644
index 000000000..7ffb4a07c
--- /dev/null
+++ b/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3Serializer.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2014 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.jackson.io;
+
+import io.jsonwebtoken.io.AbstractSerializer;
+import io.jsonwebtoken.lang.Assert;
+import tools.jackson.core.StreamReadFeature;
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.JacksonModule;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectWriter;
+import tools.jackson.databind.module.SimpleModule;
+
+import java.io.OutputStream;
+
+import static tools.jackson.core.StreamWriteFeature.AUTO_CLOSE_TARGET;
+import static tools.jackson.databind.json.JsonMapper.builder;
+
+/**
+ * Serializer using a Jackson {@link ObjectMapper}.
+ *
+ * @since 0.14.0
+ */
+public class Jackson3Serializer extends AbstractSerializer {
+
+ static final String MODULE_ID = "jjwt-jackson3";
+ static final JacksonModule MODULE;
+
+ static {
+ SimpleModule module = new SimpleModule(MODULE_ID);
+ module.addSerializer(Jackson3SupplierSerializer.INSTANCE);
+ MODULE = module;
+ }
+
+ static final ObjectMapper DEFAULT_OBJECT_MAPPER = newObjectMapper();
+
+ /**
+ * Creates and returns a new ObjectMapper with the {@code jjwt-jackson3} module registered and
+ * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true) and
+ * {@code DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES} disabled (set to false).
+ *
+ * @return a new ObjectMapper with the {@code jjwt-jackson3} module registered and
+ * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true) and
+ * {@code DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES} disabled (set to false).
+ *
+ * @since 0.12.4
+ */
+ // package protected on purpose, do not expose to the public API
+ static ObjectMapper newObjectMapper() {
+ return builder().addModule(MODULE)
+ .configure(StreamReadFeature.STRICT_DUPLICATE_DETECTION, true) // https://github.com/jwtk/jjwt/issues/877
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // https://github.com/jwtk/jjwt/issues/893
+ .build();
+ }
+
+ protected final ObjectMapper objectMapper;
+
+ /**
+ * Constructor using JJWT's default {@link ObjectMapper} singleton for serialization.
+ */
+ public Jackson3Serializer() {
+ this(DEFAULT_OBJECT_MAPPER);
+ }
+
+ /**
+ * Creates a new Jackson Serializer that uses the specified {@link ObjectMapper} for serialization.
+ *
+ * @param objectMapper the ObjectMapper to use for serialization.
+ */
+ public Jackson3Serializer(ObjectMapper objectMapper) {
+ Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
+ this.objectMapper = objectMapper.rebuild().addModule(MODULE).build();
+ }
+
+ @Override
+ protected void doSerialize(T t, OutputStream out) throws Exception {
+ Assert.notNull(out, "OutputStream cannot be null.");
+
+ ObjectWriter writer = this.objectMapper.writer().without(AUTO_CLOSE_TARGET);
+ writer.writeValue(out, t);
+ }
+}
diff --git a/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3SupplierSerializer.java b/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3SupplierSerializer.java
new file mode 100644
index 000000000..1e8a1f49f
--- /dev/null
+++ b/extensions/jackson3/src/main/java/io/jsonwebtoken/jackson/io/Jackson3SupplierSerializer.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.jackson.io;
+
+
+import io.jsonwebtoken.lang.Supplier;
+import tools.jackson.core.JacksonException;
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.databind.SerializationContext;
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.ser.std.StdSerializer;
+
+final class Jackson3SupplierSerializer extends StdSerializer> {
+
+ static final Jackson3SupplierSerializer INSTANCE = new Jackson3SupplierSerializer();
+
+ public Jackson3SupplierSerializer() {
+ super(Supplier.class, false);
+ }
+
+ /**
+ * @param supplier
+ * @param generator
+ * @param provider
+ * @throws JacksonException
+ */
+ @Override
+ public void serialize(Supplier> supplier, JsonGenerator generator, SerializationContext provider) throws JacksonException {
+ Object value = supplier.get();
+
+ if (value == null) {
+ provider.defaultSerializeNullValue(generator);
+ return;
+ }
+
+ Class> clazz = value.getClass();
+ ValueSerializer