An overview of the
Moshi is a Json deserialization and serialization framework that Square opened in June 2015. When it comes to Json, you should think of Gson, FastJson, Jackson and other well-known open source frameworks, so why need Moshi? This is mainly because of Kotlin. We know that the previous libraries are mainly for Java parsing Json, and of course they support Kotlin, but Moshi is naturally kotlin-friendly and does Java parsing just as well. So Moshi does very well in both Java and Kotlin mixes and in pure Kotlin projects. In Java, Gson is an official recommended Json framework for deserialization and serialization. In Kotlin, there is also an official library kotlinx. Serialization, hereinafter referred to as KS, which is taken out separately in Kotlinx. As with Kotlinx. Coroutines, let’s compare the official library Gson and Kotlin’s official library KS to see the characteristics of each other.
The performance comparison
Before we start the performance comparison, let’s take a quick look at how these parsing frameworks work
Method | Support language | Custom parsing | |
---|---|---|---|
Gson | reflection | Java/Kotlin | TypeAdapter |
Moshi | Reflection/annotation | Java/Kotlin | JsonAdapter |
KS | Compile the plug-in | Kotlin | KSerializer |
As can be seen from the table above, both Gson and Moshi support reflection parsing, but KS does not, and KS only supports Kotlin. The three parsing modes all support custom parsers. In Kotlin parsing, Moshi supports automatic generation of JsonAdapter, while Gson and KS need to be manually written. At the same time, KS can be cross-platform, but KS for Gradle version requirements are relatively high, need 4.7 and above.
There are a few things to be aware of when testing
- Use the real machine: try not to use the simulator, the difference between different parsing frameworks on the same simulator is too big, far beyond ms level, will bring error to the test
- The optimal solution: That is to say, when we choose the Json framework for testing, we must choose the optimal solution of the framework, which is to take into account the development efficiency and parsing efficiency. Although Gson’s TypeAdapter does not need reflection, it needs to manually write code, so the development efficiency is low, so we use Gson’s reflection to compare. Moshi annotations and KS compilation plugin parsing.
We mainly compare two points: speed and stability
speed
Here in douban API test, the address of the API is api.douban.com/v2/movie/to… This is to return the top 250 movies on Douban, but this API is limited to return 100 at most, so I forced two times, and then the two times of Json overlay together, a total of 200 data for testing, as an aside, Douban returns all the pictures in webP format, which is really excellent. Then we started testing. I only tested one set of Json, both deserialization and serialization, and then averaged a single framework 10 times, with no cached bytecode, i.e., first parsing. The reason is that the underlying implementation of open source libraries are reflected, so they will cache the bytecode, lead to a second resolution of the same class, super fast speed, because only need assignment, of course, you might say, a Json does not depend on the result of the spectrum, is readily available in the test, the first is my Json data quantity is big, and nested hierarchy, The second reason is that their underlying implementation is different, and this difference will be magnified significantly when the amount of data is large, as you will know when you look at the data in a moment.
Moshi VS Gson(Java)
Test Code
fun testGsonJava(a) {
val json = JsonUtils.getJson("douban.json".this)
val deserialstart = System.currentTimeMillis()
val doubanBean = Gson().fromJson(json, DoubanBean::class.java)
val deserizalend = System.currentTimeMillis()
val deserialConsume = deserizalend - deserialstart
val serialstart = System.currentTimeMillis()
val seriJson = Gson().toJson(doubanBean)
val serizalend = System.currentTimeMillis()
val serialConsume = serizalend - serialstart
}
fun testMoshiJava(a) {
val json = JsonUtils.getJson("douban.json".this)
val jsonAdapter = Moshi.Builder().build().adapter(DoubanBean::class.java)
val deserialstart = System.currentTimeMillis()
val douban = jsonAdapter.fromJson(json)
val deserizalend = System.currentTimeMillis()
val deserialConsume = deserizalend - deserialstart
val serialstart = System.currentTimeMillis()
val seriJson = jsonAdapter.toJson(douban)
val serizalend = System.currentTimeMillis()
val serialConsume = serizalend - serialstart
}
Copy the code
Test Result
Moshi | Gson | |
---|---|---|
Serialization(ms) | 24/24/23/23/25 | 60/60/59/59/60 |
Deserialization(ms) | 66/65/67/65/65 | 73/79/74/72/75 |
Moshi VS GSon VS KS(Kotlin)
Test Code
fun testGsonKotlin(a) {
val json = JsonUtils.getJson("douban.json".this)
val deserialstart = System.currentTimeMillis()
val doubanBean = Gson().fromJson(json, DoubanBean::class.javaObjectType)
val deserizalend = System.currentTimeMillis()
val deserialConsume = deserizalend - deserialstart
val serialstart = System.currentTimeMillis()
val seriJson = Gson().toJson(doubanBean)
val serizalend = System.currentTimeMillis()
val serialConsume = serizalend - serialstart
}
fun testMoshiKotlin(a) {
val json = JsonUtils.getJson("douban.json".this)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(DoubanBean::class.java)
val deserialstart = System.currentTimeMillis()
val douban = jsonAdapter.fromJson(json)
val deserizalend = System.currentTimeMillis()
val deserialConsume = deserizalend - deserialstart
val serialstart = System.currentTimeMillis()
val seriJson = jsonAdapter.toJson(douban)
val serizalend = System.currentTimeMillis()
val serialConsume = serizalend - serialstart
}
fun testKotlinXSerialize(a) {
val json = JsonUtils.getJson("douban.json".this)
val start = System.currentTimeMillis()
val douban = JSON.parse(DoubanBean.serializer(), json)
val end = System.currentTimeMillis()
val consume = end - start
val serialstart = System.currentTimeMillis()
val seriJson = JSON.stringify(DoubanBean.serializer(), douban)
val serizalend = System.currentTimeMillis()
val serialConsume = serizalend - serialstart
}
Copy the code
Test Result
Moshi | Gson | KS | |
---|---|---|---|
Serialization(ms) | 23/27/23/24/27 | 91/85/85/86/86 | 38/37/36/43/37 |
Deserialization(ms) | 74/74/73/73/74 | 93/93/92/94/89 | 73/72/71/73/77 |
summary
Since the underlying IO operation of Moshi uses Okio, its serialization performance is better than Gson, KS and other frameworks, which is well understood. In the process of deserialization, we see that the parsing efficiency of Moshi is basically the same as Kotlin’s official serialization tool, but a little faster than Gson. In this test, the Adapter creation time of Moshi is not included, because it can be created as a singleton, independent of parsing, and consistent with the optimal solution mentioned above.
The stability of
There are two main aspects of stability: default values and null security
The default value
We know that during Java parsing, if a field is missing in Json, our Bean object’s original value remains unchanged, but because Gson does not recognize Kotlin’s constructor, the default value is invalid. For example:
@Serializable
data class Chinese(@Optional val age: Int = 0.@Optional val country: String? = "China") {
@Optional
private val hobby: String = "travel"
}
fun main(args: Array<String>) {
val gsonBean = Gson().fromJson("""{"age":4}""", Chinese::class.javaObjectType)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val adapter = moshi.adapter(Chinese::class.java)
val moshiBean = adapter.fromJson("""{"age":4}""")
val kxBean = JSON.parse(Chinese.serializer(), """{"age":4}""")}Copy the code
We parse the above data and find that the two fields of The gsonBean object parsed by Gson are null, but the deserialized objects of Moshi and KX are both default values given by us. This is a problem that Gson doesn’t have when parsing Java, but it doesn’t work in Kotlin. The reason can be seen in the Gson source code. When Gson constructs an object instance with reflection parsing, it calls the default no-parameter constructor, so it is not surprising that there is no default value. Why not Hobby? Because Gson doesn’t know what a data class is, so he still doesn’t know Hobby.
Air safety
In Java, we can use the @nullable and NotNull annotations to indicate whether a variable or method parameter is Nullable or not. However, the annotation is troublesome, so we don’t use the annotation most of the time. So Java code in the call the resolved when Bean objects need to be judging is not empty, Kotlin perfect in this case, whether the object can be specified in the definition can be empty, so in the use of non-null object without judgment, but if for a method of parameter is empty, you pass a null value, In the same way, if we specify a non-null field in the Data class, we should report an error if the field is Null in the Json Data. Let’s take a look at the implementation logic of the three frameworks
@Serializable
data class Chinese(@Optional val age: Int = 0.@Optional val country: String? = "China") {
@Optional
private val hobby: String = "travel"
}
fun main(args: Array<String>) {
val gsonBean = Gson().fromJson("""{"age":null}""", Chinese::class.javaObjectType)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val adapter = moshi.adapter(Chinese::class.java)
val moshiBean = adapter.fromJson("""{"age":null}""")
val kxBean = JSON.parse(Chinese.serializer(), """{"age":null}""")}Copy the code
During the test, it was found that both Moshi and KS reported errors, but Gson was normal. According to the syntax of Kotlin, this is not reasonable. We need to report an error, because the age field is non-null, but an empty parameter was passed here, so there is a problem with the processing of Gson here. This is because, as we mentioned earlier, the bytecode that Kotlin eventually compiles is running on the JVM, but Gson does not distinguish between Java and Kotlin when it reflects, so it is resolved according to Java’s parsing rules, because Null is normal in Java. Even though this is no longer possible in Kotlin.
conclusion
For the above tests, the following is a summary based on the actual use of the project
-
Mixed projects: Using Moshi, both Java and Kotlin
-
Java projects: Gson is recommended, and Moshi is recommended if deserialization requirements are high, as it has built-in Okio
-
Kotlin project: If cross-platform, use KS; Non-cross-platform, if only deserialization, Moshi and KS can be used, if serialization is more, use Moshi
Basic usage of Java
Dependency
implementation 'com. Squareup. Moshi moshi: 1.8.0 comes with'
Copy the code
Bean
String json = ... ; Moshi moshi = new Moshi.Builder().build(); JsonAdapter<Bean> jsonAdapter = moshi.adapter(Bean.class); //Deserialize Bean bean = jsonAdapter.fromJson(json); //Serialize String json = jsonAdapter.toJson(bean);Copy the code
List
Moshi moshi = new Moshi.Builder().build();
Type listOfCardsType = Types.newParameterizedType(List.class, Bean.class);
JsonAdapter<List<Bean>> jsonAdapter = moshi.adapter(listOfCardsType);
//Deserialize
List<Bean> beans = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.fromJson(json);
Copy the code
Map
Moshi moshi = new Moshi.Builder().build();
ParameterizedType newMapType = Types.newParameterizedType(Map.class, String.class, Integer.class);
JsonAdapter<Map<String,Integer>> jsonAdapter = moshi.adapter(newMapType);
//Deserialize
Map<String,Integer> beans = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.fromJson(json);
Copy the code
Others
- @json: Key conversion
- Transitent: This field is skipped and not parsed
public final class Bean {
@Json(name = "lucky number") int luckyNumber;
@Json(name = "objec") int data;
@Json(name = "toatl_price") String totolPrice; private transient int total; //jump the field }Copy the code
Basic usage of Kotlin
Whereas Java can only parse by Reflection, For Kotlin, Moshi provides two ways to parse, either Reflection or Codegen, essentially, through the annotation processor. You can use either or both
Dependency
implementation : 'com. Squareup. Moshi moshi - kotlin: 1.8.0 comes with'
Copy the code
Reflection
The Data type
data class ConfigBean(
var isGood: Boolean = false.var title: String = "".var type: CustomType = CustomType.DEFAULT
)
Copy the code
Start parsing
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
Copy the code
This approach introduces the Kotlin-Reflect Jar package, which is about 2.5m in size.
Codegen
The official name for Moshi is Codegen. Since it is generated using annotations, you need to add kapt in addition to adding Moshi’s Kotlin dependencies
kapt : 'com. Squareup. Moshi moshi - kotlin - codegen: 1.8.0 comes with'
Copy the code
Transforming the Data classes
Add JsonClass annotations to our data classes
@JsonClass(generateAdapter = true)
data class ConfigBean(
var isGood: Boolean = false.var title: String = "".var type: CustomType = CustomType.DEFAULT
)
Copy the code
In this case, Moshi will generate the JsonAdapter we need at compile time, and then parse the Json data through a JsonReader traversal, which is not only independent of reflection, but also faster than Kotlin.
This kind of Adpter generated by annotations does not need to be registered. Moshi will automatically help us register in the Factory through annotations. Here is no Code.
Advanced Usage (JsonAdapter)
The JsonAdapter is a Json adapter that converts Json data to any type you want. There are many built-in JsonAdapter in Moshi, such as:
Built-in Type Adapters
- The Map: MapJsonAdapter
- Enums: EnumJsonAdapter
- The Arrays: ArrayJsonAdapter
- Object: ObjectJsonAdapter
- String: located in StandardJsonAdapters and implemented with an anonymous inner class
- Primitives (int, float, char, Boolean) : adapters for basic data types are in StandardJsonAdapters, using anonymous inner classes
Custom Type Adapters
For some relatively simple specification data, using the built-in JsonAdapter Moshi has been fully covered, but because Json only supports the transmission of basic data types, so many times can not meet the business needs, for example:
{
"type": 2."isGood": 1
"title": "TW9zaGkgaXMgZmxleGlibGU="
}
Copy the code
This is a common Json file that contains 5 fields. If we define the Bean to be parsed according to the fields returned by the server, we can parse it completely. However, when we actually call the Bean, the data is not very clean.
- I need to define an Enum conversion class to convert Int to Enum
- IsGood: Int, I need a Boolean, so I have to convert an Int to a Boolean when I use it
- title: String. This field is encrypted, either by AES or RSA. Here we just use Base64 pairs for testing purposesMoshi is flexibleDo encode for.
For the students of the client side, it seems to be ok, we used to do this, if this kind of dirty Json is less, it is ok, after more, it is very headache, every time when I use it, I need to turn again, most of the time WHEN I do this, I think it is a waste of time, but today with Moshi, We just need to define the JsonAdapter for the type we want to convert, once and for all. Moshi already defines Adapter for common data types, but the built-in Adapter is no longer sufficient for our needs. So we need to customize the JsonAdapter.
Entities defined
class ConfigBean {
public CustomType type;
public Boolean isGood;
public String title;
}
Copy the code
In this case, we define the data type not according to the Json data returned by the server, but according to the format required by our business, so we finally complete the conversion through the JsonAdapter converter, let’s start to customize the JsonAdapter.
Int->Enum
CustomType
enum CustomType {
DEFAULT(0."DEFAULT"), BAD(1."BAD"), NORMAL(2."NORMAL"), GOOD(3."NORMAL");
public int type;
public String content;
CustomType(int type, String content) {
this.type = type;
this.content = content; }}Copy the code
TypeAdapter
Defining a TypeAdapter that inherits from the JsonAdapter, passing in the corresponding generics, will automatically help us copy the fromJson and toJson methods
public class TypeAdapter {
@FromJson
public CustomType fromJson(int value) throws IOException {
CustomType type = CustomType.DEFAULT;
switch (value) {
case 1:
type = CustomType.BAD;
break;
case 2:
type = CustomType.NORMAL;
break;
case 3:
type = CustomType.GOOD;
break;
}
return type;
}
@ToJson
public Integer toJson(CustomType value) {
returnvalue ! =null ? value.type : 0; }}Copy the code
Now that we’ve done the conversion of Type, let’s use title as an example
StringDecode
TitleAdapter
public class TitleAdapter {
@FromJson
public String fromJson(String value) {
byte[] decode = Base64.getDecoder().decode(value);
return new String(decode);
}
@ToJson
public String toJson(String value) {
return newString(Base64.getEncoder().encode(value.getBytes())); }}Copy the code
Int->Boolean
BooleanAdapter
public class BooleanAdapter {
@FromJson
public Boolean fromJson(int value) {
return value == 1;
}
@ToJson
public Integer toJson(Boolean value) {
return value ? 1 : 0; }}Copy the code
Adapter test
So let’s test that out
String json = "{\n" + "\"type\": 2,\n" + "\"isGood\": 1,\n"
+ "\"title\": \"TW9zaGkgaXMgZmxleGlibGU=\"\n"+ "}";
Moshi moshi = new Moshi.Builder()
.add(new TypeAdapter())
.add(new TitleAdapter())
.add(new BooleanAdapter())
.build();
JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
ConfigBean cofig = jsonAdapter.fromJson(json);
System.out.println("=========Deserialize ========");
System.out.println(cofig);
String cofigJson = jsonAdapter.toJson(cofig);
System.out.println("=========serialize ========");
System.out.println(cofigJson);
Copy the code
Print the Log
=========Deserialize ========
ConfigBean{type=CustomType{type=2, content='NORMAL'}, isGood=true, title='Moshi is flexible'}
=========serialize ========
{"isGood":1."title":"TW9zaGkgaXMgZmxleGlibGU="."type":2}
Copy the code
In line with our expected results, and when we are developing, we only need to set Moshi as a singleton, and add all Adapter in one time, we can once and for all, and then happily develop.
The source code parsing
Moshi uses Okio for optimization, but the JsonReader, JsonWriter and other upper-layer code are directly borrowed from Gson, so no more analysis. The main features of Moshi JsonAdapter and Kotlin Codegen analysis focus on the analysis.
Builder
Moshi moshi = new Moshi.Builder().add(new BooleanAdapter()).build();
Copy the code
Moshi is built through Builder mode, support to add more than one JsonAdapter, let’s take a look at the Builder source code
public static final class Builder {
// Stores the creation mode of all Adapter. If no custom Adapter is added, the value is empty
final List<JsonAdapter.Factory> factories = new ArrayList<>();
// Add a custom Adapter and return itself
public Builder add(Object adapter) {
return add(AdapterMethodsFactory.get(adapter));
}
// Add the creation method of the JsonAdapter to the factories and return the factories themselves
public Builder add(JsonAdapter.Factory factory) {
factories.add(factory);
return this;
}
// Add the collection of JsonAdapter creation methods to the factories and return them
public Builder addAll(List<JsonAdapter.Factory> factories) {
this.factories.addAll(factories);
return this;
}
// Add Adapter creation method with Type and return itself
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
return add(new JsonAdapter.Factory() {
@Override
public @NullableJsonAdapter<? > create( Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null; }}); }// Create an instance of Moshi
public Moshi build(a) {
return new Moshi(this); }}Copy the code
Through the source found that the Builder saved all custom Adapter creation, and then called the Builder build way to create a Moshi instance, the following look at the Moshi source.
Moshi
A constructor
Moshi(Builder builder) {
List<JsonAdapter.Factory> factories = new ArrayList<>(
builder.factories.size() + BUILT_IN_FACTORIES.size());
factories.addAll(builder.factories);
factories.addAll(BUILT_IN_FACTORIES);
this.factories = Collections.unmodifiableList(factories);
}
Copy the code
Inside the constructor to create the factories, and then joined the Builder in the factories, and then added a BUILT_IN_FACTORIES, we should also can guess this is the built-in JsonAdapter Moshi, point in check
BUILT_IN_FACTORIES
static final List<JsonAdapter.Factory> BUILT_IN_FACTORIES = new ArrayList<>(5);
static {
BUILT_IN_FACTORIES.add(StandardJsonAdapters.FACTORY);
BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
}
Copy the code
BUILT_IN_FACTORIES preempts all the built-in JsonAdapters with a static code block
JsonAdapter
JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
Copy the code
Both our custom JsonAdapter and the Built-in JsonAdapter in Moshi ultimately serve our parsing, so eventually all the JSONAdapters will come together into a JsonAdapter. Let’s see how that works. Following the Adapter method of Moshi, the following method is finally called
public <T> JsonAdapter<T> adapter(Type type, Set
annotations, @Nullable String fieldName) {
type = canonicalize(type);
// If there is a cache, return the cache directly
Object cacheKey = cacheKey(type, annotations);
synchronized(adapterCache) { JsonAdapter<? > result = adapterCache.get(cacheKey);if(result ! =null) return (JsonAdapter<T>) result;
}
boolean success = false;
JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
try {
if(adapterFromCall ! =null)
return adapterFromCall;
// Iterate over the Factories until the generic T Adapter is hit
for (int i = 0, size = factories.size(); i < size; i++) {
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
if (result == null) continue;
lookupChain.adapterFound(result);
success = true;
returnresult; }}}Copy the code
When I first saw this, I was a little bit surprised, I wasn’t sure which JsonAdapter my Config hit, and I finally traced through the breakpoint, and found that it hit the ClassJsonAdapter, and since it hit it, let’s look at its implementation
ClassJsonAdapter
A constructor
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public @NullableJsonAdapter<? > create( Type type, Set<? extends Annotation> annotations, Moshi moshi) {// Omitted a lot of exception determination codeClass<? > rawType = Types.getRawType(type);// Get all types of ClassClassFactory<Object> classFactory = ClassFactory.get(rawType); Map<String, FieldBinding<? >> fields =new TreeMap<>();
for(Type t = type; t ! = Object.class; t = Types.getGenericSuperclass(t)) {// Create a binding relationship between Moshi and Filed
createFieldBindings(moshi, t, fields);
}
return newClassJsonAdapter<>(classFactory, fields).nullSafe(); }}Copy the code
When we get a JsonAdapter, basically all the construction is done, we can do the Deserialize or Serialize operation, look at Deserialize which is the fromJSON method
JsonReader&JsonWriter
For Java parsing, Moshi does not significantly improve transmission efficiency, but uses Okio for low-level IO operations. Moshi’s innovation lies in flexibility, namely JsonAdapter, which is also mentioned in Moshi’s official documentation
Moshi uses the same streaming and binding mechanisms as Gson. If you’re a Gson user you’ll find Moshi works similarly. If you try Moshi and don’t love it, you can even migrate to Gson without much violence!
The JsonAdapter for Moshi is more flexible and can be automatically generated using annotations. The JsonReader and JsonWriter are both directly derived from Gson.
fromjson
ConfigBean cofig = jsonAdapter.fromJson(json);
Copy the code
This method first calls the fromJson method of the parent JsonAdapter
public abstract T fromJson(JsonReader reader) throws IOException;
public final T fromJson(BufferedSource source) throws IOException {
return fromJson(JsonReader.of(source));
}
public final T fromJson(String string) throws IOException {
JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
T result = fromJson(reader);
return result;
Copy the code
It turns out that fromJson is an overloaded method, you can either pass a String or you can pass a BufferedSource, but what you end up calling is fromJson(JsonReader reader), BufferedSource is one of Okio’s classes, Since Moshi’s underlying IO is Okio, we find that the method that takes JsonReader is abstract, so the implementation is in the ClassJsonAdapter.
@Override public T fromJson(JsonReader reader) throws IOException {
T result = classFactory.newInstance();
try {
reader.beginObject();
while(reader.hasNext()) { int index = reader.selectName(options); // If it is not a Key, skip itif (index == -1) {
reader.skipName();
reader.skipValue();
continue; } fieldsArray[index]. Read (reader, result); } reader.endObject();returnresult; } catch (IllegalAccessException e) { throw new AssertionError(); }} // Call recursively until finally voidread(JsonReader reader, Object value) throws IOException, IllegalAccessException {
T fieldValue = adapter.fromJson(reader);
field.set(value, fieldValue);
}
Copy the code
toJson
String cofigJson = jsonAdapter.toJson(cofig);
Copy the code
As with fromJson, the toJson method of the JsonAdapter is called first
public abstract void toJson(JsonWriter writer, T value) throws IOException;
public final void toJson(BufferedSink sink, T value) throws IOException {
JsonWriter writer = JsonWriter.of(sink);
toJson(writer, value);
}
public final String toJson( T value) {
Buffer buffer = new Buffer();
try {
toJson(buffer, value);
} catch (IOException e) {
throw new AssertionError(e); // No I/O writing to a Buffer.
}
return buffer.readUtf8();
}
Copy the code
Regardless of whether the generic T or BufferedSink is passed in, toJson(JsonWriter Writer) is finally called and returns buffer.readutf8 (). Let’s move on to the concrete implementation of the Lump sum class
@Override public void toJson(JsonWriter writer, T value) throws IOException {
try {
writer.beginObject();
for(FieldBinding<? > fieldBinding : fieldsArray) { writer.name(fieldBinding.name);// Write the values of fieldsArray to writer
fieldBinding.write(writer, value);
}
writer.endObject();
} catch (IllegalAccessException e) {
throw newAssertionError(); }}Copy the code
Codegen
Moshi’s Kotlin Codegen support is an annotation processor. It generates a small and fast adapter for each of your Kotlin classes at compile time. Enable it by annotating each class that you want to encode as JSON:
Codegen, which is the Annotation we mentioned earlier, generates the corresponding JsonAdapter at compile time. Let’s take a look at adding annotations first. Let’s see how the annotations that Kotlin automatically generates for us are different from our own annotations.
CustomType
@JsonClass(generateAdapter = true)
data class CustomType(var type: Int.var content: String)
Copy the code
Let’s take a look at the corresponding Generated JsonAdapter
CustomTypeJsonAdapter
This class has a lot of methods, but let’s focus on formJson and toJson
override fun fromJson(reader: JsonReader): CustomType {
private val options: JsonReader.Options = JsonReader.Options.of("type"."content"."age")
var type: Int? = null
var content: String? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.selectName(options)) {
// Assign variables in the order in which they are defined
0 -> type = intAdapter.fromJson(reader)
1 -> content = stringAdapter.fromJson(reader)
-1 -> {
reader.skipName()
reader.skipValue()
}
}
}
reader.endObject()
// Create the object without reflection, passing in the parsed Value
var result = CustomType(type = type ,content = content )
return result
}
override fun toJson(writer: JsonWriter, value: CustomType?) {
writer.beginObject()
writer.name("type")/ / write type
intAdapter.toJson(writer, value.type)
writer.name("content")/ / write the content
stringAdapter.toJson(writer, value.content)
writer.endObject()
}
Copy the code
Before I looked at this code, I was wondering why Moshi wanted to parse the JsonReader by Int instead of by JsonReader Name, because after we get a JsonReader, we usually write:
override fun fromJson(reader: JsonReader): CustomType {
var type: Int? = null
var content: String? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
// Assign variables in the order in which they are defined
"type" -> type = reader.nextInt()
"content" -> content = reader.nextString()
else -> {
reader.skipValue()
}
}
}
reader.endObject()
// Create the object without reflection, passing in the parsed Value
var result = CustomType(type = type ,content = content )
return result
}
/ / omit toJson
Copy the code
Compared with the code we wrote, the code in the annotation generated by Moshi extracts the KEY of Json and puts it in an Options, and at the same time, it naturally generates an index. Maybe it is not easy to understand here, why is it converted to int? In this case, the efficiency is lower, isn’t it? Since you need to turn once to create the object, and you need to turn once to read the key, it is better to use String directly to fast, so let’s follow the source code, see the concrete implementation inside the selectName
/**
* If the next token is a {@linkplain Token#NAME property name} that's in {@code options}, this
* consumes it and returns its index. Otherwise this returns -1 and no name is consumed.
*/
@CheckReturnValue
public abstract int selectName(Options options) throws IOException;
Copy the code
Through the comments we can see selectName comments, we passed a Options, and returns an index, the index is the key index before we put in, it will improve the efficiency of parsing, intuitive look like an extravagance, directly give me the name of this key is good, why to 0 to 1, Readability is degraded. If your key is only repeated once, it doesn’t matter whether it’s translated to index or not, because it takes a decode to get from a binary stream to a string. If we’re parsing a list, then the same key will be decode multiple times. Decode takes time and space. So when parsing a List of keys, we only need to decode the same key once, and this is much more efficient when parsing lists.
ConfigBean
@JsonClass(generateAdapter = true)
data class ConfigBean(var isGood: Boolean ,var title: String ,var type: CustomType)
Copy the code
ConfigBeanJsonAdapter
override fun fromJson(reader: JsonReader): ConfigBean {
var isGood: Boolean? = null
var title: String? = null
var type: CustomType? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.selectName(options)) {
0 -> isGood = booleanAdapter.fromJson(reader)
1 -> title = stringAdapter.fromJson(reader)
2 -> type = customTypeAdapter.fromJson(reader)
-1 -> {
reader.skipName()
reader.skipValue()
}
}
}
reader.endObject()
var result = ConfigBean(isGood = isGood ,title = title ,type = type
return result
}
override fun toJson(writer: JsonWriter, value: ConfigBean?) {
writer.beginObject()
writer.name("isGood")
booleanAdapter.toJson(writer, value.isGood)
writer.name("title")
stringAdapter.toJson(writer, value.title)
writer.name("type")
customTypeAdapter.toJson(writer, value.type)
writer.endObject()
}
Copy the code
By looking at the generated CustomTypeJsonAdapter and ConfigBeanJsonAdapter, we can see that the Codegen generated annotation has the following advantages compared to reflection:
- High efficiency: Objects are created directly without reflection
- APK is small: there is no need to introduce the KOtlin-Reflect Jar package
Matters needing attention
When you’re doing kotlin parsing either Reflect or Codegen, you have to make sure that the type is the same, that the parent and the subclass are either Java or Kotlin, because in both cases, you end up parsing through ClassType. You must also ensure that the Koltin type is internal or public when parsing with Codegen.
conclusion
Moshi entire usage with the source code look down, in fact is not very complex, but for Java and Kotlin parsing added a flexible JsonAdapter, and can be automatically generated in Kotlin, although Gson and KS also support custom parsing, but assignment needs to be manually written, development efficiency is low. Moshi also has some drawbacks. Null support for Kotlin is not friendly. If Kotlin parses a non-nullable field, it throws an exception. If the Json type is Null, then the corresponding field will still be assigned to Null, as in the previous Gson, but the issue and MR have been mentioned from the latest official COMMIT to give a default value to the non-null field when the Key corresponding to the Json data is Null. It should be updated in 1.9.0, so keep an eye on it.