1

I have a generic class like this:

public class Pojo<T> {
    
    @JsonProperty("value")
    public T[] values;
};

The T can either hold a String, a LocalDateTime or an Integer. The differentiation between String and Integer seems to work fine, most likely because those types are represented differently in the serialized JSON file. However, when I have a datetime in my JSON object (see example), it is parsed as a string anyway.

The actual type in use for the value field is determined by the input. While I do have that knowledge in the following minimal example, this is not true for all uses of this code in my program. I can only rely on the pattern of the value of field value.

public class Main {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);

    String json = "{\"value\": \"2022-02-22T12:00:00\"}";
    Pojo<?> deserialized = mapper.readValue(json, Pojo.class);
    assert deserialized.value[0] instanceof LocalDateTime;
}

I haven't had any success tinkering with JsonTypeInfo yet. Is there a way to parse the value field as an array of LocalDateTime objects if all values for this field match the pattern yyyy-MM-dd'T'hh:mm:ss?

a.ilchinger
  • 398
  • 4
  • 13

1 Answers1

1

Here is your problem - Pojo<?> deserialized = mapper.readValue(json, Pojo.class).

ObjectMapper does not know what type to parse T to. To tell it the type you need to use either readValue overload with TypeReference, or the overload with JavaType. I find JavaType easier to read, so here is an example with it:

public class Test {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);

        String json = "{\"value\": \"2022-02-22T12:00:00\"}";
        JavaType javaType = TypeFactory.defaultInstance().constructParametricType(Pojo.class, LocalDateTime.class);
        Pojo<LocalDateTime> deserialized = mapper.readValue(json, javaType);
        System.out.println(deserialized.values[0].toLocalDate());
        System.out.println(deserialized.values[0].toLocalTime());
    }
}

When constructing the parametric JavaType the first argument is the class itself, next are concrete generic types.

Edit: Having in mind the new info, the best i can come up with is custom deserializer, which resolves the type at runtime. Something like this:

public class UnknownTypeDeserializer<T> extends StdDeserializer<Pojo<T>> {

    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

    public UnknownTypeDeserializer() {
        super((Class<?>) null);
    }

    @Override
    public Pojo<T> deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException, JacksonException {
        JsonNode node = parser.getCodec().readTree(parser);
        String value = node.get("value").asText();
        Pojo<T> pojo = new Pojo<>();
        T[] arr;
        try {
            arr = (T[]) new LocalDateTime[]{LocalDateTime.parse(value, this.formatter)};
        } catch (Exception exc) {
            try {
                arr = (T[]) new Integer[]{Integer.parseInt(value)};
            } catch (NumberFormatException numberFormatException) {
                arr = (T[]) new String[]{value};
            }
        }
        pojo.values = arr;
        return pojo;
    }
}

The idea is try to parse to LocalDateTime, if not possible, try to parse to int, if not possible again leave as String. That's quite the ugly way to do it, but i am trying just to illustrate the idea. Instead of catching exceptions, it might be better to examine the format, using regex for example, and according to which regex matches, parse to correct type. I tested it with string and int, and it actually works.

public class Test {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addDeserializer(Pojo.class, new UnknownTypeDeserializer<>());
        mapper.registerModule(simpleModule);

        String json1 = "{\"value\": \"2022-02-22T12:00:00\"}";
        Pojo<?> deserialized1 = mapper.readValue(json1, Pojo.class);
        System.out.println("is date - " + (deserialized1.values[0] instanceof LocalDateTime));

        String json2 = "{\"value\": \"bla bla bla\"}";
        Pojo<?> deserialized2 = mapper.readValue(json2, Pojo.class);
        System.out.println("is string - " + (deserialized2.values[0] instanceof String));

        String json3 = "{\"value\": 41}";
        Pojo<?> deserialized3 = mapper.readValue(json3, Pojo.class);
        System.out.println("is int - " + (deserialized3.values[0] instanceof Integer));
    }
}
Chaosfire
  • 4,818
  • 4
  • 8
  • 23
  • That is already great advice. Unfortunately, I have to admit that I forgot a crucial piece of information in the original question (already edited): I do *not* know the actual parametric type at deserialization time. Instead, I match the data to a limited set of possible types, `LocalDateTime` being on of them which is string-like, but with a specific pattern. – a.ilchinger Feb 22 '22 at 18:08
  • @a.ilchinger That complicates things a bit, but still it's doable. Check the edit for a solution with custom deserializer. – Chaosfire Feb 22 '22 at 20:57