Skip to content

Commit b7e2be5

Browse files
authored
Fix #2882: make naming rules for getters stricter (#5050)
1 parent c647976 commit b7e2be5

File tree

7 files changed

+109
-23
lines changed

7 files changed

+109
-23
lines changed

release-notes/VERSION

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Versions: 3.x (for earlier see VERSION-2.x)
77

88
3.0.0-rc2 (not yet released)
99

10+
#2882: Tighten accessor naming rules to not allow leading lower-case or
11+
non-letter character for getters/setters
1012
#5003: Extend, improve set of `JsonNode.asXxx()` methods for number types
1113
#5025: Add support for automatic detection of subtypes (like `@JsonSubTypes`)
1214
from Java 17 sealed types

src/main/java/tools/jackson/databind/introspect/DefaultAccessorNamingStrategy.java

+35-8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ public class DefaultAccessorNamingStrategy
2727
* based on various rules.
2828
*/
2929
public interface BaseNameValidator {
30+
/**
31+
* Method called to check whether given base name is acceptable: called
32+
* for accessor method name after removing prefix.
33+
* NOTE: called even if "prefix" is empy (i.e. nothing really removed)
34+
*
35+
* @param firstChar First character of the base name
36+
* @param basename Full base name (after removing prefix)
37+
* @param offset Length of prefix removed
38+
*
39+
* @return {@ocode true} if base name is acceptable; {@code false} otherwise
40+
*/
3041
public boolean accept(char firstChar, String basename, int offset);
3142
}
3243

