preface

In a recent project, I used both Moshi and Kotlinx-Serialization JSON libraries, both of which have very concise and useful apis.

Unlike Gson’s reflection mechanism, both Moshi and Kotlinx-Serialization provide a pre-compilation mechanism that generates Adapter and Serializer, respectively, during compilation. This enables serialization and deserialization of JSON in a type-safe and more efficient manner.

  • Moshi
    • Originally from Square, it is highly integrated with Retrofit and friendly to Android developers
    • The xxxjsonAdapter.kt file can be generated at compile time with the help of kapt/ KSP
  • kotlinx-serialization
    • JetBrains is an official expansion pack that can be easily integrated into Ktor
    • Generate bytecode files (Xxx$$serializer.class) at compile time based on kotlin Compiler Plugin
    • supportKMP, can be used across platforms.
      • For example, define a SET of Dtos and reuse them on the Android, iOS, front-end, desktop, and server.
    • Supports JSON, Protobuf, CBOR, Hocon, and Properties
    • There are a number of tripartite extensions supporting TOML, XML, YAML, BSON, NBT, SharePreference, Bundle, and more

If you are using Kotlin for your daily development work, it is highly recommended that you try out and use both JSON libraries.

In this article, I want to talk about serialization of polymorphic objects.

Polymorphic objects in code

In Java, any interface we define can have multiple different implementation classes. Each implementation class can declare its own unique fields and methods. Sealed class’s syntactic sugar in Kotlin further simplifies this code organization behavior.

Before you start, make sure gradle.build contains the following dependencies

plugins {
 // ...
 id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10' / / kotlinx - serializaton plug-in
 id "com.google.devtools.ksp" version "1.6.10-1.0.4"             / / KSP plug-in
}
​
dependencies {
 // ...
 ksp "Com. Squareup. Moshi moshi - kotlin - codegen: 1.13.0" // Generate Adapter at compile time, non-reflective
 implementation "Com. Squareup. Moshi moshi: 1.13.0"
 implementation "Org. Jetbrains. Kotlinx: kotlinx serialization - json: 1.3.2." "
 testImplementation 'the junit: junit: 4.13.2'
 // ...
}
Copy the code

Suppose you have a Game interface named and a GamingRoom class with a list of games, and two Game interface implementation classes Zelda and EldenRing, as shown below.

package com.devwu.dto
interface Game
-----
package com.devwu.dto
@Serializable                      // kotlinx-serialization: automatically generate GamingRoom$$serializer.class
@JsonClass(generateAdapter = true) // Moshi: automatically generate GamingRoomJsonAdapter.kt
data class GamingRoom(
  val games: List<Game>
)
-----
package com.devwu.dto
@Serializable                      // kotlinx-serialization: automatically generate Zelda$$seriliazer.class
@JsonClass(generateAdapter = true) // Moshi: automatically generate zeldajsonAdapter.kt
data class Zelda(
  val title: String,
  val platform: String,
  val releaseAt: Long
) : Game
-----
package com.devwu.dto
@Serializable                      // kotlinx-serialization: automatically generate EldenRing$$seriliazer.class
@JsonClass(generateAdapter = true) // Moshi: automatically generate eldenringjsonAdapter.kt
data class EldenRing(
  val title: String,
  val platforms: List<String>,
  val releaseAt: String
) : Game
Copy the code

Zelda differs slightly from EldenRing in the following fields,

  • Platform/platforms: inZeldaIs in theStringType in theEldenRingIs in theList<String>Type, and hassThe suffix.
  • ReleaseAt:ZeldaIs in theLongType in theEldenRingIs in theStringtype

GamingRoom’s Games list field uses the Game type and can accept both Zelda and EldenRing instance objects as members of the list, as shown below.

val room = GamingRoom(
  games = listof(
    EldenRing(title = "Eldon's Ring", platforms = listof("PlayStation"."Xbox"."PC"), releaseAt = "2022-02-25"),
    Zelda(title = "The Legend of Zelda: Breath of the Wild.", platform = "Nintendo Switch", releaseAt = 1488470400)))Copy the code

JSON serialization of polymorphic objects

After serializing the room object with JSON, the following string should theoretically be generated.

{
    "games": [{"title": "Eldon's Ring"."platforms": ["PlayStation"."Xbox"."PC"]."releaseAt": "2022-02-25"
        },
        {
            "title": "The Legend of Zelda: Breath of the Wild."."platform": "Nintendo Switch"."releaseAt": 1488470400}}]Copy the code

However, adapter/ Srilizazer provided by Moshi and Kotlinx-Serialization by default does not know the polymorphic relationship in the code.

  • Moshi
No JsonAdapter for interface com.devwu.dto.Game (with no annotations)
for interface com.devwu.dto.Game
for java.util.List<com.devwu.dto.Game> games
for class com.devwu.dto.GamingRoom
java.lang.IllegalArgumentException: No JsonAdapter for interface com.devwu.dto.Game (with no annotations)
Copy the code
  • kotlinx-serialization
