Search code examples
javaoopdesign-patternsjacksonjson-deserialization

How to deserialize to map or array class where map structure varies in Java?


I need to deserialize the response of an external REST API which is usually a JSON object but if an error occurred it will be an array of JSON objects. The API is lacking in documentation so the way I determine if error occurred or not is by the HTTP Status of the response. The problem is that non-error responses have different structure per API route (user response, product response etc.). Another problem is in my application we use an abstract class for external API response with such fields as error, hasError. I solved the problem as follows:

 public abstract class AbsractApiResponse {
  public abstract String getError();
  public abstract void setError(String error);
  public abstract String getDescription();
  public abstract void setDescription(String error);
}

public class ErrorResponse {
  private String errorCode; // getters/setters

  private String message; // getters/setters
}

public class UserResponse extends AbsractApiResponse {

  private String error; // getters/setters
  private boolean hasError; // getters/setters
  private boolean description; // getters/setters
  private String userName;
  private String userEmail;
}


public <R extends AbsractApiResponse> R deserializeResponse(
    String apiResponse, Class<R> responseType, boolean isHttpError)
    throws JsonProcessingException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
  R response;
  Object deserialized = objectMapper.readValue(apiResponse, Object.class);

  if (isHttpError && deserialized instanceof List) {
    TypeReference<List<ErrorResponse>> errorResponseType = new TypeReference<>() {};
    List<ErrorResponse> responseErrors = objectMapper.convertValue(deserialized,
        errorResponseType);
    Constructor<R> constructor = responseType.getDeclaredConstructor();
    response = constructor.newInstance();
    ErrorResponse firstError = responseErrors.get(0);
    String errorDescription = responseErrors.stream().map(ErrorResponse::toString).collect(Collectors.joining());
    response.setError(firstError.getMessage());
    response.setDescription(errorDescription);
  } else {
    response = objectMapper.convertValue(deserialized, responseType);
  }
  return response;
}

With this approach I would have to add fields like error/hasError etc. to every class which represents a response which isn't that bad I guess. Another red flag for me is the use of reflection (responseType.getDeclaredConstructor()) and the 4 checked exceptions that go with it. I'm wondering, if there's a better way to solve this?


Solution

  • I do not recommend to merge error response together with business objects. You can return given response class in case of success and throw an exception in case of error. This is what I think would be the cleanest way.

    If you do not want to throw an exception you can implement wrapper class which contains response and error objects. In case error field is set we know there was a problem. It could look like below:

    interface ApiResponse {
    }
    
    @Data
    class ResponseWrapper<R extends ApiResponse> {
        private R response;
        private Error error;
    
        public boolean hasErrors() {
            return Objects.nonNull(error);
        }
    }
    
    @Data
    class Error {
        private String error;
        private String description;
    }
    
    @Data
    class ErrorResponse {
        private String errorCode;
        private String message;
    }
    
    @Data
    class UserResponse implements ApiResponse {
        private String userName;
        private String userEmail;
    }
    

    And generic implementation of that method could look like:

    class JsonDecoder {
    
        private final ObjectMapper objectMapper = ...;
    
        public <R extends ApiResponse> ResponseWrapper<R> deserializeResponse(String apiResponse, Class<R> responseType, boolean isHttpError)
                throws JsonProcessingException {
            ResponseWrapper<R> response = new ResponseWrapper<>();
    
            if (isHttpError) {
                response.setError(deserializeError(apiResponse));
            } else {
                response.setResponse(objectMapper.readValue(apiResponse, responseType));
            }
    
            return response;
        }
    
        private Error deserializeError(String apiResponse) throws JsonProcessingException {
            final TypeReference<List<ErrorResponse>> errorResponseType = new TypeReference<>() {};
            List<ErrorResponse> errors = objectMapper.readValue(apiResponse, errorResponseType);
    
            ErrorResponse firstError = errors.get(0);
            String errorDescription = errors.stream().map(ErrorResponse::toString).collect(Collectors.joining());
    
            Error error = new Error();
            error.setError(firstError.getMessage());
            error.setDescription(errorDescription);
            return error;
        }
    }