- Moshi — JSON library for Android
- 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
- Steps 01.
- Step 02.
- Step 03.
- Step 04.
- Step 05.
- Extra
- Miscellaneous
- Advanced JSON parsing techniques using Moshi and Kotlin
- A match made in parser heaven
- The example model and JSON file
- 1. Fully manual parsing
- Introducing selectName() for performance
- Reducing boilerplate code using extension functions
- Immutability, default values and mandatory fields
- 2. Moshi’s Kotlin Code Gen
- Setup moshi-kotlin-codegen
- A look at the generated code
- Using the generated adapters
- 3. Coroutines magic for big data sources
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
and consider we have a variable called user and is defined as,
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,
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.
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,
Here, we are using toJson to get JSON from the object of the User and if we print this, we get
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,
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,
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,
Here, we have added a new data class called Name, which will look like,
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,
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,
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,
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,
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,
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,
and now when we print the toJson and FromJson in logcat like,
We get the following output,
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,
Here, the Email check is annotated with JsonQualifier which would work with specific fields.
Now, we will update the User class like,
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,
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,
And now, let’s create a user object-like,
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,
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,
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,
and now we need to set type to map the raw data like,
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,
where moshi looks like,
Now, to convert the list of data to string we will use the toJson like,
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,
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,
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,
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,
So, the data class mapping to this JSON would look like,
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,
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,
and then we setup Retrofit like,
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,
Note: MoshiConverterFactory.create(moshi) is the converter factory.
Step 05.
We will do the API Call now like,
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,
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,
Here, you can see we have added Transient annotation which will ignore the field title and print the output as,
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.
Источник
Advanced JSON parsing techniques using Moshi and Kotlin
A match made in parser heaven
Jul 30, 2018 · 11 min read
Moshi is a modern JSON library for Android and Java from Square. It can be considered as the successor to GSON, with a simpler and leaner API and an architecture enabling better performance through the use of the Okio library. It’s also the most Kotlin-friendly library you can use to parse JSON files, as it comes with Kotlin-aware extensions.
In this article I’m going to demonstrate how to take advantage of features from both the Moshi library and the Kotlin language itself in order to write efficient and robust JSON parsers.
The example model and JSON file
Consider the following model representing a person:
id and name are mandatory properties, while the age is optional with a default value of -1.
Our objective is to load a list of Person objects in our application from a JSON file or stream with the following contents:
In this simple example, the JSON object key names exactly match the Person property names and the «age» key is also optional.
1. Fully manual parsing
The most basic way of parsing JSON using Moshi is to use the streaming API, which is similar to the streaming API of GSON and the one provided by the Android Framework. This gives you the most control over the parsing process, which is especially useful when the JSON source is dirty.
Typical code would look like this:
Introducing selectName() for performance
We can further optimize the parser by leveraging a feature introduced in Moshi 1.5: the ability to compare the next bytes of the stream to expected JSON object key names or values. This is done using the JsonReader.selectName() and JsonReader.selectString() methods, respectively.
How does that improve performance? By looking at the JSON file, you can see that some string values are being repeated multiple times: the key names for the JSON objects are known in advance and match the property names of the Person class ( «id» , «name» , «age» ). So instead of letting Moshi decode the UTF-8 string and allocate memory for it every time a key name is encountered in the stream, we can skip that step and simply compare the next bytes of the stream with a preloaded collection of known key names, already encoded in UTF-8. If the byte sequence is found in the collection, its index will be returned by selectName() . This is made possible using some neat features of the lower-level Okio library.
Note: Moshi is optimized for, and only works with, JSON files encoded in UTF-8. If you still use a different character encoding in 2018, shame on you.
After replacing nextName() with selectName() in the parser, the code now looks like this:
It’s a bit less readable than before since the when expression now mentions array indices instead of the string values directly. That can be improved by declaring extra constants for these indices in the companion object, at the expense of adding more lines of code.
Reducing boilerplate code using extension functions
The code for reading a JSON array and a JSON object using a streaming API always follows the same patterns. These patterns can be extracted to extension functions to avoid the repetition. This especially improves readability and reduces the risk of errors for JSON files with multiple levels of nested arrays and objects.
It’s recommended to declare the higher-order functions as inline so that calling them introduces no additional cost compared to the original code (no extra objects allocations) and the bytecode will be identical.
We can also create another version of readArray() which goes one step further and builds and returns the list as well.
When using that function, the body of the lambda must return the item to be added to the list, or null to skip it.
With these new functions the parser code becomes much shorter:
Immutability, default values and mandatory fields
To be able to enforce the immutability of the Person object (all its fields are val s), we need to pass all the field values at once in its constructor, which means that we need to declare one extra variable for each field.
- Variables for optional fields ( age ) need to be assigned a default value matching the default value of the field.
- Variables for mandatory fields ( id , name ) are assigned an arbitrary default value outside of the range of valid values. null can also be used as default value for non-null mandatory fields. Then before instantiating the model object, we need to check that these variables have been reassigned valid values, otherwise throw an Exception or skip the item:
Manual fields validation makes the parser code both more verbose and error-prone because we need to remember updating the parser every time we change the mandatory fields or update the default values in the model.
Note: We can avoid declaring these variables entirely in the parser by making the model fully mutable (declaring all fields as var s) with an empty constructor.
- Less lines of code in the parser;
- We can rely on the model default values instead of duplicating them in the parser code.
- We lose all the benefits of immutability: coherent states, no side effects, thread safety;
- All fields are now declared as optional. We need to document which ones are in practice mandatory and the parser still needs to manually check if they have been assigned a valid value.
Because the downsides outweigh the benefits, I recommend to always favor immutability in your model objects when you can, especially since Kotlin makes this easier than Java.
2. Moshi’s Kotlin Code Gen
A better way to validate mandatory Kotlin fields and respect default values while keeping the objects immutable is to let Moshi do this work for us by using the automatic JSON to Kotlin converters it provides.
Moshi has two ways to generate these converters which are called JsonAdapter s:
- Using reflection, via the moshi-kotlin artifact. Adapters will be generated at runtime when needed, then reused. The main downside of this solution is that in order to understand metadata like nullability and default values of Kotlin properties, this artifact depends on the kotlin-reflect library, a 2.5 MiB jar file which will make your application code size grow significantly.
- Since Moshi 1.6, adapters can also be generated at compile time using the moshi-kotlin-codegen annotation processor. This is a much better solution because it provides better performance while adding no extra dependency to your application. The only limitation of that solution compared to kotlin-reflect is that it is unable to initialize private or protected fields, but the same limitations apply when writing your parsers manually.
Setup moshi-kotlin-codegen
Make sure you enable the kapt plugin in your application’s build.gradle file.
Then add the annotation processor to your dependencies.
Now for every Kotlin class for which you want to generate a JsonAdapter implementation at compile time, add the @JsonClass annotation on top of it with the generateAdapter element set to true . In this example, we’ll add it to the Person class.
By default, the generated parser will match the property names with the JSON object key names. To override that, add the @Json annotation on the property to specify its JSON key name.
Moshi also allows to fully customize the way each property is converted from or to JSON, by using custom annotations associated with your own custom type adapters. For more information, check out the official documentation.
A look at the generated code
When building the project, the annotation processor will create a new Kotlin class called PersonJsonAdapter . Let’s evaluate its code quality.
We can notice that this code is quite similar to what we wrote in the manual parser.
It uses selectName() for better performance, which is good. The options are not stored inside a companion object but it doesn’t matter because a JsonAdapter is only instantiated once then reused, making it effectively a singleton. Non-null properties are enforced. Mandatory fields are properly checked and an exception is thrown when they are missing.
Only two things aren’t perfect compared to a smart manually-written parser:
- Nullable variables are used to store values of non-null primitive types ( Long , Int ) and generic adapters are used to read them ( longAdapter , intAdapter ), which means unnecessary boxing will occur when reading these values.
- For each item, two instances of Person are created instead of one. The reason behind this is to allow reading back all the default values of optional fields from the first instance when creating the second, final instance.
This should not cause any performance issue, unless your model class includes heavy initialization code that you want to avoid doing twice.
This also means that when all fields are mandatory, only one instance will be created by the adapter.
These are minor performance issues, so I recommend that you use the automatically generated Kotlin JsonAdapter s when both your code and the JSON data source allow it.
Using the generated adapters
It’s preferred to use the Moshi API to retrieve adapter instances, because it will automatically locate the class, create a single instance of it and put it in a cache so it can be shared between parsers.
The generated adapter converts JSON to a single Person object. But the JSON file contains an array of persons so we want to retrieve a List
instead. There are two ways to achieve this.
First, we can ask Moshi to build a generic list adapter which will use the generated PersonJsonAdapter under the hood. This is done by using Moshi’s Types utility class to build a type representing List
But more interestingly, we can also use the generated adapters inside our own parsers, effectively mixing and matching manual and automatic parsing. This is especially useful when the JSON source is not very well structured or contains a lot of unnecessary layers that we want to skip, but at the same time we still want to leverage the automatic parsing in some parts of the file.
For our example, we can write a hybrid parser which builds a list of Person objects while filtering out children and people of unknown age:
We start by manually parsing the JSON array using the readArrayToList() extension function defined earlier. It was designed to not add the item to the list when the lambda returns null , so we can use takeIf() to filter out items.
Generated JsonAdapter s are a great way to remove boilerplate code from manual JSON parsers.
Of course, you can also write your own custom JsonAdapter s and reuse them in your code.
If you want to learn more about Moshi’s Kotlin Code Gen, you can read this excellent article from its main contributor, Zac Sweers.
3. Coroutines magic for big data sources
In the previous example we saw how to filter out items inside the parser. Now, what if we wanted to filter out items from outside the parser, or wanted to process parsed items one by one, without storing them all at once into memory?
We can achieve this by creating a lazy streaming parser which returns a Kotlin Sequence instead of a List . Sequences allow to keep memory usage under control when dealing with potentially huge JSON lists, as they only deal with one item at a time and all processing is delayed until their final collection. A Sequence can also be converted to an Iterable for Java compatibility.
While it is possible to manually write a custom Sequence with a custom Iterator which will resume parsing every time next() is called, it is not an easy task. It requires some mental gymnastics because the parsing code, instead of being structured hierarchically like the JSON file in one single block, has to be split into separate parts sharing the same state.
Fortunately, there is an easier way thanks to Kotlin’s coroutines which allow to build a lazy sequence using only a single block of synchronous code and the yield() suspending function.
The code is very simple. It’s similar to building a list in one go, except that the calls to add an item to the list are replaced with yield(item) . The parser will effectively stop at that point, preserving its state, then resume later when the next item of the Sequence is requested by the caller. That’s all the code needed to create a fully lazy JSON parser in Kotlin.
It’s then possible filter out items, iterate over them or do anything else lazily outside of the parser:
I hope this article was able to inspire you to make your JSON parsers cleaner, faster and fun. Feel free to share it and also ask questions or provide more examples in the comments. Happy coding!
Источник