Internals
JRAW uses OkHttp for HTTP requests and WebSocket connections and Moshi for JSON (de)serialization.
A request to /api/v1/me
returns information about the current user.
{
...
"verified": false,
"is_gold": false,
"link_karma": 10,
...
}
The easiest way to use this data is to have Moshi deserialize it as a Map<String, Object>
.
Moshi moshi = new Moshi.Builder().build();
// Create a Type that represents Map<String, Object>
Type mapType = Types.newParameterizedType(Map.class, String.class, Object.class);
// Use Moshi to create a JsonAdapter that can (de)serialize JSON into a Map<String, Object>
JsonAdapter<Map<String, Object>> mapAdapter = moshi.adapter(mapType);
// Deserialize the JSON into a Map<String, Object>
Map<String, Object> data = mapAdapter.fromJson(jsonString);
// Now we can query the data however we like:
int linkKarma = (int) data.get("link_karma");
boolean isGold = (boolean) data.get("is_gold");
boolean isVerified = (boolean) data.get("verified");
However, for this to work, the developer using JRAW has to know the keys for all data they want to inspect and has to deal with type casting. It gets even messier when the developer needs data inside a nested object. The ideal way to handle this situation is by letting Moshi bind all the properties of the JSON to an object.
class Account {
@Json(name = "link_karma") private int linkKarma;
@Json(name = "is_gold") private boolean isGold;
// Notice we don't need the @Json annotation here, the implied JSON key is "verified"
private boolean verified;
public int getLinkKarma() {
return linkKarma;
}
// omitted for brevitivity: getters for isGold and verified, equals, hashCode, and toString
// implementations.
}
Now we can access our data in a much nicer, type safe way.
// Use Moshi to create a JsonAdapter for our Account class
JsonAdapter<Account> accountAdapter = moshi.adapter(Account.class);
// Deserialize the JSON into an Account instance
Account data = accountAdapter.fromJson(jsonString);
int linkKarma = data.getLinkKarma();
boolean isGold = data.isGold();
boolean isVerified = data.isVerified();
AutoValue
While this approach is a huge step above using a Map, it still has one glaring issue: boilerplate code. For every model class like Account
(for which JRAW has 30+), we need to declare fields, write getters for those fields, and implement equals
, hashCode
, and toString
. Most of this can be generated by an IDE. However, the problem doesn't really rear its ugly head until we go to add another field. We have to add another getter and update equals
, hashCode
, and toString
. This is a pain to do manually, which is why JRAW uses AutoValue. AutoValue is a library that generates code for us. It takes care of fields and implements equals
, hashCode
, and toString
. All we have to do is provide the getter methods. Now we can shorten our Account
class to something more manageable.
@AutoValue
abstract class Account {
abstract int getLinkKarma();
abstract boolean isGold();
abstract boolean isVerified();
}
If you've gone down this road before, you'll know that this change actually breaks our deserialization process since Moshi can't create instances of abstract classes. To fix this, we're going to use the auto-value-moshi extension which will generate a JsonAdapter
for every @AutoValue
class that requests it.
@AutoValue
abstract class Account {
@Json(name = "link_karma") abstract int getLinkKarma();
@Json(name = "is_gold") abstract boolean isGold();
// Like before, we don't need the @Json annotation
abstract boolean isVerified();
// the presence of this method triggers the generation of a JsonAdapter
static JsonAdapter<Account> jsonAdapter(Moshi moshi) {
return new AutoValue_Account.MoshiJsonAdapter(moshi);
}
}
All we have to do is add this adapter to our Moshi.Builder and we're good to go!
Envelopes
Admittedly, /api/v1/me
is a cherry-picked example. In most cases, data isn't in such a flat structure. Usually, it looks something like this:
{
"kind": "<something>",
"data": { ... }
}
Where kind
is a value in KindConstants. reddit uses a kind
of t2
to state that the data
node contains an account information.
For a specific example, a call to /user/Shitty_Watercolour/about
yields JSON like this:
{
"kind": "t2",
"data": {
...
"verified": true,
"is_gold": true,
"link_karma": 357440,
...
}
}
JRAW refers to this special root structure as an "envelope." Each class that can be enveloped should be annotated with the RedditModel annotation.
@AutoValue
@RedditModel
abstract class Account {
// ...
}
Now to deserialize this data we can do this:
// Get a JsonAdapter that knows how to handle enveloped data
JsonAdapter<Account> accountAdapter = moshi.adapter(Account.class, Enveloped.class);
// Deserialization is just like before
Account data = accountAdapter.fromJson(jsonString);
See the RedditModelAdapterFactory class documentation for more.