@@ -239,16 +250,19 @@ protected boolean _isGroovyMetaClassGetter(AnnotatedMethod am) {
239250
* <li>Is-getter (for Boolean values): "is"
240251
* </li>
241252
*</ul>
242-
* and no additional restrictions on base names accepted (configurable for
243-
* limits using {@link BaseNameValidator}), allowing names like
244-
* "get_value()" and "getvalue()".
253+
* with additional restrictions on base names accepted (configurable for
254+
* limits using {@link BaseNameValidator}), preventing names like
255+
* "get_value()" and "getvalue()" (unlike Jackson 2.x that allowed these).
245256
*/
246257
public static class Provider
247258
extends AccessorNamingStrategy.Provider
248259
implements java.io.Serializable
249260
{
250261
private static final long serialVersionUID = 1L;
251262

263+
private static final BaseNameValidator DEFAULT_BASE_NAME_VALIDATOR
264+
= FirstCharBasedValidator.forFirstNameRule(false, false);
265+
252266
protected final String _setterPrefix;
253267
protected final String _withPrefix;
254268

@@ -259,7 +273,7 @@ public static class Provider
259273

260274
public Provider() {
261275
this("set", JsonPOJOBuilder.DEFAULT_WITH_PREFIX,
262-
"get", "is", null);
276+
"get", "is", DEFAULT_BASE_NAME_VALIDATOR);
263277
}
264278

265279
protected Provider(Provider p,
@@ -380,7 +394,8 @@ public Provider withIsGetterPrefix(String prefix) {
380394
public Provider withFirstCharAcceptance(boolean allowLowerCaseFirstChar,
381395
boolean allowNonLetterFirstChar) {
382396
return withBaseNameValidator(
383-
FirstCharBasedValidator.forFirstNameRule(allowLowerCaseFirstChar, allowNonLetterFirstChar));
397+
FirstCharBasedValidator.forFirstNameRule(allowLowerCaseFirstChar,
398+
allowNonLetterFirstChar));
384399
}
385400

386401
/**
@@ -409,7 +424,8 @@ public AccessorNamingStrategy forPOJO(MapperConfig<?> config, AnnotatedClass tar
409424
public AccessorNamingStrategy forBuilder(MapperConfig<?> config,
410425
AnnotatedClass builderClass, BeanDescription valueTypeDesc)
411426
{
412-
AnnotationIntrospector ai = config.isAnnotationProcessingEnabled() ? config.getAnnotationIntrospector() : null;
427+
AnnotationIntrospector ai = config.isAnnotationProcessingEnabled()
428+
? config.getAnnotationIntrospector() : null;
413429
JsonPOJOBuilder.Value builderConfig = (ai == null) ? null : ai.findPOJOBuilderConfig(config, builderClass);
414430
String mutatorPrefix = (builderConfig == null) ? _withPrefix : builderConfig.withPrefix;
415431
return new DefaultAccessorNamingStrategy(config, builderClass,
@@ -426,14 +442,17 @@ public AccessorNamingStrategy forRecord(MapperConfig<?> config, AnnotatedClass r
426442

427443
/**
428444
* Simple implementation of {@link BaseNameValidator} that checks the
429-
* first character and nothing else.
445+
* first character and nothing else if (and only if!) prefix is empty
446+
* (so {@code offset} passed is NOT zero).
430447
*<p>
431448
* Instances are to be constructed using method
432449
* {@link FirstCharBasedValidator#forFirstNameRule}.
433450
*/
434451
public static class FirstCharBasedValidator
435-
implements BaseNameValidator
452+
implements BaseNameValidator, java.io.Serializable
436453
{
454+
private static final long serialVersionUID = 3L;
455+
437456
private final boolean _allowLowerCaseFirstChar;
438457
private final boolean _allowNonLetterFirstChar;
439458

@@ -468,6 +487,14 @@ public static BaseNameValidator forFirstNameRule(boolean allowLowerCaseFirstChar
468487

469488
@Override
470489
public boolean accept(char firstChar, String basename, int offset) {
490+
// 27-Mar-2025, tatu: [databind#2882] Need to make sure we don't
491+
// try to validate cases where no prefix is removed (mostly case
492+
// for builders, but also for Record-style plain "getters" and
493+
// "setters".
494+
if (offset == 0) {
495+
return true;
496+
}
497+
471498
// Ok, so... If UTF-16 letter, then check whether lc allowed
472499
// (title-case and upper-case both assumed to be acceptable by default)
473500
if (Character.isLetter(firstChar)) {

src/main/java/tools/jackson/databind/util/BeanUtil.java

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ public class BeanUtil
2121
/**********************************************************************
2222
*/
2323

24+
/**
25+
* @deprecated since 3.0.0-rc2 Use {@link tools.jackson.databind.introspect.DefaultAccessorNamingStrategy}
26+
* instead
27+
*/
28+
@Deprecated // since 3.0.0-rc2
2429
public static String stdManglePropertyName(final String basename, final int offset)
2530
{
2631
final int end = basename.length();

src/test/java/tools/jackson/databind/introspect/AccessorNamingForBuilderTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public void testAccessorCustomWithMethod() throws Exception
6464
.accessorNaming(new DefaultAccessorNamingStrategy.Provider()
6565
.withBuilderPrefix("")
6666
)
67+
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
6768
.build();
6869
ValueClassXY xy = customMapper.readValue(json, ValueClassXY.class);
6970
assertEquals(29, xy._x);

src/test/java/tools/jackson/databind/introspect/AccessorNamingStrategyTest.java

+14-11
Original file line numberDiff line numberDiff line change
@@ -113,21 +113,21 @@ public AccessorNamingStrategy forPOJO(MapperConfig<?> config, AnnotatedClass val
113113
/********************************************************
114114
*/
115115

116-
private final ObjectMapper MAPPER = JsonMapper.builder()
116+
private final ObjectMapper ACCNAMING2800_MAPPER = JsonMapper.builder()
117117
.accessorNaming(new AccNaming2800Provider())
118118
.build();
119119

120120
@Test
121121
public void testGetterNaming() throws Exception
122122
{
123123
assertEquals(a2q("{'X':3,'Z':true}"),
124-
MAPPER.writeValueAsString(new GetterBean2800_XZ()));
124+
ACCNAMING2800_MAPPER.writeValueAsString(new GetterBean2800_XZ()));
125125
}
126126

127127
@Test
128128
public void testSetterNaming() throws Exception
129129
{
130-
SetterBean2800_Y result = MAPPER.readValue(a2q("{'Y':42}"), SetterBean2800_Y.class);
130+
SetterBean2800_Y result = ACCNAMING2800_MAPPER.readValue(a2q("{'Y':42}"), SetterBean2800_Y.class);
131131
assertEquals(42, result.yyy);
132132
}
133133

@@ -136,10 +136,10 @@ public void testFieldNaming() throws Exception
136136
{
137137
// first serialization
138138
assertEquals(a2q("{'x':1}"),
139-
MAPPER.writeValueAsString(new FieldBean2800_X()));
139+
ACCNAMING2800_MAPPER.writeValueAsString(new FieldBean2800_X()));
140140

141141
// then deserialization
142-
FieldBean2800_X result = MAPPER.readValue(a2q("{'x':28}"),
142+
FieldBean2800_X result = ACCNAMING2800_MAPPER.readValue(a2q("{'x':28}"),
143143
FieldBean2800_X.class);
144144
assertEquals(28, result._x);
145145
assertEquals(2, result.y);
@@ -217,18 +217,19 @@ public void testBaseAccessorCustomSetter() throws Exception
217217
public void testFirstLetterConfigs() throws Exception
218218
{
219219
final FirstLetterVariesBean input = new FirstLetterVariesBean();
220-
final String STD_EXP = a2q("{'4Roses':42,'land':true,'value':31337}");
221220

222-
// First: vanilla? About anything goes
223-
ObjectMapper mapper = newJsonMapper();
224-
assertEquals(STD_EXP, mapper.writeValueAsString(input));
221+
// First: new (3.0) defaults -- no weird stuff
222+
assertEquals(a2q("{'value':31337}"),
223+
newJsonMapper().writeValueAsString(input));
225224

225+
// First: let's configure with "anything goes" (2.x default):
226226
// also if explicitly configured as default:
227-
mapper = JsonMapper.builder()
227+
ObjectMapper mapper = JsonMapper.builder()
228228
.accessorNaming(new DefaultAccessorNamingStrategy.Provider()
229229
.withFirstCharAcceptance(true, true))
230230
.build();
231-
assertEquals(STD_EXP, mapper.writeValueAsString(input));
231+
assertEquals(a2q("{'4Roses':42,'land':true,'value':31337}"),
232+
mapper.writeValueAsString(input));
232233

233234
// But we can vary it
234235
mapper = JsonMapper.builder()
@@ -246,5 +247,7 @@ public void testFirstLetterConfigs() throws Exception
246247
.build();
247248
assertEquals(a2q("{'4Roses':42,'value':31337}"),
248249
mapper.writeValueAsString(input));
250+
251+
249252
}
250253
}

src/test/java/tools/jackson/databind/introspect/BeanNamingTest.java

+45-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import org.junit.jupiter.api.Test;
44

55
import tools.jackson.databind.*;
6+
import tools.jackson.databind.exc.UnrecognizedPropertyException;
67
import tools.jackson.databind.testutil.DatabindTestUtil;
78

89
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.fail;
911

1012
// Tests for [databind#653]
1113
public class BeanNamingTest extends DatabindTestUtil
@@ -22,15 +24,55 @@ public int getA() {
2224
}
2325
}
2426

27+
// [databind#2882]
28+
static class Bean2882 {
29+
// These should NOT be detected as "getters" due to naming conventions
30+
public boolean island() { return true; }
31+
public boolean is_bad() { return true; }
32+
33+
public int get_value() { return -2; }
34+
public int getter() { return -3; }
35+
36+
// This is regular and should be detected
37+
public int getX() { return 1; }
38+
39+
// And bad "setter" too
40+
public void setter(int x) {
41+
throw new IllegalStateException("Should not get called");
42+
}
43+
}
44+
45+
private final ObjectMapper MAPPER = newJsonMapper();
46+
2547
// 24-Sep-2017, tatu: Used to test for `MapperFeature.USE_STD_BEAN_NAMING`, but with 3.x
2648
// that is always enabled.
2749
@Test
2850
public void testMultipleLeadingCapitalLetters() throws Exception
2951
{
30-
ObjectMapper mapper = newJsonMapper();
3152
assertEquals(a2q("{'URL':'http:'}"),
32-
mapper.writeValueAsString(new URLBean()));
53+
MAPPER.writeValueAsString(new URLBean()));
3354
assertEquals(a2q("{'a':3}"),
34-
mapper.writeValueAsString(new ABean()));
55+
MAPPER.writeValueAsString(new ABean()));
56+
}
57+
58+
// [databind#2882]
59+
@Test
60+
void testBadCasingForGetters() throws Exception
61+
{
62+
assertEquals(a2q("{'x':1}"),
63+
MAPPER.writeValueAsString(new Bean2882()));
64+
}
65+
66+
@Test
67+
void testBadCasingForSetters() throws Exception
68+
{
69+
try {
70+
MAPPER.readerFor(Bean2882.class)
71+
.with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
72+
.readValue(a2q("{'ter':1}"));
73+
fail("Should not pass");
74+
} catch (UnrecognizedPropertyException e) {
75+
verifyException(e, "Unrecognized property \"ter\"");
76+
}
3577
}
3678
}

src/test/java/tools/jackson/databind/introspect/TestPropertyConflicts.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import tools.jackson.databind.*;
88
import tools.jackson.databind.cfg.MapperConfig;
99
import tools.jackson.databind.exc.InvalidDefinitionException;
10+
import tools.jackson.databind.json.JsonMapper;
1011
import tools.jackson.databind.testutil.DatabindTestUtil;
1112

1213
import static org.junit.jupiter.api.Assertions.*;
@@ -99,8 +100,13 @@ public String getStr() {
99100
public void testFailWithDupProps() throws Exception
100101
{
101102
BeanWithConflict bean = new BeanWithConflict();
103+
// Must allow "getx"
104+
JsonMapper mapper = JsonMapper.builder()
105+
.accessorNaming(new DefaultAccessorNamingStrategy.Provider()
106+
.withFirstCharAcceptance(true, false))
107+
.build();
102108
try {
103-
String json = MAPPER.writer().writeValueAsString(bean);
109+
String json = mapper.writeValueAsString(bean);
104110
fail("Should have failed due to conflicting accessor definitions; got JSON = "+json);
105111
} catch (InvalidDefinitionException e) {
106112
verifyException(e, "Conflicting getter definitions");

0 commit comments

Comments
 (0)