Skip to content

Commit 74b9a60

Browse files
author
John J. Aylward
committed
Adds annotation to support custom field names during Bean serialization
1 parent d402a99 commit 74b9a60

File tree

4 files changed

+302
-35
lines changed

4 files changed

+302
-35
lines changed

JSONObject.java

+200-35
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;
@@ -290,21 +291,44 @@ public JSONObject(Map<?, ?> m) {
290291
* Construct a JSONObject from an Object using bean getters. It reflects on
291292
* all of the public methods of the object. For each of the methods with no
292293
* parameters and a name starting with <code>"get"</code> or
293-
* <code>"is"</code> followed by an uppercase letter, the method is invoked,
294-
* and a key and the value returned from the getter method are put into the
295-
* new JSONObject.
294+
* <code>"is"</code>, the method is invoked, and a key and the value
295+
* returned from the getter method are put into the new JSONObject.
296296
* <p>
297297
* The key is formed by removing the <code>"get"</code> or <code>"is"</code>
298298
* prefix. If the second remaining character is not upper case, then the
299299
* first character is converted to lower case.
300300
* <p>
301+
* Methods that return <code>void</code> as well as <code>static</code>
302+
* methods are ignored.
303+
* <p>
301304
* For example, if an object has a method named <code>"getName"</code>, and
302305
* if the result of calling <code>object.getName()</code> is
303306
* <code>"Larry Fine"</code>, then the JSONObject will contain
304307
* <code>"name": "Larry Fine"</code>.
305308
* <p>
306-
* Methods that return <code>void</code> as well as <code>static</code>
307-
* methods are ignored.
309+
* The {@link JSONPropertyName} annotation can be used on a bean getter to
310+
* override key name used in the JSONObject. For example, using the object
311+
* above with the <code>getName</code> method, if we annotated it with:
312+
* <pre>
313+
* &#64;JSONPropertyName("FullName")
314+
* public String getName() { return this.name; }
315+
* </pre>
316+
* The resulting JSON object would contain <code>"FullName": "Larry Fine"</code>
317+
* <p>
318+
* The {@link JSONPropertyIgnore} annotation can be used to force the bean property
319+
* to not be serialized into JSON. If both {@link JSONPropertyIgnore} and
320+
* {@link JSONPropertyName} are defined on the same method, a depth comparison is
321+
* performed and the one closest to the concrete class being serialized is used.
322+
* If both annotations are at the same level, then the {@link JSONPropertyIgnore}
323+
* annotation takes precedent and the field is not serialized.
324+
* For example, the following declaration would prevent the <code>getName</code>
325+
* method from being serialized:
326+
* <pre>
327+
* &#64;JSONPropertyName("FullName")
328+
* &#64;JSONPropertyIgnore
329+
* public String getName() { return this.name; }
330+
* </pre>
331+
* <p>
308332
*
309333
* @param bean
310334
* An object that has getter methods that should be used to make
@@ -1409,8 +1433,8 @@ public String optString(String key, String defaultValue) {
14091433
}
14101434

14111435
/**
1412-
* Populates the internal map of the JSONObject with the bean properties.
1413-
* The bean can not be recursive.
1436+
* Populates the internal map of the JSONObject with the bean properties. The
1437+
* bean can not be recursive.
14141438
*
14151439
* @see JSONObject#JSONObject(Object)
14161440
*
@@ -1420,49 +1444,31 @@ public String optString(String key, String defaultValue) {
14201444
private void populateMap(Object bean) {
14211445
Class<?> klass = bean.getClass();
14221446

1423-
// If klass is a System class then set includeSuperClass to false.
1447+
// If klass is a System class then set includeSuperClass to false.
14241448

14251449
boolean includeSuperClass = klass.getClassLoader() != null;
14261450

1427-
Method[] methods = includeSuperClass ? klass.getMethods() : klass
1428-
.getDeclaredMethods();
1451+
Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods();
14291452
for (final Method method : methods) {
14301453
final int modifiers = method.getModifiers();
14311454
if (Modifier.isPublic(modifiers)
14321455
&& !Modifier.isStatic(modifiers)
14331456
&& method.getParameterTypes().length == 0
14341457
&& !method.isBridge()
1435-
&& method.getReturnType() != Void.TYPE ) {
1436-
final String name = method.getName();
1437-
String key;
1438-
if (name.startsWith("get")) {
1439-
if ("getClass".equals(name) || "getDeclaringClass".equals(name)) {
1440-
continue;
1441-
}
1442-
key = name.substring(3);
1443-
} else if (name.startsWith("is")) {
1444-
key = name.substring(2);
1445-
} else {
1446-
continue;
1447-
}
1448-
if (key.length() > 0
1449-
&& Character.isUpperCase(key.charAt(0))) {
1450-
if (key.length() == 1) {
1451-
key = key.toLowerCase(Locale.ROOT);
1452-
} else if (!Character.isUpperCase(key.charAt(1))) {
1453-
key = key.substring(0, 1).toLowerCase(Locale.ROOT)
1454-
+ key.substring(1);
1455-
}
1456-
1458+
&& method.getReturnType() != Void.TYPE
1459+
&& isValidMethodName(method.getName())) {
1460+
final String key = getKeyNameFromMethod(method);
1461+
if (key != null && !key.isEmpty()) {
14571462
try {
14581463
final Object result = method.invoke(bean);
14591464
if (result != null) {
14601465
this.map.put(key, wrap(result));
14611466
// we don't use the result anywhere outside of wrap
1462-
// if it's a resource we should be sure to close it after calling toString
1463-
if(result instanceof Closeable) {
1467+
// if it's a resource we should be sure to close it
1468+
// after calling toString
1469+
if (result instanceof Closeable) {
14641470
try {
1465-
((Closeable)result).close();
1471+
((Closeable) result).close();
14661472
} catch (IOException ignore) {
14671473
}
14681474
}
@@ -1476,6 +1482,165 @@ private void populateMap(Object bean) {
14761482
}
14771483
}
14781484

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

JSONPropertyIgnore.java

+43
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

+47
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)