4

I know Gson doesn't come with a similar feature, but is there a way to add support for unwrapping Json fields the way @JsonUnwrap does?

The goal is to allow a structure like:

public class Person { 
    public int age; 
    public Name name; 
} 

public class Name { 
    public String first;
    public String last; 
}

to be (de)serialized as:

{
  "age" : 18, 
  "first" : "Joey", 
  "last" : "Sixpack" 
}

instead of:

{
   "age" : 18, 
    "name" : {
        "first" : "Joey",
        "last" : "Sixpack"
    } 
 }

I understand it could get fairly complex, so I'm not looking for a full solution, just some high-level guidelines if this is even doable.

Derlin
  • 9,572
  • 2
  • 32
  • 53
kaqqao
  • 12,984
  • 10
  • 64
  • 118

2 Answers2

2

I've made a crude implementation of a deserializer that supports this. It is fully generic (type-independent), but also expensive and fragile and I will not be using it for anything serious. I am posting only to show to others what I've got, if they end up needing to do something similar.

public class UnwrappingDeserializer implements JsonDeserializer<Object> {

    //This Gson needs to be identical to the global one, sans this deserializer to prevent infinite recursion
    private Gson delegate;

    public UnwrappingDeserializer(Gson delegate) {
        this.delegate = delegate;
    }

    @Override
    public Object deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
        Object def = delegate.fromJson(json, type); //Gson doesn't care about unknown fields
        Class raw = GenericTypeReflector.erase(type);
        Set<Field> unwrappedFields = ClassUtils.getAnnotatedFields(raw, GsonUnwrap.class);
        for (Field field : unwrappedFields) {
            AnnotatedType fieldType = GenericTypeReflector.getExactFieldType(field, type);
            field.setAccessible(true);
            try {
                Object fieldValue = deserialize(json, fieldType.getType(), context);
                field.set(def, fieldValue);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

        return def;
    }
}

It can then be registered globally via new GsonBuilder().registerTypeHierarchyAdapter(Object.class, new UnwrappingDeserializer(new Gson())).create() or for a specific type via registerTypeAdapter.

Notes:

  • A real implementation should recursively check the entire class structure for the presence of GsonUnwrap, cache the result in a concurrent map, and only go through this procedure if it needs to. Otherwise it should just return def immediately
  • It should also cache discovered annotated fields to avoid scanning the hierarchy each time
  • GenericTypeReflector is coming from GeAnTyRef
  • ClassUtils#getAnnotatedFields is my own implementation, but it doesn't do anything special - it just gathers declared fields (via Class#getDeclaredFields) recursively for the class hierarchy
  • GsonUnwrap is just a simple custom annotation

I presume a similar thing can be done for serialization as well. Examples linked from Derlin's answer can be a starting point.

Community
  • 1
  • 1
kaqqao
  • 12,984
  • 10
  • 64
  • 118
  • excellent job, thanks! One little fix: I needed to change line `Object fieldValue = deserialize(json, fieldType.getType(), context);` to `Object fieldValue = context.deserialize(json, fieldType.getType());` – Petr Kozelka May 22 '19 at 13:44
1

Currently, there is no easy way to do that. Here are anyway some pointers/alternative ways to make it work.


GsonFire: GsonFire implements some useful features missing from Gson. While it does not yet offer automatic wrapping/unwrapping, it may be a good starting point to create your custom logic.

If you only need serialization, you can add getters for first and last in Person and use @ExposeMethodResult to serialize them. Unfortunately, setters are not supported (cf. Is possible to use setters when Gson deserializes a JSON?).


Another way to support the serialization is to follow the advices from How to move fields to parent object.


Custom TypeAdapters : on of the only ways to support both serialization and deserialization is to create custom TypeAdapters. This won't be generic, but it will suit your usecase.

The thread Serialize Nested Object as Attributes already gives you examples, so I won't repeat them here.

Community
  • 1
  • 1
Derlin
  • 9,572
  • 2
  • 32
  • 53