diff --git a/JSONObject.java b/JSONObject.java
index 5a6b734..ca219c9 100644
--- a/JSONObject.java
+++ b/JSONObject.java
@@ -29,6 +29,7 @@ import java.io.Closeable;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -290,21 +291,44 @@ public class JSONObject {
* Construct a JSONObject from an Object using bean getters. It reflects on
* all of the public methods of the object. For each of the methods with no
* parameters and a name starting with "get"
or
- * "is"
followed by an uppercase letter, the method is invoked,
- * and a key and the value returned from the getter method are put into the
- * new JSONObject.
+ * "is"
, the method is invoked, and a key and the value
+ * returned from the getter method are put into the new JSONObject.
*
* The key is formed by removing the "get"
or "is"
* prefix. If the second remaining character is not upper case, then the
* first character is converted to lower case.
*
+ * Methods that return void
as well as static
+ * methods are ignored.
+ *
* For example, if an object has a method named "getName"
, and
* if the result of calling object.getName()
is
* "Larry Fine"
, then the JSONObject will contain
* "name": "Larry Fine"
.
*
- * Methods that return void
as well as static
- * methods are ignored.
+ * The {@link JSONPropertyName} annotation can be used on a bean getter to
+ * override key name used in the JSONObject. For example, using the object
+ * above with the getName
method, if we annotated it with:
+ *
+ * @JSONPropertyName("FullName") + * public String getName() { return this.name; } + *+ * The resulting JSON object would contain
"FullName": "Larry Fine"
+ *
+ * The {@link JSONPropertyIgnore} annotation can be used to force the bean property
+ * to not be serialized into JSON. If both {@link JSONPropertyIgnore} and
+ * {@link JSONPropertyName} are defined on the same method, a depth comparison is
+ * performed and the one closest to the concrete class being serialized is used.
+ * If both annotations are at the same level, then the {@link JSONPropertyIgnore}
+ * annotation takes precedent and the field is not serialized.
+ * For example, the following declaration would prevent the getName
+ * method from being serialized:
+ *
+ * @JSONPropertyName("FullName") + * @JSONPropertyIgnore + * public String getName() { return this.name; } + *+ *
*
* @param bean
* An object that has getter methods that should be used to make
@@ -1409,8 +1433,8 @@ public class JSONObject {
}
/**
- * Populates the internal map of the JSONObject with the bean properties.
- * The bean can not be recursive.
+ * Populates the internal map of the JSONObject with the bean properties. The
+ * bean can not be recursive.
*
* @see JSONObject#JSONObject(Object)
*
@@ -1420,49 +1444,31 @@ public class JSONObject {
private void populateMap(Object bean) {
Class> klass = bean.getClass();
-// If klass is a System class then set includeSuperClass to false.
+ // If klass is a System class then set includeSuperClass to false.
boolean includeSuperClass = klass.getClassLoader() != null;
- Method[] methods = includeSuperClass ? klass.getMethods() : klass
- .getDeclaredMethods();
+ Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods();
for (final Method method : methods) {
final int modifiers = method.getModifiers();
if (Modifier.isPublic(modifiers)
&& !Modifier.isStatic(modifiers)
&& method.getParameterTypes().length == 0
&& !method.isBridge()
- && method.getReturnType() != Void.TYPE ) {
- final String name = method.getName();
- String key;
- if (name.startsWith("get")) {
- if ("getClass".equals(name) || "getDeclaringClass".equals(name)) {
- continue;
- }
- key = name.substring(3);
- } else if (name.startsWith("is")) {
- key = name.substring(2);
- } else {
- continue;
- }
- if (key.length() > 0
- && Character.isUpperCase(key.charAt(0))) {
- if (key.length() == 1) {
- key = key.toLowerCase(Locale.ROOT);
- } else if (!Character.isUpperCase(key.charAt(1))) {
- key = key.substring(0, 1).toLowerCase(Locale.ROOT)
- + key.substring(1);
- }
-
+ && method.getReturnType() != Void.TYPE
+ && isValidMethodName(method.getName())) {
+ final String key = getKeyNameFromMethod(method);
+ if (key != null && !key.isEmpty()) {
try {
final Object result = method.invoke(bean);
if (result != null) {
this.map.put(key, wrap(result));
// we don't use the result anywhere outside of wrap
- // if it's a resource we should be sure to close it after calling toString
- if(result instanceof Closeable) {
+ // if it's a resource we should be sure to close it
+ // after calling toString
+ if (result instanceof Closeable) {
try {
- ((Closeable)result).close();
+ ((Closeable) result).close();
} catch (IOException ignore) {
}
}
@@ -1476,6 +1482,165 @@ public class JSONObject {
}
}
+ private boolean isValidMethodName(String name) {
+ return (name.startsWith("get") || name.startsWith("is"))
+ && !"getClass".equals(name)
+ && !"getDeclaringClass".equals(name);
+ }
+
+ private String getKeyNameFromMethod(Method method) {
+ final int ignoreDepth = getAnnotationDepth(method, JSONPropertyIgnore.class);
+ if (ignoreDepth > 0) {
+ final int forcedNameDepth = getAnnotationDepth(method, JSONPropertyName.class);
+ if (forcedNameDepth < 0 || ignoreDepth <= forcedNameDepth) {
+ // the hierarchy asked to ignore, and the nearest name override
+ // was higher or non-existent
+ return null;
+ }
+ }
+ JSONPropertyName annotation = getAnnotation(method, JSONPropertyName.class);
+ if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) {
+ return annotation.value();
+ }
+ String key;
+ final String name = method.getName();
+ if (name.startsWith("get")) {
+ key = name.substring(3);
+ } else if (name.startsWith("is")) {
+ key = name.substring(2);
+ } else {
+ return null;
+ }
+ // if the first letter in the key is not uppercase, then skip.
+ // This is to maintain backwards compatibility before PR406
+ // (https://github.com/stleary/JSON-java/pull/406/)
+ if(key.isEmpty() || Character.isLowerCase(key.charAt(0))) {
+ return null;
+ }
+ if (key.length() == 1) {
+ key = key.toLowerCase(Locale.ROOT);
+ } else if (!Character.isUpperCase(key.charAt(1))) {
+ key = key.substring(0, 1).toLowerCase(Locale.ROOT) + key.substring(1);
+ }
+ return key;
+ }
+
+ /**
+ * Searches the class hierarchy to see if the method or it's super
+ * implementations and interfaces has the annotation.
+ *
+ * @param
+ * type of the annotation
+ *
+ * @param m
+ * method to check
+ * @param annotationClass
+ * annotation to look for
+ * @return the {@link Annotation} if the annotation exists on the current method
+ * or one of it's super class definitions
+ */
+ private static A getAnnotation(final Method m, final Class annotationClass) {
+ // if we have invalid data the result is null
+ if (m == null || annotationClass == null) {
+ return null;
+ }
+
+ if (m.isAnnotationPresent(annotationClass)) {
+ return m.getAnnotation(annotationClass);
+ }
+
+ // if we've already reached the Object class, return null;
+ Class> c = m.getDeclaringClass();
+ if (c.getSuperclass() == null) {
+ return null;
+ }
+
+ // check directly implemented interfaces for the method being checked
+ for (Class> i : c.getInterfaces()) {
+ try {
+ Method im = i.getMethod(m.getName(), m.getParameterTypes());
+ return getAnnotation(im, annotationClass);
+ } catch (final SecurityException ex) {
+ continue;
+ } catch (final NoSuchMethodException ex) {
+ continue;
+ }
+ }
+
+ try {
+ return getAnnotation(m.getDeclaringClass().getSuperclass().getMethod(m.getName(),
+ m.getParameterTypes()),
+ annotationClass);
+ } catch (final SecurityException ex) {
+ return null;
+ } catch (final NoSuchMethodException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Searches the class hierarchy to see if the method or it's super
+ * implementations and interfaces has the annotation. Returns the depth of the
+ * annotation in the hierarchy.
+ *
+ * @param
+ * type of the annotation
+ *
+ * @param m
+ * method to check
+ * @param annotationClass
+ * annotation to look for
+ * @return Depth of the annotation or -1 if the annotation is not on the method.
+ */
+ private static int getAnnotationDepth(final Method m, final Class extends Annotation> annotationClass) {
+ // if we have invalid data the result is -1
+ if (m == null || annotationClass == null) {
+ return -1;
+ }
+
+ if (m.isAnnotationPresent(annotationClass)) {
+ return 1;
+ }
+
+ // if we've already reached the Object class, return -1;
+ Class> c = m.getDeclaringClass();
+ if (c.getSuperclass() == null) {
+ return -1;
+ }
+
+ // check directly implemented interfaces for the method being checked
+ for (Class> i : c.getInterfaces()) {
+ try {
+ Method im = i.getMethod(m.getName(), m.getParameterTypes());
+ int d = getAnnotationDepth(im, annotationClass);
+ if (d > 0) {
+ // since the annotation was on the interface, add 1
+ return d + 1;
+ }
+ } catch (final SecurityException ex) {
+ continue;
+ } catch (final NoSuchMethodException ex) {
+ continue;
+ }
+ }
+
+ try {
+ int d = getAnnotationDepth(
+ m.getDeclaringClass().getSuperclass().getMethod(m.getName(),
+ m.getParameterTypes()),
+ annotationClass);
+ if (d > 0) {
+ // since the annotation was on the superclass, add 1
+ return d + 1;
+ }
+ return -1;
+ } catch (final SecurityException ex) {
+ return -1;
+ } catch (final NoSuchMethodException ex) {
+ return -1;
+ }
+ }
+
/**
* Put a key/boolean pair in the JSONObject.
*
diff --git a/JSONPropertyIgnore.java b/JSONPropertyIgnore.java
new file mode 100644
index 0000000..682de74
--- /dev/null
+++ b/JSONPropertyIgnore.java
@@ -0,0 +1,43 @@
+package org.json;
+
+/*
+Copyright (c) 2018 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Documented
+@Retention(RUNTIME)
+@Target({METHOD})
+/**
+ * Use this annotation on a getter method to override the Bean name
+ * parser for Bean -> JSONObject mapping. If this annotation is
+ * present at any level in the class hierarchy, then the method will
+ * not be serialized from the bean into the JSONObject.
+ */
+public @interface JSONPropertyIgnore { }
diff --git a/JSONPropertyName.java b/JSONPropertyName.java
new file mode 100644
index 0000000..a1bcd58
--- /dev/null
+++ b/JSONPropertyName.java
@@ -0,0 +1,47 @@
+package org.json;
+
+/*
+Copyright (c) 2018 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Documented
+@Retention(RUNTIME)
+@Target({METHOD})
+/**
+ * Use this annotation on a getter method to override the Bean name
+ * parser for Bean -> JSONObject mapping. A value set to empty string ""
+ * will have the Bean parser fall back to the default field name processing.
+ */
+public @interface JSONPropertyName {
+ /**
+ * @return The name of the property as to be used in the JSON Object.
+ */
+ String value();
+}
diff --git a/README.md b/README.md
index a62a0ea..e66f3c6 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,18 @@ by this package.
JSON Pointers both in the form of string representation and URI fragment
representation.
+**JSONPropertyIgnore.java**: Annotation class that can be used on Java Bean getter methods.
+When used on a bean method that would normally be serialized into a `JSONObject`, it
+overrides the getter-to-key-name logic and forces the property to be excluded from the
+resulting `JSONObject`.
+
+**JSONPropertyName.java**: Annotation class that can be used on Java Bean getter methods.
+When used on a bean method that would normally be serialized into a `JSONObject`, it
+overrides the getter-to-key-name logic and uses the value of the annotation. The Bean
+processor will look through the class hierarchy. This means you can use the annotation on
+a base class or interface and the value of the annotation will be used even if the getter
+is overridden in a child class.
+
**JSONString.java**: The `JSONString` interface requires a `toJSONString` method,
allowing an object to provide its own serialization.