Restructuring JSON with JAX-RS ReaderInterceptors and a little bit of JSON-B Magic

Have you ever needed to consume a RESTful service but the data structure of the remote service just didn’t quite match with what you had in mind in your client application? I ran into this situation earlier this week while exploring some commercial REST APIs. In my case, I had some JSON like this:

{
  "links":{
    "self":"https://api.myhost.com/services/v2/music?titleSearch=10%2C000"
  },
  "data":[
    {
      "type":"Song",
      "id":"56780987",
      "attributes":{
        "admin":"EMI Christian Music Publishing",
        "author":"Jonas Myrin and Matt Redman",
        "ccli_number":6016351,
        "copyright":
          "2011 Thankyou Music, Said And Done Music, and SHOUT! Publishing",
        "created_at":"2014-11-10T17:31:26Z",
        "hidden":false,
        "last_scheduled_at":"2021-05-30T08:49:00Z",
        "last_scheduled_short_dates":"May 30, 2021",
        "notes":"Vocal Range B - D'",
        "themes":", Adoration, Blessing, Christian Life, Praise",
        "title":"10,000 Reasons (Bless The Lord)"
      },
      "links":{"self":"https://api.myhost.com/services/v2/music/8802060"}
    }
  ],
  "included":[],
  "meta":{
    "total_count":1,
    "count":1,
    "can_order_by":["title","created_at","updated_at","last_scheduled_at"],
    "can_query_by":["author","ccli_number","themes","title"],
    "parent":{
        "id":"132275",
        "type":"Organization"
    }
  }
}

This JSON looks like a nicely formed HATEOAS response from a RESTful service providing song information. However, there are a few obstacles if we want to simply convert it into a Java object in our application like this:

public class Song {
    private String id;
    private String title;
    private String author;
    private LocalDate lastScheduled;
    // ... more fields, and public getters/setters ...
}

The first obstacle is that the object returned in the JSON response is not a Song object – it is a complex object that contains the song data (under the data field) and other RESTful navigational fields like links and meta. To overcome this obstacle, we could create some sort of MetaSong object that has fields for data, links, etc., but unless we plan to use those other fields that seems like a waste. Fortunately there is a better way!

Upon receiving the response, we could intercept the JSON data and reformat it before the JAX-RS client or MicroProfile Rest Client interface converts the JSON to a Java object. We do that with a ReaderInterceptor provider, like this:

public class DataExtractionReaderInterceptor implements ReaderInterceptor {

    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context)
        throws IOException, WebApplicationException {

        InputStream is = context.getInputStream();
        JsonObject json = Json.createReader(is).readObject();
        JsonValue data = json.get("data");
        is = new ByteArrayInputStream(data.toString().getBytes())
        context.setInputStream(is);
        return context.proceed();
    }
}

This code is executed on the client after the response is returned to the client but before the response is converted to a Java object. It uses JSON-P APIs to read the response stream and then extracts the data field, then replaces the response stream with a new stream that only includes that data object.

The second obstacle is that the song’s JSON fields don’t match the Java object’s fields. The JSON has a field called attributes, and the relevant fields like title, author, etc. are all child fields of that. But in the Java object, these fields are all directly under the Song class. We can overcome this obstacle with a little JSON-B magic:

public class Song {
    private String id;
    private String title;
    private String author;
    //...

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }

    public void setAttributes(Map<String,Object> attrs) {
        setTitle((String)attrs.get("title"));
        setAuthor((String)attrs.get("author"));
        // ...
    }
}

By default, JSON-B maps the JSON fields to Java fields by calling the appropriate setter method (e.g., JSON id is a String and would map to the setId Java method). However, since we don’t want to create a separate Attributes Java class to handle the JSON child fields, we instead create a placeholder method, setAttributes that takes a Map<String, Object> parameter. Now, JSON-B will pass in the JSON attributes object as key-value pairs that we can then map to the appropriate fields on our Song Java object.

Now all we need to do is invoke the service using either the JAX-RS Client or MicroProfile Rest Client APIs. The full source code including a runnable sample is available at https://github.com/andymc12/sample-restructure-json-data.

Thanks for checking in! As always, please let me know if you have any questions or comments.

30