Class 'EldenRing' is not registered for polymorphic serialization in the scope of 'Game'.
Mark the base class as 'sealed' or register the serializer explicitly.
kotlinx.serialization.SerializationException: Class 'EldenRing' is not registered for polymorphic serialization in the scope of 'Game'.
Mark the base class as 'sealed' or register the serializer explicitly.
Copy the code

For the above exception, we need polymorphic relationships for Moshi or Kotlinx-Serialization declared code in the code.

Serialization of polymorphic objects in Moshi

Although Moshi official document of polymorphic objects don’t have much, but the Moshi – provides a PolymorphicJsonAdapterFactory adapter libraries, can easily generate JsonAdapter factory for state object. When used, the following dependencies need to be declared in the DSL.

implementation "Com. Squareup. Moshi moshi - adapters: 1.13.0"
Copy the code

Then create a new Moshi object

val moshi = Moshi.Builder()
        // Add a polymorphic JsonAdapter factory
      .add(
        PolymorphicJsonAdapterFactory.of(Game::class.java,"type")// Base type: Game::class.java, tag Key: type
          .withSubtype(Zelda::class.java,"zelda")          // Subtype: Zelda::class. Java, tag Value: Zelda
          .withSubtype(EldenRing::class.java,"elden-ring") // Subtype: EldenRing::class. Java, tag Value: elden-ring
      )
      .build()
Copy the code

At this point Moshi knows the relationship between Game, Zelda, and EldenRing, which we can verify with the following code

serialization
val room = GamingRoom(
  games = listof(
    EldenRing(title = "Eldon's Ring", platforms = listof("PlayStation"."Xbox"."PC"), releaseAt = "2022-02-25"),
    Zelda(title = "The Legend of Zelda: Breath of the Wild.", platform = "Nintendo Switch", releaseAt = 1488470400)))val adapter = moshi.adapter(GamingRoom::class.java)
val jsonStr = adapter.toJson(room)
println(jsonStr)
Copy the code

Can see Moshi as room object generates expected JSON string, red tag in the type field to generate additional type identifier, the markers associated with when we construct the Moshi adding PolymorphicJsonAdapterFactory, You can assign any key or value to it. It is important to note, however, that the values of each subtype should be unique and non-repeatable.

val moshi = Moshi.Builder()
        // Add a polymorphic JsonAdapter factory
      .add(
        PolymorphicJsonAdapterFactory.of(Game::class.java,"CUSTOME_KEY")  // Base type: Game::class.java, tag Key: CUSTOME_KEY
          .withSubtype(Zelda::class.java,"CUSTOME_VALUE1")                // Subtype: Zelda::class. Java, tag Value: CUSTOME_VALUE1
          .withSubtype(EldenRing::class.java,"CUSTOME_VALUE2")            // Subtype: EldenRing::class.java, tag Value: CUSTOME_VALUE2
      )
      .build()
Copy the code
deserialization

Deserialize using the jsonStr that survived the previous step as follows.

val adapter = moshi.adapter(GamingRoom::class.java)
val jsonStr = "" "{" games ": [{" type" : "elden - ring", "title" : "elden ring method", "platforms" : [" PlayStation ", "the Xbox," "PC"], "releaseAt" : "2022-02-25"}, {" ty PE ":" Zelda "," Title ":" Breath of the Wild "," Platform ":"Nintendo Switch","releaseAt":1488470400}]}""
val dto = adapter.fromJson(jsonStr)!!
dto.games.forEach{
	when(it){
    is Zelda -> { assert(it.platform == "Nintendo Switch")}
    is EldenRing -> { assert(it.platforms.contains("Xbox"))}
    else- > {}}}Copy the code

Serialization of polymorphic objects in Kotlinx-serialization

The official documentation of Kotlinx-Serialization is more friendly and has a separate document describing the polymorphic problem. By looking at this document, we can declare polymorphic relationships with the following code.

