Converting Objects to Map and Back
In large enterprise applications, sometimes, we need to convert data objects to and from Map
. Usually, it is an intermediate step to a special serialization. If it is possible to use something standard, then it is better to use that, but many times, the architecture envisioned by some lead architect, the rigid environment, or some similar reason does not make it possible to use JOOQ, Hibernate, Jackson, JAX, or something similar. In such a situation, as it happened to me a few years ago, we have to convert the objects to some proprietary format being string or binary, and the first step towards that direction is to convert the object to a Map
.
Eventually, the conversion is more complex than just:
Map myMap = (Map)myObject;
Because these objects are almost never maps on their own, what we really need in the conversion is to have a Map
where each entry corresponds to a field in the MyObject
class. The key in the entry is the name of the field, and the value is the actual value of the field possibly converted to a Map
itself.
One solution is to use reflection and reflectively read the fields of the object and create the map from it. The other approach is to create a toMap()
method in the class that needs to be converted to a Map
that simply adds each field to the returned map using the name of the field. This is somewhat faster than the reflection-based solution, and the code is much simpler.
When I was facing this problem in a real application a few years ago, I was so frustrated writing the primitive but numerous toMap()
methods for each data object that I created a simple reflection-based tool that to do it just for any class we wanted. Did it solve the problem? No.
This was a professional environment where not only the functionality matters but also the quality of the code and the quality of my code, judged by my fellow programmers, was not matching. They argued that the reflection-based solution is complex and in case it becomes part of the code base then the later joining average developers will not be able to maintain it. Well, I had to admit that they were correct. In a different situation, I would have said that the developer has to learn reflection and programming in Java on a level that is needed by the code. In this case, however, we were not speaking about a specific person, but rather somebody who comes and joins the team in the future, possibly sometime when we have already left the project. This person was assumed to be an average developer, which seemed to be reasonable as we did not know anything about this person. In that sense, the quality of the code was not good, because it was too complex. The quorum of the developer team decided that maintaining the numerous manually crafted toMap()
method was going to be cheaper than finding senior and experienced developers in the future.
To be honest, I was a bit reluctant to accept their decision, but I accepted it even though I had the possibility to overrule it based simply on my position in the team. I tend to accept the decisions of the team even if I do not agree with that, but only if I can live with those decisions. If a decision is dangerous, terrible, and threatens the future of the project, then we have to keep discussing the details until we get to an agreement.
Years later, I started to create Java::Geci
as a side project that you can download from http://github.com/verhas/javageci.
Java::Geci
is a code generation tool that runs during the test phase of the Java development life cycle. Code generation in Java::Geci
is a “test.” It runs the code generation, and in case all the generated code stays put, then the test was successful. In case anything in the code base changed in a way that causes the code generator to generate different code than before, and thus the source code changes, then the test fails. When a test fails, you have to fix the bug and run the build, including tests, again. In this case, the test generates the new, by now fixed, code, therefore, all you have to do is only to run the build again.
When developing the framework, I created some simple generators to generate equals()
and hashCode()
, setters and getters, a delegator generator, and finally, I could not resist but I created a general purpose toMap()
generator. This generator generates code that converts the object to Map
just as we discussed before and also the fromMap()
that I did not mention before, but fairly obviously also needed.
Java::Geci
generators are classes that implement the Generator
interface. The Mapper
generator does that extending the abstract class AbstractJavaGenerator
. This lets the generator throw any exception easing the life of the generator developer, and also it already looks up the Java class, which was generated from the currently processed source. The generator has access to the actual Class
object via the parameter klass
and the same time to the source code via the parameter source
, which represents the source code and provides methods to create Java code to be inserted into it.
The third parameter global
is something like a map holding the configuration parameters that the source code annotation @Geci
defines.
package javax0.geci.mapper;
import ...
public class Mapper extends AbstractJavaGenerator {
...
@Override
public void process(Source source, Class<?> klass, CompoundParams global)
throws Exception {
final var gid = global.get("id");
var segment = source.open(gid);
generateToMap(source, klass, global);
generateFromMap(source, klass, global);
final var factory = global.get("factory", "new {{class}}()");
final var placeHolders = Map.of(
"mnemonic", mnemonic(),
"generatedBy", generatedAnnotation.getCanonicalName(),
"class", klass.getSimpleName(),
"factory", factory,
"Map", "java.util.Map",
"HashMap", "java.util.HashMap"
);
final var rawContent = segment.getContent();
try {
segment.setContent(Format.format(rawContent, placeHolders));
} catch (BadSyntax badSyntax) {
throw new IOException(badSyntax);
}
}
The generator itself only calls the two methods generateToMap()
and generateFromMap()
, which generate, as the names imply the toMap()
and fromMap()
methods into the class.
Both methods use the source generating support provided by the Segment
class, and they also use the templating provided by Jamal. It is also important to note that the fields are collected calling the reflection tools method getAllFieldsSorted()
, which returns all the field the class has in a definitive order that does not depend on the actual JVM vendor or version.
private void generateToMap(Source source, Class<?> klass, CompoundParams global) throws Exception {
final var fields = GeciReflectionTools.getAllFieldsSorted(klass);
final var gid = global.get("id");
var segment = source.open(gid);
segment.write_r(getResourceString("tomap.jam"));
for (final var field : fields) {
final var local = GeciReflectionTools.getParameters(field, mnemonic());
final var params = new CompoundParams(local, global);
final var filter = params.get("filter", DEFAULTS);
if (Selector.compile(filter).match(field)) {
final var name = field.getName();
if (hasToMap(field.getType())) {
segment.write("map.put(\"%s\", %s == null ? null : %s.toMap0(cache));", field2MapKey(name), name, name);
} else {
segment.write("map.put(\"%s\",%s);", field2MapKey(name), name);
}
}
}
segment.write("return map;")
._l("}\n\n");
}
The code selects only the fields that are denoted by the filter
expression.