Skip to content

Using Reflection to Encode Objects as JSON

Bill Davidson edited this page Aug 14, 2016 · 65 revisions

As of the upcoming 1.9 release, JSONUtil will no longer require you to put object data into Maps if you do not wish to do so. It can examine your objects using Java's reflection API and generate the JSON automatically. Reflection is not enabled by default. If you do not enable reflection then unrecognized objects will continue to have their toString() method called and the value encoded as a normal String.

Keep in mind that reflection can be a fair bit slower than putting things into maps on your own. If performance is an issue for you, then you may want to do some profiling to make sure that reflection is not causing you too much of a performance degradation. You can improve reflection performance by enabling the caching of reflection data using JSONConfig.setCacheReflectionData(true). This will cost you some memory space which could be significant if you reflect a lot of classes. There are always trade-offs. In my tests, caching typically reduced run times by over 80% vs. uncached reflection and sometimes even approaches the speed of loading of maps. Newer JVM's do better than older JVM's and they do better as they run longer (newer JVM's can learn and optimize over run time better than older JVM's).

There are three ways to enable reflection: selective, selective with fields, and global.

Selective Reflection
This only operates on those classes which you explicitly choose. You do this by adding classes to your JSONConfig object via JSONConfig.addReflectClass(Object) or JSONConfig.addReflectClasses(Collection). These methods are also available in JSONConfigDefaults so that you can have them reflected by default even if you don't use an explicit JSONConfig object. You can send java.lang.Class objects to these methods or you can send other objects and the necessary java.lang.Class object will be inferred from them.
Selective with Fields
This gives more precise control. You wrap your class in a JSONReflectedClass object and specify the set of field names from that object to be included and then you send the JSONReflectedClass object to JSONConfig.addReflectClass(Object) et al as if it was a normal object. This method ignores privacy settings and allows the use of static fields, transient fields and fields which do not actually exist but which have getters that look like JavaBeans compliant getter names. This works even if you have global reflection enabled because known reflected objects are looked up even when global reflection is enabled.
Global reflection
This is enabled by calling JSONConfig.setReflectUnknownObjects(boolean) with a value of true. This will use reflection on all unrecognized objects. This method is also available in JSONConfigDefaults if you want this to be the default.

Except for selective with fields, fields will only be included if they are instance fields (not static) and not transient. By definition, transient fields are not supposed to be serialized. Instance fields from super classes will be included if they satisfy the privacy criteria described below.

The reflection code looks for JavaBeans naming convention compliant getter methods to get the values of the fields. For example if you have a field called "foo" it will look for a parameterless method called "getFoo" to call to get the value. If it cannot find such a method for a given field then it will attempt to access the field directly.

If you don't specify fields, then JSONConfig.setPrivacyLevel(int) sets the privacy level for including fields in the reflected output. By default, the privacy level is set to ReflectUtil.PUBLIC which means that only fields which are public or which have a public getter method will be included. Other levels are ReflectUtil.PROTECTED, ReflectUtil.PACKAGE and ReflectUtil.PRIVATE. If you use ReflectUtil.PRIVATE then all non-transient instance fields will be included in the JSON output, even if they are private and do not have getters. This method is also available in JSONConfigDefaults if you wish to set a default level other than ReflectUtil.PUBLIC.

Be mindful of sensitive or security related data that you might have in your objects that could end up in your JSON. That sort of data is a bit more obvious when you're packing maps and can easily be forgotten when using reflection.

Selective Reflection Example

MyObj someObj = new MyObj(p1, p2, p3);
JSONConfig cfg = new JSONConfig();
cfg.addReflectClass(MyObj.class);
cfg.setPrivacyLevel(ReflectUtil.PROTECTED);    // include public and protected fields/getters.
String json = JSONUtil.toJSON(someObj, cfg);

Selective Reflection with Fields Example

MyObj someObj = new MyObj(p1, p2, p3);
Set<String> myFields = new LinkedHashSet<String>(Arrays.asList("p1", "p4"));
JSONReflectedClass refClass = new JSONReflectedClass(MyObj.class, myFields);
JSONConfig cfg = new JSONConfig();
cfg.addReflectClass(refClass);
String json = JSONUtil.toJSON(someObj, cfg);

Global Reflection Example

MyObj someObj = new MyObj(p1, p2, p3);
JSONConfig cfg = new JSONConfig();
cfg.setReflectUnknownObjects(true);
cfg.setPrivacyLevel(ReflectUtil.PRIVATE);      // include all fields.
String json = JSONUtil.toJSON(someObj, cfg);

The code to do this is currently in the repository waiting for me to release it. I'm still working on some testing and improving the comments to match some of the new features in this release. It should be released within the next few days.

Clone this wiki locally