Declares relationships between polymorphic objects
val json = Json {
      serializersModule = SerializersModule {
        polymorphic(Game::class) {   // Declare the base type
          subclass(Zelda::class) // Declare subtypes
          subclass(EldenRing::class) // Declare subtypes}}}Copy the code
serialization
val room = GamingRoom(
  games = listof(
    EldenRing(title = "Eldon's Ring", platforms = listof("PlayStation"."Xbox"."PC"), releaseAt = "2022-02-25"),
    Zelda(title = "The Legend of Zelda: Breath of the Wild.", platform = "Nintendo Switch", releaseAt = 1488470400)))val jsonStr = json.encodeToJson(room)
println(jsonStr)
Copy the code

Kotlinx-serialization adds an additional type field to identify the actual type of the current JSON object when doing the JSON serialization. The default value for the type field is the fully qualified class name of the object. We can modify this value with the @serialName annotation as follows.

+ @serialName ("zelda") // kotlinx-serialization: set the type key name@serializable // kotlinx-serialization: Automatically generate Zelda$$seriliazer.class @jsonClass (generateAdapter = true) // Moshi: ZeldaJsonAdapter. Kt Data class Zelda(Val Title: String, Val Platform: String, Val releaseAt: Long) : Game= = = = = = = =
+ @serialName ("elden-ring") // kotlinx-serialization: set type key name@serializable // kotlinx-serialization: automatically generate EldenRing$$seriliazer.class @jsonClass (generateAdapter = true) // Moshi: Kt data class EldenRing(val title: String, val platforms: List<String>, val releaseAt: String ) : GameCopy the code

It is also important to note that the type value for each type should be unique and non-repeatable. Serialization is performed again for verification as follows.

val room = GamingRoom(
  games = listof(
    EldenRing(title = "Eldon's Ring", platforms = listof("PlayStation"."Xbox"."PC"), releaseAt = "2022-02-25"),
    Zelda(title = "The Legend of Zelda: Breath of the Wild.", platform = "Nintendo Switch", releaseAt = 1488470400)))val jsonStr = json.encodeToJson(room)
println(jsonStr)
Copy the code

visibletypeThe value of the field has changed from a fully qualified class name to a custom name.

deserialization

Deserialize using the jsonStr generated in the previous step as follows.

val jsonStr = "" "{" games ": [{" type" : "elden - ring", "title" : "elden ring method", "platforms" : [" PlayStation ", "the Xbox," "PC"], "releaseAt" : "2022-02-25"}, {" ty PE ":" Zelda "," Title ":" Breath of the Wild "," Platform ":"Nintendo Switch","releaseAt":1488470400}]}""
val gamingRoom = json.decodeFromString<GamingRoom>(jsonStr).games.forEach {
  when (it) {
    is Zelda -> {
      assert(it.platform == "Nintendo Switch")}is EldenRing -> {
      assert(it.platforms.contains("Xbox"))}else- > {}}}Copy the code
Automatically derive polymorphic relationships using sealed Class

If you use sealed classes in your project, kotlinx-serialization can automatically derive relationships between polymorphic types without explicitly declaring them in advance.

@Serializable
sealed class Game2 {
  @Serializable
  @SerialName("zelda")
  data class Zelda2(
    val title: String,
    val platform: String,
    val releaseAt: Long
  ) : Game2()

  @Serializable
  @SerialName("elden-ring")
  data class EldenRing2(
    val title: String,
    val platforms: List<String>,
    val releaseAt: String
  ) : Game2()
}
-----
@Serializable
data class GamingRoom2(
  val games: List<Game2>
)
Copy the code

At this point, you can remove the polymorphic relationship that showed the declaration in the previous code.

- val json = Json {
- serializersModule = SerializersModule {
- polymorphic(Game::class) {
- subclass(Zelda::class)
- subclass(EldenRing::class)
-}
-}
-}
Copy the code

The default Json object provided by Kotlin-Serialization is then tested

serialization

val game1 = Game2.EldenRing2(
  title = "Eldon's Ring",
  platforms = listOf("PlayStation"."Xbox"."PC"),
  releaseAt = "2022-02-25"
)
val game2 = Game2.Zelda2(
  title = "The Legend of Zelda: Breath of the Wild.",
  platform = "Nintendo Switch",
  releaseAt = 1488470400
)
val gamingRoom = GamingRoom2(listOf(game1, game2))
val jsonStr = Json.encodeToString(gamingRoom)
assert(jsonStr == "" "{" games ": [{" type" : "elden - ring", "title" : "elden ring method", "platforms" : [" PlayStation ", "the Xbox," "PC"], "releaseAt" : "2022-02-25"}, {" ty PE ":" Zelda "," Title ":" Breath of the Wild "," Platform ":"Nintendo Switch","releaseAt":1488470400}]}"")
println(jsonStr)
Copy the code

deserialization

val jsonStr =
      "" "{" games ": [{" type" : "elden - ring", "title" : "elden ring method", "platforms" : [" PlayStation ", "the Xbox," "PC"], "releaseAt" : "2022-02-25"}, {" ty PE ":" Zelda "," Title ":" Breath of the Wild "," Platform ":"Nintendo Switch","releaseAt":1488470400}]}""
val gamingRoom: GamingRoom2 = Json.decodeFromString(jsonStr)
gamingRoom.games.forEach {
  when (it) {
    is Game2.Zelda2 -> {
      assert(it.platform == "Nintendo Switch")}is Game2.EldenRing2 -> {
      assert(it.platforms.contains("Xbox"))}else -> {}
  }
}
println(gamingRoom)
Copy the code

summary

Due to the shortcomings of previous JSON libraries in terms of polymorphisms, JSON polymorphisms are often deliberately avoided in actual development, and client developers rarely have access to such data. However, with the improvement of the three-party library, JSON has made great progress in dealing with the problem of polymorphism. We hope that this article can be used to expand our understanding of JSON polymorphic serialization.

The code covered in this article has been uploaded to Github and is mainly in the unit test of the DTO module. You can view it here.

Afterword.

Jackson, commonly used on the Java server, is also mature for dealing with polymorphic objects, as can be seen here. If you need to serialize polymorphic objects in your business, you can recommend this technique to your backend friends and have fun with them