Search code examples
javajsonrestjacksoncxf

Mapping a JSON object to Java using Jackson when an inner json object is conditionally empty


I am implementing a REST client for a musixmatch.com exposed API (that searches lyrics based on some params e.g: song name,artist etc.). (Total newbie to REST/JSON).

My Client Code:

import java.io.IOException;
import java.io.InputStream;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.MappingJsonFactory;

import in.soumav.musixmatch.beans.MMResponse;

public class MusixMatchClient {

    public static void main(String[] args) throws JsonParseException, IOException {
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target("https://api.musixmatch.com").path("ws").path("1.1")
                                        .path("matcher.lyrics.get")
                                        .queryParam("q_track", "Wake Me Up")
                                        .queryParam("q_artist", "Greenday")
                                        .queryParam("apikey", "...my key...");
        Builder builderRequest = target.request();
        Response response = builderRequest.get();
        if (Response.Status.OK.getStatusCode() == response.getStatus()) {
           MappingJsonFactory factory = new MappingJsonFactory();
           JsonParser parser = factory.createJsonParser((InputStream) response.getEntity());
           MMResponse resp = parser.readValueAs(MMResponse.class);
           System.out.println(resp.getMessage().getBody().getLyrics().getLyrics_body());
       }

    }
}

The JSON parser parses fine when a match is found.

The response from the API (in POSTMAN) when a match is found:

{
    "message": {
        "header": {
            "status_code": 200,
            "execute_time": 0.076157093048096
        },
        "body": {
            "lyrics": {
                "lyrics_id": 14080789,
                "explicit": 0,
                "lyrics_body": "Summer has come and passed\nThe innocent can never last\nWake me up when September ends\n\nLike my father's come to pass\nSeven years has gone so fast\nWake me up when September ends\n\nHere comes the rain again\nFalling from the stars\nDrenched in my pain again\nBecoming who we are\n\nAs my memory rests\nBut never forgets what I lost\n...\n\n******* This Lyrics is NOT for Commercial use *******",
                "script_tracking_url": "https://tracking.musixmatch.com/t1.0/m_js/e_1/sn_0/l_14080789/su_0/rs_0/tr_3vUCADW8Npey61GB2HRIObKY4Sz3low2ucLdqisdi7cAswHSwWeSM5uhan_JCxhFMVX0wKQjr_6eciPf6twsVnnD4RPIGl8wpzGhgPLQRIkUkBP0Zga626PhvTK603MycNGrL9kMxuLM8iA8b8IYiLDV_WzIffNXs2ENvhz39AuqdldwE0H-mS51SpVUU21V--VBwXN1uBD3ra2GANeMeuvOMMP4-8sa1tE3FvaEVUaP-mqxgQtDJHG2_aY01nRNfzLPZ86xPscgeQMrPiILn5lpC7mkagXpQDqXo0MpoTnHZPFKqhrgiVNxkpZzX-oqam8DZflIgIe4zMtg2Y3QE8MvjE5vp500IZUOz-Q0GhVKfND5T6Yv3-jOuvkRkSxYbI8tJmRhbVe5XX3DATCdqAB7zYHrwyijL90yRJJzv2f-SC4E6f0J/",
                "pixel_tracking_url": "https://tracking.musixmatch.com/t1.0/m_img/e_1/sn_0/l_14080789/su_0/rs_0/tr_3vUCAIcjYkNNhw02X2Dtvu6SSWbscstTuW8YKvMwxRjtwZRmYXk4v0xsOdvjifHmsY2VY5yQTLHPUONXovxMI4XXZKXkC4KwjUwXR1afVXX5JE010OvQ-IsmsFkZ7-wXEXWJEFH5WJoKALES1HdQPDGsDNy6J5mGQURtG9MsQTSnfRnNz6zJkWTLZZ2F1GnG9f7ncW8guku-lOOWYAPcs8--4U4A3uR47_hn6PBh-JvHaQ2lFUjD6L0JuKs_Bgx0nU6RN97H02EkD6xkYWtsjZytPEneUHf3IrikZMu-tWvoclj0imoww8c_8NlqvHXG8pHYFO4YLPwk5kqfRXlW2TtwmJ9u1Do048_UFyCLAIctEEZBJEfcwjSep7VwU3BOiaKhL2Cwe-xV-NdW5pSzAbRb8bZ5PRrUnD7P-7SzdDSbossTyt6O/",
                "lyrics_copyright": "Lyrics powered by www.musixmatch.com. This Lyrics is NOT for Commercial use and only 30% of the lyrics are returned.",
                "updated_time": "2016-02-06T23:07:22Z"
            }
        }
    }
}

However when a match is not found, the following response is returned (while testing through POSTMAN):

{"message":{"header":{"status_code":404,"execute_time":0.11416816711426},"body":[]}}

The actual status code in POSTMAN returns 200 OK though! And since the body equals to an empty array, the JSON parser can't parse it and throws the following exception:

Exception in thread "main" org.codehaus.jackson.map.JsonMappingException: Can not deserialize instance of in.soumav.musixmatch.beans.Body out of START_ARRAY token at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@d9345cd; line: 1, column: 74] (through reference chain: in.soumav.musixmatch.beans.MMResponse["message"]->in.soumav.musixmatch.beans.Message["body"])

I have my object mappings as follows:

             MMResponse 
                 |
              Message
           ______|____________________
          |                           | 
         Header(status_cd,           Body
       execution_time)                |
                                      |
                                    Lyrics(lyrics_id,lyrics_body etc.)

QUESTIONS:

  1. What would be a better way/approach of achieving this?
  2. If I have to make changes to my existing code, how would I be handling it?
  3. How to read the status_code in the header(in the JSON response) before even mapping it to the Java object?

Solution

  • It seems that the status code you receive from API does not match the status code within the response object. Because this response should not satisfy your if condition Response.Status.OK.getStatusCode() == response.getStatus()

    {"message":{"header":{"status_code":404,"execute_time":0.11416816711426},"body":[]}}
    

    If the API is wrong and you want to bypass it, you will need to check the status_code from the JSON response. I would suggest something like:

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.apache.commons.io.IOUtils;
    
    ...
    
    if (Response.Status.OK.getStatusCode() == response.getStatus()) {
      ObjectMapper mapper = new ObjectMapper();
      // Validate object status status_code or message.header.status_code
      String jsonString = IOUtils.toString(inputStream, StandardCharsets.UTF_8.name());
    
      JsonNode jsonObject = mapper.readTree(jsonString);
      String statusCode =
              jsonObject.get("message")
                      .get("header")
                      .get("status_code")
                      .toString();
    
      if(statusCode.equalsIgnoreCase("200")) {
        MappingJsonFactory factory = new MappingJsonFactory();
        JsonParser parser = factory.createJsonParser((InputStream) response.getEntity());
        MMResponse resp = parser.readValueAs(MMResponse.class);
        System.out.println(resp.getMessage().getBody().getLyrics().getLyrics_body());  
      }
    }
    

    Above implementation use Jackson Object Mapper and commons IOUtils. IOUtils can be found in next dependency:

    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
    
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.2</version>
    </dependency>