rsutormin
rsutormin

Reputation: 1649

Generic tuple de-serialization in Jackson

It so happens that I need to support in Java JSON data coming from external data sources. There is one common pattern. It's an array containing fixed number of elements of certain different types. We call it tuple. Here is my example of de-serialization for 3-element tuple with particular expected types of elements using FasterXML Jackson:

public class TupleTest {
    public static void main(String[] args) throws Exception {
        String person = "{\"name\":\"qqq\",\"age\":35,\"address\":\"nowhere\",\"phone\":\"(555)555-5555\",\"email\":\"[email protected]\"}";
        String jsonText = "[[" + person + ",[" + person + "," + person + "],{\"index1\":" + person + ",\"index2\":" + person + "}]]";
        ObjectMapper om = new ObjectMapper().registerModule(new TupleModule());
        List<FixedTuple3> data = om.readValue(jsonText, new TypeReference<List<FixedTuple3>>() {});
        System.out.println("Deserialization result: " + data);
        System.out.println("Serialization result: " + om.writeValueAsString(data));
    }
}

class Person {
    public String name;
    public Integer age;
    public String address;
    public String phone;
    public String email;

    @Override
    public String toString() {
        return "Person{name=" + name + ", age=" + age + ", address=" + address
                + ", phone=" + phone + ", email=" + email + "}";
    }
}

class FixedTuple3 {
    public Person e1;
    public List<Person> e2;
    public Map<String, Person> e3;

    @Override
    public String toString() {
        return "Tuple[" + e1 + ", " + e2 + ", " + e3 + "]";
    }
}

class TupleModule extends SimpleModule {
    public TupleModule() {
        super(TupleModule.class.getSimpleName(), new Version(1, 0, 0, null, null, null));
        setSerializers(new SimpleSerializers() {
            @Override
            public JsonSerializer<?> findSerializer(SerializationConfig config,
                    JavaType type, BeanDescription beanDesc) {
                if (isTuple(type.getRawClass()))
                    return new TupleSerializer();
                return super.findSerializer(config, type, beanDesc);
            }
        });
        setDeserializers(new SimpleDeserializers() {
            @Override
            public JsonDeserializer<?> findBeanDeserializer(JavaType type,
                    DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
                Class<?> rawClass = type.getRawClass();
                if (isTuple(rawClass))
                    return new TupleDeserializer(rawClass);
                return super.findBeanDeserializer(type, config, beanDesc);
            }
        });
    }

    private boolean isTuple(Class<?> rawClass) {
        return rawClass.equals(FixedTuple3.class);
    }

    public static class TupleSerializer extends JsonSerializer<Object> {
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
            try {
                jgen.writeStartArray();
                for (int i = 0; i < 3; i++) {
                    Field f = value.getClass().getField("e" + (i + 1));
                    Object res = f.get(value);
                    jgen.getCodec().writeValue(jgen, res);
                }
                jgen.writeEndArray();
            } catch (Exception ex) {
                throw new IllegalStateException(ex);
            }
        }
    }   

    public static class TupleDeserializer extends JsonDeserializer<Object> {
        private Class<?> retClass;

        public TupleDeserializer(Class<?> retClass) {
            this.retClass = retClass;
        }

        public Object deserialize(JsonParser p, DeserializationContext ctx) throws IOException, JsonProcessingException {
            try {
                Object res = retClass.newInstance();
                if (!p.isExpectedStartArrayToken()) {
                    throw new JsonMappingException("Tuple array is expected but found " + p.getCurrentToken());
                }
                JsonToken t = p.nextToken();
                for (int i = 0; i < 3; i++) {
                    final Field f = res.getClass().getField("e" + (i + 1));
                    TypeReference<?> tr = new TypeReference<Object>() {
                        @Override
                        public Type getType() {
                            return f.getGenericType();
                        }
                    };
                    Object val = p.getCodec().readValue(p, tr);
                    f.set(res, val);
                }
                t = p.nextToken();
                if (t != JsonToken.END_ARRAY)
                    throw new IOException("Unexpected ending token in tuple deserializer: " + t.name());
                return res;
            } catch (IOException ex) {
                throw ex;
            } catch (Exception ex) {
                throw new IllegalStateException(ex);
            }
        }
    }
}

But this approach means I have to make new class every time I face new type configuration in tuple of certain size. So I wonder if there is any way to define deserializer dealing with generic typing. So that it will be enough to have one tuple class per tuple size. For instance my generic tuple of size 3 could be defined like:

class Tuple3 <T1, T2, T3> {
    public T1 e1;
    public T2 e2;
    public T3 e3;

    @Override
    public String toString() {
        return "Tuple[" + e1 + ", " + e2 + ", " + e3 + "]";
    }
}

And usage of it would look like:

List<Tuple3<Person, List<Person>, Map<String, Person>>> data = 
       om.readValue(jsonText, 
               new TypeReference<List<Tuple3<Person, List<Person>, Map<String, Person>>>>() {});

Is it something doable or not?

Upvotes: 3

Views: 5546

Answers (2)

StaxMan
StaxMan

Reputation: 116582

Ok. So... there may be a simpler way to do "tuple"-style. You can actually annotate POJOs to be serialized as arrays:

@JsonFormat(shape=JsonFormat.Shape.ARRAY)
@JsonPropertyOrder({ "name", "age" }) // or use "alphabetic"
public class POJO {
   public String name;
   public int age;
}

and if so, they'll get written as arrays, read from arrays.

But if you do what to handle custom generic types, you probably need to get type parameters resolved. This can be done using TypeFactory, method findTypeParameters(...). While this may seem superfluous, it is needed for general case if you sub-type (if not, JavaType actually has accessors for direct type parameters).

Upvotes: 11

Marcos Vasconcelos
Marcos Vasconcelos

Reputation: 18276

Yes, you must use Reflection to get ALL FIELDS, not to get the known fields by name.

Upvotes: 0

Related Questions