Reputation: 13
I'm trying to create a utility to print example JSON structures from any given POJO. I've been trying Jackson and Gson to print all of the fields from a given object. I've created the following three objects as examples.
public class Model {
private String val1;
private Child child;
//getters and setters
}
public class Child {
private String val2;
private ArrayList<SubChild> subChildren;
//getters and setters
}
public class SubChild {
private String val3;
//getters and setters
}
I would like to have an example serializer that prints these objects with all of their field names even when the children are null. Below is my target output:
{
"val1" : "",
"child" : {
"val2" : "",
"subChildren" : [ {
"val3" : ""
} ]
}
}
These are the methods I've tried to print these pojos along with their outputs which don't quite match my needs
Jackson:
ObjectMapper map = new ObjectMapper().setSerializationInclusion(Include.ALWAYS);
Model testModel = new Model()
map.writerWithDefaultPrettyPrinter().writeValueAsString(testModel);
Output:
{
"val1" : null,
"child" : null
}
Gson:
Gson gson = builder.serializeNulls().setPrettyPrinting().create();
Model testModel = new Model();
gson.toJson(testModel);
Output:
{
"val1" : null,
"child" : null
}
Is there a simple way to achieve my goal without populating all of the child fields? I would like to be able to use this utility on a generic class where I wouldn't know which methods to call to populate the objects with empty values.
Upvotes: 1
Views: 2544
Reputation: 21125
I doubt that any of libraries would support this out of box, because their only responsibility is dealing with serialization/deserialization. Hence in order to make any serializer work like you want, it's much better to create an object mock and then try to adapt the mocking strategy for a particular library.
First off, let's just create a simple utility class for types:
final class Types {
private Types() {
}
static <T> Class<T> typeToClass(final Type type) {
final Class<?> clazz;
if ( type instanceof Class ) {
clazz = (Class<?>) type;
} else if ( type instanceof ParameterizedType ) {
final ParameterizedType parameterizedType = (ParameterizedType) type;
clazz = typeToClass(parameterizedType.getRawType());
} else {
throw new AssertionError(type);
}
@SuppressWarnings("unchecked")
final Class<T> castClass = (Class<T>) clazz;
return castClass;
}
}
The method above is only responsible for resolving a class from a type (at least it attempts to do that). Classes are types, but types are not necessarily classes. Well, this is them Java type system.
The next class is responsible for mocking an object by type.
It's somewhat difficult, but the comments will shed some light (ImmutableMap
and ImmutableList
are from Google Guava).
final class Mock {
// Cache immutable primitives and wrappers. It's safe
private static final Optional<Byte> defaultByte = Optional.of((byte) 0);
private static final Optional<Short> defaultShort = Optional.of((short) 0);
private static final Optional<Integer> defaultInteger = Optional.of(0);
private static final Optional<Long> defaultLong = Optional.of(0L);
private static final Optional<Float> defaultFloat = Optional.of(0F);
private static final Optional<Double> defaultDouble = Optional.of(0D);
private static final Optional<Character> defaultCharacter = Optional.of('\u0000');
private static final Optional<Boolean> defaultBoolean = Optional.of(false);
private static final Optional<String> defaultString = Optional.of("");
// This is a simple map that can return a value by a known type
private static final Map<Class<?>, Optional<?>> defaultObjectsByIndex = ImmutableMap.<Class<?>, Optional<?>>builder()
.put(byte.class, defaultByte).put(Byte.class, defaultByte)
.put(short.class, defaultShort).put(Short.class, defaultShort)
.put(int.class, defaultInteger).put(Integer.class, defaultInteger)
.put(long.class, defaultLong).put(Long.class, defaultLong)
.put(float.class, defaultFloat).put(Float.class, defaultFloat)
.put(double.class, defaultDouble).put(Double.class, defaultDouble)
.put(char.class, defaultCharacter).put(Character.class, defaultCharacter)
.put(boolean.class, defaultBoolean).put(Boolean.class, defaultBoolean)
.put(String.class, defaultString)
.build();
// Unlike the previous map, searching for an object takes a linear type
// The first-best candidate will be used
// The ifAssignable method is declared below
private static final Collection<? extends Function<? super Type, Optional<?>>> defaultObjectsByFirstBest = ImmutableList.<Function<? super Type, Optional<?>>>builder()
.add(ifAssignable(LinkedList.class, LinkedList::new))
.add(ifAssignable(ArrayList.class, ArrayList::new))
.add(ifAssignable(List.class, ArrayList::new))
.add(ifAssignable(TreeSet.class, TreeSet::new))
.add(ifAssignable(LinkedHashSet.class, LinkedHashSet::new))
.add(ifAssignable(HashSet.class, HashSet::new))
.add(ifAssignable(Set.class, HashSet::new))
.add(ifAssignable(TreeMap.class, TreeMap::new))
.add(ifAssignable(LinkedHashMap.class, LinkedHashMap::new))
.add(ifAssignable(HashMap.class, HashMap::new))
.add(ifAssignable(Map.class, HashMap::new))
.build();
private Mock() {
}
static <T> T create(
final Type type,
final Predicate<? super Type> isTypeSupported,
final Predicate<? super Field> isFieldSupported,
final Function<? super Class<T>, ? extends T> objectCreator,
final BiFunction<Object, Type, Object> postProcess
)
throws Exception {
return create(type, Mock::supplyDefaultInstance, isTypeSupported, isFieldSupported, objectCreator, postProcess);
}
static <T> T create(
final Type type, // Used to instantiate an object
final Function<? super Type, Optional<?>> defaultInstanceSupplier, // Used in order to supply a default object by type
final Predicate<? super Type> isTypeSupported, // Not all types can be serialized
final Predicate<? super Field> isFieldSupported, // Not all fields can be serialized
final Function<? super Class<T>, ? extends T> objectCreator, // If no any default object can be supplied, try to ask for it from elsewhere
final BiFunction<Object, Type, Object> postProcess // This is what is what used to post-process the result object
)
throws Exception {
// Not something we want to support?
if ( !isTypeSupported.test(type) ) {
return null;
}
// Check if we can provide a default value
final Optional<?> maybeT = defaultInstanceSupplier.apply(type);
if ( maybeT.isPresent() ) {
@SuppressWarnings("unchecked")
final T castT = (T) postProcess.apply(maybeT.get(), type);
return castT;
}
final Class<T> clazz = Types.typeToClass(type);
// No? Then let's try instantiate it
final T newT = objectCreator.apply(clazz);
// And iterate it from bottom to top to super classes (java.lang.Object does not have fields)
for ( Class<?> i = clazz; i != null && i != Object.class; i = i.getSuperclass() ) {
for ( final Field field : i.getDeclaredFields() ) {
if ( isFieldSupported.test(field) ) {
field.setAccessible(true);
// Recursively do the same for all the fields
final Object value = create(field.getGenericType(), defaultInstanceSupplier, isTypeSupported, isFieldSupported, objectCreator, postProcess);
field.set(newT, value);
}
}
}
// And then do post-processing for these "special" types
@SuppressWarnings("unchecked")
final T castNewT = (T) postProcess.apply(newT, type);
return castNewT;
}
static Optional<?> supplyDefaultInstance(final Type type) {
final Optional<?> defaultValueFromIndex = defaultObjectsByIndex.get(type);
if ( defaultValueFromIndex != null ) {
return defaultValueFromIndex;
}
return defaultObjectsByFirstBest
.stream()
.map(resolver -> resolver.apply(type))
.filter(Optional::isPresent)
.findFirst()
.orElse(Optional.empty());
}
private static Function<? super Type, Optional<?>> ifAssignable(final Class<?> expectedClass, final Supplier<?> defaultObject) {
return actualClass -> expectedClass.isAssignableFrom(Types.typeToClass(actualClass))
? Optional.of(defaultObject.get())
: Optional.empty();
}
}
Once you have it all, you can have a Gson specialization and do post-processing like adding mock elements to collections (collections are and should be empty by default).
public final class Q50515517 {
private Q50515517() {
}
private static final Gson gson = new GsonBuilder()
.serializeNulls()
.setPrettyPrinting()
.disableHtmlEscaping()
.create();
// Gson can avoid use of constructors
private static final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
public static void main(final String... args)
throws Exception {
gson.toJson(create(Object3.class, gson, Q50515517::customPostProcess), Object3.class, System.out);
}
// Here we make some Gson adaptations
private static <T> T create(final Type type, final Gson gson, final BiFunction<Object, Type, Object> postProcess)
throws Exception {
final Excluder excluder = gson.excluder();
final Predicate<? super Type> isClassSupported = t -> !excluder.excludeClass(Types.typeToClass(t), true);
final Predicate<? super Field> isFieldSupported = field -> !excluder.excludeField(field, true);
return Mock.create(type, Mock::supplyDefaultInstance, isClassSupported, isFieldSupported, Q50515517::createUnsafely, postProcess);
}
private static <T> T createUnsafely(final Class<T> clazz) {
try {
return unsafeAllocator.newInstance(clazz);
} catch ( final Exception ex ) {
throw new RuntimeException(ex);
}
}
private static Object customPostProcess(final Object object, final Type type) {
if ( object instanceof Collection ) {
if ( type instanceof ParameterizedType ) {
@SuppressWarnings("unchecked")
final Collection<Object> collection = (Collection<Object>) object;
final ParameterizedType parameterizedType = (ParameterizedType) type;
final Type elementType = parameterizedType.getActualTypeArguments()[0];
try {
final Object newElement = create(elementType, gson, Q50515517::customPostProcess);
collection.add(newElement); // This is where we add a mock element to the collection
return object;
} catch ( final Exception ex ) {
throw new RuntimeException(ex);
}
}
}
return object;
}
}
In summary, this solution supports:
Example classes from the test above:
class Object1 {
String s1;
}
class Object2
extends Object1 {
String s2;
}
class Object3
extends Object2 {
String s3;
List<Object4> lo4;
}
class Object4 {
String s4a;
String s4b;
}
Output:
{
"s3": "",
"lo4": [
{
"s4a": "",
"s4b": ""
}
],
"s2": "",
"s1": ""
}
Upvotes: 1
Reputation: 1054
I cannot see how this would be possible. What if you had a Long
as a field? Jackson would not know what to do there. What Jackson and Gson have done is correct and they have printed null
.
What you could do is write a utility to set the fields manually. However, you would have to handle different types accordingly. Something like this achieves what you have asked for, but only for List
:
public static void main(String args[]) throws IOException, IllegalAccessException {
ObjectMapper map = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.ALWAYS);
Model testModel = new Model();
instantiateFields(testModel);
String result = map.writerWithDefaultPrettyPrinter().writeValueAsString(testModel);
System.out.println(result);
}
private static void instantiateFields(Object o) throws IllegalAccessException {
Field[] fields = o.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.get(o) == null) {
Type type = field.getType();
try {
Class<?> clazz = (Class<?>) type;
Object instance = clazz.newInstance();
if (List.class.isAssignableFrom(clazz)) {
instantiateList(clazz, field, instance);
}
field.set(o, instance);
instantiateFields(instance);
} catch (ClassCastException | InstantiationException e) {
// Handle this or leave field null
}
}
}
}
private static void instantiateList(Class<?> clazz, Field field, Object instance) throws IllegalAccessException, InstantiationException {
ParameterizedType listType = (ParameterizedType) field.getGenericType();
Class<?> listClass = (Class<?>) listType.getActualTypeArguments()[0];
Object listTypeInstance = listClass.newInstance();
instantiateFields(listTypeInstance);
List<Object> list = (List<Object>) instance;
list.add(listTypeInstance);
}
Yielding the following output:
{
"val1" : "",
"child" : [ {
"val2" : "",
"subChildren" : [ {
"val3" : ""
} ]
} ]
}
Hope this helps.
Upvotes: 1