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.
Top comments (0)