Search code examples
javagenericsjacksonjson-deserialization

Delegating in custom Jackson deserializer


The situation is the following: I have managed to get Jackson to deserialize the following generic ResponseWrapper<T>.

static final class ResponseWrapper<T> {
    
    private ResponseData <T> response;
    
    protected static final class ResponseData<T> {
    
        private int    status;
        private T      data;
    
    }
    
}

Using the following ParameterizedTypeReference

public static <T> ParameterizedTypeReference <ResponseWrapper<T>> typeReferenceOf ( Class<T> tClass ) {
    return ParameterizedTypeReference.forType( ParameterizedTypeImpl.make( ResponseWrapper.class, new Type[]{ tClass }, null ) );
}

The problem: I need to handle a situation where the deseralization of T will fail because the value of data will not be an object but a String instead. I need to capture the exception and assign this value to another property in ResponseData, for example String errorMessage. My take on it has been to annotate the response property with @JsonDeserialize( using = ResponseDeserializer.class ) but I do not know how to implement the JsonDeserializer properly so that it delegates the deserialization to Jackson's implementations and I simply capture the exception when it occurs.

For more context, I am using WebClient as the HTTP client and I deal with responses using an exchange Function<ClientResponse, Mono<ResponseWrapper<T>> where the deserialization takes place.


Solution

  • In order to get the generic type, I had ResponseDeserializer implement ContextualDeserializer

    @Override
    public JsonDeserializer<?> createContextual ( DeserializationContext context, BeanProperty property ) throws JsonMappingException {
    
       JavaType wrapperType = property.getType();
       JavaType valueType = wrapperType.containedType(0);
       return new ResponseDeserializer<>( valueType );
    
    }
    

    That allowed me to create concrete instances of the deserializer with the specific JavaType. The following is the complete solution:

    private static final class ResponseDeserializer<T> extends JsonObjectDeserializer<ResponseWrapper.ResponseData<T>> implements ContextualDeserializer  {
       
       private final JavaType javaType;
    
       public ResponseDeserializer ( ) {
           this.javaType = null;
       }
       
       public ResponseDeserializer ( JavaType javaType ) {
           this.javaType = javaType;
       }
       
       @Override
       public JsonDeserializer<?> createContextual ( DeserializationContext context, BeanProperty property ) throws JsonMappingException {
    
           JavaType wrapperType = property.getType();
           JavaType valueType = wrapperType.containedType(0);
           return new ResponseDeserializer<>( valueType );
           
       }
    
       @Override
       protected ResponseWrapper.ResponseData<T> deserializeObject ( JsonParser jsonParser, DeserializationContext context, ObjectCodec codec, JsonNode tree ) throws IOException {
           
           int status = tree.get( "status" ).asInt();
           JsonNode dataNode = tree.get( "data" );
           
           try {
    
               final T data; {
                   
                   if ( dataNode == null ) {
                       data = null;
                   } else {
                       JsonParser dataParser = dataNode.traverse();
                       JsonToken token = dataParser.nextToken(); // handle?
                       data = context.readValue( dataParser, javaType );
                   }
                   
               };
               
               return new ResponseWrapper.ResponseData<T>( status, data );
               
           } catch ( InvalidDefinitionException e ) {
               
               String errorMessage = dataNode.asText();
               return new ResponseWrapper.ResponseData<T>( status, errorMessage );
               
           }
    
       }
       
    }