Skip to content

Commit 2a6b5ba

Browse files
authored
Merge pull request stleary#406 from johnjaylward/FixBeanKeyNameing
Adds annotations to customize field names during Bean serialization
2 parents 1c1ef5b + a509a28 commit 2a6b5ba

4 files changed

Lines changed: 307 additions & 32 deletions

File tree

JSONObject.java

Lines changed: 205 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ of this software and associated documentation files (the "Software"), to deal
2929
import java.io.IOException;
3030
import java.io.StringWriter;
3131
import java.io.Writer;
32+
import java.lang.annotation.Annotation;
3233
import java.lang.reflect.Field;
3334
import java.lang.reflect.InvocationTargetException;
3435
import java.lang.reflect.Method;
@@ -305,13 +306,47 @@ public JSONObject(Map<?, ?> m) {
305306
* prefix. If the second remaining character is not upper case, then the
306307
* first character is converted to lower case.
307308
* <p>
309+
* Methods that are <code>static</code>, return <code>void</code>,
310+
* have parameters, or are "bridge" methods, are ignored.
311+
* <p>
308312
* For example, if an object has a method named <code>"getName"</code>, and
309313
* if the result of calling <code>object.getName()</code> is
310314
* <code>"Larry Fine"</code>, then the JSONObject will contain
311315
* <code>"name": "Larry Fine"</code>.
312316
* <p>
313-
* Methods that return <code>void</code> as well as <code>static</code>
314-
* methods are ignored.
317+
* The {@link JSONPropertyName} annotation can be used on a bean getter to
318+
* override key name used in the JSONObject. For example, using the object
319+
* above with the <code>getName</code> method, if we annotated it with:
320+
* <pre>
321+
* &#64;JSONPropertyName("FullName")
322+
* public String getName() { return this.name; }
323+
* </pre>
324+
* The resulting JSON object would contain <code>"FullName": "Larry Fine"</code>
325+
* <p>
326+
* Similarly, the {@link JSONPropertyName} annotation can be used on non-
327+
* <code>get</code> and <code>is</code> methods. We can also override key
328+
* name used in the JSONObject as seen below even though the field would normally
329+
* be ignored:
330+
* <pre>
331+
* &#64;JSONPropertyName("FullName")
332+
* public String fullName() { return this.name; }
333+
* </pre>
334+
* The resulting JSON object would contain <code>"FullName": "Larry Fine"</code>
335+
* <p>
336+
* The {@link JSONPropertyIgnore} annotation can be used to force the bean property
337+
* to not be serialized into JSON. If both {@link JSONPropertyIgnore} and
338+
* {@link JSONPropertyName} are defined on the same method, a depth comparison is
339+
* performed and the one closest to the concrete class being serialized is used.
340+
* If both annotations are at the same level, then the {@link JSONPropertyIgnore}
341+
* annotation takes precedent and the field is not serialized.
342+
* For example, the following declaration would prevent the <code>getName</code>
343+
* method from being serialized:
344+
* <pre>
345+
* &#64;JSONPropertyName("FullName")
346+
* &#64;JSONPropertyIgnore
347+
* public String getName() { return this.name; }
348+
* </pre>
349+
* <p>
315350
*
316351
* @param bean
317352
* An object that has getter methods that should be used to make
@@ -1420,8 +1455,8 @@ public String optString(String key, String defaultValue) {
14201455
}
14211456

14221457
/**
1423-
* Populates the internal map of the JSONObject with the bean properties.
1424-
* The bean can not be recursive.
1458+
* Populates the internal map of the JSONObject with the bean properties. The
1459+
* bean can not be recursive.
14251460
*
14261461
* @see JSONObject#JSONObject(Object)
14271462
*
@@ -1431,49 +1466,31 @@ public String optString(String key, String defaultValue) {
14311466
private void populateMap(Object bean) {
14321467
Class<?> klass = bean.getClass();
14331468

1434-
// If klass is a System class then set includeSuperClass to false.
1469+
// If klass is a System class then set includeSuperClass to false.
14351470

14361471
boolean includeSuperClass = klass.getClassLoader() != null;
14371472

1438-
Method[] methods = includeSuperClass ? klass.getMethods() : klass
1439-
.getDeclaredMethods();
1473+
Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods();
14401474
for (final Method method : methods) {
14411475
final int modifiers = method.getModifiers();
14421476
if (Modifier.isPublic(modifiers)
14431477
&& !Modifier.isStatic(modifiers)
14441478
&& method.getParameterTypes().length == 0
14451479
&& !method.isBridge()
1446-
&& method.getReturnType() != Void.TYPE ) {
1447-
final String name = method.getName();
1448-
String key;
1449-
if (name.startsWith("get")) {
1450-
if ("getClass".equals(name) || "getDeclaringClass".equals(name)) {
1451-
continue;
1452-
}
1453-
key = name.substring(3);
1454-
} else if (name.startsWith("is")) {
1455-
key = name.substring(2);
1456-
} else {
1457-
continue;
1458-
}
1459-
if (key.length() > 0
1460-
&& Character.isUpperCase(key.charAt(0))) {
1461-
if (key.length() == 1) {
1462-
key = key.toLowerCase(Locale.ROOT);
1463-
} else if (!Character.isUpperCase(key.charAt(1))) {
1464-
key = key.substring(0, 1).toLowerCase(Locale.ROOT)
1465-
+ key.substring(1);
1466-
}
1467-
1480+
&& method.getReturnType() != Void.TYPE
1481+
&& isValidMethodName(method.getName())) {
1482+
final String key = getKeyNameFromMethod(method);
1483+
if (key != null && !key.isEmpty()) {
14681484
try {
14691485
final Object result = method.invoke(bean);
14701486
if (result != null) {
14711487
this.map.put(key, wrap(result));
14721488
// we don't use the result anywhere outside of wrap
1473-
// if it's a resource we should be sure to close it after calling toString
1474-
if(result instanceof Closeable) {
1489+
// if it's a resource we should be sure to close it
1490+
// after calling toString
1491+
if (result instanceof Closeable) {
14751492
try {
1476-
((Closeable)result).close();
1493+
((Closeable) result).close();
14771494
} catch (IOException ignore) {
14781495
}
14791496
}
@@ -1487,6 +1504,162 @@ private void populateMap(Object bean) {
14871504
}
14881505
}
14891506

1507+
private boolean isValidMethodName(String name) {
1508+
return !"getClass".equals(name) && !"getDeclaringClass".equals(name);
1509+
}
1510+
1511+
private String getKeyNameFromMethod(Method method) {
1512+
final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class);
1513+
if (ignoreDepth > 0) {
1514+
final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class);
1515+
if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) {
1516+
// the hierarchy asked to ignore, and the nearest name override
1517+
// was higher or non-existent
1518+
return null;
1519+
}
1520+
}
1521+
JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class);
1522+
if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) {
1523+
return annotation.value();
1524+
}
1525+
String key;
1526+
final String name = method.getName();
1527+
if (name.startsWith("get") && name.length() > 3) {
1528+
key = name.substring(3);
1529+
} else if (name.startsWith("is") && name.length() > 2) {
1530+
key = name.substring(2);
1531+
} else {
1532+
return null;
1533+
}
1534+
// if the first letter in the key is not uppercase, then skip.
1535+
// This is to maintain backwards compatibility before PR406
1536+
// (https://github.com/stleary/JSON-java/pull/406/)
1537+
if (Character.isLowerCase(key.charAt(0))) {
1538+
return null;
1539+
}
1540+
if (key.length() == 1) {
1541+
key = key.toLowerCase(Locale.ROOT);
1542+
} else if (!Character.isUpperCase(key.charAt(1))) {
1543+
key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1);
1544+
}
1545+
return key;
1546+
}
1547+
1548+
/**
1549+
* Searches the class hierarchy to see if the method or it's super
1550+
* implementations and interfaces has the annotation.
1551+
*
1552+
* @param <A>
1553+
* type of the annotation
1554+
*
1555+
* @param m
1556+
* method to check
1557+
* @param annotationClass
1558+
* annotation to look for
1559+
* @return the {@link Annotation} if the annotation exists on the current method
1560+
* or one of it's super class definitions
1561+
*/
1562+
private static <A extends Annotation> A getAnnotation(final Method m, final Class<A> annotationClass) {
1563+
// if we have invalid data the result is null
1564+
if (m == null || annotationClass == null) {
1565+
return null;
1566+
}
1567+
1568+
if (m.isAnnotationPresent(annotationClass)) {
1569+
return m.getAnnotation(annotationClass);
1570+
}
1571+
1572+
// if we've already reached the Object class, return null;
1573+
Class<?> c = m.getDeclaringClass();
1574+
if (c.getSuperclass() == null) {
1575+
return null;
1576+
}
1577+
1578+
// check directly implemented interfaces for the method being checked
1579+
for (Class<?> i : c.getInterfaces()) {
1580+
try {
1581+
Method im = i.getMethod(m.getName(), m.getParameterTypes());
1582+
return getAnnotation(im, annotationClass);
1583+
} catch (final SecurityException ex) {
1584+
continue;
1585+
} catch (final NoSuchMethodException ex) {
1586+
continue;
1587+
}
1588+
}
1589+
1590+
try {
1591+
return getAnnotation(
1592+
c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()),
1593+
annotationClass);
1594+
} catch (final SecurityException ex) {
1595+
return null;
1596+
} catch (final NoSuchMethodException ex) {
1597+
return null;
1598+
}
1599+
}
1600+
1601+
/**
1602+
* Searches the class hierarchy to see if the method or it's super
1603+
* implementations and interfaces has the annotation. Returns the depth of the
1604+
* annotation in the hierarchy.
1605+
*
1606+
* @param <A>
1607+
* type of the annotation
1608+
*
1609+
* @param m
1610+
* method to check
1611+
* @param annotationClass
1612+
* annotation to look for
1613+
* @return Depth of the annotation or -1 if the annotation is not on the method.
1614+
*/
1615+
private static int getAnnotationDepth(final Method m, final Class<? extends Annotation> annotationClass) {
1616+
// if we have invalid data the result is -1
1617+
if (m == null || annotationClass == null) {
1618+
return -1;
1619+
}
1620+
1621+
if (m.isAnnotationPresent(annotationClass)) {
1622+
return 1;
1623+
}
1624+
1625+
// if we've already reached the Object class, return -1;
1626+
Class<?> c = m.getDeclaringClass();
1627+
if (c.getSuperclass() == null) {
1628+
return -1;
1629+
}
1630+
1631+
// check directly implemented interfaces for the method being checked
1632+
for (Class<?> i : c.getInterfaces()) {
1633+
try {
1634+
Method im = i.getMethod(m.getName(), m.getParameterTypes());
1635+
int d = getAnnotationDepth(im, annotationClass);
1636+
if (d > 0) {
1637+
// since the annotation was on the interface, add 1
1638+
return d + 1;
1639+
}
1640+
} catch (final SecurityException ex) {
1641+
continue;
1642+
} catch (final NoSuchMethodException ex) {
1643+
continue;
1644+
}
1645+
}
1646+
1647+
try {
1648+
int d = getAnnotationDepth(
1649+
c.getSuperclass().getMethod(m.getName(), m.getParameterTypes()),
1650+
annotationClass);
1651+
if (d > 0) {
1652+
// since the annotation was on the superclass, add 1
1653+
return d + 1;
1654+
}
1655+
return -1;
1656+
} catch (final SecurityException ex) {
1657+
return -1;
1658+
} catch (final NoSuchMethodException ex) {
1659+
return -1;
1660+
}
1661+
}
1662+
14901663
/**
14911664
* Put a key/boolean pair in the JSONObject.
14921665
*

JSONPropertyIgnore.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.json;
2+
3+
/*
4+
Copyright (c) 2018 JSON.org
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
The Software shall be used for Good, not Evil.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.
25+
*/
26+
27+
import static java.lang.annotation.ElementType.METHOD;
28+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
29+
30+
import java.lang.annotation.Documented;
31+
import java.lang.annotation.Retention;
32+
import java.lang.annotation.Target;
33+
34+
@Documented
35+
@Retention(RUNTIME)
36+
@Target({METHOD})
37+
/**
38+
* Use this annotation on a getter method to override the Bean name
39+
* parser for Bean -&gt; JSONObject mapping. If this annotation is
40+
* present at any level in the class hierarchy, then the method will
41+
* not be serialized from the bean into the JSONObject.
42+
*/
43+
public @interface JSONPropertyIgnore { }

JSONPropertyName.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.json;
2+
3+
/*
4+
Copyright (c) 2018 JSON.org
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
The Software shall be used for Good, not Evil.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24+
SOFTWARE.
25+
*/
26+
27+
import static java.lang.annotation.ElementType.METHOD;
28+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
29+
30+
import java.lang.annotation.Documented;
31+
import java.lang.annotation.Retention;
32+
import java.lang.annotation.Target;
33+
34+
@Documented
35+
@Retention(RUNTIME)
36+
@Target({METHOD})
37+
/**
38+
* Use this annotation on a getter method to override the Bean name
39+
* parser for Bean -&gt; JSONObject mapping. A value set to empty string <code>""</code>
40+
* will have the Bean parser fall back to the default field name processing.
41+
*/
42+
public @interface JSONPropertyName {
43+
/**
44+
* @return The name of the property as to be used in the JSON Object.
45+
*/
46+
String value();
47+
}

0 commit comments

Comments
 (0)