diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SimpleNameIdResolver.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SimpleNameIdResolver.java
new file mode 100644
index 0000000000..94724f5f64
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SimpleNameIdResolver.java
@@ -0,0 +1,205 @@
+package com.fasterxml.jackson.databind.jsontype.impl;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.DatabindContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.jsontype.NamedType;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * {@link com.fasterxml.jackson.databind.jsontype.TypeIdResolver} implementation
+ * that converts between (JSON) Strings and simple Java class names.
+ *
+ * @since 2.16
+ */
+public class SimpleNameIdResolver
+ extends TypeIdResolverBase
+{
+ protected final MapperConfig> _config;
+
+ /**
+ * Mappings from class name to type id, used for serialization.
+ *
+ * Since lazily constructed will require synchronization (either internal
+ * by type, or external)
+ */
+ protected final ConcurrentHashMap _typeToId;
+
+ /**
+ * Mappings from type id to JavaType, used for deserialization.
+ *
+ * Eagerly constructed, not modified, can use regular unsynchronized {@link Map}.
+ */
+ protected final Map _idToType;
+
+ protected final boolean _caseInsensitive;
+
+ protected SimpleNameIdResolver(MapperConfig> config, JavaType baseType,
+ ConcurrentHashMap typeToId,
+ HashMap idToType)
+ {
+ super(baseType, config.getTypeFactory());
+ _config = config;
+ _typeToId = typeToId;
+ _idToType = idToType;
+ _caseInsensitive = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES);
+ }
+
+ public static SimpleNameIdResolver construct(MapperConfig> config, JavaType baseType,
+ Collection subtypes, boolean forSer, boolean forDeser)
+ {
+ // sanity check
+ if (forSer == forDeser) throw new IllegalArgumentException();
+
+ final ConcurrentHashMap typeToId;
+ final HashMap idToType;
+
+ if (forSer) {
+ // Only need Class-to-id for serialization; but synchronized since may be
+ // lazily built (if adding type-id-mappings dynamically)
+ typeToId = new ConcurrentHashMap<>();
+ idToType = null;
+ } else {
+ idToType = new HashMap<>();
+ // 14-Apr-2016, tatu: Apparently needed for special case of `defaultImpl`;
+ // see [databind#1198] for details: but essentially we only need room
+ // for a single value.
+ typeToId = new ConcurrentHashMap<>(4);
+ }
+ final boolean caseInsensitive = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES);
+
+ if (subtypes != null) {
+ for (NamedType t : subtypes) {
+ // no name? Need to figure out default; for now, let's just
+ // use non-qualified class name
+ Class> cls = t.getType();
+ String id = t.hasName() ? t.getName() : _defaultTypeId(cls);
+ if (forSer) {
+ typeToId.put(cls.getName(), id);
+ }
+ if (forDeser) {
+ // [databind#1983]: for case-insensitive lookups must canonicalize:
+ if (caseInsensitive) {
+ id = id.toLowerCase();
+ }
+ // One more problem; sometimes we have same name for multiple types;
+ // if so, use most specific
+ JavaType prev = idToType.get(id); // lgtm [java/dereferenced-value-may-be-null]
+ if (prev != null) { // Can only override if more specific
+ if (cls.isAssignableFrom(prev.getRawClass())) { // nope, more generic (or same)
+ continue;
+ }
+ }
+ idToType.put(id, config.constructType(cls));
+ }
+ }
+ }
+ return new SimpleNameIdResolver(config, baseType, typeToId, idToType);
+ }
+
+ @Override
+ public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.SIMPLE_NAME; }
+
+ @Override
+ public String idFromValue(Object value) {
+ return idFromClass(value.getClass());
+ }
+
+ protected String idFromClass(Class> clazz)
+ {
+ if (clazz == null) {
+ return null;
+ }
+ // NOTE: although we may need to let `TypeModifier` change actual type to use
+ // for id, we can use original type as key for more efficient lookup:
+ final String key = clazz.getName();
+ String name = _typeToId.get(key);
+
+ if (name == null) {
+ // 29-Nov-2019, tatu: As per test in `TestTypeModifierNameResolution` somehow
+ // we need to do this odd piece here which seems unnecessary but isn't.
+ Class> cls = _typeFactory.constructType(clazz).getRawClass();
+ // 24-Feb-2011, tatu: As per [JACKSON-498], may need to dynamically look up name
+ // can either throw an exception, or use default name...
+ if (_config.isAnnotationProcessingEnabled()) {
+ BeanDescription beanDesc = _config.introspectClassAnnotations(cls);
+ name = _config.getAnnotationIntrospector().findTypeName(beanDesc.getClassInfo());
+ }
+ if (name == null) {
+ // And if still not found, let's choose default?
+ name = _defaultTypeId(cls);
+ }
+ _typeToId.put(key, name);
+ }
+ return name;
+ }
+
+ @Override
+ public String idFromValueAndType(Object value, Class> type) {
+ // 18-Jan-2013, tatu: We may be called with null value occasionally
+ // it seems; nothing much we can figure out that way.
+ if (value == null) {
+ return idFromClass(type);
+ }
+ return idFromValue(value);
+ }
+
+ @Override
+ public JavaType typeFromId(DatabindContext context, String id) {
+ return _typeFromId(id);
+ }
+
+ protected JavaType _typeFromId(String id) {
+ // [databind#1983]: for case-insensitive lookups must canonicalize:
+ if (_caseInsensitive) {
+ id = id.toLowerCase();
+ }
+ // Now: if no type is found, should we try to locate it by
+ // some other means? (specifically, if in same package as base type,
+ // could just try Class.forName)
+ // For now let's not add any such workarounds; can add if need be
+ return _idToType.get(id);
+ }
+
+ @Override
+ public String getDescForKnownTypeIds() {
+ // 05-May-2020, tatu: As per [databind#1919], only include ids for
+ // non-abstract types
+ final TreeSet ids = new TreeSet<>();
+ for (Map.Entry entry : _idToType.entrySet()) {
+ if (entry.getValue().isConcrete()) {
+ ids.add(entry.getKey());
+ }
+ }
+ return ids.toString();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s; id-to-type=%s]", getClass().getName(), _idToType);
+ }
+
+ /*
+ /*********************************************************
+ /* Helper methods
+ /*********************************************************
+ */
+
+ /**
+ * If no name was explicitly given for a class, we will just
+ * use simple class name
+ */
+ protected static String _defaultTypeId(Class> cls)
+ {
+ String n = cls.getName();
+ int ix = Math.max(n.lastIndexOf('.'), n.lastIndexOf('$'));
+ return (ix < 0) ? n : n.substring(ix+1);
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
index 43580d0ec4..ed6ff31fd9 100644
--- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
+++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java
@@ -368,6 +368,8 @@ protected TypeIdResolver idResolver(MapperConfig> config,
return ClassNameIdResolver.construct(baseType, config, subtypeValidator);
case MINIMAL_CLASS:
return MinimalClassNameIdResolver.construct(baseType, config, subtypeValidator);
+ case SIMPLE_NAME:
+ return SimpleNameIdResolver.construct(config, baseType, subtypes, forSer, forDeser);
case NAME:
return TypeNameIdResolver.construct(config, baseType, subtypes, forSer, forDeser);
case NONE: // hmmh. should never get this far with 'none'
diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/JsonTypeInfoSimpleClassName4061Test.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/JsonTypeInfoSimpleClassName4061Test.java
new file mode 100644
index 0000000000..d5cc00eda6
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/JsonTypeInfoSimpleClassName4061Test.java
@@ -0,0 +1,288 @@
+package com.fasterxml.jackson.databind.jsontype;
+
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.databind.BaseMapTest;
+import com.fasterxml.jackson.databind.DeserializationConfig;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
+import com.fasterxml.jackson.databind.jsontype.impl.SimpleNameIdResolver;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test for
+ * [databind#4061] Add JsonTypeInfo.Id.SIMPLE_NAME
+ *
+ * @since 2.16
+ */
+public class JsonTypeInfoSimpleClassName4061Test extends BaseMapTest
+{
+
+ @JsonTypeInfo(
+ use = JsonTypeInfo.Id.SIMPLE_NAME)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = InnerSub4061A.class),
+ @JsonSubTypes.Type(value = InnerSub4061B.class)
+ })
+ static class InnerSuper4061 { }
+
+ static class InnerSub4061A extends InnerSuper4061 { }
+
+ static class InnerSub4061B extends InnerSuper4061 { }
+
+ @JsonTypeInfo(
+ use = JsonTypeInfo.Id.MINIMAL_CLASS)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = MinimalInnerSub4061A.class),
+ @JsonSubTypes.Type(value = MinimalInnerSub4061B.class)
+ })
+ static class MinimalInnerSuper4061 { }
+
+ static class MinimalInnerSub4061A extends MinimalInnerSuper4061 { }
+
+ static class MinimalInnerSub4061B extends MinimalInnerSuper4061 { }
+
+ @JsonTypeInfo(
+ use = JsonTypeInfo.Id.SIMPLE_NAME)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = MixedSub4061A.class),
+ @JsonSubTypes.Type(value = MixedSub4061B.class)
+ })
+ static class MixedSuper4061 { }
+
+ @JsonTypeInfo(
+ use = JsonTypeInfo.Id.MINIMAL_CLASS)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = MixedMinimalSub4061A.class),
+ @JsonSubTypes.Type(value = MixedMinimalSub4061B.class)
+ })
+ static class MixedMinimalSuper4061 { }
+
+ static class Root {
+ @JsonMerge
+ public MergeChild child;
+ }
+
+ @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = MergeChildA.class, name = "MergeChildA"),
+ @JsonSubTypes.Type(value = MergeChildB.class, name = "MergeChildB")
+ })
+ static abstract class MergeChild {
+ }
+
+ static class MergeChildA extends MergeChild {
+ public String name;
+ }
+
+ static class MergeChildB extends MergeChild {
+ public String code;
+ }
+
+ static class PolyWrapperForAlias {
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.WRAPPER_ARRAY)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = AliasBean.class,name = "ab")})
+ public Object value;
+
+ protected PolyWrapperForAlias() { }
+
+ public PolyWrapperForAlias(Object v) { value = v; }
+ }
+
+ static class AliasBean {
+ @JsonAlias({ "nm", "Name" })
+ public String name;
+ int _xyz;
+ int _a;
+
+ @JsonCreator
+ public AliasBean(@JsonProperty("a") @JsonAlias("A") int a) {
+ _a = a;
+ }
+
+ @JsonAlias({ "Xyz" })
+ public void setXyz(int x) {
+ _xyz = x;
+ }
+ }
+
+ @JsonTypeInfo(
+ use = JsonTypeInfo.Id.SIMPLE_NAME)
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = DuplicateSubClass.class),
+ @JsonSubTypes.Type(value = com.fasterxml.jackson.databind.jsontype.DuplicateSubClass.class)
+ })
+ static class DuplicateSuperClass { }
+
+ static class DuplicateSubClass extends DuplicateSuperClass { }
+
+ /*
+ /**********************************************************
+ /* Unit tests
+ /**********************************************************
+ */
+
+ private final ObjectMapper MAPPER = newJsonMapper();
+
+ // inner class that has contains dollar sign
+ public void testInnerClass() throws Exception
+ {
+ String jsonStr = a2q("{'@type':'InnerSub4061A'}");
+
+ // ser
+ assertEquals(jsonStr, MAPPER.writeValueAsString(new InnerSub4061A()));
+
+ // deser <- breaks!
+ InnerSuper4061 bean = MAPPER.readValue(jsonStr, InnerSuper4061.class);
+ assertType(bean, InnerSuper4061.class);
+ }
+
+ // inner class that has contains dollar sign
+ public void testMinimalInnerClass() throws Exception
+ {
+ String jsonStr = a2q("{'@c':'.JsonTypeInfoSimpleClassName4061Test$MinimalInnerSub4061A'}");
+
+ // ser
+ assertEquals(jsonStr, MAPPER.writeValueAsString(new MinimalInnerSub4061A()));
+
+ // deser <- breaks!
+ MinimalInnerSuper4061 bean = MAPPER.readValue(jsonStr, MinimalInnerSuper4061.class);
+ assertType(bean, MinimalInnerSuper4061.class);
+ assertNotNull(bean);
+ }
+
+ // Basic : non-inner class, without dollar sign
+ public void testBasicClass() throws Exception
+ {
+ String jsonStr = a2q("{'@type':'BasicSub4061A'}");
+
+ // ser
+ assertEquals(jsonStr, MAPPER.writeValueAsString(new BasicSub4061A()));
+
+ // deser
+ BasicSuper4061 bean = MAPPER.readValue(jsonStr, BasicSuper4061.class);
+ assertType(bean, BasicSuper4061.class);
+ assertType(bean, BasicSub4061A.class);
+ }
+
+ // Mixed SimpleClassName : parent as inner, subtype as basic
+ public void testMixedClass() throws Exception
+ {
+ String jsonStr = a2q("{'@type':'MixedSub4061A'}");
+
+ // ser
+ assertEquals(jsonStr, MAPPER.writeValueAsString(new MixedSub4061A()));
+
+ // deser
+ MixedSuper4061 bean = MAPPER.readValue(jsonStr, MixedSuper4061.class);
+ assertType(bean, MixedSuper4061.class);
+ assertType(bean, MixedSub4061A.class);
+ }
+
+ // Mixed MinimalClass : parent as inner, subtype as basic
+ public void testMixedMinimalClass() throws Exception
+ {
+ String jsonStr = a2q("{'@c':'.MixedMinimalSub4061A'}");
+
+ // ser
+ assertEquals(jsonStr, MAPPER.writeValueAsString(new MixedMinimalSub4061A()));
+
+ // deser
+ MixedMinimalSuper4061 bean = MAPPER.readValue(jsonStr, MixedMinimalSuper4061.class);
+ assertType(bean, MixedMinimalSuper4061.class);
+ assertType(bean, MixedMinimalSub4061A.class);
+ }
+
+ public void testPolymorphicNewObject() throws Exception
+ {
+ String jsonStr = "{\"child\": { \"@type\": \"MergeChildA\", \"name\": \"I'm child A\" }}";
+
+ Root root = MAPPER.readValue(jsonStr, Root.class);
+
+ assertTrue(root.child instanceof MergeChildA);
+ assertEquals("I'm child A", ((MergeChildA) root.child).name);
+ }
+
+ // case insenstive type name
+ public void testPolymorphicNewObjectCaseInsensitive() throws Exception
+ {
+ String jsonStr = "{\"child\": { \"@type\": \"mergechilda\", \"name\": \"I'm child A\" }}";
+ ObjectMapper mapper = jsonMapperBuilder()
+ .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES)
+ .build();
+
+ Root root = mapper.readValue(jsonStr, Root.class);
+
+ assertTrue(root.child instanceof MergeChildA);
+ assertEquals("I'm child A", ((MergeChildA) root.child).name);
+ }
+
+ public void testPolymorphicNewObjectUnknownTypeId() throws Exception
+ {
+ try {
+ MAPPER.readValue("{\"child\": { \"@type\": \"UnknownChildA\", \"name\": \"I'm child A\" }}", Root.class);
+ } catch (InvalidTypeIdException e) {
+ verifyException(e, "Could not resolve type id 'UnknownChildA' as a subtype of");
+ }
+ }
+
+ public void testAliasWithPolymorphic() throws Exception
+ {
+ String jsonStr = a2q("{'value': ['ab', {'nm' : 'Bob', 'A' : 17} ] }");
+
+ PolyWrapperForAlias value = MAPPER.readValue(jsonStr, PolyWrapperForAlias.class);
+
+ assertNotNull(value.value);
+ AliasBean bean = (AliasBean) value.value;
+ assertEquals("Bob", bean.name);
+ assertEquals(17, bean._a);
+ }
+
+ public void testGetMechanism()
+ {
+ final DeserializationConfig config = MAPPER.getDeserializationConfig();
+ JavaType javaType = config.constructType(InnerSub4061B.class);
+ List namedTypes = new ArrayList<>();
+ namedTypes.add(new NamedType(InnerSub4061A.class));
+ namedTypes.add(new NamedType(InnerSub4061B.class));
+
+ SimpleNameIdResolver idResolver = SimpleNameIdResolver.construct(config, javaType, namedTypes, false, true);
+
+ assertEquals(JsonTypeInfo.Id.SIMPLE_NAME, idResolver.getMechanism());
+ }
+
+ public void testDuplicateNameLastOneWins() throws Exception
+ {
+ String jsonStr = a2q("{'@type':'DuplicateSubClass'}");
+
+ // deser
+ DuplicateSuperClass bean = MAPPER.readValue(jsonStr, DuplicateSuperClass.class);
+ assertType(bean, com.fasterxml.jackson.databind.jsontype.DuplicateSubClass.class);
+ }
+}
+
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.SIMPLE_NAME)
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = BasicSub4061A.class),
+ @JsonSubTypes.Type(value = BasicSub4061B.class)
+})
+class BasicSuper4061 { }
+
+class BasicSub4061A extends BasicSuper4061 { }
+
+class BasicSub4061B extends BasicSuper4061 { }
+
+class MixedSub4061A extends JsonTypeInfoSimpleClassName4061Test.MixedSuper4061 { }
+
+class MixedSub4061B extends JsonTypeInfoSimpleClassName4061Test.MixedSuper4061 { }
+
+class MixedMinimalSub4061A extends JsonTypeInfoSimpleClassName4061Test.MixedMinimalSuper4061 { }
+
+class MixedMinimalSub4061B extends JsonTypeInfoSimpleClassName4061Test.MixedMinimalSuper4061 { }
+
+class DuplicateSubClass extends JsonTypeInfoSimpleClassName4061Test.DuplicateSuperClass { }