Skip to content

Commit 0816c74

Browse files
authored
Add DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED (#5027)
1 parent d6c6455 commit 0816c74

File tree

6 files changed

+194
-12
lines changed

6 files changed

+194
-12
lines changed

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ Project: jackson-databind
7676
failure of `java.util.Optional` (de)serialization without Java 8 module
7777
#5014: Add `java.lang.Runnable` as unsafe base type in `DefaultBaseTypeLimitingValidator`
7878
#5020: Support new `@JsonProperty.isRequired` for overridable definition of "required-ness"
79+
#5027: Add `DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED`
80+
(contributed by @pjfanning)
7981
#5052: Minor bug in `FirstCharBasedValidator.forFirstNameRule()`: returns `null`
8082
in non-default case
8183

src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public enum DeserializationFeature implements ConfigFeature
253253
FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY(true),
254254

255255
/**
256-
* Feature that determines behaviour for data-binding after binding the root value.
256+
* Feature that determines behavior for data-binding after binding the root value.
257257
* If feature is enabled, one more call to
258258
* {@link com.fasterxml.jackson.core.JsonParser#nextToken} is made to ensure that
259259
* no more tokens are found (and if any is found,
@@ -272,6 +272,24 @@ public enum DeserializationFeature implements ConfigFeature
272272
*/
273273
FAIL_ON_TRAILING_TOKENS(false),
274274

275+
/**
276+
* Feature that determines behavior when deserializing polymorphic types that use
277+
* Class-based Type Id mechanism (either
278+
* {@code JsonTypeInfo.Id.CLASS} or {@code JsonTypeInfo.Id.MINIMAL_CLASS}):
279+
* If enabled, an exception will be
280+
* thrown if a subtype (Class) is encountered that has not been explicitly registered (by
281+
* calling {@link ObjectMapper#registerSubtypes} or
282+
* {@link com.fasterxml.jackson.annotation.JsonSubTypes}).
283+
*<p>
284+
* Note that for Type Name - based Type Id mechanism ({@code JsonTypeInfo.Id.NAME})
285+
* you already need to register the subtypes but with so this feature has no effect.
286+
*<p>
287+
* Feature is disabled by default.
288+
*
289+
* @since 2.19
290+
*/
291+
FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED(false),
292+
275293
/**
276294
* Feature that determines whether Jackson code should catch
277295
* and wrap {@link Exception}s (but never {@link Error}s!)

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/ClassNameIdResolver.java

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import com.fasterxml.jackson.databind.*;
99
import com.fasterxml.jackson.databind.cfg.MapperConfig;
10+
import com.fasterxml.jackson.databind.jsontype.NamedType;
1011
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
1112
import com.fasterxml.jackson.databind.type.TypeFactory;
1213
import com.fasterxml.jackson.databind.util.ClassUtil;
@@ -26,6 +27,11 @@ public class ClassNameIdResolver
2627

2728
protected final PolymorphicTypeValidator _subTypeValidator;
2829

30+
/**
31+
* @since 2.19 (to support {@code DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED})
32+
*/
33+
protected final Set<String> _allowedSubtypes;
34+
2935
/**
3036
* @deprecated Since 2.10 use variant that takes {@link PolymorphicTypeValidator}
3137
*/
@@ -36,21 +42,56 @@ protected ClassNameIdResolver(JavaType baseType, TypeFactory typeFactory) {
3642

3743
/**
3844
* @since 2.10
45+
* @deprecated Since 2.19 use variant that takes {@code Collection<NamedType>}
3946
*/
47+
@Deprecated
4048
public ClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
4149
PolymorphicTypeValidator ptv) {
50+
this(baseType, typeFactory, null, ptv);
51+
}
52+
53+
/**
54+
* @since 2.19
55+
*/
56+
public ClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
57+
Collection<NamedType> subtypes, PolymorphicTypeValidator ptv) {
4258
super(baseType, typeFactory);
4359
_subTypeValidator = ptv;
60+
Set<String> allowedSubtypes = null;
61+
if (subtypes != null) {
62+
for (NamedType t : subtypes) {
63+
if (allowedSubtypes == null) {
64+
allowedSubtypes = new HashSet<>();
65+
}
66+
allowedSubtypes.add(t.getType().getName());
67+
}
68+
}
69+
_allowedSubtypes = (allowedSubtypes == null) ? Collections.emptySet() : allowedSubtypes;
4470
}
4571

72+
/**
73+
* @deprecated since 2.19
74+
*/
75+
@Deprecated
76+
public static ClassNameIdResolver construct(JavaType baseType,
77+
MapperConfig<?> config, PolymorphicTypeValidator ptv) {
78+
return new ClassNameIdResolver(baseType, config.getTypeFactory(), ptv);
79+
}
80+
81+
/**
82+
* @since 2.19
83+
*/
4684
public static ClassNameIdResolver construct(JavaType baseType, MapperConfig<?> config,
85+
Collection<NamedType> subtypes,
4786
PolymorphicTypeValidator ptv) {
48-
return new ClassNameIdResolver(baseType, config.getTypeFactory(), ptv);
87+
return new ClassNameIdResolver(baseType, config.getTypeFactory(), subtypes, ptv);
4988
}
5089

