-
Notifications
You must be signed in to change notification settings - Fork 36
Using Reflection to Encode Objects as JSON
As of the 1.9 release, JSONUtil no longer requires you to put object data into Maps if you do not wish to do so. You can send your objects directly to JSONUtil and it can examine your objects using Java's reflection API to 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.
Fields can be selected by privacy level or by name. Field names can be aliased in the JSON output.
Internally, reflected objects are converted to Maps with the attribute names being the keys and their values becoming the values in the Map. These Maps are traversed and encoded as JSON objects like any other Map.
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 objects of many different types. There are always trade-offs. In my tests, caching typically reduced run times by over 60% 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). Java 5 is dramatically slower than all newer versions of Java for all three types (Maps, reflection and reflection with caching).
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 class. 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.
Fields can be aliased in the JSON output. For example, if you want a field called "foo" in the object to be called "bar" in the JSON output, you can use an alias. This uses the same basic mechanism as selective with fields. You set an alias Map in a JSONReflectedClass object which wraps the class you want reflected. You can do this with either the constructor or with a setter.
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.
JSONConfig cfg = new JSONConfig();
cfg.addReflectClass(MyObj.class);
cfg.setPrivacyLevel(ReflectUtil.PROTECTED); // include public and protected fields/getters.
MyObj someObj = new MyObj(p1, p2, p3);
String json = JSONUtil.toJSON(someObj, cfg);
JSONConfig cfg = new JSONConfig();
cfg.addReflectClass(new JSONReflectedClass(MyObj.class, Arrays.asList("p1", "p4")));
MyObj someObj = new MyObj(p1, p2, p3);
String json = JSONUtil.toJSON(someObj, cfg);
JSONConfig cfg = new JSONConfig();
Map<String,String> fieldAliases = new HashMap<>();
fieldAliases.put("p3", "zip");
cfg.addReflectClass(new JSONReflectedClass(MyObj.class, fieldAliases));
MyObj someObj = new MyObj(p1, p2, p3);
String json = JSONUtil.toJSON(someObj, cfg);
JSONConfig cfg = new JSONConfig();
Map<String,String> fieldAliases = new HashMap<>();
fieldAliases.put("p4", "zip");
cfg.addReflectClass(new JSONReflectedClass(MyObj.class, Arrays.asList("p1", "p4"), fieldAliases));
MyObj someObj = new MyObj(p1, p2, p3);
String json = JSONUtil.toJSON(someObj, cfg);
Selective Reflection with Fields and Aliases Example using addReflectClassByName() (same effect as above)
JSONConfig cfg = new JSONConfig();
cfg.addReflectClassByName("org.example.MyObj,p1,p4,p4=zip");
MyObj someObj = new MyObj(p1, p2, p3);
String json = JSONUtil.toJSON(someObj, cfg);
JSONConfig cfg = new JSONConfig();
cfg.setReflectUnknownObjects(true);
cfg.setPrivacyLevel(ReflectUtil.PRIVATE); // include all fields.
MyObj someObj = new MyObj(p1, p2, p3);
String json = JSONUtil.toJSON(someObj, cfg);