I have the following class
public class Customer<T>{
private String name;
private String store;
private T details;
}
public class Details{
private String dob;
private boolean validated;
}
The deserialization works when the JSON is the following
{
"name": "John Smith",
"store": "Walmart",
"details": {
"dob": "1900/01/01",
"validated": true
}
}
But when the given JSON response is like below, it fails.
{
"name": "John Smith",
"store": "Walmart",
"details": [
"Failed customer does not exist in the system", "Please contact administrator"
]
}
Is there any way with Jackson mapper to handle these scenarios? If details
is provided as POJO, then deserialize it as such, otherwise deserialize details
as a String
. I defined my mapper as such:
private static final ObjectMapper Mapper = new ObjectMapper() {{
setSerializationInclusion(JsonInclude.Include.NON_NULL);
setDateFormat(new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z", Locale.US));
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}};
If before the deserialization you knew already what the json contained for the field details
, you could simply read a generic Customer
with a TypeReference
. In this scenario, you would have your error messages as a List<String>
instead of a String
though, sticking more to the json structure.
//Reading a generic Customer with a Details pojo
Customer<Details> c1 = mapper.readValue(jsonPojo, new TypeReference<Customer<Details>>() {});
//Reading a generic Customer with an array of String error messages
Customer<List<String>> c2 = mapper.readValue(jsonArray, new TypeReference<Customer<List<String>>>() {});
However, this solution requires knowing what the json contains in advance (which cannot be the case), or analyzing it beforehand and then proceeding with the appropriate deserialization (which can be tedious).
So, in order to offer a more generic solution while maintaining the flexibility of generics, it's necessary to employ a custom deserializer for the field details
, as the logic is quite specific and configurations on the ObjectMapper
alone won't do it.
If details is provided as pojo then write as such otherwise write details as a string value.
However, defining a custom deserializer for a generic type T
makes little sense, as after implementing the deserialization logic for the two cases Details
and String
, we would be forced to cast the result to the generic type T
, thwarting the whole point of using generics and losing their main advantage of type safety at compile time.
What we could do instead is to find a common type denominator among the possible data types for the field details
. At the moment, details
can be either an instance of Details
or a String
. The String
class implements the interface Serializable
, so if the class Details
would implement it too, we could use Serializable
as an upper bound for the generic type T
and define the custom deserializer for Serializable
. This approach implies that every type passed in place of T
must a sub-type of Serializable
. This is not a really strict requirement as most Java classes already implement the interface, while our classes could easily implement it too without having to provide any implementation.
public class Customer<T extends Serializable> {
private String name;
private String store;
@JsonDeserialize(using = MyDeserializer.class)
private T details;
// ... implementation ...
}
public class Details implements Serializable {
private String dob;
private boolean validated;
// ... implementation ...
}
public class MyDeserializer extends StdDeserializer<Serializable> {
public MyDeserializer() {
this(null);
}
public MyDeserializer(Class<?> c) {
super(c);
}
@Override
public Serializable deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
JsonNode node = p.getCodec().readTree(p);
if (node.isArray()) {
return StreamSupport.<JsonNode>stream(Spliterators.spliteratorUnknownSize(node.iterator(), Spliterator.ORDERED), false)
.map(JsonNode::textValue)
.collect(Collectors.joining(" - "));
}
return ctxt.readTreeAsValue(node, Details.class);
}
}
public class Main {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper() {{
setSerializationInclusion(JsonInclude.Include.NON_NULL);
setDateFormat(new SimpleDateFormat("MM/dd/yyyy HH:mm:ss z", Locale.US));
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}};
String jsonPojo = """
{
"name": "John Smith",
"store": "Walmart",
"details": {
"dob": "1900/01/01",
"validated": true
}
}
""";
Customer<Serializable> c1 = mapper.readValue(jsonPojo, new TypeReference<Customer<Serializable>>() {});
System.out.println(c1.getDetails().getClass().getSimpleName()); //Prints Details
System.out.println(c1);
String jsonArray = """
{
"name": "John Smith",
"store": "Walmart",
"details": [
"Failed customer does not exist in the system", "Please contact administrator"
]
}
""";
Customer<Serializable> c2 = mapper.readValue(jsonArray, new TypeReference<Customer<Serializable>>() {});
System.out.println(c2.getDetails().getClass().getSimpleName()); //Prints String
System.out.println(c2);
}
}
Here is also a demo at OneCompiler showcasing the code above.