5190
@Override
5291
public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.CLASS; }
5392

93+
// 28-Mar-2025, tatu: Why is this here; not overridden so... ?
94+
@Deprecated // since 2.19
5495
public void registerSubtype(Class<?> type, String name) {
5596
// not used with class name - based resolvers
5697
}
@@ -72,14 +113,21 @@ public JavaType typeFromId(DatabindContext context, String id) throws IOExceptio
72113

73114
protected JavaType _typeFromId(String id, DatabindContext ctxt) throws IOException
74115
{
75-
// 24-Apr-2019, tatu: [databind#2195] validate as well as resolve:
76-
JavaType t = ctxt.resolveAndValidateSubType(_baseType, id, _subTypeValidator);
77-
if (t == null) {
78-
if (ctxt instanceof DeserializationContext) {
79-
// First: we may have problem handlers that can deal with it?
80-
return ((DeserializationContext) ctxt).handleUnknownTypeId(_baseType, id, this, "no such class found");
116+
DeserializationContext deserializationContext = null;
117+
if (ctxt instanceof DeserializationContext) {
118+
deserializationContext = (DeserializationContext) ctxt;
119+
}
120+
if ((_allowedSubtypes != null) && (deserializationContext != null)
121+
&& deserializationContext.isEnabled(
122+
DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED)) {
123+
if (!_allowedSubtypes.contains(id)) {
124+
throw deserializationContext.invalidTypeIdException(_baseType, id,
125+
"`DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED` is enabled and the input class is not registered using `@JsonSubTypes` annotation");
81126
}
82-
// ... meaning that we really should never get here.
127+
}
128+
final JavaType t = ctxt.resolveAndValidateSubType(_baseType, id, _subTypeValidator);
129+
if (t == null && deserializationContext != null) {
130+
return deserializationContext.handleUnknownTypeId(_baseType, id, this, "no such class found");
83131
}
84132
return t;
85133
}

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/MinimalClassNameIdResolver.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.fasterxml.jackson.databind.jsontype.impl;
22

33
import java.io.IOException;
4+
import java.util.Collection;
45

56
import com.fasterxml.jackson.annotation.JsonTypeInfo;
67

78
import com.fasterxml.jackson.databind.DatabindContext;
89
import com.fasterxml.jackson.databind.JavaType;
910
import com.fasterxml.jackson.databind.cfg.MapperConfig;
11+
import com.fasterxml.jackson.databind.jsontype.NamedType;
1012
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
1113
import com.fasterxml.jackson.databind.type.TypeFactory;
1214

@@ -32,10 +34,24 @@ public class MinimalClassNameIdResolver
3234
*/
3335
protected final String _basePackagePrefix;
3436

37+
/**
38+
* @deprecated since 2.19
39+
*/
40+
@Deprecated
41+
protected MinimalClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
42+
PolymorphicTypeValidator ptv)
43+
{
44+
this(baseType, typeFactory, null, ptv);
45+
}
46+
47+
/**
48+
* @since 2.19
49+
*/
3550
protected MinimalClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
51+
Collection<NamedType> subtypes,
3652
PolymorphicTypeValidator ptv)
3753
{
38-
super(baseType, typeFactory, ptv);
54+
super(baseType, typeFactory, subtypes, ptv);
3955
String base = baseType.getRawClass().getName();
4056
int ix = base.lastIndexOf('.');
4157
if (ix < 0) { // can this ever occur?
@@ -47,11 +63,24 @@ protected MinimalClassNameIdResolver(JavaType baseType, TypeFactory typeFactory,
4763
}
4864
}
4965

66+
/**
67+
* @deprecated since 2.19
68+
*/
69+
@Deprecated
5070
public static MinimalClassNameIdResolver construct(JavaType baseType, MapperConfig<?> config,
5171
PolymorphicTypeValidator ptv) {
5272
return new MinimalClassNameIdResolver(baseType, config.getTypeFactory(), ptv);
5373
}
5474

75+
/**
76+
* @since 2.19
77+
*/
78+
public static MinimalClassNameIdResolver construct(JavaType baseType, MapperConfig<?> config,
79+
Collection<NamedType> subtypes,
80+
PolymorphicTypeValidator ptv) {
81+
return new MinimalClassNameIdResolver(baseType, config.getTypeFactory(), subtypes, ptv);
82+
}
83+
5584
@Override
5685
public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.MINIMAL_CLASS; }
5786

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,9 @@ protected TypeIdResolver idResolver(MapperConfig<?> config,
356356
switch (_idType) {
357357
case DEDUCTION: // Deduction produces class names to be resolved
358358
case CLASS:
359-
return ClassNameIdResolver.construct(baseType, config, subtypeValidator);
359+
return ClassNameIdResolver.construct(baseType, config, subtypes, subtypeValidator);
360360
case MINIMAL_CLASS:
361-
return MinimalClassNameIdResolver.construct(baseType, config, subtypeValidator);
361+
return MinimalClassNameIdResolver.construct(baseType, config, subtypes, subtypeValidator);
362362
case SIMPLE_NAME:
363363
return SimpleNameIdResolver.construct(config, baseType, subtypes, forSer, forDeser);
364364
case NAME:
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.fasterxml.jackson.databind.jsontype;
2+
3+
import com.fasterxml.jackson.annotation.JsonSubTypes;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
import com.fasterxml.jackson.databind.DeserializationFeature;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
8+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
9+
import org.junit.jupiter.api.Test;
10+
11+
import static org.junit.jupiter.api.Assertions.assertThrows;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
14+
// For [databind#5027]
15+
public class RegisteredClassDeser5027Test extends DatabindTestUtil
16+
{
17+
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
18+
@JsonSubTypes({@JsonSubTypes.Type(value = FooClassImpl.class)})
19+
static abstract class FooClass { }
20+
static class FooClassImpl extends FooClass { }
21+
static class FooClassImpl2 extends FooClass { }
22+
23+
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
24+
static abstract class FooClassNoRegSubTypes { }
25+
static class FooClassNoRegSubTypesImpl extends FooClassNoRegSubTypes { }
26+
27+
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
28+
@JsonSubTypes({@JsonSubTypes.Type(value = FooMinClassImpl.class)})
29+
static abstract class FooMinClass { }
30+
static class FooMinClassImpl extends FooMinClass { }
31+
static class FooMinClassImpl2 extends FooMinClass { }
32+
33+
/*
34+
/************************************************************
35+
/* Unit tests, valid
36+
/************************************************************
37+
*/
38+
39+
private final ObjectMapper MAPPER = jsonMapperBuilder()
40+
.enable(DeserializationFeature.FAIL_ON_SUBTYPE_CLASS_NOT_REGISTERED)
41+
.build();
42+
43+
@Test
44+
public void testDeserializationIdClass() throws Exception
45+
{
46+
//trying to test if JsonSubTypes enforced
47+
final String foo1 = MAPPER.writeValueAsString(new FooClassImpl());
48+
final String foo2 = MAPPER.writeValueAsString(new FooClassImpl2());
49+
FooClass res1 = MAPPER.readValue(foo1, FooClass.class);
50+
assertTrue(res1 instanceof FooClassImpl);
51+
// next bit should fail because FooClassImpl2 is not listed as a subtype (see mapper config)
52+
assertThrows(InvalidTypeIdException.class, () -> MAPPER.readValue(foo2, FooClass.class));
53+
}
54+
55+
@Test
56+
public void testDeserializationIdClassNoReg() throws Exception
57+
{
58+
final ObjectMapper mapper = newJsonMapper();
59+
final String foo1 = mapper.writeValueAsString(new FooClassNoRegSubTypesImpl());
60+
// the default mapper should be able to deserialize the object (sub type check not enforced)
61+
FooClassNoRegSubTypes res1 = mapper.readValue(foo1, FooClassNoRegSubTypes.class);
62+
assertTrue(res1 instanceof FooClassNoRegSubTypesImpl);
63+
}
64+
65+
@Test
66+
public void testDefaultDeserializationIdClassNoReg() throws Exception
67+
{
68+
//trying to test if JsonSubTypes enforced
69+
final String foo1 = MAPPER.writeValueAsString(new FooClassNoRegSubTypesImpl());
70+
// next bit should fail because FooClassImpl2 is not listed as a subtype (see mapper config)
71+
assertThrows(InvalidTypeIdException.class, () -> MAPPER.readValue(foo1, FooClassNoRegSubTypes.class));
72+
}
73+
74+
@Test
75+
public void testDeserializationIdMinimalClass() throws Exception
76+
{
77+
//trying to test if JsonSubTypes enforced
78+
final String foo1 = MAPPER.writeValueAsString(new FooMinClassImpl());
79+
final String foo2 = MAPPER.writeValueAsString(new FooMinClassImpl2());
80+
FooMinClass res1 = MAPPER.readValue(foo1, FooMinClass.class);
81+
assertTrue(res1 instanceof FooMinClassImpl);
82+
// next bit should fail because FooMinClassImpl2 is not listed as a subtype (see mapper config)
83+
assertThrows(InvalidTypeIdException.class, () -> MAPPER.readValue(foo2, FooMinClass.class));
84+
}
85+
}

0 commit comments

Comments
 (0)