Skip to content

Commit f807c8c

Browse files
committed
Fix #963: Add JsonPropertyNamingStrategy.KEBAB_CASE for hyphen-delimited (List-style) names
1 parent cebfddb commit f807c8c

File tree

6 files changed

+150
-49
lines changed

6 files changed

+150
-49
lines changed

release-notes/CREDITS

+10-6
Original file line numberDiff line numberDiff line change
@@ -359,19 +359,23 @@ Miles Kaufmann (milesk-amzn@github)
359359
* Reported #432: `StdValueInstantiator` unwraps exceptions, losing context
360360
(2.7.0)
361361

362+
Thomas Mortagne (tmortagne@github)
363+
* Suggested #857: Add support for java.beans.Transient
364+
(2.7.0)
365+
366+
Jonas Konrad (yawkat@github)
367+
* Suggested #905: Add support for `@ConstructorProperties`
368+
(2.7.0)
369+
362370
Jirka Kremser (Jiri-Kremser@github)
363371
* Suggested #924: SequenceWriter.writeAll() could accept Iterable
364372
(2.7.0)
365373

366-
Thomas Mortagne (tmortagne@github)
367-
* Suggested #857: Add support for java.beans.Transient
374+
Daniel Mischler (danielmischler@github)
375+
* Requested #963: Add PropertyNameStrategy `KEBAB_CASE`
368376
(2.7.0)
369377

370378
Shumpei Akai (flexfrank@github)
371379
* Reported #978: ObjectMapper#canSerialize(Object.class) returns false even though
372380
FAIL_ON_EMPTY_BEANS is disabled
373381
(2.7.0)
374-
375-
Jonas Konrad (yawkat@github)
376-
* Suggested #905: Add support for `@ConstructorProperties`
377-
(2.7.0)

release-notes/VERSION

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Project: jackson-databind
3636
(contributed by Jesse W)
3737
#957: Merge `datatype-jdk7` stuff in (java.nio.file.Path handling)
3838
#959: Schema generation: consider active view, discard non-included properties
39+
#963: Add PropertyNameStrategy `KEBAB_CASE`
40+
(requested by Daniel M)
3941
#978: ObjectMapper#canSerialize(Object.class) returns false even though FAIL_ON_EMPTY_BEANS is disabled
4042
(reported by Shumpei A)
4143
#997: Add `MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS`

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

