Moshi - JSON library for Android
In this blog, we are going to talk about the JSON library by Square called Moshi. Moshi helps us to serialize and deserialize the JSON in a better and simpler way.
So, before starting let us divide the blog into the following sections,
- Why we need a library for serializing and deserializing in android?
- How do we use Moshi?
- Features of Moshi
- Using Moshi with List.
- Using Moshi with Retrofit
Why we need a library for serializing and deserializing in android?
In Android, when we do an API call we get JSON as a response majority of times. We can call that JSON and parse it manually and work with it or we can just use a library like Moshi to serialize and deserialize. Using Moshi can help us reduce the number of lines we write and reduce the possibility of getting errors.
How do we use Moshi?
In this section, we are going to understand how we would work with Moshi.
Let say we have a data class, User like
data class User(val name: String, val email: String)
and consider we have a variable called user and is defined as,
val user = User("Himanshu", "himanshu@gmail.com")
Now, with the help of Moshi, we will convert this as a JSON structure. To work with Moshi we have JsonAdapter class and builder pattern.
So, we build Moshi using,
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
and we will also create a variable of type JsonAdapter, which will help us work to and fro in converting JSON to Object class and vice-a-versa.
val jsonAdapter: JsonAdapter<User> = moshi.adapter(User::class.java)
Here, we have passed the User data class as a structure on which we want to perform the actions. Now, to convert the object of the User class to JSON we use,
jsonAdapter.toJson(user)
Here, we are using toJson to get JSON from the object of the User and if we print this, we get
{"email":"himanshu@gmail.com","name":"Himanshu"}
Similarly, we can map a JSON to a class using Moshi. Let's say we want to convert the above JSON to User class, we will use,
jsonAdapter.fromJson("{\"email\":\"himanshu@gmail.com\",\"name\":\"Himanshu\"}")
If you see, we are using fromJson here which helps us to convert the JSON which we have passed as a string. Now, if we print this we will see,
User(name=Himanshu, email=himanshu@gmail.com)
This is how we convert JSON to object and object to JSON using Moshi.
Features of Moshi
Moshi supports almost all the data type by default like,
- Integer, Float, etc
- Arrays and Collections
- Strings
- Enums
In Moshi, we can create our own type apart from the mentioned ones above. Let us understand this by an example,
Let's update the User class like,
data class User(val name:Name)
Here, we have added a new data class called Name, which will look like,
data class Name(val firstName: String, val lastName: String)
Here, in Name class, we take two parameters called firstName and lastName.
Now, when getting a JSON from this class, I want the full name of the user like firstName + lastName . Either we can do it manually every time we parse the JSON or we can add our own adapter using Moshi to do it for us.
So, we will create a class called NameAdapter,
class NameAdapter { }
And inside this class, we are going to do our conversion. We will add two functions named as
fun fullName()
and
fun getIndividualNames().
The class now looks like,
class NameAdapter {
@ToJson
fun fullName(name: Name): String {
}
@FromJson
fun getIndividualNames(json: String): Name {
}
}
Here, you can see we have annotated fullName with
ToJson
and
getIndividualNames
with
FromJson
.
It means, that when using this adapter with Moshi, Moshi will look for the annotations.
Let's say we want to concatenate both first and last names to return the full name and return it in JSON, we will do it inside the fullName function which is annotated with
ToJson
. Now, the fullName function will look like,
@ToJson
fun fullName(name: Name) = name.firstName + " " + name.lastName
Similarly, since we added the ToJson conversion for the JSON parsing, we would also need to update the getIndividualNames function which is annotated with FromJson which will convert the JSON values to individual elements in Name data class when mapping the JSON to class.
So, the getIndividaulNames will look like,
@FromJson
fun getIndividualNames(fullName: String): Name {
val name = fullName.split(" ")
return Name(name[0], name[1])
}
Here, we have splitter the string fullName from the first empty space and got the two strings that are first name and last name in a list of string.
And finally, to use this Adapter we add it in Moshi object while building it like,
val moshi = Moshi.Builder()
.add(NameAdapter())
.add(KotlinJsonAdapterFactory())
.build()
Note: Moshi’s adapters are ordered by precedence, so you always want to add the Kotlin adapter after your own custom adapters. Otherwise the
KotlinJsonAdapterFactory
will take precedence and your custom adapters will not be called.
The JsonAdapter would be the same as above like,
val jsonAdapter: JsonAdapter<User> = moshi.adapter(User::class.java)
and now when we print the toJson and FromJson in logcat like,
jsonAdapter.toJson(user)
and,
jsonAdapter.fromJson("{\"name\":\"Himanshu Singh\"}").toString()
We get the following output,
{"name":"Himanshu Singh"}
User(name=Name(firstName=Himanshu, lastName=Singh))
This is how you can create your own conversion adapter.
Now, let's say we want to put some condition check on only one field in the class or different fields of the same type. We can perform that as well where we create an adapter and that will only affect the only field mentioned. We would do it using annotation.
First, we will create an annotation like,
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class EmailCheck
Here, the Email check is annotated with JsonQualifier which would work with specific fields.
Now, we will update the User class like,
@JsonClass(generateAdapter = true)
data class User(val name: Name, @EmailCheck val email: String?)
Here, you can see we have annotated the email with EmailCheck, which means that the check with all the EmailCheck annotations will work with only the email field. The Name class remains the same.
Now, we will create an adapter which would have two functions, namely toJson and fromJson and will look like,
class EmailAdapter {
fun String.isValidEmail(): Boolean {
return !TextUtils.isEmpty(this) && Patterns.EMAIL_ADDRESS.matcher(this).matches()
}
@ToJson
fun toJson(@EmailCheck email: String?) = email
@FromJson
@EmailCheck
fun fromJson(email: String): String? {
return if (!email.isValidEmail()) {
"No Email Found"
} else email
}
}
Here, in the EmailAdapter we have annotated the fromJson function to EmailCheck and here we will check if the JSON we are parsing to map it to the class is not a valid email then, we will return No Email Found in the email field else we return the email itself.
Similarly, we also annotated the email parameter in toJson with EmailCheck, which would mean only keys with EmailCheck annotation are allowed.
Finally, we will also update the Moshi builder with a new adapter factory called
KotlinJsonAdapterFactory
like,
val moshi = Moshi.Builder()
.add(EmailAdapter())
.add(KotlinJsonAdapterFactory())
.build()
And now, let's create a user object-like,
val user = User(Name("Himanshu", "Singh"), email = "00")
and the jsonAdapter remains the same as how we have used it above. Now, if we log the jsonAdapter.toJson(user), we get the following as response,
{"name":{"firstName":"Himanshu","lastName":"Singh"},"email":"00"}
Here, you can see we are not passing a valid email and is just an integer value.
Finally, when we parse this JSON using fromJson() then we get the output,
User(name=Name(firstName=Himanshu, lastName=Singh), email=No Email Found)
You can see in the email field we get
No Email Found
because the email was not a valid email. This is how we can create adapters for individual fields and put some condition-specific to it.
Using Moshi with List
In this section, we will learn how to convert a List to String and then String to a list.
Consider an example, where we get a list of objects from our server and we need to store it in shared preference of our app. We would like to convert the list of objects to string as it only saves a primitive data type to save it and when we need to use it we will convert it back to a list.
Now, let's say we have a List of String like,
val names = listOf("Himanshu","Amit", "Ali", "Sumit")
and now we need to set type to map the raw data like,
val type = Types.newParameterizedType(List::class.java, String::class.java)
Here, We have list of String data, so it takes the String::class as class to identify what are the type of elements and List is the type of data which has to be converted.
Then to use this type, we will use it with Moshi adapter like,
val adapter = moshi.adapter<List<String>>(type)
where moshi looks like,
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
Now, to convert the list of data to string we will use the toJson like,
val namesInString: String = adapter.toJson(names)
This would map the list data to string and now if we want we can store in sharedpreference.
Now, if we want to reverse the mapping from string to list again, we will use the same adapter which we created and by using fromJson of moshi we will convert it like,
val allNames: List<String>? = adapter.fromJson(namesInString)
Here we need to pass the string which we need to convert back to a List.
Using Moshi with Retrofit
In this section, we are going to talk about how we can do an API call using Moshi as the converter in Android. We are going to call,
https://jsonplaceholder.typicode.com/posts/1
To get a single post using Retrofit. So, first, let's break it down in steps.
Steps 01.
We will first setup the dependency on the build.gradle like,
implementation "com.squareup.retrofit2:retrofit:2.8.1"
implementation "com.squareup.retrofit2:converter-moshi:2.6.2"
Step 02.
We will not create a data class called PostsResponse for the JSON response we will get from the API. The JSON looks like,
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "tiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
So, the data class mapping to this JSON would look like,
data class PostsResponse(@Json(name = "id")
val id: Int = 0,
@Json(name = "title")
val title: String = "",
@Json(name = "body")
val body: String = "",
@Json(name = "userId")
val userId: Int = 0)
Here, you can see we have used @Json to all the fields in the data class.
@Json takes the key name in the original JSON response, and just because we are using @Json we can give any name to the data class constructor variables and the JSON would still be mapped to the data class because the mapping here is happening because of the @Json annotation.
Step 03.
Now, we will create the interface to hold the API call. We will call it as APIService and will look like,
interface APIService {
@GET("/posts/1")
fun getSinglePost(): Call<PostsResponse>
}
It will have only one function called getSinglePost and will return the only post from the URL.
Step 04.
Now, to setup Retrofit, we will first create an instance of Moshi like,
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
and then we setup Retrofit like,
val retrofit = Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
Here, we have passed the based URL and the converter factory for Moshi. We also passed the Moshi as a parameter which we created earlier.
And, to support KotlinJsonAdapterFactory additionally, needs the following dependency,
implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
Note:
MoshiConverterFactory.create(moshi)
is the converter factory.
Step 05.
We will do the API Call now like,
retrofit.create(APIService::class.java).getSinglePost().enqueue(object : Callback<PostsResponse> {
override fun onFailure(call: Call<PostsResponse>, t: Throwable) {
Log.d("MainActivity", t.localizedMessage)
}
override fun onResponse(call: Call<PostsResponse>, response: Response<PostsResponse>) {
Log.d("MainActivity", response.body().toString())
}
})
And before running the app, do not forget to add the Internet permission in the Manifest file.
Now, if we run the app, we get,
PostsResponse(id=1, title=sunt aut facere repellat provident occaecati excepturi optio reprehenderit, body=quia et suscipit
suscipit recusandae consequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto, userId=1)
So, the API call was successful.
Extra
Let's say if we want to ignore title in the API response, we have updated the model like,
data class PostsResponse(@Json(name = "id")
val id: Int = 0,
@Json(name = "title")
@Transient
val title: String = "",
@Json(name = "body")
val body: String = "",
@Json(name = "userId")
val userId: Int = 0)
Here, you can see we have added Transient annotation which will ignore the field title and print the output as,
PostsResponse(id=1, title=, body=quia et suscipit
suscipit recusandae consequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto, userId=1)
Here, the title field is coming empty.
Miscellaneous
- Moshi is very light weighted.
- It uses the same mechanisms of GSON.
- We can add custom field names using @Json annotation.
This is how we can use Moshi in our Android project.
Happy learning.
Team MindOrks.