1

I want to deserialize an arbitrary JSON into a Map<String, Object>. The value of this map is either some primitive (such as an Integer, String, LocalDate, ...) or another Map<String, Object> (recursive).

To get the primitive, some kind of custom client callback should be called for each property. Depending on the key, certain deserialization will happen. For example (pseudo-code):

{
    "name": "Bill",
    "age": 53,
    "timestamp": "2012-04-23T18:25:43.511Z",
    "coordinates": "51.507351;-0.127758",
    "address": {
        "street": "Wallstreet",
        "city": "NY"
    }
}

Object convert(key, value) {
     if ("name".equals(key)) {
          return value.toString();
     } else if ("timestamp".equals(key)) {
          return LocalDate.parse(value);
     } else if ("coordinates".equals(key)) {
          return Coordinates.parse(value);
     }
     ...
}

In SO Jackson - Recursive parsing into Map<String, Object> a simple generic solution is provided. However, this simply deserializes each non-object property to a String. Is it possible to add a custom client callback, as shown above, to the deserialization process?

Community
  • 1
  • 1
Appelsien Sap
  • 363
  • 1
  • 3
  • 9
  • 1
    I don't understand. Why does this have to be _by key_? The value for `"name"` is a JSON string so its Java equivalent will be a `java.lang.String` value. `"age"` is a JSON number so its Java equivalent will be a `java.lang.Integer` (or whatever Jackson uses as a default). `"address"` is a JSON object so its equivalent will be a `Map`. `"street"` is a JSON string, so...What else do you need? – Sotirios Delimanolis Jan 06 '16 at 21:50
  • I have a dynamic schema which holds a list of `property -> type` pairs. Indeed, some types are simple strings, integers, ..., while other types can be a `LocalDate`, a `OurCustomType`, ... Since the schema is dynamic, it is not possible to create a traditional POJO with custom deserializers. – Appelsien Sap Jan 06 '16 at 21:53
  • Dynamic how? `property` will always map to `type`, `"name"` will always be a `String`. Why won't a POJO work here? Please edit your question to demonstrate that. – Sotirios Delimanolis Jan 06 '16 at 21:54
  • 2
    It is dynamic because it is not known at compile time. The `property -> type` pairs are read from a configuration file at runtime. I don't think there is any reason to edit my question here. – Appelsien Sap Jan 06 '16 at 21:56

1 Answers1

-1

Out of the box:

new ObjectMapper.readValue("\"name\": ...", Map.class);

Jackson converts the input from your example to:

class java.util.LinkedHashMap(
    name -> class java.lang.String(Bill), 
    age -> class java.lang.Integer(53), 
    address -> class java.util.LinkedHashMap(
        street -> class java.lang.String(Wallstreet), 
        city -> class java.lang.String(NY)
    )
)

If you want some custom processing (e.g. you want BigInteger instead of integer for age), you can use a custom LinkedHashMap implementation, something like:

public class CustomMap extends LinkedHashMap<String, Object> {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private Object convertPrimitive(String key, Object value) {
        switch (key) {
            case "age":
                return new BigInteger(value.toString());
            case "city":
                return value.toString().toLowerCase();
            default:
                return value;
        }
    }

    private Object convertMap(String key, Object value) {
        return OBJECT_MAPPER.convertValue(value, CustomMap.class);
    }

    @Override
    public Object put(String key, Object value) {
        return super.put(key, (value instanceof Map) ? convertMap(key, value) : convertPrimitive(key, value));
    }
}

This time,

new ObjectMapper.readValue("\"name\": ...", CustomMap.class);

will result in:

class org.example.CustomMap(
    name -> class java.lang.String(Bill), 
    age -> class java.math.BigInteger(53), 
    address -> class org.example.CustomMap(
        street -> class java.lang.String(Wallstreet), 
        city -> class java.lang.String(ny)
    )
)