+58-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* Methods are passed information about POJO member for which name is needed,
1414
* as well as default name that would be used if no custom strategy was used.
1515
*<p>
16-
* Default implementation returns suggested ("default") name unmodified.
16+
* Default (empty) implementation returns suggested ("default") name unmodified.
1717
*<p>
1818
* Note that the strategy is guaranteed to be called once per logical property
1919
* (which may be represented by multiple members; such as pair of a getter and
@@ -66,7 +66,16 @@ public class PropertyNamingStrategy // NOTE: was abstract until 2.7
6666
* @since 2.4
6767
*/
6868
public static final PropertyNamingStrategy LOWER_CASE = new LowerCaseStrategy();
69-
69+
70+
/**
71+
* Naming convention used in languages like Lisp, where words are in lower-case
72+
* letters, separated by hyphens.
73+
* See {@link KebabCaseStrategy} for details.
74+
*
75+
* @since 2.7
76+
*/
77+
public static final PropertyNamingStrategy KEBAB_CASE = new KebabCaseStrategy();
78+
7079
/*
7180
/**********************************************************
7281
/* API
@@ -341,6 +350,53 @@ public String translate(String input) {
341350
}
342351
}
343352

353+
/**
354+
* Naming strategy similar to {@link SnakeCaseStrategy}, but instead of underscores
355+
* as separators, uses hyphens. Naming convention traditionally used for languages
356+
* like Lisp.
357+
*
358+
* @since 2.7
359+
*/
360+
public static class KebabCaseStrategy extends PropertyNamingStrategyBase
361+
{
362+
@Override
363+
public String translate(String input)
364+
{
365+
if (input == null) return input; // garbage in, garbage out
366+
int length = input.length();
367+
if (length == 0) {
368+
return input;
369+
}
370+
371+
StringBuilder result = new StringBuilder(length + (length >> 1));
372+
373+
int upperCount = 0;
374+
375+
for (int i = 0; i < length; ++i) {
376+
char ch = input.charAt(i);
377+
char lc = Character.toLowerCase(ch);
378+
379+
if (lc == ch) { // lower-case letter means we can get new word
380+
// but need to check for multi-letter upper-case (acronym), where assumption
381+
// is that the last upper-case char is start of a new word
382+
if (upperCount > 1) {
383+
// so insert hyphen before the last character now
384+
result.insert(result.length() - 1, '-');
385+
}
386+
upperCount = 0;
387+
} else {
388+
// Otherwise starts new word, unless beginning of string
389+
if ((upperCount == 0) && (i > 0)) {
390+
result.append('-');
391+
}
392+
++upperCount;
393+
}
394+
result.append(lc);
395+
}
396+
return result.toString();
397+
}
398+
}
399+
344400
/*
345401
/**********************************************************
346402
/* Deprecated variants, aliases

src/test/java/com/fasterxml/jackson/databind/creators/TestCreatorWithNamingStrategy556.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public String findImplicitPropertyName(AnnotatedMember param) {
5858
}
5959

6060
private final ObjectMapper MAPPER = new ObjectMapper()
61-
.setPropertyNamingStrategy(PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE)
61+
.setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE)
6262
;
6363
{
6464
MAPPER.setAnnotationIntrospector(new MyParamIntrospector());

src/test/java/com/fasterxml/jackson/databind/introspect/TestNamingStrategyStd.java

+78-39
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,16 @@
33
import java.util.Arrays;
44
import java.util.List;
55

6-
import org.junit.Test;
7-
86
import com.fasterxml.jackson.annotation.*;
9-
import com.fasterxml.jackson.databind.BaseMapTest;
10-
import com.fasterxml.jackson.databind.MapperFeature;
11-
import com.fasterxml.jackson.databind.ObjectMapper;
12-
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
7+
8+
import com.fasterxml.jackson.databind.*;
139
import com.fasterxml.jackson.databind.annotation.JsonNaming;
1410
import com.fasterxml.jackson.databind.introspect.TestNamingStrategyCustom.PersonBean;
1511
import com.fasterxml.jackson.databind.node.ObjectNode;
1612

1713
/**
18-
* Unit tests to verify functioning of
19-
* {@link PropertyNamingStrategy#CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES}
20-
* and
21-
* {@link PropertyNamingStrategy#PASCAL_CASE_TO_CAMEL_CASE }
22-
* inside the context of an ObjectMapper.
23-
* PASCAL_CASE_TO_CAMEL_CASE was added in Jackson 2.1,
24-
* as per [JACKSON-63].
14+
* Unit tests to verify functioning of standard {@link PropertyNamingStrategy}
15+
* implementations Jackson includes out of the box.
2516
*/
2617
public class TestNamingStrategyStd extends BaseMapTest
2718
{
@@ -110,14 +101,21 @@ static class ExplicitBean {
110101
static class DefaultNaming {
111102
public int someValue = 3;
112103
}
113-
104+
105+
static class FirstNameBean {
106+
public String firstName;
107+
108+
protected FirstNameBean() { }
109+
public FirstNameBean(String n) { firstName = n; }
110+
}
111+
114112
/*
115113
/**********************************************************
116114
/* Set up
117115
/**********************************************************
118116
*/
119117

120-
public static List<Object[]> NAME_TRANSLATIONS = Arrays.asList(new Object[][] {
118+
public static List<Object[]> SNAKE_CASE_NAME_TRANSLATIONS = Arrays.asList(new Object[][] {
121119
{null, null},
122120
{"", ""},
123121
{"a", "a"},
@@ -176,32 +174,29 @@ public void setUp() throws Exception
176174
{
177175
super.setUp();
178176
_lcWithUndescoreMapper = new ObjectMapper();
179-
_lcWithUndescoreMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
177+
_lcWithUndescoreMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
180178
}
181179

182180
/*
183181
/**********************************************************
184-
/* Test methods for CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES
182+
/* Test methods for SNAKE_CASE
185183
/**********************************************************
186184
*/
187185

188186
/**
189187
* Unit test to verify translations of
190-
* {@link PropertyNamingStrategy#CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES}
188+
* {@link PropertyNamingStrategy#SNAKE_CASE}
191189
* outside the context of an ObjectMapper.
192-
* CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES was added in Jackson 1.9,
193-
* as per [JACKSON-598].
194190
*/
195-
@Test
196191
public void testLowerCaseStrategyStandAlone()
197192
{
198-
for (Object[] pair : NAME_TRANSLATIONS) {
199-
String translatedJavaName = PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES.nameForField(null, null,
193+
for (Object[] pair : SNAKE_CASE_NAME_TRANSLATIONS) {
194+
String translatedJavaName = PropertyNamingStrategy.SNAKE_CASE.nameForField(null, null,
200195
(String) pair[0]);
201196
assertEquals((String) pair[1], translatedJavaName);
202197
}
203198
}
204-
199+
205200
public void testLowerCaseTranslations() throws Exception
206201
{
207202
// First serialize
@@ -256,34 +251,32 @@ public void testLowerCaseUnchangedNames() throws Exception
256251
assertEquals("from7user", result.from7user);
257252
assertEquals("_x", result._x);
258253
}
259-
254+
260255
/*
261256
/**********************************************************
262-
/* Test methods for PASCAL_CASE_TO_CAMEL_CASE (added in 2.1)
257+
/* Test methods for UPPER_CAMEL_CASE
263258
/**********************************************************
264259
*/
265260

266261
/**
267262
* Unit test to verify translations of
268-
* {@link PropertyNamingStrategy#PASCAL_CASE_TO_CAMEL_CASE }
263+
* {@link PropertyNamingStrategy#UPPER_CAMEL_CASE }
269264
* outside the context of an ObjectMapper.
270-
* PASCAL_CASE_TO_CAMEL_CASE was added in Jackson 2.1.0,
271-
* as per [JACKSON-63].
272265
*/
273266
public void testPascalCaseStandAlone()
274267
{
275-
String translatedJavaName = PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE.nameForField
268+
String translatedJavaName = PropertyNamingStrategy.UPPER_CAMEL_CASE.nameForField
276269
(null, null, "userName");
277270
assertEquals("UserName", translatedJavaName);
278271

279-
translatedJavaName = PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE.nameForField
272+
translatedJavaName = PropertyNamingStrategy.UPPER_CAMEL_CASE.nameForField
280273
(null, null, "User");
281274
assertEquals("User", translatedJavaName);
282275

283-
translatedJavaName = PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE.nameForField
276+
translatedJavaName = PropertyNamingStrategy.UPPER_CAMEL_CASE.nameForField
284277
(null, null, "user");
285278
assertEquals("User", translatedJavaName);
286-
translatedJavaName = PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE.nameForField
279+
translatedJavaName = PropertyNamingStrategy.UPPER_CAMEL_CASE.nameForField
287280
(null, null, "x");
288281
assertEquals("X", translatedJavaName);
289282
}
@@ -294,14 +287,20 @@ public void testPascalCaseStandAlone()
294287
public void testIssue428PascalWithOverrides() throws Exception {
295288

296289
String json = new ObjectMapper()
297-
.setPropertyNamingStrategy(PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE)
290+
.setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE)
298291
.writeValueAsString(new Bean428());
299292

300293
if (!json.contains(quote("fooBar"))) {
301294
fail("Should use name 'fooBar', does not: "+json);
302295
}
303296
}
304297

298+
/*
299+
/**********************************************************
300+
/* Test methods for LOWER_CASE
301+
/**********************************************************
302+
*/
303+
305304
/**
306305
* For [databind#461]
307306
*/
@@ -314,13 +313,52 @@ public void testSimpleLowerCase() throws Exception
314313
m.writeValueAsString(input));
315314
}
316315

316+
/*
317+
/**********************************************************
318+
/* Test methods for KEBAB_CASE
319+
/**********************************************************
320+
*/
321+
322+
public void testKebabCaseStrategyStandAlone()
323+
{
324+
assertEquals("some-value",
325+
PropertyNamingStrategy.KEBAB_CASE.nameForField(null, null, "someValue"));
326+
assertEquals("some-value",
327+
PropertyNamingStrategy.KEBAB_CASE.nameForField(null, null, "SomeValue"));
328+
assertEquals("url",
329+
PropertyNamingStrategy.KEBAB_CASE.nameForField(null, null, "URL"));
330+
assertEquals("url-stuff",
331+
PropertyNamingStrategy.KEBAB_CASE.nameForField(null, null, "URLStuff"));
332+
assertEquals("some-url-stuff",
333+
PropertyNamingStrategy.KEBAB_CASE.nameForField(null, null, "SomeURLStuff"));
334+
}
335+
336+
public void testSimpleKebabCase() throws Exception
337+
{
338+
final FirstNameBean input = new FirstNameBean("Bob");
339+
ObjectMapper m = new ObjectMapper()
340+
.setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE);
341+
342+
assertEquals(aposToQuotes("{'first-name':'Bob'}"), m.writeValueAsString(input));
343+
344+
FirstNameBean result = m.readValue(aposToQuotes("{'first-name':'Billy'}"),
345+
FirstNameBean.class);
346+
assertEquals("Billy", result.firstName);
347+
}
348+
349+
/*
350+
/**********************************************************
351+
/* Test methods, other
352+
/**********************************************************
353+
*/
354+
317355
/**
318356
* Test [databind#815], problems with ObjectNode, naming strategy
319357
*/
320358
public void testNamingWithObjectNode() throws Exception
321359
{
322-
ObjectMapper m = new ObjectMapper();
323-
m.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE);
360+
ObjectMapper m = new ObjectMapper()
361+
.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE);
324362
ClassWithObjectNodeField result =
325363
m.readValue(
326364
"{ \"id\": \"1\", \"json\": { \"foo\": \"bar\", \"baz\": \"bing\" } }",
@@ -332,16 +370,17 @@ public void testNamingWithObjectNode() throws Exception
332370
assertEquals("bing", result.json.path("baz").asText());
333371
}
334372

335-
public void testExplicitRename() throws Exception {
373+
public void testExplicitRename() throws Exception
374+
{
336375
ObjectMapper m = new ObjectMapper();
337-
m.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
376+
m.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
338377
m.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);
339378
// by default, renaming will not take place on explicitly named fields
340379
assertEquals(aposToQuotes("{'firstName':'Peter','lastName':'Venkman','user_age':'35'}"),
341380
m.writeValueAsString(new ExplicitBean()));
342381

343382
m = new ObjectMapper();
344-
m.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
383+
m.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
345384
m.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);
346385
m.enable(MapperFeature.ALLOW_EXPLICIT_PROPERTY_RENAMING);
347386
// w/ feature enabled, ALL property names should get re-written

src/test/java/com/fasterxml/jackson/failing/ImplicitParamsForCreator806Test.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void testImplicitNameWithNamingStrategy() throws Exception
4242
{
4343
ObjectMapper mapper = new ObjectMapper()
4444
.setAnnotationIntrospector(new MyParamIntrospector())
45-
.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES)
45+
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
4646
;
4747
XY value = mapper.readValue(aposToQuotes("{'param_name0':1,'param_name1':2}"), XY.class);
4848
assertNotNull(value);

0 commit comments

Comments
 (0)